14 Commits

Author SHA1 Message Date
3b0cdd5f65 2.2.1
Some checks failed
Default (tags) / security (push) Failing after 22s
Default (tags) / test (push) Failing after 13s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-10-11 06:16:45 +00:00
2bb86552e2 fix(syncedinstance): Prevent same-instance syncs and sanitize post update payloads; update tests and docs 2025-10-11 06:16:44 +00:00
00dd0c69a5 update 2025-10-10 17:02:41 +00:00
b289cb67cf update tests 2025-10-10 16:55:15 +00:00
4b2c828dd6 2.2.0
Some checks failed
Default (tags) / security (push) Failing after 22s
Default (tags) / test (push) Failing after 13s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-10-10 12:57:32 +00:00
11a9b23802 feat(apiclient): Add native Admin & Content API clients, JWT generator, and tag visibility features; remove external @tryghost deps and update docs 2025-10-10 12:57:31 +00:00
719bfafb93 2.1.0
Some checks failed
Default (tags) / security (push) Failing after 21s
Default (tags) / test (push) Failing after 12s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-10-08 09:57:59 +00:00
d493d9fd01 feat(syncedinstance): Add SyncedInstance for multi-instance content synchronization, export it, add tests, and expand README 2025-10-08 09:57:59 +00:00
63e514c1da 2.0.0
Some checks failed
Default (tags) / security (push) Failing after 22s
Default (tags) / test (push) Failing after 14s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-10-08 09:14:46 +00:00
b3f08fb64c BREAKING CHANGE(classes.ghost): Remove Settings and Webhooks browse/read APIs, remove noisy console.error logs, and update tests/docs 2025-10-08 09:14:45 +00:00
7251e90395 1.4.1
Some checks failed
Default (tags) / security (push) Failing after 12s
Default (tags) / test (push) Failing after 13s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-10-07 14:31:54 +00:00
62839e2f54 fix(tests): Remove updated_at from post and page update test payloads 2025-10-07 14:31:54 +00:00
2d9844eb61 1.4.0
Some checks failed
Default (tags) / security (push) Failing after 21s
Default (tags) / test (push) Failing after 13s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-10-07 14:29:36 +00:00
76c2b714b5 feat(classes.ghost): Add members, settings and webhooks support; implement Member class and add tests 2025-10-07 14:29:36 +00:00
32 changed files with 11808 additions and 680 deletions

1
.serena/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/cache

67
.serena/project.yml Normal file
View File

@@ -0,0 +1,67 @@
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
# * For C, use cpp
# * For JavaScript, use typescript
# Special requirements:
# * csharp: Requires the presence of a .sln file in the project folder.
language: typescript
# whether to use the project's gitignore file to ignore files
# Added on 2025-04-07
ignore_all_files_in_gitignore: true
# list of additional paths to ignore
# same syntax as gitignore, so you can use * and **
# Was previously called `ignored_dirs`, please update your config if you are using that.
# Added (renamed) on 2025-04-07
ignored_paths: []
# whether the project is in read-only mode
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
# Added on 2025-04-18
read_only: false
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run scripts/print_tool_overview.py`.
#
# * `activate_project`: Activates a project by name.
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
# * `create_text_file`: Creates/overwrites a file in the project directory.
# * `delete_lines`: Deletes a range of lines within a file.
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
# * `execute_shell_command`: Executes a shell command.
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
# * `initial_instructions`: Gets the initial instructions for the current project.
# Should only be used in settings where the system prompt cannot be set,
# e.g. in clients you have no control over, like Claude Desktop.
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
# * `insert_at_line`: Inserts content at a given line in a file.
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
# * `list_memories`: Lists memories in Serena's project-specific memory store.
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
# * `read_file`: Reads a file within the project directory.
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
# * `remove_project`: Removes a project from the Serena configuration.
# * `replace_lines`: Replaces a range of lines within a file with new content.
# * `replace_symbol_body`: Replaces the full definition of a symbol.
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
# * `search_for_pattern`: Performs a search for a pattern in the project.
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
# * `switch_modes`: Activates modes by providing a list of their names
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
excluded_tools: []
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""
project_name: "ghost"

View File

@@ -1,5 +1,60 @@
# Changelog # Changelog
## 2025-10-11 - 2.2.1 - fix(syncedinstance)
Prevent same-instance syncs and sanitize post update payloads; update tests and docs
- SyncedInstance now validates and normalizes source and target base URLs (trailing slashes and case) and throws a clear error when attempting to sync an instance to itself to prevent circular syncs.
- Post.update signature changed to accept Partial<IPost>. Update logic now builds a sanitized payload and removes read-only/computed fields (uuid, comment_id, url, excerpt, reading_time, created_at, primary_author, primary_tag, etc.) before calling the Admin API to avoid conflicts.
- Added/updated integration tests (dates, syncedinstance validation) and adjusted tag tests to be resilient; README expanded with examples, usage notes, and multi-instance sync safety details.
- Improved tag sync/update to preserve slug when updating tags on targets.
## 2025-10-10 - 2.2.0 - feat(apiclient)
Add native Admin & Content API clients, JWT generator, and tag visibility features; remove external @tryghost deps and update docs
- Add native GhostAdminAPI and GhostContentAPI implementations (ts/apiclient/*) using global fetch with support for browse/read/add/edit/delete, webhooks, and image upload.
- Add generateToken JWT implementation compatible with Ghost Admin API (ts/apiclient/ghost.jwt.ts) with Web Crypto and Node fallbacks.
- Switch Ghost class to use built-in apiclient implementations and bump default API version to v6.0.
- Use Admin API to fetch ALL tags (including zero-count) and add visibility filtering and minimatch filtering in getTags; add convenience methods getPublicTags and getInternalTags.
- Add Tag methods getVisibility(), isInternal(), and isPublic() to expose visibility information.
- Replace imports of @tryghost/* with local apiclient implementations in ts/ghost.plugins.ts and remove @tryghost dependencies from package.json.
- Update README to document tag visibility, new tag-related APIs/examples, and other notes about using Admin API for tags.
## 2025-10-08 - 2.1.0 - feat(syncedinstance)
Add SyncedInstance for multi-instance content synchronization, export it, add tests, and expand README
- Introduce SyncedInstance class (ts/classes.syncedinstance.ts) to synchronize tags, posts and pages across Ghost instances with mapping and history support. New APIs: syncTags, syncPosts, syncPages, syncAll, getSyncStatus, clearSyncHistory, clearMappings.
- Export SyncedInstance from the package entry point (ts/index.ts).
- Add integration tests for SyncedInstance (test/test.syncedinstance.node.ts).
- Expand README (readme.md) with Quick Start, detailed Multi-Instance Synchronization docs, examples and updated API reference.
## 2025-10-08 - 2.0.0 - BREAKING CHANGE(classes.ghost)
Remove Settings and Webhooks browse/read APIs, remove noisy console.error logs, and update tests/docs
- Removed Settings API methods from Ghost: getSettings and updateSettings (breaking change).
- Removed Webhooks browsing/reading methods from Ghost: getWebhooks and getWebhookById. createWebhook, updateWebhook and deleteWebhook remain.
- Removed test/test.settings.node.ts and simplified test/test.webhook.node.ts to only exercise create/update/delete webhook flows without feature-availability guarding.
- Stripped console.error debug logging across multiple classes (Author, Ghost, Member, Page, Post, Tag) to reduce noisy runtime output.
- Updated README: removed 'Site Settings' section and clarified webhook API limitations supported by the underlying Ghost Admin SDK.
## 2025-10-07 - 1.4.1 - fix(tests)
Remove updated_at from post and page update test payloads
- Stop setting updated_at in test update payloads to avoid mutating server-managed timestamps
- Changed test/test.post.node.ts: removed updated_at assignment when updating a post
- Changed test/test.page.node.ts: removed updated_at assignment when updating a page
## 2025-10-07 - 1.4.0 - feat(classes.ghost)
Add members, settings and webhooks support; implement Member class and add tests
- Introduce IMember and Member class (ts/classes.member.ts) with CRUD (update, delete) and JSON helpers
- Add member management API to Ghost: getMembers (with minimatch filtering), getMemberById, getMemberByEmail, createMember
- Add site settings API to Ghost: getSettings and updateSettings (admin API wrappers)
- Add webhooks management to Ghost: getWebhooks, getWebhookById, createWebhook, updateWebhook, deleteWebhook
- Wire smartmatch filtering for members using @push.rocks/smartmatch
- Export Member from ts/index.ts so it's part of the public API
- Add comprehensive node tests for Ghost, Author, Member, Page, Post, Tag, Settings and Webhooks (test/*.node.ts)
- Fix Post construction usages in classes.ghost to pass the Ghost instance when creating Post objects
## 2025-10-07 - 1.3.0 - feat(core) ## 2025-10-07 - 1.3.0 - feat(core)
Add Ghost CMS API client classes and README documentation Add Ghost CMS API client classes and README documentation

8158
deno.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "@apiclient.xyz/ghost", "name": "@apiclient.xyz/ghost",
"version": "1.3.0", "version": "2.2.1",
"private": false, "private": false,
"description": "An unofficial Ghost CMS API package enabling content and admin functionality for managing posts.", "description": "An unofficial Ghost CMS API package enabling content and admin functionality for managing posts.",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",
@@ -17,15 +17,13 @@
"@git.zone/tsbuild": "^2.6.8", "@git.zone/tsbuild": "^2.6.8",
"@git.zone/tsbundle": "^2.5.1", "@git.zone/tsbundle": "^2.5.1",
"@git.zone/tsrun": "^1.3.3", "@git.zone/tsrun": "^1.3.3",
"@git.zone/tstest": "^2.3.8", "@git.zone/tstest": "^2.4.2",
"@push.rocks/qenv": "^6.1.3", "@push.rocks/qenv": "^6.1.3",
"@push.rocks/tapbundle": "^6.0.3", "@push.rocks/tapbundle": "^6.0.3",
"@types/node": "^22.12.0" "@types/node": "^22.12.0"
}, },
"dependencies": { "dependencies": {
"@push.rocks/smartmatch": "^2.0.0", "@push.rocks/smartmatch": "^2.0.0"
"@tryghost/admin-api": "^1.14.0",
"@tryghost/content-api": "^1.12.0"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

414
pnpm-lock.yaml generated
View File

@@ -11,12 +11,6 @@ importers:
'@push.rocks/smartmatch': '@push.rocks/smartmatch':
specifier: ^2.0.0 specifier: ^2.0.0
version: 2.0.0 version: 2.0.0
'@tryghost/admin-api':
specifier: ^1.14.0
version: 1.14.0
'@tryghost/content-api':
specifier: ^1.12.0
version: 1.12.0
devDependencies: devDependencies:
'@git.zone/tsbuild': '@git.zone/tsbuild':
specifier: ^2.6.8 specifier: ^2.6.8
@@ -28,8 +22,8 @@ importers:
specifier: ^1.3.3 specifier: ^1.3.3
version: 1.3.3 version: 1.3.3
'@git.zone/tstest': '@git.zone/tstest':
specifier: ^2.3.8 specifier: ^2.4.2
version: 2.3.8(@aws-sdk/credential-providers@3.734.0)(socks@2.8.7)(typescript@5.9.2) version: 2.4.2(@aws-sdk/credential-providers@3.734.0)(socks@2.8.7)(typescript@5.9.2)
'@push.rocks/qenv': '@push.rocks/qenv':
specifier: ^6.1.3 specifier: ^6.1.3
version: 6.1.3 version: 6.1.3
@@ -323,8 +317,8 @@ packages:
'@borewit/text-codec@0.1.1': '@borewit/text-codec@0.1.1':
resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==} resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==}
'@cloudflare/workers-types@4.20251004.0': '@cloudflare/workers-types@4.20251008.0':
resolution: {integrity: sha512-FkTBHEyOBwphbW4SLQ2XLCgNntD2wz0v1Si7NwJeN0JAPW/39/w6zhsKy3rsh+203tuSfBgsoP34+Os4RaySOw==} resolution: {integrity: sha512-dZLkO4PbCL0qcCSKzuW7KE4GYe49lI12LCfQ5y9XeSwgYBoAUbwH4gmJ6A0qUIURiTJTkGkRkhVPqpq2XNgYRA==}
'@colors/colors@1.6.0': '@colors/colors@1.6.0':
resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==}
@@ -673,8 +667,8 @@ packages:
resolution: {integrity: sha512-DDzWunkxXLtXJTxBf4EioXLwhuqdA2VzdTmOzWrw4Z4Qnms/YM67q36yajwNohAajPYyRz5DayU0ikrceFXyVw==} resolution: {integrity: sha512-DDzWunkxXLtXJTxBf4EioXLwhuqdA2VzdTmOzWrw4Z4Qnms/YM67q36yajwNohAajPYyRz5DayU0ikrceFXyVw==}
hasBin: true hasBin: true
'@git.zone/tstest@2.3.8': '@git.zone/tstest@2.4.2':
resolution: {integrity: sha512-rt7rpR2UwzHXjpqquEvWG4LfzGOGeI6lcR2YyO8pc7lqjhH+xsuaWPUQ5IwFl4Vw4VnR9ZoHBCqkjvxF8ow1wQ==} resolution: {integrity: sha512-Lcxuruk/ii1xFKNbf2b1lVYtl9d8ppTpqfF7qtWlcEMdLYW4/42wZ3dcG+jQlCPikQngEYfqSVaJSLyAWzkEGQ==}
hasBin: true hasBin: true
'@hapi/bourne@3.0.0': '@hapi/bourne@3.0.0':
@@ -845,8 +839,8 @@ packages:
resolution: {integrity: sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==} resolution: {integrity: sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==}
engines: {node: '>=12'} engines: {node: '>=12'}
'@puppeteer/browsers@2.10.10': '@puppeteer/browsers@2.10.11':
resolution: {integrity: sha512-3ZG500+ZeLql8rE0hjfhkycJjDj0pI/btEh3L9IkWUYcOrgP0xCNRq3HbtbqOPbvDhFaAWD88pDFtlLv8ns8gA==} resolution: {integrity: sha512-kp3ORGce+oC3qUMJ+g5NH9W4Q7mMG7gV2I+alv0bCbfkZ36B2V/xKCg9uYavSgjmsElhwBneahWjJP7A6fuKLw==}
engines: {node: '>=18'} engines: {node: '>=18'}
hasBin: true hasBin: true
@@ -1318,6 +1312,10 @@ packages:
resolution: {integrity: sha512-XJ4z5FxvY/t0Dibms/+gLJrI5niRoY0BCmE02fwmPcRYFPI4KI876xaE79YGWIKnEslMbuQPsIEsoU/DXa0DoA==} resolution: {integrity: sha512-XJ4z5FxvY/t0Dibms/+gLJrI5niRoY0BCmE02fwmPcRYFPI4KI876xaE79YGWIKnEslMbuQPsIEsoU/DXa0DoA==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/core@3.15.0':
resolution: {integrity: sha512-VJWncXgt+ExNn0U2+Y7UywuATtRYaodGQKFo9mDyh70q+fJGedfrqi2XuKU1BhiLeXgg6RZrW7VEKfeqFhHAJA==}
engines: {node: '>=18.0.0'}
'@smithy/credential-provider-imds@4.2.0': '@smithy/credential-provider-imds@4.2.0':
resolution: {integrity: sha512-SOhFVvFH4D5HJZytb0bLKxCrSnwcqPiNlrw+S4ZXjMnsC+o9JcUQzbZOEQcA8yv9wJFNhfsUiIUKiEnYL68Big==} resolution: {integrity: sha512-SOhFVvFH4D5HJZytb0bLKxCrSnwcqPiNlrw+S4ZXjMnsC+o9JcUQzbZOEQcA8yv9wJFNhfsUiIUKiEnYL68Big==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
@@ -1346,6 +1344,10 @@ packages:
resolution: {integrity: sha512-BG3KSmsx9A//KyIfw+sqNmWFr1YBUr+TwpxFT7yPqAk0yyDh7oSNgzfNH7pS6OC099EGx2ltOULvumCFe8bcgw==} resolution: {integrity: sha512-BG3KSmsx9A//KyIfw+sqNmWFr1YBUr+TwpxFT7yPqAk0yyDh7oSNgzfNH7pS6OC099EGx2ltOULvumCFe8bcgw==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/fetch-http-handler@5.3.1':
resolution: {integrity: sha512-3AvYYbB+Dv5EPLqnJIAgYw/9+WzeBiUYS8B+rU0pHq5NMQMvrZmevUROS4V2GAt0jEOn9viBzPLrZE+riTNd5Q==}
engines: {node: '>=18.0.0'}
'@smithy/hash-blob-browser@4.2.0': '@smithy/hash-blob-browser@4.2.0':
resolution: {integrity: sha512-MWmrRTPqVKpN8NmxmJPTeQuhewTt8Chf+waB38LXHZoA02+BeWYVQ9ViAwHjug8m7lQb1UWuGqp3JoGDOWvvuA==} resolution: {integrity: sha512-MWmrRTPqVKpN8NmxmJPTeQuhewTt8Chf+waB38LXHZoA02+BeWYVQ9ViAwHjug8m7lQb1UWuGqp3JoGDOWvvuA==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
@@ -1382,10 +1384,18 @@ packages:
resolution: {integrity: sha512-jFVjuQeV8TkxaRlcCNg0GFVgg98tscsmIrIwRFeC74TIUyLE3jmY9xgc1WXrPQYRjQNK3aRoaIk6fhFRGOIoGw==} resolution: {integrity: sha512-jFVjuQeV8TkxaRlcCNg0GFVgg98tscsmIrIwRFeC74TIUyLE3jmY9xgc1WXrPQYRjQNK3aRoaIk6fhFRGOIoGw==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/middleware-endpoint@4.3.1':
resolution: {integrity: sha512-JtM4SjEgImLEJVXdsbvWHYiJ9dtuKE8bqLlvkvGi96LbejDL6qnVpVxEFUximFodoQbg0Gnkyff9EKUhFhVJFw==}
engines: {node: '>=18.0.0'}
'@smithy/middleware-retry@4.4.0': '@smithy/middleware-retry@4.4.0':
resolution: {integrity: sha512-yaVBR0vQnOnzex45zZ8ZrPzUnX73eUC8kVFaAAbn04+6V7lPtxn56vZEBBAhgS/eqD6Zm86o6sJs6FuQVoX5qg==} resolution: {integrity: sha512-yaVBR0vQnOnzex45zZ8ZrPzUnX73eUC8kVFaAAbn04+6V7lPtxn56vZEBBAhgS/eqD6Zm86o6sJs6FuQVoX5qg==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/middleware-retry@4.4.1':
resolution: {integrity: sha512-wXxS4ex8cJJteL0PPQmWYkNi9QKDWZIpsndr0wZI2EL+pSSvA/qqxXU60gBOJoIc2YgtZSWY/PE86qhKCCKP1w==}
engines: {node: '>=18.0.0'}
'@smithy/middleware-serde@4.2.0': '@smithy/middleware-serde@4.2.0':
resolution: {integrity: sha512-rpTQ7D65/EAbC6VydXlxjvbifTf4IH+sADKg6JmAvhkflJO2NvDeyU9qsWUNBelJiQFcXKejUHWRSdmpJmEmiw==} resolution: {integrity: sha512-rpTQ7D65/EAbC6VydXlxjvbifTf4IH+sADKg6JmAvhkflJO2NvDeyU9qsWUNBelJiQFcXKejUHWRSdmpJmEmiw==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
@@ -1434,6 +1444,10 @@ packages:
resolution: {integrity: sha512-3BDx/aCCPf+kkinYf5QQhdQ9UAGihgOVqI3QO5xQfSaIWvUE4KYLtiGRWsNe1SR7ijXC0QEPqofVp5Sb0zC8xQ==} resolution: {integrity: sha512-3BDx/aCCPf+kkinYf5QQhdQ9UAGihgOVqI3QO5xQfSaIWvUE4KYLtiGRWsNe1SR7ijXC0QEPqofVp5Sb0zC8xQ==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/smithy-client@4.7.1':
resolution: {integrity: sha512-WXVbiyNf/WOS/RHUoFMkJ6leEVpln5ojCjNBnzoZeMsnCg3A0BRhLK3WYc4V7PmYcYPZh9IYzzAg9XcNSzYxYQ==}
engines: {node: '>=18.0.0'}
'@smithy/types@4.6.0': '@smithy/types@4.6.0':
resolution: {integrity: sha512-4lI9C8NzRPOv66FaY1LL1O/0v0aLVrq/mXP/keUa9mJOApEeae43LsLd2kZRUJw91gxOQfLIrV3OvqPgWz1YsA==} resolution: {integrity: sha512-4lI9C8NzRPOv66FaY1LL1O/0v0aLVrq/mXP/keUa9mJOApEeae43LsLd2kZRUJw91gxOQfLIrV3OvqPgWz1YsA==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
@@ -1446,6 +1460,10 @@ packages:
resolution: {integrity: sha512-+erInz8WDv5KPe7xCsJCp+1WCjSbah9gWcmUXc9NqmhyPx59tf7jqFz+za1tRG1Y5KM1Cy1rWCcGypylFp4mvA==} resolution: {integrity: sha512-+erInz8WDv5KPe7xCsJCp+1WCjSbah9gWcmUXc9NqmhyPx59tf7jqFz+za1tRG1Y5KM1Cy1rWCcGypylFp4mvA==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/util-base64@4.3.0':
resolution: {integrity: sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==}
engines: {node: '>=18.0.0'}
'@smithy/util-body-length-browser@4.2.0': '@smithy/util-body-length-browser@4.2.0':
resolution: {integrity: sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==} resolution: {integrity: sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
@@ -1454,6 +1472,10 @@ packages:
resolution: {integrity: sha512-U8q1WsSZFjXijlD7a4wsDQOvOwV+72iHSfq1q7VD+V75xP/pdtm0WIGuaFJ3gcADDOKj2MIBn4+zisi140HEnQ==} resolution: {integrity: sha512-U8q1WsSZFjXijlD7a4wsDQOvOwV+72iHSfq1q7VD+V75xP/pdtm0WIGuaFJ3gcADDOKj2MIBn4+zisi140HEnQ==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/util-body-length-node@4.2.1':
resolution: {integrity: sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==}
engines: {node: '>=18.0.0'}
'@smithy/util-buffer-from@2.2.0': '@smithy/util-buffer-from@2.2.0':
resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
@@ -1470,10 +1492,18 @@ packages:
resolution: {integrity: sha512-qzHp7ZDk1Ba4LDwQVCNp90xPGqSu7kmL7y5toBpccuhi3AH7dcVBIT/pUxYcInK4jOy6FikrcTGq5wxcka8UaQ==} resolution: {integrity: sha512-qzHp7ZDk1Ba4LDwQVCNp90xPGqSu7kmL7y5toBpccuhi3AH7dcVBIT/pUxYcInK4jOy6FikrcTGq5wxcka8UaQ==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/util-defaults-mode-browser@4.3.0':
resolution: {integrity: sha512-H4MAj8j8Yp19Mr7vVtGgi7noJjvjJbsKQJkvNnLlrIFduRFT5jq5Eri1k838YW7rN2g5FTnXpz5ktKVr1KVgPQ==}
engines: {node: '>=18.0.0'}
'@smithy/util-defaults-mode-node@4.2.0': '@smithy/util-defaults-mode-node@4.2.0':
resolution: {integrity: sha512-FxUHS3WXgx3bTWR6yQHNHHkQHZm/XKIi/CchTnKvBulN6obWpcbzJ6lDToXn+Wp0QlVKd7uYAz2/CTw1j7m+Kg==} resolution: {integrity: sha512-FxUHS3WXgx3bTWR6yQHNHHkQHZm/XKIi/CchTnKvBulN6obWpcbzJ6lDToXn+Wp0QlVKd7uYAz2/CTw1j7m+Kg==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/util-defaults-mode-node@4.2.1':
resolution: {integrity: sha512-PuDcgx7/qKEMzV1QFHJ7E4/MMeEjaA7+zS5UNcHCLPvvn59AeZQ0DSDGMpqC2xecfa/1cNGm4l8Ec/VxCuY7Ug==}
engines: {node: '>=18.0.0'}
'@smithy/util-endpoints@3.2.0': '@smithy/util-endpoints@3.2.0':
resolution: {integrity: sha512-TXeCn22D56vvWr/5xPqALc9oO+LN+QpFjrSM7peG/ckqEPoI3zaKZFp+bFwfmiHhn5MGWPaLCqDOJPPIixk9Wg==} resolution: {integrity: sha512-TXeCn22D56vvWr/5xPqALc9oO+LN+QpFjrSM7peG/ckqEPoI3zaKZFp+bFwfmiHhn5MGWPaLCqDOJPPIixk9Wg==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
@@ -1494,6 +1524,10 @@ packages:
resolution: {integrity: sha512-vtO7ktbixEcrVzMRmpQDnw/Ehr9UWjBvSJ9fyAbadKkC4w5Cm/4lMO8cHz8Ysb8uflvQUNRcuux/oNHKPXkffg==} resolution: {integrity: sha512-vtO7ktbixEcrVzMRmpQDnw/Ehr9UWjBvSJ9fyAbadKkC4w5Cm/4lMO8cHz8Ysb8uflvQUNRcuux/oNHKPXkffg==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/util-stream@4.5.0':
resolution: {integrity: sha512-0TD5M5HCGu5diEvZ/O/WquSjhJPasqv7trjoqHyWjNh/FBeBl7a0ztl9uFMOsauYtRfd8jvpzIAQhDHbx+nvZw==}
engines: {node: '>=18.0.0'}
'@smithy/util-uri-escape@4.2.0': '@smithy/util-uri-escape@4.2.0':
resolution: {integrity: sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==} resolution: {integrity: sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
@@ -1537,12 +1571,6 @@ packages:
'@tootallnate/quickjs-emscripten@0.23.0': '@tootallnate/quickjs-emscripten@0.23.0':
resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==}
'@tryghost/admin-api@1.14.0':
resolution: {integrity: sha512-FKAs4uHYhgqAXqRHdGNvODq60vK0H9FK2f26ib50dKsoSwB/vTspXxjcmqlPLyZfeQpOnaRlFK+ykpriYCOuTg==}
'@tryghost/content-api@1.12.0':
resolution: {integrity: sha512-rU9yrBHlVIohOLSpC9PeH9evMeNCk4OKGvI0VEwNwuwWlsQIpBn3o79DzleEN0tbMynlObYAG0IJ/EGkIumtJA==}
'@tsclass/tsclass@3.0.48': '@tsclass/tsclass@3.0.48':
resolution: {integrity: sha512-hC65UvDlp9qvsl6OcIZXz0JNiWZ0gyzsTzbXpg215sGxopgbkOLCr6E0s4qCTnweYm95gt2AdY95uP7M7kExaQ==} resolution: {integrity: sha512-hC65UvDlp9qvsl6OcIZXz0JNiWZ0gyzsTzbXpg215sGxopgbkOLCr6E0s4qCTnweYm95gt2AdY95uP7M7kExaQ==}
@@ -1920,8 +1948,16 @@ packages:
bare-events@2.7.0: bare-events@2.7.0:
resolution: {integrity: sha512-b3N5eTW1g7vXkw+0CXh/HazGTcO5KYuu/RCNaJbDMPI6LHDi+7qe8EmxKUVe1sUbY2KZOVZFyj62x0OEz9qyAA==} resolution: {integrity: sha512-b3N5eTW1g7vXkw+0CXh/HazGTcO5KYuu/RCNaJbDMPI6LHDi+7qe8EmxKUVe1sUbY2KZOVZFyj62x0OEz9qyAA==}
bare-fs@4.4.5: bare-events@2.8.0:
resolution: {integrity: sha512-TCtu93KGLu6/aiGWzMr12TmSRS6nKdfhAnzTQRbXoSWxkbb9eRd53jQ51jG7g1gYjjtto3hbBrrhzg6djcgiKg==} resolution: {integrity: sha512-AOhh6Bg5QmFIXdViHbMc2tLDsBIRxdkIaIddPslJF9Z5De3APBScuqGP2uThXnIpqFrgoxMNC6km7uXNIMLHXA==}
peerDependencies:
bare-abort-controller: '*'
peerDependenciesMeta:
bare-abort-controller:
optional: true
bare-fs@4.4.10:
resolution: {integrity: sha512-arqVF+xX/rJHwrONZaSPhlzleT2gXwVs9rsAe1p1mIVwWZI2A76/raio+KwwxfWMO8oV9Wo90EaUkS2QwVmy4w==}
engines: {bare: '>=1.16.0'} engines: {bare: '>=1.16.0'}
peerDependencies: peerDependencies:
bare-buffer: '*' bare-buffer: '*'
@@ -1997,9 +2033,6 @@ packages:
buffer-crc32@0.2.13: buffer-crc32@0.2.13:
resolution: {integrity: sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=} resolution: {integrity: sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=}
buffer-equal-constant-time@1.0.1:
resolution: {integrity: sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=}
buffer-json@2.0.0: buffer-json@2.0.0:
resolution: {integrity: sha512-+jjPFVqyfF1esi9fvfUs3NqM0pH1ziZ36VP4hmA/y/Ssfo/5w5xHKfTw9BwQjoJ1w/oVtpLomqwUHKdefGyuHw==} resolution: {integrity: sha512-+jjPFVqyfF1esi9fvfUs3NqM0pH1ziZ36VP4hmA/y/Ssfo/5w5xHKfTw9BwQjoJ1w/oVtpLomqwUHKdefGyuHw==}
@@ -2358,9 +2391,6 @@ packages:
eastasianwidth@0.2.0: eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
ecdsa-sig-formatter@1.0.11:
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
ee-first@1.1.1: ee-first@1.1.1:
resolution: {integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=} resolution: {integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=}
@@ -2523,8 +2553,8 @@ packages:
resolution: {integrity: sha512-cB507r5T3D55DfclY01GLkninZLfU7HXV/mhVRTnTRm5k2u+fY7Fof2dBkr80p5t7G7dlA/G5dI87QiMdPpMCQ==} resolution: {integrity: sha512-cB507r5T3D55DfclY01GLkninZLfU7HXV/mhVRTnTRm5k2u+fY7Fof2dBkr80p5t7G7dlA/G5dI87QiMdPpMCQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
fake-indexeddb@6.2.2: fake-indexeddb@6.2.3:
resolution: {integrity: sha512-SGbf7fzjeHz3+12NO1dYigcYn4ivviaeULV5yY5rdGihBvvgwMds4r4UBbNIUMwkze57KTDm32rq3j1Az8mzEw==} resolution: {integrity: sha512-idzJXFtDIHNShFZ9ssS8IdsRgAP0t9zwWvSdCKsWK2dgh2xcXA6/2Oteaxar5GJqmwzZXCrKRO6F5IEiR4yJzw==}
engines: {node: '>=18'} engines: {node: '>=18'}
fast-deep-equal@3.1.3: fast-deep-equal@3.1.3:
@@ -3066,16 +3096,6 @@ packages:
jsonfile@6.2.0: jsonfile@6.2.0:
resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
jsonwebtoken@9.0.2:
resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
engines: {node: '>=12', npm: '>=6'}
jwa@1.4.2:
resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==}
jws@3.2.2:
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
keygrip@1.1.0: keygrip@1.1.0:
resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==} resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@@ -3170,36 +3190,15 @@ packages:
lodash.clonedeep@4.5.0: lodash.clonedeep@4.5.0:
resolution: {integrity: sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=} resolution: {integrity: sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=}
lodash.includes@4.3.0:
resolution: {integrity: sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=}
lodash.isarguments@3.1.0: lodash.isarguments@3.1.0:
resolution: {integrity: sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=} resolution: {integrity: sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=}
lodash.isarray@3.0.4: lodash.isarray@3.0.4:
resolution: {integrity: sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=} resolution: {integrity: sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=}
lodash.isboolean@3.0.3:
resolution: {integrity: sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=}
lodash.isinteger@4.0.4:
resolution: {integrity: sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=}
lodash.isnumber@3.0.3:
resolution: {integrity: sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=}
lodash.isplainobject@4.0.6:
resolution: {integrity: sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=}
lodash.isstring@4.0.1:
resolution: {integrity: sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=}
lodash.keys@3.1.2: lodash.keys@3.1.2:
resolution: {integrity: sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=} resolution: {integrity: sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=}
lodash.once@4.1.1:
resolution: {integrity: sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=}
lodash.restparam@3.6.1: lodash.restparam@3.6.1:
resolution: {integrity: sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=} resolution: {integrity: sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=}
@@ -3797,12 +3796,12 @@ packages:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'} engines: {node: '>=6'}
puppeteer-core@24.23.0: puppeteer-core@24.24.0:
resolution: {integrity: sha512-yl25C59gb14sOdIiSnJ08XiPP+O2RjuyZmEG+RjYmCXO7au0jcLf7fRiyii96dXGUBW7Zwei/mVKfxMx/POeFw==} resolution: {integrity: sha512-RR5AeQ6dIbSepDe9PTtfgK1fgD7TuA9qqyGxPbFCyGfvfkbR7MiqNYdE7AhbTaFIqG3hFBtWwbVKVZF8oEqj7Q==}
engines: {node: '>=18'} engines: {node: '>=18'}
puppeteer@24.23.0: puppeteer@24.24.0:
resolution: {integrity: sha512-BVR1Lg8sJGKXY79JARdIssFWK2F6e1j+RyuJP66w4CUmpaXjENicmA3nNpUXA8lcTdDjAndtP+oNdni3T/qQqA==} resolution: {integrity: sha512-jRn6T8rSrQZXIplXICpH2zYJ2XrIFY7Ug0+TxRTuwY8ZTL7+MKDvFH0aLG7Xx3ts4twzxIKZmiYo+qg7whNpZw==}
engines: {node: '>=18'} engines: {node: '>=18'}
hasBin: true hasBin: true
@@ -3978,6 +3977,11 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
semver@7.7.3:
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
engines: {node: '>=10'}
hasBin: true
send@1.2.0: send@1.2.0:
resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==}
engines: {node: '>= 18'} engines: {node: '>= 18'}
@@ -4526,7 +4530,7 @@ snapshots:
'@api.global/typedrequest': 3.1.10 '@api.global/typedrequest': 3.1.10
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
'@api.global/typedsocket': 3.0.1 '@api.global/typedsocket': 3.0.1
'@cloudflare/workers-types': 4.20251004.0 '@cloudflare/workers-types': 4.20251008.0
'@design.estate/dees-comms': 1.0.27 '@design.estate/dees-comms': 1.0.27
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
'@push.rocks/smartchok': 1.1.1 '@push.rocks/smartchok': 1.1.1
@@ -4649,26 +4653,26 @@ snapshots:
'@aws-sdk/util-user-agent-browser': 3.734.0 '@aws-sdk/util-user-agent-browser': 3.734.0
'@aws-sdk/util-user-agent-node': 3.734.0 '@aws-sdk/util-user-agent-node': 3.734.0
'@smithy/config-resolver': 4.3.0 '@smithy/config-resolver': 4.3.0
'@smithy/core': 3.14.0 '@smithy/core': 3.15.0
'@smithy/fetch-http-handler': 5.3.0 '@smithy/fetch-http-handler': 5.3.1
'@smithy/hash-node': 4.2.0 '@smithy/hash-node': 4.2.0
'@smithy/invalid-dependency': 4.2.0 '@smithy/invalid-dependency': 4.2.0
'@smithy/middleware-content-length': 4.2.0 '@smithy/middleware-content-length': 4.2.0
'@smithy/middleware-endpoint': 4.3.0 '@smithy/middleware-endpoint': 4.3.1
'@smithy/middleware-retry': 4.4.0 '@smithy/middleware-retry': 4.4.1
'@smithy/middleware-serde': 4.2.0 '@smithy/middleware-serde': 4.2.0
'@smithy/middleware-stack': 4.2.0 '@smithy/middleware-stack': 4.2.0
'@smithy/node-config-provider': 4.3.0 '@smithy/node-config-provider': 4.3.0
'@smithy/node-http-handler': 4.3.0 '@smithy/node-http-handler': 4.3.0
'@smithy/protocol-http': 5.3.0 '@smithy/protocol-http': 5.3.0
'@smithy/smithy-client': 4.7.0 '@smithy/smithy-client': 4.7.1
'@smithy/types': 4.6.0 '@smithy/types': 4.6.0
'@smithy/url-parser': 4.2.0 '@smithy/url-parser': 4.2.0
'@smithy/util-base64': 4.2.0 '@smithy/util-base64': 4.3.0
'@smithy/util-body-length-browser': 4.2.0 '@smithy/util-body-length-browser': 4.2.0
'@smithy/util-body-length-node': 4.2.0 '@smithy/util-body-length-node': 4.2.1
'@smithy/util-defaults-mode-browser': 4.2.0 '@smithy/util-defaults-mode-browser': 4.3.0
'@smithy/util-defaults-mode-node': 4.2.0 '@smithy/util-defaults-mode-node': 4.2.1
'@smithy/util-endpoints': 3.2.0 '@smithy/util-endpoints': 3.2.0
'@smithy/util-middleware': 4.2.0 '@smithy/util-middleware': 4.2.0
'@smithy/util-retry': 4.2.0 '@smithy/util-retry': 4.2.0
@@ -4755,26 +4759,26 @@ snapshots:
'@aws-sdk/util-user-agent-browser': 3.734.0 '@aws-sdk/util-user-agent-browser': 3.734.0
'@aws-sdk/util-user-agent-node': 3.734.0 '@aws-sdk/util-user-agent-node': 3.734.0
'@smithy/config-resolver': 4.3.0 '@smithy/config-resolver': 4.3.0
'@smithy/core': 3.14.0 '@smithy/core': 3.15.0
'@smithy/fetch-http-handler': 5.3.0 '@smithy/fetch-http-handler': 5.3.1
'@smithy/hash-node': 4.2.0 '@smithy/hash-node': 4.2.0
'@smithy/invalid-dependency': 4.2.0 '@smithy/invalid-dependency': 4.2.0
'@smithy/middleware-content-length': 4.2.0 '@smithy/middleware-content-length': 4.2.0
'@smithy/middleware-endpoint': 4.3.0 '@smithy/middleware-endpoint': 4.3.1
'@smithy/middleware-retry': 4.4.0 '@smithy/middleware-retry': 4.4.1
'@smithy/middleware-serde': 4.2.0 '@smithy/middleware-serde': 4.2.0
'@smithy/middleware-stack': 4.2.0 '@smithy/middleware-stack': 4.2.0
'@smithy/node-config-provider': 4.3.0 '@smithy/node-config-provider': 4.3.0
'@smithy/node-http-handler': 4.3.0 '@smithy/node-http-handler': 4.3.0
'@smithy/protocol-http': 5.3.0 '@smithy/protocol-http': 5.3.0
'@smithy/smithy-client': 4.7.0 '@smithy/smithy-client': 4.7.1
'@smithy/types': 4.6.0 '@smithy/types': 4.6.0
'@smithy/url-parser': 4.2.0 '@smithy/url-parser': 4.2.0
'@smithy/util-base64': 4.2.0 '@smithy/util-base64': 4.3.0
'@smithy/util-body-length-browser': 4.2.0 '@smithy/util-body-length-browser': 4.2.0
'@smithy/util-body-length-node': 4.2.0 '@smithy/util-body-length-node': 4.2.1
'@smithy/util-defaults-mode-browser': 4.2.0 '@smithy/util-defaults-mode-browser': 4.3.0
'@smithy/util-defaults-mode-node': 4.2.0 '@smithy/util-defaults-mode-node': 4.2.1
'@smithy/util-endpoints': 3.2.0 '@smithy/util-endpoints': 3.2.0
'@smithy/util-middleware': 4.2.0 '@smithy/util-middleware': 4.2.0
'@smithy/util-retry': 4.2.0 '@smithy/util-retry': 4.2.0
@@ -4830,12 +4834,12 @@ snapshots:
'@aws-sdk/core@3.734.0': '@aws-sdk/core@3.734.0':
dependencies: dependencies:
'@aws-sdk/types': 3.734.0 '@aws-sdk/types': 3.734.0
'@smithy/core': 3.14.0 '@smithy/core': 3.15.0
'@smithy/node-config-provider': 4.3.0 '@smithy/node-config-provider': 4.3.0
'@smithy/property-provider': 4.2.0 '@smithy/property-provider': 4.2.0
'@smithy/protocol-http': 5.3.0 '@smithy/protocol-http': 5.3.0
'@smithy/signature-v4': 5.3.0 '@smithy/signature-v4': 5.3.0
'@smithy/smithy-client': 4.7.0 '@smithy/smithy-client': 4.7.1
'@smithy/types': 4.6.0 '@smithy/types': 4.6.0
'@smithy/util-middleware': 4.2.0 '@smithy/util-middleware': 4.2.0
fast-xml-parser: 4.4.1 fast-xml-parser: 4.4.1
@@ -4890,13 +4894,13 @@ snapshots:
dependencies: dependencies:
'@aws-sdk/core': 3.734.0 '@aws-sdk/core': 3.734.0
'@aws-sdk/types': 3.734.0 '@aws-sdk/types': 3.734.0
'@smithy/fetch-http-handler': 5.3.0 '@smithy/fetch-http-handler': 5.3.1
'@smithy/node-http-handler': 4.3.0 '@smithy/node-http-handler': 4.3.0
'@smithy/property-provider': 4.2.0 '@smithy/property-provider': 4.2.0
'@smithy/protocol-http': 5.3.0 '@smithy/protocol-http': 5.3.0
'@smithy/smithy-client': 4.7.0 '@smithy/smithy-client': 4.7.1
'@smithy/types': 4.6.0 '@smithy/types': 4.6.0
'@smithy/util-stream': 4.4.0 '@smithy/util-stream': 4.5.0
tslib: 2.8.1 tslib: 2.8.1
optional: true optional: true
@@ -5069,7 +5073,7 @@ snapshots:
'@aws-sdk/credential-provider-web-identity': 3.734.0 '@aws-sdk/credential-provider-web-identity': 3.734.0
'@aws-sdk/nested-clients': 3.734.0 '@aws-sdk/nested-clients': 3.734.0
'@aws-sdk/types': 3.734.0 '@aws-sdk/types': 3.734.0
'@smithy/core': 3.14.0 '@smithy/core': 3.15.0
'@smithy/credential-provider-imds': 4.2.0 '@smithy/credential-provider-imds': 4.2.0
'@smithy/property-provider': 4.2.0 '@smithy/property-provider': 4.2.0
'@smithy/types': 4.6.0 '@smithy/types': 4.6.0
@@ -5189,7 +5193,7 @@ snapshots:
'@aws-sdk/core': 3.734.0 '@aws-sdk/core': 3.734.0
'@aws-sdk/types': 3.734.0 '@aws-sdk/types': 3.734.0
'@aws-sdk/util-endpoints': 3.734.0 '@aws-sdk/util-endpoints': 3.734.0
'@smithy/core': 3.14.0 '@smithy/core': 3.15.0
'@smithy/protocol-http': 5.3.0 '@smithy/protocol-http': 5.3.0
'@smithy/types': 4.6.0 '@smithy/types': 4.6.0
tslib: 2.8.1 tslib: 2.8.1
@@ -5220,26 +5224,26 @@ snapshots:
'@aws-sdk/util-user-agent-browser': 3.734.0 '@aws-sdk/util-user-agent-browser': 3.734.0
'@aws-sdk/util-user-agent-node': 3.734.0 '@aws-sdk/util-user-agent-node': 3.734.0
'@smithy/config-resolver': 4.3.0 '@smithy/config-resolver': 4.3.0
'@smithy/core': 3.14.0 '@smithy/core': 3.15.0
'@smithy/fetch-http-handler': 5.3.0 '@smithy/fetch-http-handler': 5.3.1
'@smithy/hash-node': 4.2.0 '@smithy/hash-node': 4.2.0
'@smithy/invalid-dependency': 4.2.0 '@smithy/invalid-dependency': 4.2.0
'@smithy/middleware-content-length': 4.2.0 '@smithy/middleware-content-length': 4.2.0
'@smithy/middleware-endpoint': 4.3.0 '@smithy/middleware-endpoint': 4.3.1
'@smithy/middleware-retry': 4.4.0 '@smithy/middleware-retry': 4.4.1
'@smithy/middleware-serde': 4.2.0 '@smithy/middleware-serde': 4.2.0
'@smithy/middleware-stack': 4.2.0 '@smithy/middleware-stack': 4.2.0
'@smithy/node-config-provider': 4.3.0 '@smithy/node-config-provider': 4.3.0
'@smithy/node-http-handler': 4.3.0 '@smithy/node-http-handler': 4.3.0
'@smithy/protocol-http': 5.3.0 '@smithy/protocol-http': 5.3.0
'@smithy/smithy-client': 4.7.0 '@smithy/smithy-client': 4.7.1
'@smithy/types': 4.6.0 '@smithy/types': 4.6.0
'@smithy/url-parser': 4.2.0 '@smithy/url-parser': 4.2.0
'@smithy/util-base64': 4.2.0 '@smithy/util-base64': 4.3.0
'@smithy/util-body-length-browser': 4.2.0 '@smithy/util-body-length-browser': 4.2.0
'@smithy/util-body-length-node': 4.2.0 '@smithy/util-body-length-node': 4.2.1
'@smithy/util-defaults-mode-browser': 4.2.0 '@smithy/util-defaults-mode-browser': 4.3.0
'@smithy/util-defaults-mode-node': 4.2.0 '@smithy/util-defaults-mode-node': 4.2.1
'@smithy/util-endpoints': 3.2.0 '@smithy/util-endpoints': 3.2.0
'@smithy/util-middleware': 4.2.0 '@smithy/util-middleware': 4.2.0
'@smithy/util-retry': 4.2.0 '@smithy/util-retry': 4.2.0
@@ -5433,7 +5437,7 @@ snapshots:
'@borewit/text-codec@0.1.1': {} '@borewit/text-codec@0.1.1': {}
'@cloudflare/workers-types@4.20251004.0': {} '@cloudflare/workers-types@4.20251008.0': {}
'@colors/colors@1.6.0': {} '@colors/colors@1.6.0': {}
@@ -5727,7 +5731,7 @@ snapshots:
'@push.rocks/smartshell': 3.2.2 '@push.rocks/smartshell': 3.2.2
tsx: 4.19.2 tsx: 4.19.2
'@git.zone/tstest@2.3.8(@aws-sdk/credential-providers@3.734.0)(socks@2.8.7)(typescript@5.9.2)': '@git.zone/tstest@2.4.2(@aws-sdk/credential-providers@3.734.0)(socks@2.8.7)(typescript@5.9.2)':
dependencies: dependencies:
'@api.global/typedserver': 3.0.79 '@api.global/typedserver': 3.0.79
'@git.zone/tsbundle': 2.5.1 '@git.zone/tsbundle': 2.5.1
@@ -5760,6 +5764,7 @@ snapshots:
- '@nuxt/kit' - '@nuxt/kit'
- '@swc/helpers' - '@swc/helpers'
- aws-crt - aws-crt
- bare-abort-controller
- bare-buffer - bare-buffer
- bufferutil - bufferutil
- gcp-metadata - gcp-metadata
@@ -6044,16 +6049,17 @@ snapshots:
'@pnpm/network.ca-file': 1.0.2 '@pnpm/network.ca-file': 1.0.2
config-chain: 1.1.13 config-chain: 1.1.13
'@puppeteer/browsers@2.10.10': '@puppeteer/browsers@2.10.11':
dependencies: dependencies:
debug: 4.4.3 debug: 4.4.3
extract-zip: 2.0.1 extract-zip: 2.0.1
progress: 2.0.3 progress: 2.0.3
proxy-agent: 6.5.0 proxy-agent: 6.5.0
semver: 7.7.2 semver: 7.7.3
tar-fs: 3.1.1 tar-fs: 3.1.1
yargs: 17.7.2 yargs: 17.7.2
transitivePeerDependencies: transitivePeerDependencies:
- bare-abort-controller
- bare-buffer - bare-buffer
- react-native-b4a - react-native-b4a
- supports-color - supports-color
@@ -6171,6 +6177,7 @@ snapshots:
'@push.rocks/smartpuppeteer': 2.0.5(typescript@5.9.2) '@push.rocks/smartpuppeteer': 2.0.5(typescript@5.9.2)
'@push.rocks/smartunique': 3.0.9 '@push.rocks/smartunique': 3.0.9
transitivePeerDependencies: transitivePeerDependencies:
- bare-abort-controller
- bare-buffer - bare-buffer
- bufferutil - bufferutil
- react-native-b4a - react-native-b4a
@@ -6501,7 +6508,7 @@ snapshots:
'@design.estate/dees-element': 2.1.2 '@design.estate/dees-element': 2.1.2
'@happy-dom/global-registrator': 15.11.7 '@happy-dom/global-registrator': 15.11.7
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
fake-indexeddb: 6.2.2 fake-indexeddb: 6.2.3
transitivePeerDependencies: transitivePeerDependencies:
- '@nuxt/kit' - '@nuxt/kit'
- react - react
@@ -6539,6 +6546,7 @@ snapshots:
pdf-lib: 1.17.1 pdf-lib: 1.17.1
pdf2json: 3.2.0 pdf2json: 3.2.0
transitivePeerDependencies: transitivePeerDependencies:
- bare-abort-controller
- bare-buffer - bare-buffer
- bufferutil - bufferutil
- react-native-b4a - react-native-b4a
@@ -6559,9 +6567,10 @@ snapshots:
dependencies: dependencies:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartshell': 3.3.0 '@push.rocks/smartshell': 3.3.0
puppeteer: 24.23.0(typescript@5.9.2) puppeteer: 24.24.0(typescript@5.9.2)
tree-kill: 1.2.2 tree-kill: 1.2.2
transitivePeerDependencies: transitivePeerDependencies:
- bare-abort-controller
- bare-buffer - bare-buffer
- bufferutil - bufferutil
- react-native-b4a - react-native-b4a
@@ -7009,6 +7018,20 @@ snapshots:
'@smithy/uuid': 1.1.0 '@smithy/uuid': 1.1.0
tslib: 2.8.1 tslib: 2.8.1
'@smithy/core@3.15.0':
dependencies:
'@smithy/middleware-serde': 4.2.0
'@smithy/protocol-http': 5.3.0
'@smithy/types': 4.6.0
'@smithy/util-base64': 4.3.0
'@smithy/util-body-length-browser': 4.2.0
'@smithy/util-middleware': 4.2.0
'@smithy/util-stream': 4.5.0
'@smithy/util-utf8': 4.2.0
'@smithy/uuid': 1.1.0
tslib: 2.8.1
optional: true
'@smithy/credential-provider-imds@4.2.0': '@smithy/credential-provider-imds@4.2.0':
dependencies: dependencies:
'@smithy/node-config-provider': 4.3.0 '@smithy/node-config-provider': 4.3.0
@@ -7055,6 +7078,15 @@ snapshots:
'@smithy/util-base64': 4.2.0 '@smithy/util-base64': 4.2.0
tslib: 2.8.1 tslib: 2.8.1
'@smithy/fetch-http-handler@5.3.1':
dependencies:
'@smithy/protocol-http': 5.3.0
'@smithy/querystring-builder': 4.2.0
'@smithy/types': 4.6.0
'@smithy/util-base64': 4.3.0
tslib: 2.8.1
optional: true
'@smithy/hash-blob-browser@4.2.0': '@smithy/hash-blob-browser@4.2.0':
dependencies: dependencies:
'@smithy/chunked-blob-reader': 5.2.0 '@smithy/chunked-blob-reader': 5.2.0
@@ -7111,6 +7143,18 @@ snapshots:
'@smithy/util-middleware': 4.2.0 '@smithy/util-middleware': 4.2.0
tslib: 2.8.1 tslib: 2.8.1
'@smithy/middleware-endpoint@4.3.1':
dependencies:
'@smithy/core': 3.15.0
'@smithy/middleware-serde': 4.2.0
'@smithy/node-config-provider': 4.3.0
'@smithy/shared-ini-file-loader': 4.3.0
'@smithy/types': 4.6.0
'@smithy/url-parser': 4.2.0
'@smithy/util-middleware': 4.2.0
tslib: 2.8.1
optional: true
'@smithy/middleware-retry@4.4.0': '@smithy/middleware-retry@4.4.0':
dependencies: dependencies:
'@smithy/node-config-provider': 4.3.0 '@smithy/node-config-provider': 4.3.0
@@ -7123,6 +7167,19 @@ snapshots:
'@smithy/uuid': 1.1.0 '@smithy/uuid': 1.1.0
tslib: 2.8.1 tslib: 2.8.1
'@smithy/middleware-retry@4.4.1':
dependencies:
'@smithy/node-config-provider': 4.3.0
'@smithy/protocol-http': 5.3.0
'@smithy/service-error-classification': 4.2.0
'@smithy/smithy-client': 4.7.1
'@smithy/types': 4.6.0
'@smithy/util-middleware': 4.2.0
'@smithy/util-retry': 4.2.0
'@smithy/uuid': 1.1.0
tslib: 2.8.1
optional: true
'@smithy/middleware-serde@4.2.0': '@smithy/middleware-serde@4.2.0':
dependencies: dependencies:
'@smithy/protocol-http': 5.3.0 '@smithy/protocol-http': 5.3.0
@@ -7200,6 +7257,17 @@ snapshots:
'@smithy/util-stream': 4.4.0 '@smithy/util-stream': 4.4.0
tslib: 2.8.1 tslib: 2.8.1
'@smithy/smithy-client@4.7.1':
dependencies:
'@smithy/core': 3.15.0
'@smithy/middleware-endpoint': 4.3.1
'@smithy/middleware-stack': 4.2.0
'@smithy/protocol-http': 5.3.0
'@smithy/types': 4.6.0
'@smithy/util-stream': 4.5.0
tslib: 2.8.1
optional: true
'@smithy/types@4.6.0': '@smithy/types@4.6.0':
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
@@ -7216,6 +7284,13 @@ snapshots:
'@smithy/util-utf8': 4.2.0 '@smithy/util-utf8': 4.2.0
tslib: 2.8.1 tslib: 2.8.1
'@smithy/util-base64@4.3.0':
dependencies:
'@smithy/util-buffer-from': 4.2.0
'@smithy/util-utf8': 4.2.0
tslib: 2.8.1
optional: true
'@smithy/util-body-length-browser@4.2.0': '@smithy/util-body-length-browser@4.2.0':
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
@@ -7224,6 +7299,11 @@ snapshots:
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
'@smithy/util-body-length-node@4.2.1':
dependencies:
tslib: 2.8.1
optional: true
'@smithy/util-buffer-from@2.2.0': '@smithy/util-buffer-from@2.2.0':
dependencies: dependencies:
'@smithy/is-array-buffer': 2.2.0 '@smithy/is-array-buffer': 2.2.0
@@ -7246,6 +7326,14 @@ snapshots:
bowser: 2.12.1 bowser: 2.12.1
tslib: 2.8.1 tslib: 2.8.1
'@smithy/util-defaults-mode-browser@4.3.0':
dependencies:
'@smithy/property-provider': 4.2.0
'@smithy/smithy-client': 4.7.1
'@smithy/types': 4.6.0
tslib: 2.8.1
optional: true
'@smithy/util-defaults-mode-node@4.2.0': '@smithy/util-defaults-mode-node@4.2.0':
dependencies: dependencies:
'@smithy/config-resolver': 4.3.0 '@smithy/config-resolver': 4.3.0
@@ -7256,6 +7344,17 @@ snapshots:
'@smithy/types': 4.6.0 '@smithy/types': 4.6.0
tslib: 2.8.1 tslib: 2.8.1
'@smithy/util-defaults-mode-node@4.2.1':
dependencies:
'@smithy/config-resolver': 4.3.0
'@smithy/credential-provider-imds': 4.2.0
'@smithy/node-config-provider': 4.3.0
'@smithy/property-provider': 4.2.0
'@smithy/smithy-client': 4.7.1
'@smithy/types': 4.6.0
tslib: 2.8.1
optional: true
'@smithy/util-endpoints@3.2.0': '@smithy/util-endpoints@3.2.0':
dependencies: dependencies:
'@smithy/node-config-provider': 4.3.0 '@smithy/node-config-provider': 4.3.0
@@ -7288,6 +7387,18 @@ snapshots:
'@smithy/util-utf8': 4.2.0 '@smithy/util-utf8': 4.2.0
tslib: 2.8.1 tslib: 2.8.1
'@smithy/util-stream@4.5.0':
dependencies:
'@smithy/fetch-http-handler': 5.3.1
'@smithy/node-http-handler': 4.3.0
'@smithy/types': 4.6.0
'@smithy/util-base64': 4.3.0
'@smithy/util-buffer-from': 4.2.0
'@smithy/util-hex-encoding': 4.2.0
'@smithy/util-utf8': 4.2.0
tslib: 2.8.1
optional: true
'@smithy/util-uri-escape@4.2.0': '@smithy/util-uri-escape@4.2.0':
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
@@ -7337,20 +7448,6 @@ snapshots:
'@tootallnate/quickjs-emscripten@0.23.0': {} '@tootallnate/quickjs-emscripten@0.23.0': {}
'@tryghost/admin-api@1.14.0':
dependencies:
axios: 1.12.2(debug@4.4.3)
form-data: 4.0.4
jsonwebtoken: 9.0.2
transitivePeerDependencies:
- debug
'@tryghost/content-api@1.12.0':
dependencies:
axios: 1.12.2(debug@4.4.3)
transitivePeerDependencies:
- debug
'@tsclass/tsclass@3.0.48': '@tsclass/tsclass@3.0.48':
dependencies: dependencies:
type-fest: 2.19.0 type-fest: 2.19.0
@@ -7805,14 +7902,18 @@ snapshots:
bare-events@2.7.0: {} bare-events@2.7.0: {}
bare-fs@4.4.5: bare-events@2.8.0:
optional: true
bare-fs@4.4.10:
dependencies: dependencies:
bare-events: 2.7.0 bare-events: 2.8.0
bare-path: 3.0.0 bare-path: 3.0.0
bare-stream: 2.7.0(bare-events@2.7.0) bare-stream: 2.7.0(bare-events@2.8.0)
bare-url: 2.2.2 bare-url: 2.2.2
fast-fifo: 1.3.2 fast-fifo: 1.3.2
transitivePeerDependencies: transitivePeerDependencies:
- bare-abort-controller
- react-native-b4a - react-native-b4a
optional: true optional: true
@@ -7824,11 +7925,11 @@ snapshots:
bare-os: 3.6.2 bare-os: 3.6.2
optional: true optional: true
bare-stream@2.7.0(bare-events@2.7.0): bare-stream@2.7.0(bare-events@2.8.0):
dependencies: dependencies:
streamx: 2.23.0 streamx: 2.23.0
optionalDependencies: optionalDependencies:
bare-events: 2.7.0 bare-events: 2.8.0
transitivePeerDependencies: transitivePeerDependencies:
- react-native-b4a - react-native-b4a
optional: true optional: true
@@ -7892,8 +7993,6 @@ snapshots:
buffer-crc32@0.2.13: {} buffer-crc32@0.2.13: {}
buffer-equal-constant-time@1.0.1: {}
buffer-json@2.0.0: {} buffer-json@2.0.0: {}
buffer@6.0.3: buffer@6.0.3:
@@ -8216,10 +8315,6 @@ snapshots:
eastasianwidth@0.2.0: {} eastasianwidth@0.2.0: {}
ecdsa-sig-formatter@1.0.11:
dependencies:
safe-buffer: 5.2.1
ee-first@1.1.1: {} ee-first@1.1.1: {}
elliptic@6.6.1: elliptic@6.6.1:
@@ -8465,7 +8560,7 @@ snapshots:
fake-indexeddb@5.0.2: {} fake-indexeddb@5.0.2: {}
fake-indexeddb@6.2.2: {} fake-indexeddb@6.2.3: {}
fast-deep-equal@3.1.3: {} fast-deep-equal@3.1.3: {}
@@ -9090,30 +9185,6 @@ snapshots:
optionalDependencies: optionalDependencies:
graceful-fs: 4.2.11 graceful-fs: 4.2.11
jsonwebtoken@9.0.2:
dependencies:
jws: 3.2.2
lodash.includes: 4.3.0
lodash.isboolean: 3.0.3
lodash.isinteger: 4.0.4
lodash.isnumber: 3.0.3
lodash.isplainobject: 4.0.6
lodash.isstring: 4.0.1
lodash.once: 4.1.1
ms: 2.1.3
semver: 7.7.2
jwa@1.4.2:
dependencies:
buffer-equal-constant-time: 1.0.1
ecdsa-sig-formatter: 1.0.11
safe-buffer: 5.2.1
jws@3.2.2:
dependencies:
jwa: 1.4.2
safe-buffer: 5.2.1
keygrip@1.1.0: keygrip@1.1.0:
dependencies: dependencies:
tsscmp: 1.0.6 tsscmp: 1.0.6
@@ -9238,30 +9309,16 @@ snapshots:
lodash.clonedeep@4.5.0: {} lodash.clonedeep@4.5.0: {}
lodash.includes@4.3.0: {}
lodash.isarguments@3.1.0: {} lodash.isarguments@3.1.0: {}
lodash.isarray@3.0.4: {} lodash.isarray@3.0.4: {}
lodash.isboolean@3.0.3: {}
lodash.isinteger@4.0.4: {}
lodash.isnumber@3.0.3: {}
lodash.isplainobject@4.0.6: {}
lodash.isstring@4.0.1: {}
lodash.keys@3.1.2: lodash.keys@3.1.2:
dependencies: dependencies:
lodash._getnative: 3.9.1 lodash._getnative: 3.9.1
lodash.isarguments: 3.1.0 lodash.isarguments: 3.1.0
lodash.isarray: 3.0.4 lodash.isarray: 3.0.4
lodash.once@4.1.1: {}
lodash.restparam@3.6.1: {} lodash.restparam@3.6.1: {}
lodash@4.17.21: {} lodash@4.17.21: {}
@@ -10000,9 +10057,9 @@ snapshots:
punycode@2.3.1: {} punycode@2.3.1: {}
puppeteer-core@24.23.0: puppeteer-core@24.24.0:
dependencies: dependencies:
'@puppeteer/browsers': 2.10.10 '@puppeteer/browsers': 2.10.11
chromium-bidi: 9.1.0(devtools-protocol@0.0.1508733) chromium-bidi: 9.1.0(devtools-protocol@0.0.1508733)
debug: 4.4.3 debug: 4.4.3
devtools-protocol: 0.0.1508733 devtools-protocol: 0.0.1508733
@@ -10010,21 +10067,23 @@ snapshots:
webdriver-bidi-protocol: 0.3.6 webdriver-bidi-protocol: 0.3.6
ws: 8.18.3 ws: 8.18.3
transitivePeerDependencies: transitivePeerDependencies:
- bare-abort-controller
- bare-buffer - bare-buffer
- bufferutil - bufferutil
- react-native-b4a - react-native-b4a
- supports-color - supports-color
- utf-8-validate - utf-8-validate
puppeteer@24.23.0(typescript@5.9.2): puppeteer@24.24.0(typescript@5.9.2):
dependencies: dependencies:
'@puppeteer/browsers': 2.10.10 '@puppeteer/browsers': 2.10.11
chromium-bidi: 9.1.0(devtools-protocol@0.0.1508733) chromium-bidi: 9.1.0(devtools-protocol@0.0.1508733)
cosmiconfig: 9.0.0(typescript@5.9.2) cosmiconfig: 9.0.0(typescript@5.9.2)
devtools-protocol: 0.0.1508733 devtools-protocol: 0.0.1508733
puppeteer-core: 24.23.0 puppeteer-core: 24.24.0
typed-query-selector: 2.12.0 typed-query-selector: 2.12.0
transitivePeerDependencies: transitivePeerDependencies:
- bare-abort-controller
- bare-buffer - bare-buffer
- bufferutil - bufferutil
- react-native-b4a - react-native-b4a
@@ -10254,6 +10313,8 @@ snapshots:
semver@7.7.2: {} semver@7.7.2: {}
semver@7.7.3: {}
send@1.2.0: send@1.2.0:
dependencies: dependencies:
debug: 4.4.3 debug: 4.4.3
@@ -10508,9 +10569,10 @@ snapshots:
pump: 3.0.3 pump: 3.0.3
tar-stream: 3.1.7 tar-stream: 3.1.7
optionalDependencies: optionalDependencies:
bare-fs: 4.4.5 bare-fs: 4.4.10
bare-path: 3.0.0 bare-path: 3.0.0
transitivePeerDependencies: transitivePeerDependencies:
- bare-abort-controller
- bare-buffer - bare-buffer
- react-native-b4a - react-native-b4a

1068
readme.md

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,107 @@
import { expect, tap } from '@push.rocks/tapbundle';
import * as qenv from '@push.rocks/qenv';
const testQenv = new qenv.Qenv('./', './.nogit/');
import * as ghost from '../ts/index.js';
let testGhostInstance: ghost.Ghost;
tap.test('initialize Ghost instance', async () => {
testGhostInstance = new ghost.Ghost({
baseUrl: 'http://localhost:2368',
adminApiKey: await testQenv.getEnvVarOnDemand('ADMIN_APIKEY'),
contentApiKey: await testQenv.getEnvVarOnDemand('CONTENT_APIKEY'),
});
expect(testGhostInstance).toBeInstanceOf(ghost.Ghost);
});
tap.test('should get all authors', async () => {
const authors = await testGhostInstance.getAuthors();
expect(authors).toBeArray();
console.log(`Found ${authors.length} authors`);
if (authors.length > 0) {
expect(authors[0]).toBeInstanceOf(ghost.Author);
console.log(`First author: ${authors[0].getName()} (${authors[0].getSlug()})`);
}
});
tap.test('should get authors with limit', async () => {
const authors = await testGhostInstance.getAuthors({ limit: 2 });
expect(authors).toBeArray();
expect(authors.length).toBeLessThanOrEqual(2);
});
tap.test('should filter authors with minimatch pattern', async () => {
const authors = await testGhostInstance.getAuthors();
if (authors.length > 0) {
const firstAuthorSlug = authors[0].getSlug();
const pattern = `${firstAuthorSlug.charAt(0)}*`;
const filteredAuthors = await testGhostInstance.getAuthors({ filter: pattern });
expect(filteredAuthors).toBeArray();
console.log(`Filtered authors with pattern '${pattern}': found ${filteredAuthors.length}`);
filteredAuthors.forEach((author) => {
expect(author.getSlug()).toMatch(new RegExp(`^${firstAuthorSlug.charAt(0)}`));
});
}
});
tap.test('should get author by slug', async () => {
const authors = await testGhostInstance.getAuthors({ limit: 1 });
if (authors.length > 0) {
const author = await testGhostInstance.getAuthorBySlug(authors[0].getSlug());
expect(author).toBeInstanceOf(ghost.Author);
expect(author.getSlug()).toEqual(authors[0].getSlug());
console.log(`Got author by slug: ${author.getName()}`);
}
});
tap.test('should get author by ID', async () => {
const authors = await testGhostInstance.getAuthors({ limit: 1 });
if (authors.length > 0) {
const author = await testGhostInstance.getAuthorById(authors[0].getId());
expect(author).toBeInstanceOf(ghost.Author);
expect(author.getId()).toEqual(authors[0].getId());
}
});
tap.test('should access author methods', async () => {
const authors = await testGhostInstance.getAuthors({ limit: 1 });
if (authors.length > 0) {
const author = authors[0];
expect(author.getId()).toBeTruthy();
expect(author.getName()).toBeTruthy();
expect(author.getSlug()).toBeTruthy();
const json = author.toJson();
expect(json).toBeTruthy();
expect(json.id).toEqual(author.getId());
}
});
tap.test('should update author bio', async () => {
try {
const authors = await testGhostInstance.getAuthors({ limit: 1 });
if (authors.length > 0) {
const author = authors[0];
const originalBio = author.getBio();
const updatedAuthor = await author.update({
bio: 'Updated bio for testing'
});
expect(updatedAuthor).toBeInstanceOf(ghost.Author);
expect(updatedAuthor.getBio()).toEqual('Updated bio for testing');
console.log(`Updated author bio: ${updatedAuthor.getName()}`);
await updatedAuthor.update({
bio: originalBio
});
}
} catch (error: any) {
if (error.type === 'NotImplementedError') {
console.log('Author updates not supported in this Ghost version - skipping test');
} else {
throw error;
}
}
});
export default tap.start();

206
test/test.dates.node.ts Normal file
View File

@@ -0,0 +1,206 @@
import { expect, tap } from '@push.rocks/tapbundle';
import * as qenv from '@push.rocks/qenv';
const testQenv = new qenv.Qenv('./', './.nogit/');
import * as ghost from '../ts/index.js';
let testGhostInstance: ghost.Ghost;
let createdPost: ghost.Post;
let createdMember: ghost.Member;
tap.test('initialize Ghost instance', async () => {
testGhostInstance = new ghost.Ghost({
baseUrl: 'http://localhost:2368',
adminApiKey: await testQenv.getEnvVarOnDemand('ADMIN_APIKEY'),
contentApiKey: await testQenv.getEnvVarOnDemand('CONTENT_APIKEY'),
});
expect(testGhostInstance).toBeInstanceOf(ghost.Ghost);
});
tap.test('should return dates as strings in posts', async () => {
const posts = await testGhostInstance.getPosts({ limit: 1 });
if (posts.length > 0) {
const post = posts[0];
expect(typeof post.postData.created_at).toEqual('string');
expect(typeof post.postData.updated_at).toEqual('string');
expect(typeof post.postData.published_at).toEqual('string');
console.log(`Post dates are strings: created_at=${post.postData.created_at}`);
}
});
tap.test('should have valid ISO 8601 date format in posts', async () => {
const posts = await testGhostInstance.getPosts({ limit: 1 });
if (posts.length > 0) {
const post = posts[0];
// Check if dates can be parsed
const createdDate = new Date(post.postData.created_at);
const updatedDate = new Date(post.postData.updated_at);
const publishedDate = new Date(post.postData.published_at);
expect(createdDate.toString()).not.toEqual('Invalid Date');
expect(updatedDate.toString()).not.toEqual('Invalid Date');
expect(publishedDate.toString()).not.toEqual('Invalid Date');
// Check if dates are valid timestamps
expect(isNaN(createdDate.getTime())).toEqual(false);
expect(isNaN(updatedDate.getTime())).toEqual(false);
expect(isNaN(publishedDate.getTime())).toEqual(false);
console.log(`Parsed dates: created=${createdDate.toISOString()}, updated=${updatedDate.toISOString()}, published=${publishedDate.toISOString()}`);
}
});
tap.test('should have ISO 8601 format with timezone offset', async () => {
const posts = await testGhostInstance.getPosts({ limit: 1 });
if (posts.length > 0) {
const post = posts[0];
// ISO 8601 with timezone: YYYY-MM-DDTHH:mm:ss.sss±HH:mm
const iso8601WithTimezonePattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{2}:\d{2}$/;
expect(iso8601WithTimezonePattern.test(post.postData.created_at)).toEqual(true);
expect(iso8601WithTimezonePattern.test(post.postData.updated_at)).toEqual(true);
expect(iso8601WithTimezonePattern.test(post.postData.published_at)).toEqual(true);
console.log(`Dates match ISO 8601 with timezone pattern`);
}
});
tap.test('should create published post and have published_at set', async () => {
const timestamp = Date.now();
createdPost = await testGhostInstance.adminApi.posts.add({
title: `Date Test Post ${timestamp}`,
html: '<p>Testing date handling</p>',
status: 'published'
}, { source: 'html' });
createdPost = new ghost.Post(testGhostInstance, createdPost);
expect(createdPost).toBeInstanceOf(ghost.Post);
expect(createdPost.postData.status).toEqual('published');
expect(createdPost.postData.published_at).toBeTruthy();
// Published date should be a valid date
const publishedDate = new Date(createdPost.postData.published_at);
expect(publishedDate.toString()).not.toEqual('Invalid Date');
console.log(`Created published post with published_at: ${createdPost.postData.published_at}`);
});
tap.test('should preserve published_at when updating post', async () => {
if (createdPost) {
const originalPublishedAt = createdPost.postData.published_at;
const originalPublishedDate = new Date(originalPublishedAt);
await createdPost.update({
html: '<p>Updated content</p>'
});
const updatedPublishedDate = new Date(createdPost.postData.published_at);
// The published_at date should remain the same (within a second tolerance for time parsing)
expect(Math.abs(updatedPublishedDate.getTime() - originalPublishedDate.getTime())).toBeLessThan(1000);
console.log(`Published date preserved after update: original=${originalPublishedAt}, updated=${createdPost.postData.published_at}`);
}
});
tap.test('should have updated_at change when updating metadata fields', async () => {
if (createdPost) {
const originalUpdatedAt = new Date(createdPost.postData.updated_at);
const originalTitle = createdPost.postData.title;
// Wait a moment to ensure time difference
await new Promise(resolve => setTimeout(resolve, 1000));
// Update a metadata field (not just HTML) to trigger updated_at change
await createdPost.update({
title: `${originalTitle} - Modified`
});
const newUpdatedAt = new Date(createdPost.postData.updated_at);
// The updated_at should be newer when metadata fields are updated
expect(newUpdatedAt.getTime()).toBeGreaterThan(originalUpdatedAt.getTime());
console.log(`updated_at changed: ${originalUpdatedAt.toISOString()} -> ${newUpdatedAt.toISOString()}`);
}
});
tap.test('should delete test post', async () => {
if (createdPost) {
await createdPost.delete();
console.log(`Deleted test post: ${createdPost.getId()}`);
}
});
tap.test('should return dates as strings in members', async () => {
const members = await testGhostInstance.getMembers({ limit: 1 });
if (members.length > 0) {
const member = members[0];
expect(typeof member.memberData.created_at).toEqual('string');
expect(typeof member.memberData.updated_at).toEqual('string');
console.log(`Member dates are strings: created_at=${member.memberData.created_at}`);
} else {
console.log('No members to test - skipping member date test');
}
});
tap.test('should have valid date format in members', async () => {
const members = await testGhostInstance.getMembers({ limit: 1 });
if (members.length > 0) {
const member = members[0];
const createdDate = new Date(member.memberData.created_at);
const updatedDate = new Date(member.memberData.updated_at);
expect(createdDate.toString()).not.toEqual('Invalid Date');
expect(updatedDate.toString()).not.toEqual('Invalid Date');
console.log(`Member dates parsed: created=${createdDate.toISOString()}, updated=${updatedDate.toISOString()}`);
} else {
console.log('No members to test - skipping member date validation');
}
});
tap.test('should create member and verify dates', async () => {
const timestamp = Date.now();
createdMember = await testGhostInstance.createMember({
email: `datetest-${timestamp}@example.com`,
name: `Date Test User ${timestamp}`
});
expect(createdMember).toBeInstanceOf(ghost.Member);
expect(typeof createdMember.memberData.created_at).toEqual('string');
expect(typeof createdMember.memberData.updated_at).toEqual('string');
const createdDate = new Date(createdMember.memberData.created_at);
expect(createdDate.toString()).not.toEqual('Invalid Date');
console.log(`Created member with dates: created_at=${createdMember.memberData.created_at}`);
});
tap.test('should have recent created_at date for new member', async () => {
if (createdMember) {
const createdDate = new Date(createdMember.memberData.created_at);
const now = new Date();
// Should be created within the last minute
const timeDiff = now.getTime() - createdDate.getTime();
expect(timeDiff).toBeLessThan(60000); // Less than 1 minute
expect(timeDiff).toBeGreaterThanOrEqual(0); // Not in the future
console.log(`Member created ${Math.round(timeDiff / 1000)} seconds ago`);
}
});
tap.test('should delete test member', async () => {
if (createdMember) {
await createdMember.delete();
console.log(`Deleted test member: ${createdMember.getId()}`);
}
});
export default tap.start();

View File

@@ -0,0 +1,28 @@
import { expect, tap } from '@push.rocks/tapbundle';
import * as qenv from '@push.rocks/qenv';
const testQenv = new qenv.Qenv('./', './.nogit/');
import * as ghost from '../ts/index.js';
let testGhostInstance: ghost.Ghost;
tap.test('should create a valid instance of Ghost', async () => {
testGhostInstance = new ghost.Ghost({
baseUrl: 'http://localhost:2368',
adminApiKey: await testQenv.getEnvVarOnDemand('ADMIN_APIKEY'),
contentApiKey: await testQenv.getEnvVarOnDemand('CONTENT_APIKEY'),
});
expect(testGhostInstance).toBeInstanceOf(ghost.Ghost);
expect(testGhostInstance.options.baseUrl).toEqual('http://localhost:2368');
});
tap.test('should have adminApi configured', async () => {
expect(testGhostInstance.adminApi).toBeTruthy();
});
tap.test('should have contentApi configured', async () => {
expect(testGhostInstance.contentApi).toBeTruthy();
});
export default tap.start();
export { testGhostInstance };

View File

@@ -0,0 +1,151 @@
import { expect, tap } from '@push.rocks/tapbundle';
import * as qenv from '@push.rocks/qenv';
const testQenv = new qenv.Qenv('./', './.nogit/');
import * as ghost from '../ts/index.js';
let testGhostInstance: ghost.Ghost;
let createdMember: ghost.Member;
tap.test('initialize Ghost instance', async () => {
testGhostInstance = new ghost.Ghost({
baseUrl: 'http://localhost:2368',
adminApiKey: await testQenv.getEnvVarOnDemand('ADMIN_APIKEY'),
contentApiKey: await testQenv.getEnvVarOnDemand('CONTENT_APIKEY'),
});
expect(testGhostInstance).toBeInstanceOf(ghost.Ghost);
});
tap.test('should get all members', async () => {
try {
const members = await testGhostInstance.getMembers({ limit: 10 });
expect(members).toBeArray();
console.log(`Found ${members.length} members`);
if (members.length > 0) {
expect(members[0]).toBeInstanceOf(ghost.Member);
console.log(`First member: ${members[0].getEmail()}`);
}
} catch (error: any) {
if (error.message?.includes('members') || error.statusCode === 403) {
console.log('Members feature not available or requires permissions - skipping test');
} else {
throw error;
}
}
});
tap.test('should get members with limit', async () => {
try {
const members = await testGhostInstance.getMembers({ limit: 2 });
expect(members).toBeArray();
expect(members.length).toBeLessThanOrEqual(2);
} catch (error: any) {
if (error.message?.includes('members') || error.statusCode === 403) {
console.log('Members feature not available - skipping test');
} else {
throw error;
}
}
});
tap.test('should filter members with minimatch pattern', async () => {
try {
const members = await testGhostInstance.getMembers({ filter: '*@gmail.com' });
expect(members).toBeArray();
console.log(`Found ${members.length} Gmail members`);
if (members.length > 0) {
members.forEach((member) => {
expect(member.getEmail()).toContain('@gmail.com');
});
}
} catch (error: any) {
if (error.message?.includes('members') || error.statusCode === 403) {
console.log('Members feature not available - skipping test');
} else {
throw error;
}
}
});
tap.test('should create member', async () => {
try {
const timestamp = Date.now();
createdMember = await testGhostInstance.createMember({
email: `test${timestamp}@example.com`,
name: `Test Member ${timestamp}`
});
expect(createdMember).toBeInstanceOf(ghost.Member);
expect(createdMember.getEmail()).toEqual(`test${timestamp}@example.com`);
console.log(`Created member: ${createdMember.getId()}`);
} catch (error: any) {
if (error.message?.includes('members') || error.statusCode === 403) {
console.log('Members feature not available - skipping test');
} else {
throw error;
}
}
});
tap.test('should access member methods', async () => {
if (createdMember) {
expect(createdMember.getId()).toBeTruthy();
expect(createdMember.getEmail()).toBeTruthy();
expect(createdMember.getName()).toBeTruthy();
const json = createdMember.toJson();
expect(json).toBeTruthy();
expect(json.id).toEqual(createdMember.getId());
}
});
tap.test('should get member by email', async () => {
if (createdMember) {
try {
const member = await testGhostInstance.getMemberByEmail(createdMember.getEmail());
expect(member).toBeInstanceOf(ghost.Member);
expect(member.getEmail()).toEqual(createdMember.getEmail());
} catch (error: any) {
if (error.message?.includes('members') || error.statusCode === 403 || error.type === 'RequestNotAcceptableError') {
console.log('Member by email not supported in this Ghost version - using ID instead');
const member = await testGhostInstance.getMemberById(createdMember.getId());
expect(member).toBeInstanceOf(ghost.Member);
} else {
throw error;
}
}
}
});
tap.test('should update member', async () => {
if (createdMember) {
try {
const updatedMember = await createdMember.update({
note: 'Updated by automated tests'
});
expect(updatedMember).toBeInstanceOf(ghost.Member);
console.log(`Updated member: ${updatedMember.getId()}`);
} catch (error: any) {
if (error.message?.includes('members') || error.statusCode === 403) {
console.log('Members feature not available - skipping test');
} else {
throw error;
}
}
}
});
tap.test('should delete member', async () => {
if (createdMember) {
try {
await createdMember.delete();
console.log(`Deleted member: ${createdMember.getId()}`);
} catch (error: any) {
if (error.message?.includes('members') || error.statusCode === 403) {
console.log('Members feature not available - skipping test');
} else {
throw error;
}
}
}
});
export default tap.start();

106
test/test.page.node+deno.ts Normal file
View File

@@ -0,0 +1,106 @@
import { expect, tap } from '@push.rocks/tapbundle';
import * as qenv from '@push.rocks/qenv';
const testQenv = new qenv.Qenv('./', './.nogit/');
import * as ghost from '../ts/index.js';
let testGhostInstance: ghost.Ghost;
let createdPage: ghost.Page;
tap.test('initialize Ghost instance', async () => {
testGhostInstance = new ghost.Ghost({
baseUrl: 'http://localhost:2368',
adminApiKey: await testQenv.getEnvVarOnDemand('ADMIN_APIKEY'),
contentApiKey: await testQenv.getEnvVarOnDemand('CONTENT_APIKEY'),
});
expect(testGhostInstance).toBeInstanceOf(ghost.Ghost);
});
tap.test('should get all pages', async () => {
const pages = await testGhostInstance.getPages();
expect(pages).toBeArray();
console.log(`Found ${pages.length} pages`);
if (pages.length > 0) {
expect(pages[0]).toBeInstanceOf(ghost.Page);
console.log(`First page: ${pages[0].getTitle()} (${pages[0].getSlug()})`);
}
});
tap.test('should get pages with limit', async () => {
const pages = await testGhostInstance.getPages({ limit: 3 });
expect(pages).toBeArray();
expect(pages.length).toBeLessThanOrEqual(3);
});
tap.test('should filter pages with minimatch pattern', async () => {
const pages = await testGhostInstance.getPages();
if (pages.length > 0) {
const firstPageSlug = pages[0].getSlug();
const pattern = `${firstPageSlug.charAt(0)}*`;
const filteredPages = await testGhostInstance.getPages({ filter: pattern });
expect(filteredPages).toBeArray();
console.log(`Filtered pages with pattern '${pattern}': found ${filteredPages.length}`);
}
});
tap.test('should get page by slug', async () => {
const pages = await testGhostInstance.getPages({ limit: 1 });
if (pages.length > 0) {
const page = await testGhostInstance.getPageBySlug(pages[0].getSlug());
expect(page).toBeInstanceOf(ghost.Page);
expect(page.getSlug()).toEqual(pages[0].getSlug());
console.log(`Got page by slug: ${page.getTitle()}`);
}
});
tap.test('should get page by ID', async () => {
const pages = await testGhostInstance.getPages({ limit: 1 });
if (pages.length > 0) {
const page = await testGhostInstance.getPageById(pages[0].getId());
expect(page).toBeInstanceOf(ghost.Page);
expect(page.getId()).toEqual(pages[0].getId());
}
});
tap.test('should create page', async () => {
const timestamp = Date.now();
createdPage = await testGhostInstance.createPage({
title: `Test Page ${timestamp}`,
html: '<p>This is a test page created by automated tests.</p>',
status: 'published'
} as any);
expect(createdPage).toBeInstanceOf(ghost.Page);
expect(createdPage.getTitle()).toEqual(`Test Page ${timestamp}`);
console.log(`Created page: ${createdPage.getId()}`);
});
tap.test('should access page methods', async () => {
if (createdPage) {
expect(createdPage.getId()).toBeTruthy();
expect(createdPage.getTitle()).toBeTruthy();
expect(createdPage.getSlug()).toBeTruthy();
const json = createdPage.toJson();
expect(json).toBeTruthy();
expect(json.id).toEqual(createdPage.getId());
}
});
tap.test('should update page', async () => {
if (createdPage) {
const updatedPage = await createdPage.update({
...createdPage.pageData,
html: '<p>This page has been updated.</p>'
});
expect(updatedPage).toBeInstanceOf(ghost.Page);
console.log(`Updated page: ${updatedPage.getId()}`);
}
});
tap.test('should delete page', async () => {
if (createdPage) {
await createdPage.delete();
console.log(`Deleted page: ${createdPage.getId()}`);
}
});
export default tap.start();

144
test/test.post.node+deno.ts Normal file
View File

@@ -0,0 +1,144 @@
import { expect, tap } from '@push.rocks/tapbundle';
import * as qenv from '@push.rocks/qenv';
const testQenv = new qenv.Qenv('./', './.nogit/');
import * as ghost from '../ts/index.js';
let testGhostInstance: ghost.Ghost;
let createdPost: ghost.Post;
tap.test('initialize Ghost instance', async () => {
testGhostInstance = new ghost.Ghost({
baseUrl: 'http://localhost:2368',
adminApiKey: await testQenv.getEnvVarOnDemand('ADMIN_APIKEY'),
contentApiKey: await testQenv.getEnvVarOnDemand('CONTENT_APIKEY'),
});
expect(testGhostInstance).toBeInstanceOf(ghost.Ghost);
});
tap.test('should get all posts', async () => {
const posts = await testGhostInstance.getPosts();
expect(posts).toBeArray();
if (posts.length > 0) {
expect(posts[0]).toBeInstanceOf(ghost.Post);
console.log(`Found ${posts.length} posts`);
}
});
tap.test('should get posts with limit', async () => {
const posts = await testGhostInstance.getPosts({ limit: 5 });
expect(posts).toBeArray();
expect(posts.length).toBeLessThanOrEqual(5);
});
tap.test('should filter posts by tag', async () => {
const tags = await testGhostInstance.getTags({ limit: 1 });
if (tags.length > 0) {
const posts = await testGhostInstance.getPosts({ tag: tags[0].slug, limit: 5 });
expect(posts).toBeArray();
console.log(`Found ${posts.length} posts with tag '${tags[0].name}'`);
}
});
tap.test('should filter posts by author', async () => {
const authors = await testGhostInstance.getAuthors({ limit: 1 });
if (authors.length > 0) {
const posts = await testGhostInstance.getPosts({ author: authors[0].getSlug(), limit: 5 });
expect(posts).toBeArray();
console.log(`Found ${posts.length} posts by author '${authors[0].getName()}'`);
}
});
tap.test('should filter posts by featured status', async () => {
const featuredPosts = await testGhostInstance.getPosts({ featured: true, limit: 5 });
expect(featuredPosts).toBeArray();
console.log(`Found ${featuredPosts.length} featured posts`);
if (featuredPosts.length > 0) {
expect(featuredPosts[0].postData.featured).toEqual(true);
}
});
tap.test('should search posts by title', async () => {
const searchResults = await testGhostInstance.searchPosts('the', { limit: 5 });
expect(searchResults).toBeArray();
console.log(`Found ${searchResults.length} posts matching 'the'`);
});
tap.test('should get post by ID', async () => {
const posts = await testGhostInstance.getPosts({ limit: 1 });
if (posts.length > 0) {
const post = await testGhostInstance.getPostById(posts[0].getId());
expect(post).toBeInstanceOf(ghost.Post);
expect(post.getId()).toEqual(posts[0].getId());
}
});
tap.test('should get related posts', async () => {
const posts = await testGhostInstance.getPosts({ limit: 1 });
if (posts.length > 0) {
const relatedPosts = await testGhostInstance.getRelatedPosts(posts[0].getId(), 3);
expect(relatedPosts).toBeArray();
console.log(`Found ${relatedPosts.length} related posts for '${posts[0].getTitle()}'`);
}
});
tap.test('should create post from HTML', async () => {
const timestamp = Date.now();
createdPost = await testGhostInstance.createPostFromHtml({
title: `Test Post ${timestamp}`,
html: '<p>This is a test post created by automated tests.</p>',
status: 'published'
} as any);
expect(createdPost).toBeInstanceOf(ghost.Post);
expect(createdPost.getTitle()).toEqual(`Test Post ${timestamp}`);
console.log(`Created post: ${createdPost.getId()}`);
});
tap.test('should access post methods', async () => {
if (createdPost) {
expect(createdPost.getId()).toBeTruthy();
expect(createdPost.getTitle()).toBeTruthy();
const json = createdPost.toJson();
expect(json).toBeTruthy();
expect(json.id).toEqual(createdPost.getId());
}
});
tap.test('should update post', async () => {
if (createdPost) {
const updatedPost = await createdPost.update({
...createdPost.postData,
html: '<p>This post has been updated.</p>'
});
expect(updatedPost).toBeInstanceOf(ghost.Post);
console.log(`Updated post: ${updatedPost.getId()}`);
}
});
tap.test('should delete post', async () => {
if (createdPost) {
await createdPost.delete();
console.log(`Deleted post: ${createdPost.getId()}`);
}
});
tap.test('should bulk update posts', async () => {
const posts = await testGhostInstance.getPosts({ limit: 2 });
if (posts.length >= 2) {
const postIds = posts.map(p => p.getId());
const originalFeatured = posts[0].postData.featured;
const updatedPosts = await testGhostInstance.bulkUpdatePosts(postIds, {
featured: !originalFeatured
});
expect(updatedPosts).toBeArray();
expect(updatedPosts.length).toEqual(postIds.length);
await testGhostInstance.bulkUpdatePosts(postIds, {
featured: originalFeatured
});
console.log(`Bulk updated ${updatedPosts.length} posts`);
}
});
export default tap.start();

View File

@@ -0,0 +1,185 @@
import { expect, tap } from '@push.rocks/tapbundle';
import * as qenv from '@push.rocks/qenv';
const testQenv = new qenv.Qenv('./', './.nogit/');
import * as ghost from '../ts/index.js';
let sourceGhost: ghost.Ghost;
let targetGhost: ghost.Ghost;
let syncedInstance: ghost.SyncedInstance;
let testTag: ghost.Tag;
let testPost: ghost.Post;
let testPage: ghost.Page;
tap.test('initialize source and target Ghost instances', async () => {
sourceGhost = new ghost.Ghost({
baseUrl: 'http://localhost:2368',
adminApiKey: await testQenv.getEnvVarOnDemand('ADMIN_APIKEY'),
contentApiKey: await testQenv.getEnvVarOnDemand('CONTENT_APIKEY'),
});
targetGhost = new ghost.Ghost({
baseUrl: 'http://localhost:2368',
adminApiKey: await testQenv.getEnvVarOnDemand('ADMIN_APIKEY'),
contentApiKey: await testQenv.getEnvVarOnDemand('CONTENT_APIKEY'),
});
expect(sourceGhost).toBeInstanceOf(ghost.Ghost);
expect(targetGhost).toBeInstanceOf(ghost.Ghost);
});
tap.test('create SyncedInstance', async () => {
syncedInstance = new ghost.SyncedInstance(sourceGhost, [targetGhost]);
expect(syncedInstance).toBeInstanceOf(ghost.SyncedInstance);
expect(syncedInstance.sourceGhost).toEqual(sourceGhost);
expect(syncedInstance.targetGhosts).toBeArray();
expect(syncedInstance.targetGhosts.length).toEqual(1);
});
tap.test('create test tag on source', async () => {
const timestamp = Date.now();
testTag = await sourceGhost.createTag({
name: `Sync Test Tag ${timestamp}`,
slug: `sync-test-tag-${timestamp}`,
description: 'This is a test tag for syncing'
});
expect(testTag).toBeInstanceOf(ghost.Tag);
});
tap.test('sync tags from source to target', async () => {
const report = await syncedInstance.syncTags();
expect(report).toBeTruthy();
expect(report.contentType).toEqual('tags');
expect(report.totalItems).toBeGreaterThan(0);
expect(report.targetReports).toBeArray();
expect(report.targetReports.length).toEqual(1);
const targetReport = report.targetReports[0];
expect(targetReport.results).toBeArray();
});
tap.test('verify sync status was tracked', async () => {
const status = syncedInstance.getSyncStatus();
expect(status.totalMappings).toBeGreaterThan(0);
expect(status.recentSyncs).toBeArray();
expect(status.recentSyncs.length).toBeGreaterThan(0);
});
tap.test('create test post on source', async () => {
const timestamp = Date.now();
testPost = await sourceGhost.createPost({
title: `Sync Test Post ${timestamp}`,
slug: `sync-test-post-${timestamp}`,
html: '<p>This is a test post for syncing</p>',
status: 'published',
tags: [{ id: testTag.tagData.id }]
});
expect(testPost).toBeInstanceOf(ghost.Post);
});
tap.test('sync posts from source to target', async () => {
const report = await syncedInstance.syncPosts();
expect(report).toBeTruthy();
expect(report.contentType).toEqual('posts');
expect(report.totalItems).toBeGreaterThan(0);
expect(report.targetReports).toBeArray();
expect(report.targetReports.length).toEqual(1);
});
tap.test('verify post sync in status', async () => {
const status = syncedInstance.getSyncStatus();
expect(status.recentSyncs).toBeArray();
const postSync = status.recentSyncs.find(s => s.contentType === 'posts');
expect(postSync).toBeTruthy();
});
tap.test('create test page on source', async () => {
const timestamp = Date.now();
testPage = await sourceGhost.createPage({
title: `Sync Test Page ${timestamp}`,
slug: `sync-test-page-${timestamp}`,
html: '<p>This is a test page for syncing</p>',
status: 'published'
});
expect(testPage).toBeInstanceOf(ghost.Page);
});
tap.test('sync pages from source to target', async () => {
const report = await syncedInstance.syncPages();
expect(report).toBeTruthy();
expect(report.contentType).toEqual('pages');
expect(report.totalItems).toBeGreaterThan(0);
expect(report.targetReports).toBeArray();
expect(report.targetReports.length).toEqual(1);
});
tap.test('verify page sync in status', async () => {
const status = syncedInstance.getSyncStatus();
const pageSync = status.recentSyncs.find(s => s.contentType === 'pages');
expect(pageSync).toBeTruthy();
});
tap.test('test syncAll method', async () => {
const reports = await syncedInstance.syncAll({
types: ['tags', 'posts', 'pages'],
syncOptions: { dryRun: true }
});
expect(reports).toBeArray();
expect(reports.length).toEqual(3);
expect(reports[0].contentType).toEqual('tags');
expect(reports[1].contentType).toEqual('posts');
expect(reports[2].contentType).toEqual('pages');
});
tap.test('test clear methods', async () => {
syncedInstance.clearMappings();
syncedInstance.clearSyncHistory();
const status = syncedInstance.getSyncStatus();
expect(status.totalMappings).toEqual(0);
expect(status.recentSyncs.length).toEqual(0);
});
tap.test('update tag and re-sync', async () => {
await testTag.update({
description: 'Updated description for sync test'
});
const report = await syncedInstance.syncTags();
expect(report).toBeTruthy();
expect(report.totalItems).toBeGreaterThan(0);
});
tap.test('cleanup - delete synced content', async () => {
if (testPost) {
await testPost.delete();
const targetPosts = await targetGhost.getPosts({ filter: `slug:${testPost.postData.slug}` });
if (targetPosts.length > 0) {
await targetPosts[0].delete();
}
}
if (testPage) {
await testPage.delete();
try {
const targetPage = await targetGhost.getPageBySlug(testPage.pageData.slug);
await targetPage.delete();
} catch (error) {
}
}
if (testTag) {
await testTag.delete();
try {
const targetTag = await targetGhost.getTagBySlug(testTag.tagData.slug);
await targetTag.delete();
} catch (error) {
}
}
});
export default tap.start();

View File

@@ -0,0 +1,95 @@
import { expect, tap } from '@push.rocks/tapbundle';
import * as qenv from '@push.rocks/qenv';
const testQenv = new qenv.Qenv('./', './.nogit/');
import * as ghost from '../ts/index.js';
let testGhostInstance: ghost.Ghost;
tap.test('initialize Ghost instance', async () => {
testGhostInstance = new ghost.Ghost({
baseUrl: 'http://localhost:2368',
adminApiKey: await testQenv.getEnvVarOnDemand('ADMIN_APIKEY'),
contentApiKey: await testQenv.getEnvVarOnDemand('CONTENT_APIKEY'),
});
expect(testGhostInstance).toBeInstanceOf(ghost.Ghost);
});
tap.test('should throw error when creating SyncedInstance with same instance', async () => {
let errorThrown = false;
let errorMessage = '';
try {
new ghost.SyncedInstance(testGhostInstance, [testGhostInstance]);
} catch (error) {
errorThrown = true;
errorMessage = error instanceof Error ? error.message : String(error);
}
expect(errorThrown).toEqual(true);
expect(errorMessage).toContain('Cannot sync to the same instance');
expect(errorMessage).toContain('localhost:2368');
console.log(`Correctly prevented same-instance sync: ${errorMessage}`);
});
tap.test('should throw error when target array includes same instance', async () => {
let errorThrown = false;
let errorMessage = '';
const anotherInstance = new ghost.Ghost({
baseUrl: 'http://localhost:2368',
adminApiKey: await testQenv.getEnvVarOnDemand('ADMIN_APIKEY'),
contentApiKey: await testQenv.getEnvVarOnDemand('CONTENT_APIKEY'),
});
try {
new ghost.SyncedInstance(testGhostInstance, [anotherInstance]);
} catch (error) {
errorThrown = true;
errorMessage = error instanceof Error ? error.message : String(error);
}
expect(errorThrown).toEqual(true);
expect(errorMessage).toContain('Cannot sync to the same instance');
console.log(`Correctly prevented sync with duplicate URL: ${errorMessage}`);
});
tap.test('should normalize URLs when comparing (trailing slash)', async () => {
let errorThrown = false;
const instanceWithTrailingSlash = new ghost.Ghost({
baseUrl: 'http://localhost:2368/',
adminApiKey: await testQenv.getEnvVarOnDemand('ADMIN_APIKEY'),
contentApiKey: await testQenv.getEnvVarOnDemand('CONTENT_APIKEY'),
});
try {
new ghost.SyncedInstance(testGhostInstance, [instanceWithTrailingSlash]);
} catch (error) {
errorThrown = true;
}
expect(errorThrown).toEqual(true);
console.log('Correctly detected same instance despite trailing slash difference');
});
tap.test('should normalize URLs when comparing (case insensitive)', async () => {
let errorThrown = false;
const instanceWithUpperCase = new ghost.Ghost({
baseUrl: 'http://LOCALHOST:2368',
adminApiKey: await testQenv.getEnvVarOnDemand('ADMIN_APIKEY'),
contentApiKey: await testQenv.getEnvVarOnDemand('CONTENT_APIKEY'),
});
try {
new ghost.SyncedInstance(testGhostInstance, [instanceWithUpperCase]);
} catch (error) {
errorThrown = true;
}
expect(errorThrown).toEqual(true);
console.log('Correctly detected same instance despite case difference');
});
export default tap.start();

118
test/test.tag.node+deno.ts Normal file
View File

@@ -0,0 +1,118 @@
import { expect, tap } from '@push.rocks/tapbundle';
import * as qenv from '@push.rocks/qenv';
const testQenv = new qenv.Qenv('./', './.nogit/');
import * as ghost from '../ts/index.js';
let testGhostInstance: ghost.Ghost;
let createdTag: ghost.Tag;
tap.test('initialize Ghost instance', async () => {
testGhostInstance = new ghost.Ghost({
baseUrl: 'http://localhost:2368',
adminApiKey: await testQenv.getEnvVarOnDemand('ADMIN_APIKEY'),
contentApiKey: await testQenv.getEnvVarOnDemand('CONTENT_APIKEY'),
});
expect(testGhostInstance).toBeInstanceOf(ghost.Ghost);
});
tap.test('should get all tags', async () => {
const tags = await testGhostInstance.getTags();
expect(tags).toBeArray();
console.log(`Found ${tags.length} tags`);
if (tags.length > 0) {
console.log(`First tag: ${tags[0].name} (${tags[0].slug})`);
}
});
tap.test('should get tags with limit', async () => {
const tags = await testGhostInstance.getTags({ limit: 3 });
expect(tags).toBeArray();
expect(tags.length).toBeLessThanOrEqual(3);
});
tap.test('should filter tags with minimatch pattern', async () => {
const allTags = await testGhostInstance.getTags();
if (allTags.length > 0) {
const firstTagSlug = allTags[0].slug;
const pattern = `${firstTagSlug.charAt(0)}*`;
const filteredTags = await testGhostInstance.getTags({ filter: pattern });
expect(filteredTags).toBeArray();
console.log(`Filtered tags with pattern '${pattern}': found ${filteredTags.length}`);
filteredTags.forEach((tag) => {
expect(tag.slug).toMatch(new RegExp(`^${firstTagSlug.charAt(0)}`));
});
}
});
tap.test('should create tag', async () => {
const timestamp = Date.now();
createdTag = await testGhostInstance.createTag({
name: `Test Tag ${timestamp}`,
slug: `test-tag-${timestamp}`,
description: 'A test tag created by automated tests'
});
expect(createdTag).toBeInstanceOf(ghost.Tag);
expect(createdTag.getName()).toEqual(`Test Tag ${timestamp}`);
console.log(`Created tag: ${createdTag.getId()}`);
});
tap.test('should get tag by slug using created tag', async () => {
if (createdTag) {
// Note: Content API only returns tags with posts, so this test may not work
// for newly created tags without posts. Using Admin API via getTags instead.
const tags = await testGhostInstance.getTags({
filter: `slug:${createdTag.getSlug()}`,
limit: 1
});
expect(tags).toBeArray();
if (tags.length > 0) {
expect(tags[0].slug).toEqual(createdTag.getSlug());
console.log(`Found tag by slug via Admin API: ${tags[0].name}`);
}
}
});
tap.test('should verify created tag exists in getTags list', async () => {
if (createdTag) {
// Admin API getTags() should include our newly created tag
// Note: We can't filter by ID directly, so we verify the tag exists
const allTags = await testGhostInstance.getTags({ limit: 5 });
expect(allTags).toBeArray();
expect(allTags.length).toBeGreaterThan(0);
console.log(`getTags returned ${allTags.length} tags, created tag ID: ${createdTag.getId()}`);
}
});
tap.test('should access tag methods', async () => {
if (createdTag) {
expect(createdTag.getId()).toBeTruthy();
expect(createdTag.getName()).toBeTruthy();
expect(createdTag.getSlug()).toBeTruthy();
expect(createdTag.getDescription()).toBeTruthy();
const json = createdTag.toJson();
expect(json).toBeTruthy();
expect(json.id).toEqual(createdTag.getId());
}
});
tap.test('should update tag', async () => {
if (createdTag) {
const updatedTag = await createdTag.update({
description: 'Updated description for test tag'
});
expect(updatedTag).toBeInstanceOf(ghost.Tag);
expect(updatedTag.getDescription()).toEqual('Updated description for test tag');
console.log(`Updated tag: ${updatedTag.getId()}`);
}
});
tap.test('should delete tag', async () => {
if (createdTag) {
await createdTag.delete();
console.log(`Deleted tag: ${createdTag.getId()}`);
}
});
export default tap.start();

View File

@@ -1,141 +0,0 @@
import { expect, tap } from '@push.rocks/tapbundle';
import * as qenv from '@push.rocks/qenv';
const testQenv = new qenv.Qenv('./', './.nogit/');
import * as ghost from '../ts/index.js';
// make sure we can import the IPost type
import {type IPost} from '../ts/index.js';
let testGhostInstance: ghost.Ghost;
tap.test('should create a valid instance of Ghost', async () => {
testGhostInstance = new ghost.Ghost({
baseUrl: 'http://localhost:2368',
adminApiKey: await testQenv.getEnvVarOnDemand('ADMIN_APIKEY'),
contentApiKey: await testQenv.getEnvVarOnDemand('CONTENT_APIKEY'),
});
expect(testGhostInstance).toBeInstanceOf(ghost.Ghost);
});
tap.test('should get posts', async () => {
const posts = await testGhostInstance.getPosts();
expect(posts).toBeArray();
expect(posts[0]).toBeInstanceOf(ghost.Post);
console.log(JSON.stringify(posts[0].postData, null, 2));
posts.map((post) => {
// console.log(JSON.stringify(post.postData, null, 2));
console.log(`-> ${post.getTitle()}`);
console.log(`by ${post.getAuthor().name}`)
console.log(post.getExcerpt());
console.log(`===============`)
})
})
tap.test('should get all tags', async () => {
const tags = await testGhostInstance.getTags();
expect(tags).toBeArray();
console.log(`Found ${tags.length} tags:`);
tags.forEach((tag) => {
console.log(`-> ${tag.name} (${tag.slug})`);
});
});
tap.test('should filter tags with minimatch pattern', async () => {
const allTags = await testGhostInstance.getTags();
if (allTags.length > 0) {
const firstTagSlug = allTags[0].slug;
const pattern = `${firstTagSlug.charAt(0)}*`;
const filteredTags = await testGhostInstance.getTags({ filter: pattern });
expect(filteredTags).toBeArray();
console.log(`Filtered tags with pattern '${pattern}':`);
filteredTags.forEach((tag) => {
console.log(`-> ${tag.name} (${tag.slug})`);
expect(tag.slug).toMatch(new RegExp(`^${firstTagSlug.charAt(0)}`));
});
} else {
console.log('No tags available to test filtering');
}
});
tap.test('should get tag by slug', async () => {
const tags = await testGhostInstance.getTags({ limit: 1 });
if (tags.length > 0) {
const tag = await testGhostInstance.getTagBySlug(tags[0].slug);
expect(tag).toBeInstanceOf(ghost.Tag);
console.log(`Got tag: ${tag.getName()} (${tag.getSlug()})`);
}
});
tap.test('should get all authors', async () => {
const authors = await testGhostInstance.getAuthors();
expect(authors).toBeArray();
console.log(`Found ${authors.length} authors:`);
authors.forEach((author) => {
console.log(`-> ${author.getName()} (${author.getSlug()})`);
});
});
tap.test('should filter authors with minimatch pattern', async () => {
const authors = await testGhostInstance.getAuthors();
if (authors.length > 0) {
const firstAuthorSlug = authors[0].getSlug();
const pattern = `${firstAuthorSlug.charAt(0)}*`;
const filteredAuthors = await testGhostInstance.getAuthors({ filter: pattern });
expect(filteredAuthors).toBeArray();
console.log(`Filtered authors with pattern '${pattern}':`);
filteredAuthors.forEach((author) => {
console.log(`-> ${author.getName()} (${author.getSlug()})`);
});
}
});
tap.test('should get all pages', async () => {
const pages = await testGhostInstance.getPages();
expect(pages).toBeArray();
console.log(`Found ${pages.length} pages:`);
pages.forEach((page) => {
console.log(`-> ${page.getTitle()} (${page.getSlug()})`);
});
});
tap.test('should filter posts by tag', async () => {
const tags = await testGhostInstance.getTags({ limit: 1 });
if (tags.length > 0) {
const posts = await testGhostInstance.getPosts({ tag: tags[0].slug, limit: 5 });
expect(posts).toBeArray();
console.log(`Found ${posts.length} posts with tag '${tags[0].name}'`);
}
});
tap.test('should filter posts by featured status', async () => {
const featuredPosts = await testGhostInstance.getPosts({ featured: true, limit: 5 });
expect(featuredPosts).toBeArray();
console.log(`Found ${featuredPosts.length} featured posts`);
});
tap.test('should search posts', async () => {
const searchResults = await testGhostInstance.searchPosts('the', { limit: 5 });
expect(searchResults).toBeArray();
console.log(`Found ${searchResults.length} posts matching 'the':`);
searchResults.forEach((post) => {
console.log(`-> ${post.getTitle()}`);
});
});
tap.test('should get related posts', async () => {
const posts = await testGhostInstance.getPosts({ limit: 1 });
if (posts.length > 0) {
const relatedPosts = await testGhostInstance.getRelatedPosts(posts[0].getId(), 3);
expect(relatedPosts).toBeArray();
console.log(`Found ${relatedPosts.length} related posts for '${posts[0].getTitle()}'`);
relatedPosts.forEach((post) => {
console.log(`-> ${post.getTitle()}`);
});
}
});
tap.start()

View File

@@ -0,0 +1,41 @@
import { expect, tap } from '@push.rocks/tapbundle';
import * as qenv from '@push.rocks/qenv';
const testQenv = new qenv.Qenv('./', './.nogit/');
import * as ghost from '../ts/index.js';
let testGhostInstance: ghost.Ghost;
let createdWebhook: any;
tap.test('initialize Ghost instance', async () => {
testGhostInstance = new ghost.Ghost({
baseUrl: 'http://localhost:2368',
adminApiKey: await testQenv.getEnvVarOnDemand('ADMIN_APIKEY'),
contentApiKey: await testQenv.getEnvVarOnDemand('CONTENT_APIKEY'),
});
expect(testGhostInstance).toBeInstanceOf(ghost.Ghost);
});
tap.test('should create webhook', async () => {
const timestamp = Date.now();
createdWebhook = await testGhostInstance.createWebhook({
event: 'post.published',
target_url: `https://example.com/webhook/${timestamp}`,
name: `Test Webhook ${timestamp}`
});
expect(createdWebhook).toBeTruthy();
expect(createdWebhook.id).toBeTruthy();
});
tap.test('should update webhook', async () => {
const updatedWebhook = await testGhostInstance.updateWebhook(createdWebhook.id, {
target_url: 'https://example.com/webhook/updated'
});
expect(updatedWebhook).toBeTruthy();
});
tap.test('should delete webhook', async () => {
await testGhostInstance.deleteWebhook(createdWebhook.id);
});
export default tap.start();

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@apiclient.xyz/ghost', name: '@apiclient.xyz/ghost',
version: '1.3.0', version: '2.2.1',
description: 'An unofficial Ghost CMS API package enabling content and admin functionality for managing posts.' description: 'An unofficial Ghost CMS API package enabling content and admin functionality for managing posts.'
} }

View File

@@ -0,0 +1,318 @@
/**
* Ghost Admin API Client
* Full CRUD operations for Ghost content using native fetch
*/
import { generateToken } from './ghost.jwt.js';
import type { THttpMethod, IBrowseOptions, IGhostAPIResponse, IGhostErrorResponse } from './ghost.types.js';
import * as fs from 'node:fs';
import * as path from 'node:path';
export interface IGhostAdminAPIOptions {
url: string;
key: string;
version?: string;
ghostPath?: string;
}
export class GhostAdminAPI {
private url: string;
private key: string;
private version: string;
private ghostPath: string;
private acceptVersionHeader: string;
constructor(options: IGhostAdminAPIOptions) {
this.url = options.url.replace(/\/$/, ''); // Remove trailing slash
this.key = options.key;
this.version = options.version || 'v3';
this.ghostPath = options.ghostPath || 'ghost';
if (!this.url) {
throw new Error('GhostAdminAPI: url is required');
}
if (!this.key) {
throw new Error('GhostAdminAPI: key is required');
}
// Set accept version header
if (this.version.match(/^v\d+$/)) {
this.acceptVersionHeader = `${this.version}.0`;
} else if (this.version.match(/^v\d+\.\d+$/)) {
this.acceptVersionHeader = this.version;
} else {
this.acceptVersionHeader = 'v6.0';
}
}
/**
* Build the API prefix based on version
*/
private getAPIPrefix(): string {
// v5+ doesn't need version prefix
if (this.version === 'v5' || this.version === 'v6' || this.version.match(/^v[5-9]\.\d+/)) {
return `/admin/`;
}
// v2-v4 and canary need version prefix
return `/${this.version}/admin/`;
}
/**
* Build full API URL
*/
private buildUrl(resource: string, identifier?: string, params?: Record<string, any>): string {
const apiPrefix = this.getAPIPrefix();
let endpoint = `${this.url}/${this.ghostPath}/api${apiPrefix}${resource}/`;
if (identifier) {
endpoint += `${identifier}/`;
}
// Build query string if params exist
if (params && Object.keys(params).length > 0) {
const queryString = Object.keys(params)
.filter(key => params[key] !== undefined && params[key] !== null)
.map(key => {
const value = Array.isArray(params[key])
? params[key].join(',')
: params[key];
return `${key}=${encodeURIComponent(value)}`;
})
.join('&');
if (queryString) {
endpoint += `?${queryString}`;
}
}
return endpoint;
}
/**
* Get authorization token
*/
private async getAuthToken(): Promise<string> {
const audience = this.getAPIPrefix();
return await generateToken(this.key, audience);
}
/**
* Make API request
*/
private async makeRequest<T>(
resource: string,
method: THttpMethod = 'GET',
identifier?: string,
body?: any,
queryParams?: Record<string, any>
): Promise<T | T[] | void> {
const url = this.buildUrl(resource, identifier, queryParams);
const token = await this.getAuthToken();
const headers: Record<string, string> = {
'Authorization': `Ghost ${token}`,
'Accept-Version': this.acceptVersionHeader,
'Accept': 'application/json',
'User-Agent': 'GhostAdminAPI/2.0'
};
const requestOptions: RequestInit = {
method,
headers
};
// Add body for POST/PUT
if (body && (method === 'POST' || method === 'PUT')) {
headers['Content-Type'] = 'application/json';
requestOptions.body = JSON.stringify(body);
}
try {
const response = await fetch(url, requestOptions);
if (!response.ok) {
const errorData: IGhostErrorResponse = await response.json().catch(() => ({
errors: [{ type: 'UnknownError', message: response.statusText }]
}));
const error = errorData.errors?.[0];
const err = new Error(error?.message || response.statusText);
Object.assign(err, {
name: error?.type || 'GhostAdminAPIError',
statusCode: response.status,
...error
});
throw err;
}
// DELETE returns empty response
if (method === 'DELETE') {
return;
}
const data: IGhostAPIResponse<T> = await response.json();
// Extract the resource data
const resourceData = data[resource];
if (!resourceData) {
return data as any; // For some special endpoints that don't follow standard format
}
// If it's an array and has meta, attach meta to the array
if (Array.isArray(resourceData) && data.meta) {
return Object.assign(resourceData, { meta: data.meta });
}
// If it's an array with single item and no meta, return the item
if (Array.isArray(resourceData) && resourceData.length === 1 && !data.meta) {
return resourceData[0];
}
return resourceData as T | T[];
} catch (error) {
throw error;
}
}
/**
* Create resource API methods factory
*/
private createResourceAPI(resourceType: string) {
return {
browse: (options?: IBrowseOptions) => {
return this.makeRequest(resourceType, 'GET', undefined, undefined, options);
},
read: (data: { id?: string; slug?: string; email?: string }, queryParams?: Record<string, any>) => {
if (data.slug) {
return this.makeRequest(resourceType, 'GET', `slug/${data.slug}`, undefined, queryParams);
}
if (data.email) {
return this.makeRequest(resourceType, 'GET', `email/${data.email}`, undefined, queryParams);
}
if (data.id) {
return this.makeRequest(resourceType, 'GET', data.id, undefined, queryParams);
}
throw new Error('Must provide id, slug, or email');
},
add: (data: any, queryParams?: Record<string, any>) => {
if (!data || !Object.keys(data).length) {
return Promise.reject(new Error('Missing data'));
}
const body: any = {};
body[resourceType] = [data];
return this.makeRequest(resourceType, 'POST', undefined, body, queryParams);
},
edit: (data: any, queryParams?: Record<string, any>) => {
if (!data) {
return Promise.reject(new Error('Missing data'));
}
if (!data.id) {
return Promise.reject(new Error('Must include data.id'));
}
const id = data.id;
const updateData = { ...data };
delete updateData.id;
const body: any = {};
body[resourceType] = [updateData];
return this.makeRequest(resourceType, 'PUT', id, body, queryParams);
},
delete: (data: { id?: string; email?: string }, queryParams?: Record<string, any>) => {
if (!data) {
return Promise.reject(new Error('Missing data'));
}
if (!data.id && !data.email) {
return Promise.reject(new Error('Must include either data.id or data.email'));
}
const identifier = data.email ? `email/${data.email}` : data.id;
return this.makeRequest(resourceType, 'DELETE', identifier, undefined, queryParams);
}
};
}
// Resource APIs
public posts = this.createResourceAPI('posts');
public pages = this.createResourceAPI('pages');
public tags = this.createResourceAPI('tags');
public members = this.createResourceAPI('members');
public users = this.createResourceAPI('users');
// Webhooks (limited operations)
public webhooks = {
add: (data: any, queryParams?: Record<string, any>) => {
const body: any = { webhooks: [data] };
return this.makeRequest('webhooks', 'POST', undefined, body, queryParams);
},
edit: (data: any, queryParams?: Record<string, any>) => {
if (!data.id) {
return Promise.reject(new Error('Must include data.id'));
}
const id = data.id;
const updateData = { ...data };
delete updateData.id;
const body: any = { webhooks: [updateData] };
return this.makeRequest('webhooks', 'PUT', id, body, queryParams);
},
delete: (data: { id: string }, queryParams?: Record<string, any>) => {
if (!data.id) {
return Promise.reject(new Error('Must include data.id'));
}
return this.makeRequest('webhooks', 'DELETE', data.id, undefined, queryParams);
}
};
// Image upload
public images = {
upload: async (data: { file: string }) => {
if (!data || !data.file) {
throw new Error('Must provide file path');
}
const url = this.buildUrl('images', 'upload');
const token = await this.getAuthToken();
// Read file
const fileBuffer = fs.readFileSync(data.file);
const fileName = path.basename(data.file);
// Create FormData
const formData = new FormData();
// Convert Buffer to ArrayBuffer for Blob
const arrayBuffer = fileBuffer.buffer.slice(fileBuffer.byteOffset, fileBuffer.byteOffset + fileBuffer.byteLength) as ArrayBuffer;
const blob = new Blob([arrayBuffer], { type: 'image/*' });
formData.append('file', blob, fileName);
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Ghost ${token}`,
'Accept-Version': this.acceptVersionHeader,
'User-Agent': 'GhostAdminAPI/2.0'
},
body: formData
});
if (!response.ok) {
const errorData: IGhostErrorResponse = await response.json().catch(() => ({
errors: [{ type: 'UnknownError', message: response.statusText }]
}));
const error = errorData.errors?.[0];
const err = new Error(error?.message || response.statusText);
throw err;
}
const result = await response.json();
return result.images?.[0] || result;
}
};
}

View File

@@ -0,0 +1,191 @@
/**
* Ghost Content API Client
* Read-only API for published content using native fetch
*/
import type { IBrowseOptions, IReadOptions, IGhostAPIResponse, IGhostErrorResponse } from './ghost.types.js';
export interface IGhostContentAPIOptions {
url: string;
key: string;
version?: string;
ghostPath?: string;
}
export class GhostContentAPI {
private url: string;
private key: string;
private version: string;
private ghostPath: string;
constructor(options: IGhostContentAPIOptions) {
this.url = options.url.replace(/\/$/, ''); // Remove trailing slash
this.key = options.key;
this.version = options.version || 'v3';
this.ghostPath = options.ghostPath || 'ghost';
if (!this.url) {
throw new Error('GhostContentAPI: url is required');
}
if (!this.key) {
throw new Error('GhostContentAPI: key is required');
}
}
/**
* Build the API prefix based on version
*/
private getAPIPrefix(): string {
// v5+ doesn't need version prefix
if (this.version === 'v5' || this.version === 'v6' || this.version.match(/^v[5-9]\.\d+/)) {
return `/content/`;
}
// v2-v4 and canary need version prefix
return `/${this.version}/content/`;
}
/**
* Build full API URL
*/
private buildUrl(resource: string, identifier?: string, params?: Record<string, any>): string {
const apiPrefix = this.getAPIPrefix();
let endpoint = `${this.url}/${this.ghostPath}/api${apiPrefix}${resource}/`;
if (identifier) {
endpoint += `${identifier}/`;
}
// Add key to params
const queryParams = {
key: this.key,
...params
};
// Build query string
const queryString = Object.keys(queryParams)
.filter(key => queryParams[key] !== undefined && queryParams[key] !== null)
.map(key => {
const value = Array.isArray(queryParams[key])
? queryParams[key].join(',')
: queryParams[key];
return `${key}=${encodeURIComponent(value)}`;
})
.join('&');
return queryString ? `${endpoint}?${queryString}` : endpoint;
}
/**
* Make API request
*/
private async makeRequest<T>(
resource: string,
identifier?: string,
params?: Record<string, any>
): Promise<T | T[]> {
const url = this.buildUrl(resource, identifier, params);
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'Accept': 'application/json',
'Accept-Version': this.version.match(/^\d+\.\d+$/) ? `v${this.version}` : this.version,
'User-Agent': 'GhostContentAPI/2.0'
}
});
if (!response.ok) {
const errorData: IGhostErrorResponse = await response.json().catch(() => ({
errors: [{ type: 'UnknownError', message: response.statusText }]
}));
const error = errorData.errors?.[0];
const err = new Error(error?.message || response.statusText);
Object.assign(err, {
name: error?.type || 'GhostContentAPIError',
statusCode: response.status,
...error
});
throw err;
}
const data: IGhostAPIResponse<T> = await response.json();
// Extract the resource data
const resourceData = data[resource];
if (!resourceData) {
throw new Error(`Response missing ${resource} property`);
}
// If it's an array and has meta, attach meta to the array
if (Array.isArray(resourceData) && data.meta) {
return Object.assign(resourceData, { meta: data.meta });
}
// If it's an array with single item and no meta, return the item
if (Array.isArray(resourceData) && resourceData.length === 1 && !data.meta) {
return resourceData[0];
}
return resourceData as T | T[];
} catch (error) {
throw error;
}
}
/**
* Create resource API methods
*/
public posts = {
browse: (options?: IBrowseOptions) => this.makeRequest('posts', undefined, options),
read: (options: IReadOptions) => {
if (options.slug) {
return this.makeRequest('posts', `slug/${options.slug}`, options);
}
if (options.id) {
return this.makeRequest('posts', options.id, options);
}
throw new Error('Must provide id or slug');
}
};
public pages = {
browse: (options?: IBrowseOptions) => this.makeRequest('pages', undefined, options),
read: (options: IReadOptions) => {
if (options.slug) {
return this.makeRequest('pages', `slug/${options.slug}`, options);
}
if (options.id) {
return this.makeRequest('pages', options.id, options);
}
throw new Error('Must provide id or slug');
}
};
public tags = {
browse: (options?: IBrowseOptions) => this.makeRequest('tags', undefined, options),
read: (options: IReadOptions) => {
if (options.slug) {
return this.makeRequest('tags', `slug/${options.slug}`, options);
}
if (options.id) {
return this.makeRequest('tags', options.id, options);
}
throw new Error('Must provide id or slug');
}
};
public authors = {
browse: (options?: IBrowseOptions) => this.makeRequest('authors', undefined, options),
read: (options: IReadOptions) => {
if (options.slug) {
return this.makeRequest('authors', `slug/${options.slug}`, options);
}
if (options.id) {
return this.makeRequest('authors', options.id, options);
}
throw new Error('Must provide id or slug');
}
};
}

116
ts/apiclient/ghost.jwt.ts Normal file
View File

@@ -0,0 +1,116 @@
/**
* JWT token generator for Ghost Admin API
* Implements HS256 signing compatible with Ghost's authentication
*/
/**
* Base64 URL encode (without padding)
*/
function base64UrlEncode(data: Uint8Array): string {
const base64 = typeof Buffer !== 'undefined'
? Buffer.from(data).toString('base64')
: btoa(String.fromCharCode(...data));
return base64
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
/**
* Convert hex string to Uint8Array
*/
function hexToBytes(hex: string): Uint8Array {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
}
return bytes;
}
/**
* Generate JWT token for Ghost Admin API
* @param key - Admin API key in format {id}:{secret}
* @param audience - Token audience (API prefix like '/admin/')
* @returns JWT token string
*/
export async function generateToken(key: string, audience: string): Promise<string> {
// Parse the admin key
const [keyId, secret] = key.split(':');
if (!keyId || !secret) {
throw new Error('Invalid admin API key format. Expected {id}:{secret}');
}
if (keyId.length !== 24 || secret.length !== 64) {
throw new Error('Invalid admin API key format. Expected 24 hex chars for id and 64 for secret');
}
// Create JWT header
const header = {
alg: 'HS256',
typ: 'JWT',
kid: keyId
};
// Create JWT payload
const now = Math.floor(Date.now() / 1000);
const payload = {
iat: now,
exp: now + 300, // 5 minutes
aud: audience
};
// Encode header and payload
const headerEncoded = base64UrlEncode(
new TextEncoder().encode(JSON.stringify(header))
);
const payloadEncoded = base64UrlEncode(
new TextEncoder().encode(JSON.stringify(payload))
);
// Create signature data
const signatureData = `${headerEncoded}.${payloadEncoded}`;
// Convert secret from hex to bytes
const secretBytes = hexToBytes(secret);
// Import key for HMAC
let cryptoKey: CryptoKey;
// Try to use Web Crypto API (works in Node 15+ and all modern browsers)
try {
const crypto = globalThis.crypto || (await import('crypto')).webcrypto;
// Convert to proper BufferSource type
const secretBuffer = secretBytes.buffer.slice(secretBytes.byteOffset, secretBytes.byteOffset + secretBytes.byteLength) as ArrayBuffer;
cryptoKey = await crypto.subtle.importKey(
'raw',
secretBuffer,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
// Sign the data
const signature = await crypto.subtle.sign(
'HMAC',
cryptoKey,
new TextEncoder().encode(signatureData)
);
// Encode signature
const signatureEncoded = base64UrlEncode(new Uint8Array(signature));
// Return complete JWT
return `${signatureData}.${signatureEncoded}`;
} catch (error) {
// Fallback for older Node versions using crypto module directly
const crypto = await import('crypto');
const hmac = crypto.createHmac('sha256', secretBytes);
hmac.update(signatureData);
const signature = hmac.digest();
const signatureEncoded = base64UrlEncode(signature);
return `${signatureData}.${signatureEncoded}`;
}
}

View File

@@ -0,0 +1,66 @@
/**
* Shared types for Ghost API clients
*/
export interface IGhostAPIResponse<T> {
[key: string]: T | T[] | IGhostMeta;
meta?: IGhostMeta;
}
export interface IGhostMeta {
pagination?: {
page: number;
limit: number;
pages: number;
total: number;
next: number | null;
prev: number | null;
};
}
export interface IGhostError {
type: string;
message: string;
context?: string;
property?: string;
help?: string;
code?: string;
id?: string;
ghostErrorCode?: string;
}
export interface IGhostErrorResponse {
errors: IGhostError[];
}
export type THttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
export interface IRequestOptions {
method?: THttpMethod;
headers?: Record<string, string>;
body?: string;
signal?: AbortSignal;
}
/**
* Query parameters for browse operations
*/
export interface IBrowseOptions {
limit?: number;
page?: number;
filter?: string;
include?: string;
fields?: string;
order?: string;
}
/**
* Options for read operations (by ID, slug, or email)
*/
export interface IReadOptions {
id?: string;
slug?: string;
email?: string;
include?: string;
fields?: string;
}

View File

@@ -43,7 +43,6 @@ export class Author {
this.authorData = updatedAuthorData; this.authorData = updatedAuthorData;
return this; return this;
} catch (error) { } catch (error) {
console.error('Error updating author:', error);
throw error; throw error;
} }
} }

View File

@@ -3,6 +3,7 @@ import { Post, type IPost, type ITag, type IAuthor } from './classes.post.js';
import { Author } from './classes.author.js'; import { Author } from './classes.author.js';
import { Tag } from './classes.tag.js'; import { Tag } from './classes.tag.js';
import { Page, type IPage } from './classes.page.js'; import { Page, type IPage } from './classes.page.js';
import { Member, type IMember } from './classes.member.js';
export interface IGhostConstructorOptions { export interface IGhostConstructorOptions {
baseUrl: string; baseUrl: string;
@@ -21,13 +22,13 @@ export class Ghost {
this.adminApi = new plugins.GhostAdminAPI({ this.adminApi = new plugins.GhostAdminAPI({
url: this.options.baseUrl, url: this.options.baseUrl,
key: this.options.adminApiKey, key: this.options.adminApiKey,
version: 'v3', version: 'v6.0',
}); });
this.contentApi = new plugins.GhostContentAPI({ this.contentApi = new plugins.GhostContentAPI({
url: this.options.baseUrl, url: this.options.baseUrl,
key: this.options.contentApiKey, key: this.options.contentApiKey,
version: 'v3', version: 'v6.0',
}); });
} }
@@ -64,7 +65,6 @@ export class Ghost {
}); });
return postsData.map((postData: IPost) => new Post(this, postData)); return postsData.map((postData: IPost) => new Post(this, postData));
} catch (error) { } catch (error) {
console.error('Error fetching posts:', error);
throw error; throw error;
} }
} }
@@ -72,9 +72,8 @@ export class Ghost {
public async getPostById(id: string): Promise<Post> { public async getPostById(id: string): Promise<Post> {
try { try {
const postData = await this.contentApi.posts.read({ id }); const postData = await this.contentApi.posts.read({ id });
return new Post(postData, this.adminApi); return new Post(this, postData);
} catch (error) { } catch (error) {
console.error(`Error fetching post with id ${id}:`, error);
throw error; throw error;
} }
} }
@@ -82,9 +81,8 @@ export class Ghost {
public async createPost(postData: IPost): Promise<Post> { public async createPost(postData: IPost): Promise<Post> {
try { try {
const createdPostData = await this.adminApi.posts.add(postData); const createdPostData = await this.adminApi.posts.add(postData);
return new Post(createdPostData, this.adminApi); return new Post(this, createdPostData);
} catch (error) { } catch (error) {
console.error('Error creating post:', error);
throw error; throw error;
} }
} }
@@ -97,29 +95,55 @@ export class Ghost {
return new Post(this, postData); return new Post(this, postData);
} }
public async getTags(optionsArg?: { filter?: string; limit?: number }): Promise<ITag[]> { public async getTags(optionsArg?: {
filter?: string;
limit?: number;
visibility?: 'public' | 'internal' | 'all';
include?: string;
}): Promise<ITag[]> {
try { try {
const limit = optionsArg?.limit || 1000; const limit = optionsArg?.limit || 1000;
const tagsData = await this.contentApi.tags.browse({ limit }); const visibility = optionsArg?.visibility || 'all';
// Use Admin API to get ALL tags including those with zero posts
const browseOptions: any = { limit };
// Add visibility filter if not 'all'
if (visibility !== 'all') {
browseOptions.filter = `visibility:${visibility}`;
}
if (optionsArg?.include) {
browseOptions.include = optionsArg.include;
}
const tagsData = await this.adminApi.tags.browse(browseOptions);
// Apply minimatch filter if provided
if (optionsArg?.filter) { if (optionsArg?.filter) {
const matcher = new plugins.smartmatch.SmartMatch(optionsArg.filter); const matcher = new plugins.smartmatch.SmartMatch(optionsArg.filter);
return tagsData.filter((tag: ITag) => matcher.match(tag.slug)); return tagsData.filter((tag: ITag) => matcher.match(tag.slug));
} }
return tagsData; return tagsData;
} catch (error) { } catch (error) {
console.error('Error fetching tags:', error);
throw error; throw error;
} }
} }
public async getPublicTags(optionsArg?: { filter?: string; limit?: number }): Promise<ITag[]> {
return this.getTags({ ...optionsArg, visibility: 'public' });
}
public async getInternalTags(optionsArg?: { filter?: string; limit?: number }): Promise<ITag[]> {
return this.getTags({ ...optionsArg, visibility: 'internal' });
}
public async getTagById(id: string): Promise<Tag> { public async getTagById(id: string): Promise<Tag> {
try { try {
const tagData = await this.contentApi.tags.read({ id }); const tagData = await this.contentApi.tags.read({ id });
return new Tag(this, tagData); return new Tag(this, tagData);
} catch (error) { } catch (error) {
console.error(`Error fetching tag with id ${id}:`, error);
throw error; throw error;
} }
} }
@@ -129,7 +153,6 @@ export class Ghost {
const tagData = await this.contentApi.tags.read({ slug }); const tagData = await this.contentApi.tags.read({ slug });
return new Tag(this, tagData); return new Tag(this, tagData);
} catch (error) { } catch (error) {
console.error(`Error fetching tag with slug ${slug}:`, error);
throw error; throw error;
} }
} }
@@ -139,7 +162,6 @@ export class Ghost {
const createdTagData = await this.adminApi.tags.add(tagData); const createdTagData = await this.adminApi.tags.add(tagData);
return new Tag(this, createdTagData); return new Tag(this, createdTagData);
} catch (error) { } catch (error) {
console.error('Error creating tag:', error);
throw error; throw error;
} }
} }
@@ -158,7 +180,6 @@ export class Ghost {
return authorsData.map((author: IAuthor) => new Author(this, author)); return authorsData.map((author: IAuthor) => new Author(this, author));
} catch (error) { } catch (error) {
console.error('Error fetching authors:', error);
throw error; throw error;
} }
} }
@@ -168,7 +189,6 @@ export class Ghost {
const authorData = await this.contentApi.authors.read({ id }); const authorData = await this.contentApi.authors.read({ id });
return new Author(this, authorData); return new Author(this, authorData);
} catch (error) { } catch (error) {
console.error(`Error fetching author with id ${id}:`, error);
throw error; throw error;
} }
} }
@@ -178,7 +198,6 @@ export class Ghost {
const authorData = await this.contentApi.authors.read({ slug }); const authorData = await this.contentApi.authors.read({ slug });
return new Author(this, authorData); return new Author(this, authorData);
} catch (error) { } catch (error) {
console.error(`Error fetching author with slug ${slug}:`, error);
throw error; throw error;
} }
} }
@@ -197,7 +216,6 @@ export class Ghost {
return pagesData.map((pageData: IPage) => new Page(this, pageData)); return pagesData.map((pageData: IPage) => new Page(this, pageData));
} catch (error) { } catch (error) {
console.error('Error fetching pages:', error);
throw error; throw error;
} }
} }
@@ -207,7 +225,6 @@ export class Ghost {
const pageData = await this.contentApi.pages.read({ id }); const pageData = await this.contentApi.pages.read({ id });
return new Page(this, pageData); return new Page(this, pageData);
} catch (error) { } catch (error) {
console.error(`Error fetching page with id ${id}:`, error);
throw error; throw error;
} }
} }
@@ -217,7 +234,6 @@ export class Ghost {
const pageData = await this.contentApi.pages.read({ slug }); const pageData = await this.contentApi.pages.read({ slug });
return new Page(this, pageData); return new Page(this, pageData);
} catch (error) { } catch (error) {
console.error(`Error fetching page with slug ${slug}:`, error);
throw error; throw error;
} }
} }
@@ -227,7 +243,6 @@ export class Ghost {
const createdPageData = await this.adminApi.pages.add(pageData); const createdPageData = await this.adminApi.pages.add(pageData);
return new Page(this, createdPageData); return new Page(this, createdPageData);
} catch (error) { } catch (error) {
console.error('Error creating page:', error);
throw error; throw error;
} }
} }
@@ -242,7 +257,6 @@ export class Ghost {
}); });
return postsData.map((postData: IPost) => new Post(this, postData)); return postsData.map((postData: IPost) => new Post(this, postData));
} catch (error) { } catch (error) {
console.error('Error searching posts:', error);
throw error; throw error;
} }
} }
@@ -252,7 +266,6 @@ export class Ghost {
const result = await this.adminApi.images.upload({ file: filePath }); const result = await this.adminApi.images.upload({ file: filePath });
return result.url; return result.url;
} catch (error) { } catch (error) {
console.error('Error uploading image:', error);
throw error; throw error;
} }
} }
@@ -265,7 +278,6 @@ export class Ghost {
}); });
return await Promise.all(updatePromises); return await Promise.all(updatePromises);
} catch (error) { } catch (error) {
console.error('Error bulk updating posts:', error);
throw error; throw error;
} }
} }
@@ -278,7 +290,6 @@ export class Ghost {
}); });
await Promise.all(deletePromises); await Promise.all(deletePromises);
} catch (error) { } catch (error) {
console.error('Error bulk deleting posts:', error);
throw error; throw error;
} }
} }
@@ -301,7 +312,82 @@ export class Ghost {
return postsData.map((postData: IPost) => new Post(this, postData)); return postsData.map((postData: IPost) => new Post(this, postData));
} catch (error) { } catch (error) {
console.error('Error fetching related posts:', error); throw error;
}
}
public async getMembers(optionsArg?: { filter?: string; limit?: number }): Promise<Member[]> {
try {
const limit = optionsArg?.limit || 1000;
const membersData = await this.adminApi.members.browse({ limit });
if (optionsArg?.filter) {
const matcher = new plugins.smartmatch.SmartMatch(optionsArg.filter);
return membersData
.filter((member: IMember) => matcher.match(member.email))
.map((member: IMember) => new Member(this, member));
}
return membersData.map((member: IMember) => new Member(this, member));
} catch (error) {
throw error;
}
}
public async getMemberById(id: string): Promise<Member> {
try {
const memberData = await this.adminApi.members.read({ id });
return new Member(this, memberData);
} catch (error) {
throw error;
}
}
public async getMemberByEmail(email: string): Promise<Member> {
try {
const memberData = await this.adminApi.members.read({ email });
return new Member(this, memberData);
} catch (error) {
throw error;
}
}
public async createMember(memberData: Partial<IMember>): Promise<Member> {
try {
const createdMemberData = await this.adminApi.members.add(memberData);
return new Member(this, createdMemberData);
} catch (error) {
throw error;
}
}
public async createWebhook(webhookData: {
event: string;
target_url: string;
name?: string;
secret?: string;
api_version?: string;
integration_id?: string;
}): Promise<any> {
try {
return await this.adminApi.webhooks.add(webhookData);
} catch (error) {
throw error;
}
}
public async updateWebhook(id: string, webhookData: any): Promise<any> {
try {
return await this.adminApi.webhooks.edit({ ...webhookData, id });
} catch (error) {
throw error;
}
}
public async deleteWebhook(id: string): Promise<void> {
try {
await this.adminApi.webhooks.delete({ id });
} catch (error) {
throw error; throw error;
} }
} }

85
ts/classes.member.ts Normal file
View File

@@ -0,0 +1,85 @@
import type { Ghost } from './classes.ghost.js';
export interface IMember {
id: string;
uuid: string;
email: string;
name?: string;
note?: string;
geolocation?: string;
enable_comment_notifications?: boolean;
subscribed?: boolean;
email_count?: number;
email_opened_count?: number;
email_open_rate?: number;
status?: string;
created_at: string;
updated_at: string;
labels?: Array<{
id: string;
name: string;
slug: string;
}>;
subscriptions?: any[];
avatar_image?: string;
comped?: boolean;
email_suppression?: {
suppressed: boolean;
info?: string;
};
}
export class Member {
public ghostInstanceRef: Ghost;
public memberData: IMember;
constructor(ghostInstanceRefArg: Ghost, memberData: IMember) {
this.ghostInstanceRef = ghostInstanceRefArg;
this.memberData = memberData;
}
public getId(): string {
return this.memberData.id;
}
public getEmail(): string {
return this.memberData.email;
}
public getName(): string | undefined {
return this.memberData.name;
}
public getStatus(): string | undefined {
return this.memberData.status;
}
public getLabels(): Array<{ id: string; name: string; slug: string }> | undefined {
return this.memberData.labels;
}
public toJson(): IMember {
return this.memberData;
}
public async update(memberData: Partial<IMember>): Promise<Member> {
try {
const updatedMemberData = await this.ghostInstanceRef.adminApi.members.edit({
...memberData,
id: this.getId()
});
this.memberData = updatedMemberData;
return this;
} catch (error) {
throw error;
}
}
public async delete(): Promise<void> {
try {
await this.ghostInstanceRef.adminApi.members.delete({ id: this.getId() });
} catch (error) {
throw error;
}
}
}

View File

@@ -49,7 +49,6 @@ export class Page {
this.pageData = updatedPageData; this.pageData = updatedPageData;
return this; return this;
} catch (error) { } catch (error) {
console.error('Error updating page:', error);
throw error; throw error;
} }
} }
@@ -58,7 +57,6 @@ export class Page {
try { try {
await this.ghostInstanceRef.adminApi.pages.delete({ id: this.getId() }); await this.ghostInstanceRef.adminApi.pages.delete({ id: this.getId() });
} catch (error) { } catch (error) {
console.error(`Error deleting page with id ${this.getId()}:`, error);
throw error; throw error;
} }
} }

View File

@@ -118,13 +118,37 @@ export class Post {
return this.postData; return this.postData;
} }
public async update(postData: IPost): Promise<Post> { public async update(postData: Partial<IPost>): Promise<Post> {
try { try {
const updatedPostData = await this.ghostInstanceRef.adminApi.posts.edit(postData); // Only send fields that should be updated, not the entire post object with nested relations
const updatePayload: any = {
id: this.postData.id,
updated_at: this.postData.updated_at, // Required for conflict detection
...postData
};
// Remove read-only or computed fields that shouldn't be sent
delete updatePayload.uuid;
delete updatePayload.comment_id;
delete updatePayload.url;
delete updatePayload.excerpt;
delete updatePayload.reading_time;
delete updatePayload.created_at; // Don't send created_at in updates
delete updatePayload.primary_author;
delete updatePayload.primary_tag;
delete updatePayload.count;
delete updatePayload.email;
delete updatePayload.newsletter;
// Remove nested objects if they're not being updated
if (!postData.authors) delete updatePayload.authors;
if (!postData.tags) delete updatePayload.tags;
if (!postData.tiers) delete updatePayload.tiers;
const updatedPostData = await this.ghostInstanceRef.adminApi.posts.edit(updatePayload);
this.postData = updatedPostData; this.postData = updatedPostData;
return this; return this;
} catch (error) { } catch (error) {
console.error('Error updating post:', error);
throw error; throw error;
} }
} }
@@ -133,7 +157,6 @@ export class Post {
try { try {
await this.ghostInstanceRef.adminApi.posts.delete({ id: this.getId() }); await this.ghostInstanceRef.adminApi.posts.delete({ id: this.getId() });
} catch (error) { } catch (error) {
console.error(`Error deleting post with id ${this.getId()}:`, error);
throw error; throw error;
} }
} }

View File

@@ -0,0 +1,421 @@
import * as plugins from './ghost.plugins.js';
import { Ghost } from './classes.ghost.js';
import { type IPost } from './classes.post.js';
import { type IPage } from './classes.page.js';
import { type ITag } from './classes.post.js';
export interface ISyncOptions {
incremental?: boolean;
filter?: string;
dryRun?: boolean;
}
export interface ISyncItemResult {
sourceId: string;
sourceSlug: string;
targetId?: string;
status: 'created' | 'updated' | 'skipped' | 'failed';
error?: string;
}
export interface ISyncTargetReport {
targetUrl: string;
results: ISyncItemResult[];
successCount: number;
failureCount: number;
}
export interface ISyncReport {
contentType: 'posts' | 'pages' | 'tags';
totalItems: number;
targetReports: ISyncTargetReport[];
duration: number;
timestamp: Date;
}
export interface ISyncMapping {
sourceId: string;
sourceSlug: string;
targetMappings: Array<{
targetUrl: string;
targetId: string;
lastSynced: Date;
}>;
}
export class SyncedInstance {
public sourceGhost: Ghost;
public targetGhosts: Ghost[];
private syncMappings: Map<string, ISyncMapping>;
private syncHistory: ISyncReport[];
constructor(sourceGhost: Ghost, targetGhosts: Ghost[]) {
// Validate that no target instance is the same as the source instance
const sourceUrl = sourceGhost.options.baseUrl.replace(/\/$/, '').toLowerCase();
for (const targetGhost of targetGhosts) {
const targetUrl = targetGhost.options.baseUrl.replace(/\/$/, '').toLowerCase();
if (sourceUrl === targetUrl) {
throw new Error(
`Cannot sync to the same instance. Source and target both point to: ${sourceUrl}. ` +
`This would create a circular sync and cause excessive API calls.`
);
}
}
this.sourceGhost = sourceGhost;
this.targetGhosts = targetGhosts;
this.syncMappings = new Map();
this.syncHistory = [];
}
private addMapping(contentType: string, sourceId: string, sourceSlug: string, targetUrl: string, targetId: string) {
const key = `${contentType}:${sourceId}`;
const existing = this.syncMappings.get(key);
if (existing) {
const targetMapping = existing.targetMappings.find(tm => tm.targetUrl === targetUrl);
if (targetMapping) {
targetMapping.targetId = targetId;
targetMapping.lastSynced = new Date();
} else {
existing.targetMappings.push({
targetUrl,
targetId,
lastSynced: new Date()
});
}
} else {
this.syncMappings.set(key, {
sourceId,
sourceSlug,
targetMappings: [{
targetUrl,
targetId,
lastSynced: new Date()
}]
});
}
}
private getMapping(contentType: string, sourceId: string, targetUrl: string): string | undefined {
const key = `${contentType}:${sourceId}`;
const mapping = this.syncMappings.get(key);
if (!mapping) return undefined;
const targetMapping = mapping.targetMappings.find(tm => tm.targetUrl === targetUrl);
return targetMapping?.targetId;
}
public async syncTags(optionsArg?: ISyncOptions): Promise<ISyncReport> {
const startTime = Date.now();
const report: ISyncReport = {
contentType: 'tags',
totalItems: 0,
targetReports: [],
duration: 0,
timestamp: new Date()
};
const sourceTags = await this.sourceGhost.getTags(optionsArg?.filter ? { filter: optionsArg.filter } : {});
report.totalItems = sourceTags.length;
for (const targetGhost of this.targetGhosts) {
const targetReport: ISyncTargetReport = {
targetUrl: targetGhost.options.baseUrl,
results: [],
successCount: 0,
failureCount: 0
};
for (const sourceTag of sourceTags) {
try {
let targetTag: any;
let status: 'created' | 'updated' | 'skipped' = 'created';
try {
targetTag = await targetGhost.getTagBySlug(sourceTag.slug);
if (!optionsArg?.dryRun) {
await targetTag.update({
name: sourceTag.name,
slug: sourceTag.slug,
description: sourceTag.description,
feature_image: sourceTag.feature_image,
visibility: sourceTag.visibility,
meta_title: sourceTag.meta_title,
meta_description: sourceTag.meta_description
});
}
status = 'updated';
} catch (error) {
if (!optionsArg?.dryRun) {
targetTag = await targetGhost.createTag({
name: sourceTag.name,
slug: sourceTag.slug,
description: sourceTag.description,
feature_image: sourceTag.feature_image,
visibility: sourceTag.visibility,
meta_title: sourceTag.meta_title,
meta_description: sourceTag.meta_description
});
}
}
if (!optionsArg?.dryRun && targetTag) {
this.addMapping('tags', sourceTag.id, sourceTag.slug, targetGhost.options.baseUrl, targetTag.tagData.id);
}
targetReport.results.push({
sourceId: sourceTag.id,
sourceSlug: sourceTag.slug,
targetId: targetTag?.tagData?.id,
status
});
targetReport.successCount++;
} catch (error) {
targetReport.results.push({
sourceId: sourceTag.id,
sourceSlug: sourceTag.slug,
status: 'failed',
error: error instanceof Error ? error.message : String(error)
});
targetReport.failureCount++;
}
}
report.targetReports.push(targetReport);
}
report.duration = Date.now() - startTime;
this.syncHistory.push(report);
return report;
}
public async syncPosts(optionsArg?: ISyncOptions): Promise<ISyncReport> {
const startTime = Date.now();
const report: ISyncReport = {
contentType: 'posts',
totalItems: 0,
targetReports: [],
duration: 0,
timestamp: new Date()
};
const sourcePosts = await this.sourceGhost.getPosts(optionsArg?.filter ? { filter: optionsArg.filter } : {});
report.totalItems = sourcePosts.length;
for (const targetGhost of this.targetGhosts) {
const targetReport: ISyncTargetReport = {
targetUrl: targetGhost.options.baseUrl,
results: [],
successCount: 0,
failureCount: 0
};
for (const sourcePost of sourcePosts) {
try {
const postData = sourcePost.postData;
const tagSlugs = postData.tags?.map(t => t.slug) || [];
const targetTagIds: string[] = [];
for (const tagSlug of tagSlugs) {
try {
const targetTag = await targetGhost.getTagBySlug(tagSlug);
targetTagIds.push(targetTag.tagData.id);
} catch (error) {
}
}
const syncData: any = {
title: postData.title,
slug: postData.slug,
html: postData.html,
feature_image: postData.feature_image,
featured: postData.featured,
status: postData.status,
visibility: postData.visibility,
meta_title: postData.meta_title,
meta_description: postData.meta_description,
published_at: postData.published_at,
custom_excerpt: postData.custom_excerpt,
tags: targetTagIds.length > 0 ? targetTagIds.map(id => ({ id })) : undefined
};
let targetPost: any;
let status: 'created' | 'updated' = 'created';
try {
targetPost = await targetGhost.contentApi.posts.read({ slug: postData.slug }, { formats: ['html'] });
if (!optionsArg?.dryRun) {
const updated = await targetGhost.adminApi.posts.edit({
...syncData,
id: targetPost.id,
updated_at: targetPost.updated_at
});
targetPost = updated;
}
status = 'updated';
} catch (error) {
if (!optionsArg?.dryRun) {
targetPost = await targetGhost.adminApi.posts.add(syncData);
}
}
if (!optionsArg?.dryRun && targetPost) {
this.addMapping('posts', postData.id, postData.slug, targetGhost.options.baseUrl, targetPost.id);
}
targetReport.results.push({
sourceId: postData.id,
sourceSlug: postData.slug,
targetId: targetPost?.id,
status
});
targetReport.successCount++;
} catch (error) {
targetReport.results.push({
sourceId: sourcePost.postData.id,
sourceSlug: sourcePost.postData.slug,
status: 'failed',
error: error instanceof Error ? error.message : String(error)
});
targetReport.failureCount++;
}
}
report.targetReports.push(targetReport);
}
report.duration = Date.now() - startTime;
this.syncHistory.push(report);
return report;
}
public async syncPages(optionsArg?: ISyncOptions): Promise<ISyncReport> {
const startTime = Date.now();
const report: ISyncReport = {
contentType: 'pages',
totalItems: 0,
targetReports: [],
duration: 0,
timestamp: new Date()
};
const sourcePages = await this.sourceGhost.getPages(optionsArg?.filter ? { filter: optionsArg.filter } : {});
report.totalItems = sourcePages.length;
for (const targetGhost of this.targetGhosts) {
const targetReport: ISyncTargetReport = {
targetUrl: targetGhost.options.baseUrl,
results: [],
successCount: 0,
failureCount: 0
};
for (const sourcePage of sourcePages) {
try {
const pageData = sourcePage.pageData;
const syncData: Partial<IPage> = {
title: pageData.title,
slug: pageData.slug,
html: pageData.html,
feature_image: pageData.feature_image,
featured: pageData.featured,
status: pageData.status,
visibility: pageData.visibility,
meta_title: pageData.meta_title,
meta_description: pageData.meta_description,
published_at: pageData.published_at,
custom_excerpt: pageData.custom_excerpt
};
let targetPage: any;
let status: 'created' | 'updated' = 'created';
try {
targetPage = await targetGhost.contentApi.pages.read({ slug: pageData.slug }, { formats: ['html'] });
if (!optionsArg?.dryRun) {
const updated = await targetGhost.adminApi.pages.edit({
...syncData,
id: targetPage.id,
updated_at: targetPage.updated_at
});
targetPage = updated;
}
status = 'updated';
} catch (error) {
if (!optionsArg?.dryRun) {
targetPage = await targetGhost.adminApi.pages.add(syncData);
}
}
if (!optionsArg?.dryRun && targetPage) {
this.addMapping('pages', pageData.id, pageData.slug, targetGhost.options.baseUrl, targetPage.id);
}
targetReport.results.push({
sourceId: pageData.id,
sourceSlug: pageData.slug,
targetId: targetPage?.id,
status
});
targetReport.successCount++;
} catch (error) {
targetReport.results.push({
sourceId: sourcePage.pageData.id,
sourceSlug: sourcePage.pageData.slug,
status: 'failed',
error: error instanceof Error ? error.message : String(error)
});
targetReport.failureCount++;
}
}
report.targetReports.push(targetReport);
}
report.duration = Date.now() - startTime;
this.syncHistory.push(report);
return report;
}
public async syncAll(optionsArg?: { types?: Array<'posts' | 'pages' | 'tags'>; syncOptions?: ISyncOptions }): Promise<ISyncReport[]> {
const types = optionsArg?.types || ['tags', 'posts', 'pages'];
const reports: ISyncReport[] = [];
for (const type of types) {
if (type === 'tags') {
reports.push(await this.syncTags(optionsArg?.syncOptions));
} else if (type === 'posts') {
reports.push(await this.syncPosts(optionsArg?.syncOptions));
} else if (type === 'pages') {
reports.push(await this.syncPages(optionsArg?.syncOptions));
}
}
return reports;
}
public getSyncStatus(): {
totalMappings: number;
mappings: ISyncMapping[];
recentSyncs: ISyncReport[];
} {
return {
totalMappings: this.syncMappings.size,
mappings: Array.from(this.syncMappings.values()),
recentSyncs: this.syncHistory.slice(-10)
};
}
public clearSyncHistory(): void {
this.syncHistory = [];
}
public clearMappings(): void {
this.syncMappings.clear();
}
}

View File

@@ -26,6 +26,18 @@ export class Tag {
return this.tagData.description; return this.tagData.description;
} }
public getVisibility(): string {
return this.tagData.visibility;
}
public isInternal(): boolean {
return this.tagData.visibility === 'internal';
}
public isPublic(): boolean {
return this.tagData.visibility === 'public';
}
public toJson(): ITag { public toJson(): ITag {
return this.tagData; return this.tagData;
} }
@@ -39,7 +51,6 @@ export class Tag {
this.tagData = updatedTagData; this.tagData = updatedTagData;
return this; return this;
} catch (error) { } catch (error) {
console.error('Error updating tag:', error);
throw error; throw error;
} }
} }
@@ -48,7 +59,6 @@ export class Tag {
try { try {
await this.ghostInstanceRef.adminApi.tags.delete({ id: this.getId() }); await this.ghostInstanceRef.adminApi.tags.delete({ id: this.getId() });
} catch (error) { } catch (error) {
console.error(`Error deleting tag with id ${this.getId()}:`, error);
throw error; throw error;
} }
} }

View File

@@ -1,5 +1,5 @@
import GhostContentAPI from '@tryghost/content-api'; import { GhostContentAPI } from './apiclient/ghost.contentapi.js';
import GhostAdminAPI from '@tryghost/admin-api'; import { GhostAdminAPI } from './apiclient/ghost.adminapi.js';
import * as smartmatch from '@push.rocks/smartmatch'; import * as smartmatch from '@push.rocks/smartmatch';
export { export {

View File

@@ -2,4 +2,6 @@ export * from './classes.ghost.js';
export * from './classes.post.js'; export * from './classes.post.js';
export * from './classes.author.js'; export * from './classes.author.js';
export * from './classes.tag.js'; export * from './classes.tag.js';
export * from './classes.page.js'; export * from './classes.page.js';
export * from './classes.member.js';
export * from './classes.syncedinstance.js';