@sanoma-ai/connector-ghost 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +105 -0
- package/README.md +121 -2
- package/dist/connector.d.ts +67 -0
- package/dist/connector.js +43 -0
- package/dist/connector.js.map +1 -0
- package/dist/driver.d.ts +11 -0
- package/dist/driver.js +151 -0
- package/dist/driver.js.map +1 -0
- package/dist/fake.d.ts +18 -0
- package/dist/fake.js +43 -0
- package/dist/fake.js.map +1 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/steps.d.ts +6 -0
- package/dist/steps.js +27 -0
- package/dist/steps.js.map +1 -0
- package/features/post.feature +33 -0
- package/package.json +63 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Functional Source License, Version 1.1, ALv2 Future License
|
|
2
|
+
|
|
3
|
+
## Abbreviation
|
|
4
|
+
|
|
5
|
+
FSL-1.1-ALv2
|
|
6
|
+
|
|
7
|
+
## Notice
|
|
8
|
+
|
|
9
|
+
Copyright 2026 Sanoma-AI
|
|
10
|
+
|
|
11
|
+
## Terms and Conditions
|
|
12
|
+
|
|
13
|
+
### Licensor ("We")
|
|
14
|
+
|
|
15
|
+
The party offering the Software under these Terms and Conditions.
|
|
16
|
+
|
|
17
|
+
### The Software
|
|
18
|
+
|
|
19
|
+
The "Software" is each version of the software that we make available under
|
|
20
|
+
these Terms and Conditions, as indicated by our inclusion of these Terms and
|
|
21
|
+
Conditions with the Software.
|
|
22
|
+
|
|
23
|
+
### License Grant
|
|
24
|
+
|
|
25
|
+
Subject to your compliance with this License Grant and the Patents,
|
|
26
|
+
Redistribution and Trademark clauses below, we hereby grant you the right to
|
|
27
|
+
use, copy, modify, create derivative works, publicly perform, publicly display
|
|
28
|
+
and redistribute the Software for any Permitted Purpose identified below.
|
|
29
|
+
|
|
30
|
+
### Permitted Purpose
|
|
31
|
+
|
|
32
|
+
A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
|
|
33
|
+
means making the Software available to others in a commercial product or
|
|
34
|
+
service that:
|
|
35
|
+
|
|
36
|
+
1. substitutes for the Software;
|
|
37
|
+
|
|
38
|
+
2. substitutes for any other product or service we offer using the Software
|
|
39
|
+
that exists as of the date we make the Software available; or
|
|
40
|
+
|
|
41
|
+
3. offers the same or substantially similar functionality as the Software.
|
|
42
|
+
|
|
43
|
+
Permitted Purposes specifically include using the Software:
|
|
44
|
+
|
|
45
|
+
1. for your internal use and access;
|
|
46
|
+
|
|
47
|
+
2. for non-commercial education;
|
|
48
|
+
|
|
49
|
+
3. for non-commercial research; and
|
|
50
|
+
|
|
51
|
+
4. in connection with professional services that you provide to a licensee
|
|
52
|
+
using the Software in accordance with these Terms and Conditions.
|
|
53
|
+
|
|
54
|
+
### Patents
|
|
55
|
+
|
|
56
|
+
To the extent your use for a Permitted Purpose would necessarily infringe our
|
|
57
|
+
patents, the license grant above includes a license under our patents. If you
|
|
58
|
+
make a claim against any party that the Software infringes or contributes to
|
|
59
|
+
the infringement of any patent, then your patent license to the Software ends
|
|
60
|
+
immediately.
|
|
61
|
+
|
|
62
|
+
### Redistribution
|
|
63
|
+
|
|
64
|
+
The Terms and Conditions apply to all copies, modifications and derivatives of
|
|
65
|
+
the Software.
|
|
66
|
+
|
|
67
|
+
If you redistribute any copies, modifications or derivatives of the Software,
|
|
68
|
+
you must include a copy of or a link to these Terms and Conditions and not
|
|
69
|
+
remove any copyright notices provided in or with the Software.
|
|
70
|
+
|
|
71
|
+
### Disclaimer
|
|
72
|
+
|
|
73
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
|
74
|
+
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
|
|
75
|
+
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
|
|
76
|
+
|
|
77
|
+
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
|
|
78
|
+
SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
|
|
79
|
+
EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
|
|
80
|
+
|
|
81
|
+
### Trademarks
|
|
82
|
+
|
|
83
|
+
Except for displaying the License Details and identifying us as the origin of
|
|
84
|
+
the Software, you have no right under these Terms and Conditions to use our
|
|
85
|
+
trademarks, trade names, service marks or product names.
|
|
86
|
+
|
|
87
|
+
## Grant of Future License
|
|
88
|
+
|
|
89
|
+
We hereby irrevocably grant you an additional license to use the Software under
|
|
90
|
+
the Apache License, Version 2.0 that is effective on the second anniversary of
|
|
91
|
+
the date we make the Software available. On or after that date, you may use the
|
|
92
|
+
Software under the Apache License, Version 2.0, in which case the following
|
|
93
|
+
will apply:
|
|
94
|
+
|
|
95
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
96
|
+
this file except in compliance with the License.
|
|
97
|
+
|
|
98
|
+
You may obtain a copy of the License at
|
|
99
|
+
|
|
100
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
101
|
+
|
|
102
|
+
Unless required by applicable law or agreed to in writing, software distributed
|
|
103
|
+
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
104
|
+
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
105
|
+
specific language governing permissions and limitations under the License.
|
package/README.md
CHANGED
|
@@ -1,3 +1,122 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @sanoma-ai/connector-ghost
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[Ghost Admin API](https://ghost.org/docs/admin-api/) operations for [`@sanoma-ai/workflows`](https://www.npmjs.com/package/@sanoma-ai/workflows).
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @sanoma-ai/connector-ghost
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
| Operation | Effect | What it does |
|
|
12
|
+
| -------------------- | --------- | -------------------------------------------------------- |
|
|
13
|
+
| `ghost.post.create` | `write` | Create a draft post. Drafts are not visible on the site. |
|
|
14
|
+
| `ghost.post.publish` | `publish` | Publish a draft post on the site. Idempotent. |
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { ghost } from "@sanoma-ai/connector-ghost";
|
|
18
|
+
import { defineWorkflow } from "@sanoma-ai/workflows";
|
|
19
|
+
|
|
20
|
+
defineWorkflow({
|
|
21
|
+
// ...
|
|
22
|
+
uses: [ghost.post.create, ghost.post.publish],
|
|
23
|
+
run: async (ctx, input) => {
|
|
24
|
+
const post = await ctx.ghost.post.create({ title: input.title, html: input.body, status: "draft" });
|
|
25
|
+
return ctx.ghost.post.publish({ id: post.id });
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The logo the app shows beside these operations is Ghost's glyph from [simple-icons](https://simpleicons.org/?q=ghost) (CC0), in near-black or white. It is there only to identify the service an operation calls. Ghost is a trademark of Ghost Foundation.
|
|
31
|
+
|
|
32
|
+
## Driver
|
|
33
|
+
|
|
34
|
+
`@sanoma-ai/connector-ghost/driver` calls the [Ghost Admin API](https://docs.ghost.org/admin-api/). The `ghost` connector the package root exports carries it, made once per resolved config, and its fake (`withSetup` from `@sanoma-ai/workflows`), so a config names the connector alone and workflows never import the driver (the lint refuses that):
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { ghost } from "@sanoma-ai/connector-ghost";
|
|
38
|
+
|
|
39
|
+
export default defineConfig({ workflows, connectors: [ghost], policy });
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`./driver` (`ghostDriver`; `ghostDriver({ timeoutMs })`) and `./fake` (`fakeGhost`) are internal entries, for tests.
|
|
43
|
+
|
|
44
|
+
- `post.create` posts the HTML with `?source=html`, which Ghost converts to its editor format (lossy; see [Creating a post](https://docs.ghost.org/admin-api/posts/creating-a-post)). A draft's `url` is its preview link; it becomes the public URL when published.
|
|
45
|
+
- `post.publish` reads the post, then saves it with `status: "published"` and the `updated_at` it read, which Ghost requires to refuse a save over someone else's. If someone saved the post in between (`UPDATE_COLLISION`), it reads the post again and tries once more. Publishing sends no newsletter email: Ghost emails a post only when a `newsletter` is named.
|
|
46
|
+
|
|
47
|
+
### Environment
|
|
48
|
+
|
|
49
|
+
It reads two environment variables on every call, never when the config loads:
|
|
50
|
+
|
|
51
|
+
| Variable | What it is |
|
|
52
|
+
| --------------------- | --------------------------------------------------------------------------------------------- |
|
|
53
|
+
| `GHOST_ADMIN_URL` | The site's admin URL, such as `https://example.ghost.io` (on Ghost(Pro), the `ghost.io` one). |
|
|
54
|
+
| `GHOST_ADMIN_API_KEY` | A custom integration's Admin API key, `<id>:<secret>`. |
|
|
55
|
+
|
|
56
|
+
To make the key: in Ghost Admin, **Settings → Integrations → Add custom integration**, name it, and copy its **Admin API key** (not the Content API key). A missing or malformed variable fails the call, naming the variable, and is not retried. The driver declares them in its `env` ([Credentials](https://www.npmjs.com/package/@sanoma-ai/workflows#credentials)), so a worker refuses to start while one is missing or malformed, and the app's connector page shows each one's status and lets you set it.
|
|
57
|
+
|
|
58
|
+
### Idempotency
|
|
59
|
+
|
|
60
|
+
Ghost takes no idempotency key, and the runtime never retries `post.create`. If its reply is lost, the run fails and a draft stays in Ghost; the next run makes a new one. A draft is not visible on the site; delete it in Ghost Admin. `post.publish` returns a post that is already published as it is, without saving it, so a retry after a lost reply changes nothing; so too a post Ghost sent as an email only (`status: "sent"`), which is not a draft.
|
|
61
|
+
|
|
62
|
+
### Errors and retries
|
|
63
|
+
|
|
64
|
+
A timeout (10 s; `ghostDriver({ timeoutMs })` changes it), a network failure, a 408, a 429 or a 5xx is retryable; any other 4xx is not. A failure carries Ghost's HTTP `status` and, as `vendorCode`, its error `code` when it has one (`UPDATE_COLLISION`) or else its `type` (`ValidationError`, `UnauthorizedError`). Ghost documents no rate limit for the Admin API; Ghost(Pro) may answer 429 to protect itself.
|
|
65
|
+
|
|
66
|
+
### Plan
|
|
67
|
+
|
|
68
|
+
You need a Ghost site of your own. Self-hosted Ghost is free and open source (`docker run -p 2368:2368 -e NODE_ENV=development ghost:6-alpine` runs one locally). Ghost(Pro) has a free trial but no free plan, and its Starter plan has no custom integrations or Admin API: you need Publisher or above ([pricing](https://ghost.org/pricing/)).
|
|
69
|
+
|
|
70
|
+
### Testing the driver
|
|
71
|
+
|
|
72
|
+
`test/driver.test.ts` replays Ghost's recorded replies (`test/fixtures`) with [`@sanoma-ai/testing/replay`](https://github.com/Sanoma-AI/sanoma/blob/main/packages/testing/README.md#replaying-a-vendors-api), which says what `SANOMA_LIVE` and `SANOMA_RECORD` do: `pnpm vitest run connectors/ghost` needs no site. Live, they create posts titled `sanoma test <timestamp>` on the site, publish some, and delete them at the end:
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
SANOMA_LIVE=1 GHOST_ADMIN_URL=https://example.ghost.io GHOST_ADMIN_API_KEY=<id>:<secret> pnpm vitest run connectors/ghost
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
A recording keeps only the fields the driver reads (no authors or emails), puts every URL on `https://blog.example.test`, and renumbers ids. The error cases only replay; `update-collision.json` was taken by hand from a local Ghost, since a test cannot make Ghost collide on cue.
|
|
79
|
+
|
|
80
|
+
## Steps and features
|
|
81
|
+
|
|
82
|
+
The connector carries steps for [scenarios](https://github.com/Sanoma-AI/sanoma/blob/main/packages/workflows/README.md#steps-a-connector-ships) (`ghostSteps`, attached with `withSetup`), which a company's scenario can use beside the generic steps and other connectors' ones:
|
|
83
|
+
|
|
84
|
+
| Keyword | Step | What it does |
|
|
85
|
+
| ------- | -------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
86
|
+
| Given | `a Ghost draft titled {string}` | Seeds a draft through `ghost.post.create`, named `draft` (`$draft.id` in an operation's own test) |
|
|
87
|
+
| Then | `the Ghost post {string} is published` | The post with that title, seeded or created by the run, was published |
|
|
88
|
+
|
|
89
|
+
`features/post.feature` creates and publishes posts, with these steps and the generic ones for an operation's own test, on the fake and on Ghost's recorded replies (`create.json`, `publish-draft.json` and `publish-again.json`): `test/features.test.ts` runs them with [`describeFeatures`](https://github.com/Sanoma-AI/sanoma/blob/main/packages/testing/README.md#a-connectors-own-features). A scenario tagged `@fake-only` has no recording. The package ships `features/`.
|
|
90
|
+
|
|
91
|
+
## Testing
|
|
92
|
+
|
|
93
|
+
`@sanoma-ai/connector-ghost/fake` is an in-memory Ghost, the fake the connector carries: each sandbox run makes one of its own. A test that reads or seeds one attaches it to the connector itself, then checks `state.posts` and `calls`.
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { withSetup } from "@sanoma-ai/workflows";
|
|
97
|
+
import { ghost } from "@sanoma-ai/connector-ghost";
|
|
98
|
+
import { fakeGhost } from "@sanoma-ai/connector-ghost/fake";
|
|
99
|
+
|
|
100
|
+
const fake = fakeGhost();
|
|
101
|
+
const worker = await startWorker({
|
|
102
|
+
connectors: [withSetup(ghost, { driver: () => fake.driver, fake: () => fake })],
|
|
103
|
+
/* ... */
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ...run a workflow, then:
|
|
107
|
+
Object.values(fake.state.posts).map((p) => p.status); // ["published"]
|
|
108
|
+
fake.calls.map((c) => c.op); // ["ghost.post.create", "ghost.post.publish"]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
A repeated idempotency key gets the first reply and changes nothing, as a real vendor that dedupes would. To test what a workflow does when Ghost misbehaves, set up the next call to an operation before the run makes it:
|
|
112
|
+
|
|
113
|
+
- `fake.failNext("ghost.post.publish", err?)` throws `err` (default: a retryable `DriverError`).
|
|
114
|
+
- `fake.loseReply("ghost.post.publish")` publishes, then throws, as if the reply was lost.
|
|
115
|
+
- `fake.rateLimit("ghost.post.publish")` throws a `DriverError` with status 429.
|
|
116
|
+
- `const release = fake.hold("ghost.post.publish")` makes the call wait until `release()`.
|
|
117
|
+
|
|
118
|
+
`fake.reset()` empties it between tests. Pass `{ calls }` with one array to several fakes to see their calls in one order.
|
|
119
|
+
|
|
120
|
+
[`@sanoma-ai/testing`](https://github.com/Sanoma-AI/sanoma/blob/main/packages/testing/README.md) (in the sanoma repository, not published) has `startTestWorker` and `testDatabaseUrl` to run a test's worker with the fake on a database of its own.
|
|
121
|
+
|
|
122
|
+
License: FSL-1.1-ALv2.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** A post, as the operations return it. */
|
|
3
|
+
export declare const Post: z.ZodObject<{
|
|
4
|
+
id: z.ZodString;
|
|
5
|
+
url: z.ZodString;
|
|
6
|
+
slug: z.ZodString;
|
|
7
|
+
status: z.ZodEnum<{
|
|
8
|
+
draft: "draft";
|
|
9
|
+
published: "published";
|
|
10
|
+
scheduled: "scheduled";
|
|
11
|
+
sent: "sent";
|
|
12
|
+
}>;
|
|
13
|
+
publishedAt: z.ZodNullable<z.ZodString>;
|
|
14
|
+
}, z.core.$strip>;
|
|
15
|
+
/** Ghost Admin API: https://ghost.org/docs/admin-api/ */
|
|
16
|
+
export declare const ghostOps: import("@sanoma-ai/workflows").Connector<"ghost", {
|
|
17
|
+
readonly post: {
|
|
18
|
+
readonly create: {
|
|
19
|
+
readonly effect: "write";
|
|
20
|
+
readonly description: "Create a post. Drafts are not visible on the site.";
|
|
21
|
+
readonly phrases: {
|
|
22
|
+
readonly given: "a post titled {title} exists";
|
|
23
|
+
readonly expect: "a post titled {title} is created";
|
|
24
|
+
};
|
|
25
|
+
readonly input: z.ZodObject<{
|
|
26
|
+
title: z.ZodString;
|
|
27
|
+
html: z.ZodString;
|
|
28
|
+
status: z.ZodDefault<z.ZodLiteral<"draft">>;
|
|
29
|
+
}, z.core.$strip>;
|
|
30
|
+
readonly output: z.ZodObject<{
|
|
31
|
+
id: z.ZodString;
|
|
32
|
+
url: z.ZodString;
|
|
33
|
+
slug: z.ZodString;
|
|
34
|
+
status: z.ZodEnum<{
|
|
35
|
+
draft: "draft";
|
|
36
|
+
published: "published";
|
|
37
|
+
scheduled: "scheduled";
|
|
38
|
+
sent: "sent";
|
|
39
|
+
}>;
|
|
40
|
+
publishedAt: z.ZodNullable<z.ZodString>;
|
|
41
|
+
}, z.core.$strip>;
|
|
42
|
+
};
|
|
43
|
+
readonly publish: {
|
|
44
|
+
readonly effect: "publish";
|
|
45
|
+
readonly description: "Publish a draft post on the site. Visible to everyone.";
|
|
46
|
+
readonly phrases: {
|
|
47
|
+
readonly expect: "post {id} is published";
|
|
48
|
+
};
|
|
49
|
+
readonly idempotent: true;
|
|
50
|
+
readonly input: z.ZodObject<{
|
|
51
|
+
id: z.ZodString;
|
|
52
|
+
}, z.core.$strip>;
|
|
53
|
+
readonly output: z.ZodObject<{
|
|
54
|
+
id: z.ZodString;
|
|
55
|
+
url: z.ZodString;
|
|
56
|
+
slug: z.ZodString;
|
|
57
|
+
status: z.ZodEnum<{
|
|
58
|
+
draft: "draft";
|
|
59
|
+
published: "published";
|
|
60
|
+
scheduled: "scheduled";
|
|
61
|
+
sent: "sent";
|
|
62
|
+
}>;
|
|
63
|
+
publishedAt: z.ZodNullable<z.ZodString>;
|
|
64
|
+
}, z.core.$strip>;
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
}>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { defineConnector } from "@sanoma-ai/workflows";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
/** A post, as the operations return it. */
|
|
4
|
+
export const Post = z.object({
|
|
5
|
+
id: z.string(),
|
|
6
|
+
url: z.string(),
|
|
7
|
+
slug: z.string(),
|
|
8
|
+
/** `sent`: emailed to a newsletter only, never on the site. */
|
|
9
|
+
status: z.enum(["draft", "scheduled", "published", "sent"]),
|
|
10
|
+
publishedAt: z.string().nullable(),
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* Ghost's glyph, from simple-icons (https://simpleicons.org/?q=ghost, CC0; drawn from Ghost's
|
|
14
|
+
* own admin icon), in one colour: Ghost's near-black, white on dark backgrounds. Ghost publishes
|
|
15
|
+
* no SVG of its own.
|
|
16
|
+
*/
|
|
17
|
+
const glyph = (fill) => `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="${fill}" d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm.256 2.313c2.47.005 5.116 2.008 5.898 2.962l.244.3c1.64 1.994 3.569 4.34 3.569 6.966 0 3.719-2.98 5.808-6.158 7.508-1.433.766-2.98 1.508-4.748 1.508-4.543 0-8.366-3.569-8.366-8.112 0-.706.17-1.425.342-2.15.122-.515.244-1.033.307-1.549.548-4.539 2.967-6.795 8.422-7.408a4.29 4.29 0 01.49-.026Z"/></svg>`;
|
|
18
|
+
/** Ghost Admin API: https://ghost.org/docs/admin-api/ */
|
|
19
|
+
export const ghostOps = defineConnector("ghost", {
|
|
20
|
+
post: {
|
|
21
|
+
create: {
|
|
22
|
+
effect: "write",
|
|
23
|
+
description: "Create a post. Drafts are not visible on the site.",
|
|
24
|
+
phrases: { given: "a post titled {title} exists", expect: "a post titled {title} is created" },
|
|
25
|
+
input: z.object({ title: z.string().min(1), html: z.string(), status: z.literal("draft").default("draft") }),
|
|
26
|
+
output: Post,
|
|
27
|
+
},
|
|
28
|
+
publish: {
|
|
29
|
+
effect: "publish",
|
|
30
|
+
description: "Publish a draft post on the site. Visible to everyone.",
|
|
31
|
+
phrases: { expect: "post {id} is published" },
|
|
32
|
+
idempotent: true,
|
|
33
|
+
input: z.object({ id: z.string() }),
|
|
34
|
+
output: Post,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
}, {
|
|
38
|
+
title: "Ghost",
|
|
39
|
+
logo: { svg: glyph("#15171A"), dark: glyph("white") },
|
|
40
|
+
package: "@sanoma-ai/connector-ghost",
|
|
41
|
+
homepage: "https://github.com/Sanoma-AI/sanoma/tree/main/connectors/ghost#readme",
|
|
42
|
+
});
|
|
43
|
+
//# sourceMappingURL=connector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connector.js","sourceRoot":"","sources":["../src/connector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,2CAA2C;AAC3C,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3B,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE;IACd,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE;IACf,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,+DAA+D;IAC/D,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,CAAC,CAAC;IAC3D,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACnC,CAAC,CAAC;AAEH;;;;GAIG;AACH,MAAM,KAAK,GAAG,CAAC,IAAY,EAAE,EAAE,CAC7B,2EAA2E,IAAI,4XAA4X,CAAC;AAE9c,yDAAyD;AACzD,MAAM,CAAC,MAAM,QAAQ,GAAG,eAAe,CACrC,OAAO,EACP;IACE,IAAI,EAAE;QACJ,MAAM,EAAE;YACN,MAAM,EAAE,OAAO;YACf,WAAW,EAAE,oDAAoD;YACjE,OAAO,EAAE,EAAE,KAAK,EAAE,8BAA8B,EAAE,MAAM,EAAE,kCAAkC,EAAE;YAC9F,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;YAC5G,MAAM,EAAE,IAAI;SACb;QACD,OAAO,EAAE;YACP,MAAM,EAAE,SAAS;YACjB,WAAW,EAAE,wDAAwD;YACrE,OAAO,EAAE,EAAE,MAAM,EAAE,wBAAwB,EAAE;YAC7C,UAAU,EAAE,IAAI;YAChB,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;YACnC,MAAM,EAAE,IAAI;SACb;KACF;CACF,EACD;IACE,KAAK,EAAE,OAAO;IACd,IAAI,EAAE,EAAE,GAAG,EAAE,KAAK,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,EAAE;IACrD,OAAO,EAAE,4BAA4B;IACrC,QAAQ,EAAE,uEAAuE;CAClF,CACF,CAAC"}
|
package/dist/driver.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface GhostDriverOptions {
|
|
2
|
+
/** How long one request to Ghost may take, in milliseconds. Default 10 000. */
|
|
3
|
+
timeoutMs?: number;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* A token for the Admin API, signed with an Admin API key (`<id>:<secret>`), as Ghost asks:
|
|
7
|
+
* HS256, for `/admin/`, valid five minutes, the longest Ghost accepts.
|
|
8
|
+
*/
|
|
9
|
+
export declare function adminToken(key: string): Promise<string>;
|
|
10
|
+
/** The real Ghost, through the Admin API, with `GHOST_ADMIN_URL` and `GHOST_ADMIN_API_KEY`. See the README. */
|
|
11
|
+
export declare function ghostDriver(options?: GhostDriverOptions): import("@sanoma-ai/workflows").Driver;
|
package/dist/driver.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { DriverError, defineDriver, retryableStatus } from "@sanoma-ai/workflows";
|
|
2
|
+
import { SignJWT } from "jose";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { ghostOps, Post } from "./connector.js";
|
|
5
|
+
/** The minimum API version the driver needs: Ghost 5 and later answer it. */
|
|
6
|
+
const ACCEPT_VERSION = "v5.0";
|
|
7
|
+
/** Ghost's post object, as far as the driver reads it: the connector's `Post`, in Ghost's names. */
|
|
8
|
+
const GhostPost = Post.omit({ publishedAt: true }).extend({
|
|
9
|
+
published_at: z.string().nullable(),
|
|
10
|
+
updated_at: z.string(),
|
|
11
|
+
});
|
|
12
|
+
/** Ghost wraps the post it creates, reads or saves in a list of one. */
|
|
13
|
+
const GhostPosts = z.object({ posts: z.tuple([GhostPost]) });
|
|
14
|
+
/** Ghost's error body, `{ errors: [{ message, type, code, context, ... }] }`: the driver reads the first. */
|
|
15
|
+
const GhostErrors = z.object({
|
|
16
|
+
errors: z.array(z.object({
|
|
17
|
+
message: z.string(),
|
|
18
|
+
type: z.string().optional(),
|
|
19
|
+
code: z.string().nullish(),
|
|
20
|
+
// Which field failed, and sometimes the message again.
|
|
21
|
+
context: z.string().nullish(),
|
|
22
|
+
})),
|
|
23
|
+
});
|
|
24
|
+
const out = (post) => ({
|
|
25
|
+
id: post.id,
|
|
26
|
+
url: post.url,
|
|
27
|
+
slug: post.slug,
|
|
28
|
+
status: post.status,
|
|
29
|
+
publishedAt: post.published_at,
|
|
30
|
+
});
|
|
31
|
+
const misconfigured = (message) => new DriverError(`ghost: ${message}`, { retryable: false });
|
|
32
|
+
const ADMIN_API_KEY = /^([0-9a-f]{24}):([0-9a-f]{64})$/i;
|
|
33
|
+
const ADMIN_API_KEY_FORMAT = "<24 hex digits>:<64 hex digits>";
|
|
34
|
+
/** The environment variables the driver reads, on every call. */
|
|
35
|
+
const env = z.object({
|
|
36
|
+
GHOST_ADMIN_URL: z.url().describe("the site's admin URL, such as https://example.ghost.io"),
|
|
37
|
+
GHOST_ADMIN_API_KEY: z
|
|
38
|
+
.string()
|
|
39
|
+
.regex(ADMIN_API_KEY, `an Admin API key, ${ADMIN_API_KEY_FORMAT}`)
|
|
40
|
+
.describe("a custom integration's Admin API key, <id>:<secret>"),
|
|
41
|
+
});
|
|
42
|
+
/**
|
|
43
|
+
* A token for the Admin API, signed with an Admin API key (`<id>:<secret>`), as Ghost asks:
|
|
44
|
+
* HS256, for `/admin/`, valid five minutes, the longest Ghost accepts.
|
|
45
|
+
*/
|
|
46
|
+
export async function adminToken(key) {
|
|
47
|
+
const [, id, secret] = ADMIN_API_KEY.exec(key) ?? [];
|
|
48
|
+
if (!id || !secret) {
|
|
49
|
+
throw misconfigured(`GHOST_ADMIN_API_KEY is not an Admin API key (${ADMIN_API_KEY_FORMAT})`);
|
|
50
|
+
}
|
|
51
|
+
return new SignJWT({})
|
|
52
|
+
.setProtectedHeader({ alg: "HS256", kid: id, typ: "JWT" })
|
|
53
|
+
.setIssuedAt()
|
|
54
|
+
.setExpirationTime("5m")
|
|
55
|
+
.setAudience("/admin/")
|
|
56
|
+
.sign(Buffer.from(secret, "hex"));
|
|
57
|
+
}
|
|
58
|
+
/** Reads the credentials, when an operation is called, and returns a function that sends one request. */
|
|
59
|
+
function connect(timeoutMs) {
|
|
60
|
+
const url = process.env.GHOST_ADMIN_URL;
|
|
61
|
+
const key = process.env.GHOST_ADMIN_API_KEY;
|
|
62
|
+
if (!url)
|
|
63
|
+
throw misconfigured(`GHOST_ADMIN_URL is not set (${env.shape.GHOST_ADMIN_URL.description})`);
|
|
64
|
+
if (!URL.canParse(url))
|
|
65
|
+
throw misconfigured("GHOST_ADMIN_URL is not a URL");
|
|
66
|
+
if (!key)
|
|
67
|
+
throw misconfigured(`GHOST_ADMIN_API_KEY is not set (${env.shape.GHOST_ADMIN_API_KEY.description})`);
|
|
68
|
+
const base = `${url.replace(/\/+$/, "")}/ghost/api/admin`;
|
|
69
|
+
return async (method, path, body) => {
|
|
70
|
+
// Each request gets its own token: one lasts at most five minutes.
|
|
71
|
+
const token = await adminToken(key);
|
|
72
|
+
const where = `${method} ${path}`;
|
|
73
|
+
let status;
|
|
74
|
+
let json;
|
|
75
|
+
try {
|
|
76
|
+
const res = await fetch(`${base}${path}`, {
|
|
77
|
+
method,
|
|
78
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
79
|
+
headers: {
|
|
80
|
+
Authorization: `Ghost ${token}`,
|
|
81
|
+
"Accept-Version": ACCEPT_VERSION,
|
|
82
|
+
"Content-Type": "application/json",
|
|
83
|
+
},
|
|
84
|
+
...(body !== undefined && { body: JSON.stringify(body) }),
|
|
85
|
+
});
|
|
86
|
+
status = res.status;
|
|
87
|
+
// A body that is not JSON (a proxy's HTML error page) is no answer to read; a body cut off is.
|
|
88
|
+
json = await res.json().catch((err) => {
|
|
89
|
+
if (err instanceof SyntaxError)
|
|
90
|
+
return undefined;
|
|
91
|
+
throw err;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
catch (cause) {
|
|
95
|
+
// No answer: the request may or may not have reached Ghost.
|
|
96
|
+
const why = cause.name === "TimeoutError" ? `no reply in ${timeoutMs} ms` : "the request failed";
|
|
97
|
+
throw new DriverError(`ghost: ${where}: ${why}`, { retryable: true, cause });
|
|
98
|
+
}
|
|
99
|
+
if (status < 200 || status >= 300) {
|
|
100
|
+
const err = GhostErrors.safeParse(json).data?.errors[0];
|
|
101
|
+
const detail = err?.context && err.context !== err.message ? ` ${err.context}` : "";
|
|
102
|
+
const message = err ? `${err.message}${detail}` : `HTTP ${status}`;
|
|
103
|
+
throw new DriverError(`ghost: ${where}: ${message}`, {
|
|
104
|
+
retryable: retryableStatus(status),
|
|
105
|
+
status,
|
|
106
|
+
vendorCode: err?.code ?? err?.type,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const parsed = GhostPosts.safeParse(json);
|
|
110
|
+
if (!parsed.success) {
|
|
111
|
+
throw new DriverError(`ghost: ${where}: the reply is not a post; is GHOST_ADMIN_URL the site's admin URL?`, {
|
|
112
|
+
retryable: false,
|
|
113
|
+
status,
|
|
114
|
+
cause: parsed.error,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return parsed.data.posts[0];
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/** The real Ghost, through the Admin API, with `GHOST_ADMIN_URL` and `GHOST_ADMIN_API_KEY`. See the README. */
|
|
121
|
+
export function ghostDriver(options = {}) {
|
|
122
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
123
|
+
return defineDriver(ghostOps, {
|
|
124
|
+
post: {
|
|
125
|
+
create: async ({ title, html, status }) => {
|
|
126
|
+
const send = connect(timeoutMs);
|
|
127
|
+
return out(await send("POST", "/posts/?source=html", { posts: [{ title, html, status }] }));
|
|
128
|
+
},
|
|
129
|
+
publish: async ({ id }) => {
|
|
130
|
+
const send = connect(timeoutMs);
|
|
131
|
+
const path = `/posts/${encodeURIComponent(id)}/`;
|
|
132
|
+
const save = async () => {
|
|
133
|
+
const post = await send("GET", path);
|
|
134
|
+
// Published already, or sent as an email only: it is no draft to publish.
|
|
135
|
+
if (post.status === "published" || post.status === "sent")
|
|
136
|
+
return post;
|
|
137
|
+
return send("PUT", path, { posts: [{ status: "published", updated_at: post.updated_at }] });
|
|
138
|
+
};
|
|
139
|
+
// Ghost refuses a save whose `updated_at` is not the post's latest (UPDATE_COLLISION):
|
|
140
|
+
// someone saved it in between. Read it again and save once more.
|
|
141
|
+
const post = await save().catch((err) => {
|
|
142
|
+
if (err instanceof DriverError && err.vendorCode === "UPDATE_COLLISION")
|
|
143
|
+
return save();
|
|
144
|
+
throw err;
|
|
145
|
+
});
|
|
146
|
+
return out(post);
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
}, { env });
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=driver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"driver.js","sourceRoot":"","sources":["../src/driver.ts"],"names":[],"mappings":"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"}
|
package/dist/fake.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type FakeOptions } from "@sanoma-ai/workflows/fake";
|
|
2
|
+
export interface FakeGhostPost {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
html: string;
|
|
6
|
+
slug: string;
|
|
7
|
+
status: "draft" | "scheduled" | "published";
|
|
8
|
+
publishedAt: string | null;
|
|
9
|
+
}
|
|
10
|
+
export interface FakeGhostState extends Record<string, unknown> {
|
|
11
|
+
seq: number;
|
|
12
|
+
posts: Record<string, FakeGhostPost>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* An in-memory Ghost for tests: posts live in `state.posts`, and their URLs are on
|
|
16
|
+
* `blog.example.test`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function fakeGhost(options?: FakeOptions): import("@sanoma-ai/workflows/fake").Fake<FakeGhostState, "ghost.post.create" | "ghost.post.publish">;
|
package/dist/fake.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { DriverError } from "@sanoma-ai/workflows";
|
|
2
|
+
import { defineFake } from "@sanoma-ai/workflows/fake";
|
|
3
|
+
import { ghostOps } from "./connector.js";
|
|
4
|
+
const out = (p) => ({
|
|
5
|
+
id: p.id,
|
|
6
|
+
url: `https://blog.example.test/${p.slug}/`,
|
|
7
|
+
slug: p.slug,
|
|
8
|
+
status: p.status,
|
|
9
|
+
publishedAt: p.publishedAt,
|
|
10
|
+
});
|
|
11
|
+
/**
|
|
12
|
+
* An in-memory Ghost for tests: posts live in `state.posts`, and their URLs are on
|
|
13
|
+
* `blog.example.test`.
|
|
14
|
+
*/
|
|
15
|
+
export function fakeGhost(options = {}) {
|
|
16
|
+
return defineFake(ghostOps, {
|
|
17
|
+
initial: () => ({ seq: 0, posts: {} }),
|
|
18
|
+
ops: (state) => ({
|
|
19
|
+
post: {
|
|
20
|
+
create: async ({ title, html, status }) => {
|
|
21
|
+
const id = `post_${String(++state.seq).padStart(4, "0")}`;
|
|
22
|
+
const slug = title
|
|
23
|
+
.toLowerCase()
|
|
24
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
25
|
+
.replace(/^-|-$/g, "");
|
|
26
|
+
state.posts[id] = { id, title, html, slug, status, publishedAt: null };
|
|
27
|
+
return out(state.posts[id]);
|
|
28
|
+
},
|
|
29
|
+
publish: async ({ id }) => {
|
|
30
|
+
const post = state.posts[id];
|
|
31
|
+
if (!post)
|
|
32
|
+
throw new DriverError(`ghost: no post ${id}`, { retryable: false, status: 404 });
|
|
33
|
+
if (post.status !== "published") {
|
|
34
|
+
post.status = "published";
|
|
35
|
+
post.publishedAt = new Date().toISOString();
|
|
36
|
+
}
|
|
37
|
+
return out(post);
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
}, options);
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=fake.js.map
|
package/dist/fake.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fake.js","sourceRoot":"","sources":["../src/fake.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,UAAU,EAAoB,MAAM,2BAA2B,CAAC;AACzE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAgB1C,MAAM,GAAG,GAAG,CAAC,CAAgB,EAAE,EAAE,CAAC,CAAC;IACjC,EAAE,EAAE,CAAC,CAAC,EAAE;IACR,GAAG,EAAE,6BAA6B,CAAC,CAAC,IAAI,GAAG;IAC3C,IAAI,EAAE,CAAC,CAAC,IAAI;IACZ,MAAM,EAAE,CAAC,CAAC,MAAM;IAChB,WAAW,EAAE,CAAC,CAAC,WAAW;CAC3B,CAAC,CAAC;AAEH;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,OAAO,GAAgB,EAAE;IACjD,OAAO,UAAU,CACf,QAAQ,EACR;QACE,OAAO,EAAE,GAAmB,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;QACtD,GAAG,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YACf,IAAI,EAAE;gBACJ,MAAM,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE;oBACxC,MAAM,EAAE,GAAG,QAAQ,MAAM,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;oBAC1D,MAAM,IAAI,GAAG,KAAK;yBACf,WAAW,EAAE;yBACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;yBAC3B,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;oBACzB,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;oBACvE,OAAO,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;gBAC9B,CAAC;gBACD,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE;oBACxB,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;oBAC7B,IAAI,CAAC,IAAI;wBAAE,MAAM,IAAI,WAAW,CAAC,kBAAkB,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;oBAC5F,IAAI,IAAI,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;wBAChC,IAAI,CAAC,MAAM,GAAG,WAAW,CAAC;wBAC1B,IAAI,CAAC,WAAW,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;oBAC9C,CAAC;oBACD,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC;gBACnB,CAAC;aACF;SACF,CAAC;KACH,EACD,OAAO,CACR,CAAC;AACJ,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export { Post } from "./connector.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Ghost Admin API posts: `post.create` and `post.publish`. It carries its live driver
|
|
4
|
+
* (`ghostDriver`), which reads its credentials when called, its fake (`fakeGhost`), of which
|
|
5
|
+
* each sandbox run makes its own, and its steps for Gherkin tests (`ghostSteps`).
|
|
6
|
+
*/
|
|
7
|
+
export declare const ghost: import("@sanoma-ai/workflows").Connector<"ghost", {
|
|
8
|
+
readonly post: {
|
|
9
|
+
readonly create: {
|
|
10
|
+
readonly effect: "write";
|
|
11
|
+
readonly description: "Create a post. Drafts are not visible on the site.";
|
|
12
|
+
readonly phrases: {
|
|
13
|
+
readonly given: "a post titled {title} exists";
|
|
14
|
+
readonly expect: "a post titled {title} is created";
|
|
15
|
+
};
|
|
16
|
+
readonly input: import("zod").ZodObject<{
|
|
17
|
+
title: import("zod").ZodString;
|
|
18
|
+
html: import("zod").ZodString;
|
|
19
|
+
status: import("zod").ZodDefault<import("zod").ZodLiteral<"draft">>;
|
|
20
|
+
}, import("zod/v4/core").$strip>;
|
|
21
|
+
readonly output: import("zod").ZodObject<{
|
|
22
|
+
id: import("zod").ZodString;
|
|
23
|
+
url: import("zod").ZodString;
|
|
24
|
+
slug: import("zod").ZodString;
|
|
25
|
+
status: import("zod").ZodEnum<{
|
|
26
|
+
draft: "draft";
|
|
27
|
+
published: "published";
|
|
28
|
+
scheduled: "scheduled";
|
|
29
|
+
sent: "sent";
|
|
30
|
+
}>;
|
|
31
|
+
publishedAt: import("zod").ZodNullable<import("zod").ZodString>;
|
|
32
|
+
}, import("zod/v4/core").$strip>;
|
|
33
|
+
};
|
|
34
|
+
readonly publish: {
|
|
35
|
+
readonly effect: "publish";
|
|
36
|
+
readonly description: "Publish a draft post on the site. Visible to everyone.";
|
|
37
|
+
readonly phrases: {
|
|
38
|
+
readonly expect: "post {id} is published";
|
|
39
|
+
};
|
|
40
|
+
readonly idempotent: true;
|
|
41
|
+
readonly input: import("zod").ZodObject<{
|
|
42
|
+
id: import("zod").ZodString;
|
|
43
|
+
}, import("zod/v4/core").$strip>;
|
|
44
|
+
readonly output: import("zod").ZodObject<{
|
|
45
|
+
id: import("zod").ZodString;
|
|
46
|
+
url: import("zod").ZodString;
|
|
47
|
+
slug: import("zod").ZodString;
|
|
48
|
+
status: import("zod").ZodEnum<{
|
|
49
|
+
draft: "draft";
|
|
50
|
+
published: "published";
|
|
51
|
+
scheduled: "scheduled";
|
|
52
|
+
sent: "sent";
|
|
53
|
+
}>;
|
|
54
|
+
publishedAt: import("zod").ZodNullable<import("zod").ZodString>;
|
|
55
|
+
}, import("zod/v4/core").$strip>;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
}>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { withSetup } from "@sanoma-ai/workflows";
|
|
2
|
+
import { ghostOps } from "./connector.js";
|
|
3
|
+
import { ghostDriver } from "./driver.js";
|
|
4
|
+
import { fakeGhost } from "./fake.js";
|
|
5
|
+
import { ghostSteps } from "./steps.js";
|
|
6
|
+
export { Post } from "./connector.js";
|
|
7
|
+
/**
|
|
8
|
+
* Ghost Admin API posts: `post.create` and `post.publish`. It carries its live driver
|
|
9
|
+
* (`ghostDriver`), which reads its credentials when called, its fake (`fakeGhost`), of which
|
|
10
|
+
* each sandbox run makes its own, and its steps for Gherkin tests (`ghostSteps`).
|
|
11
|
+
*/
|
|
12
|
+
export const ghost = withSetup(ghostOps, { driver: ghostDriver, fake: fakeGhost, steps: ghostSteps });
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAEtC;;;;GAIG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,SAAS,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,CAAC"}
|
package/dist/steps.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ghost's steps for Gherkin tests: a draft on the blog before the run (named `draft`, so an
|
|
3
|
+
* operation's own test can use `$draft.id`), and a post the run published, found by its title
|
|
4
|
+
* among the drafts seeded and created.
|
|
5
|
+
*/
|
|
6
|
+
export declare const ghostSteps: import("@sanoma-ai/workflows").Steps;
|
package/dist/steps.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { defineSteps } from "@sanoma-ai/workflows";
|
|
2
|
+
import { ghostOps } from "./connector.js";
|
|
3
|
+
const { create, publish } = ghostOps.post;
|
|
4
|
+
/**
|
|
5
|
+
* Ghost's steps for Gherkin tests: a draft on the blog before the run (named `draft`, so an
|
|
6
|
+
* operation's own test can use `$draft.id`), and a post the run published, found by its title
|
|
7
|
+
* among the drafts seeded and created.
|
|
8
|
+
*/
|
|
9
|
+
export const ghostSteps = defineSteps("ghost", ({ given, then }) => {
|
|
10
|
+
given("a Ghost draft titled {string}", (title) => ({ op: create, input: { title }, as: "draft" }));
|
|
11
|
+
then("the Ghost post {string} is published", (title) => ({
|
|
12
|
+
op: publish,
|
|
13
|
+
called: true,
|
|
14
|
+
check: (run) => {
|
|
15
|
+
const ids = run
|
|
16
|
+
.made(create)
|
|
17
|
+
.filter((c) => c.input.title === title)
|
|
18
|
+
.map((c) => c.output.id);
|
|
19
|
+
if (ids.length === 0)
|
|
20
|
+
return `no Ghost post titled "${title}" was drafted`;
|
|
21
|
+
if (run.calls(publish).some((c) => ids.includes(c.input.id)))
|
|
22
|
+
return undefined;
|
|
23
|
+
return `the Ghost post "${title}" (${ids.join(", ")}) was not published`;
|
|
24
|
+
},
|
|
25
|
+
}));
|
|
26
|
+
});
|
|
27
|
+
//# sourceMappingURL=steps.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"steps.js","sourceRoot":"","sources":["../src/steps.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,QAAQ,CAAC,IAAI,CAAC;AAE1C;;;;GAIG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,WAAW,CAAC,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,EAAE;IACjE,KAAK,CAAC,+BAA+B,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC;IAC3G,IAAI,CAAC,sCAAsC,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC,CAAC;QAC/D,EAAE,EAAE,OAAO;QACX,MAAM,EAAE,IAAI;QACZ,KAAK,EAAE,CAAC,GAAG,EAAE,EAAE;YACb,MAAM,GAAG,GAAG,GAAG;iBACZ,IAAI,CAAC,MAAM,CAAC;iBACZ,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,KAAK,KAAK,CAAC;iBACtC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YAC3B,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,yBAAyB,KAAK,eAAe,CAAC;YAC3E,IAAI,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;gBAAE,OAAO,SAAS,CAAC;YAC/E,OAAO,mBAAmB,KAAK,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,qBAAqB,CAAC;QAC3E,CAAC;KACF,CAAC,CAAC,CAAC;AACN,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Feature: Ghost posts
|
|
2
|
+
Drafts are created, then published. Each scenario runs on the fake and on Ghost's recorded
|
|
3
|
+
replies (test/fixtures/, named after the scenario), unless tagged @fake-only.
|
|
4
|
+
|
|
5
|
+
Scenario: Create
|
|
6
|
+
When ghost.post.create is called with
|
|
7
|
+
| title | Launch |
|
|
8
|
+
| html | <p>Hello</p> |
|
|
9
|
+
Then it returns
|
|
10
|
+
| status | draft |
|
|
11
|
+
|
|
12
|
+
Scenario: Publish draft
|
|
13
|
+
Given a Ghost draft titled "Launch"
|
|
14
|
+
When ghost.post.publish is called with
|
|
15
|
+
| id | $draft.id |
|
|
16
|
+
Then it returns
|
|
17
|
+
| status | published |
|
|
18
|
+
And the Ghost post "Launch" is published
|
|
19
|
+
|
|
20
|
+
Scenario: Publish again
|
|
21
|
+
Given a Ghost draft titled "Launch"
|
|
22
|
+
When ghost.post.publish is called with
|
|
23
|
+
| id | $draft.id |
|
|
24
|
+
And ghost.post.publish is called with
|
|
25
|
+
| id | $draft.id |
|
|
26
|
+
Then it returns
|
|
27
|
+
| status | published |
|
|
28
|
+
|
|
29
|
+
@fake-only
|
|
30
|
+
Scenario: Publish a post that is not there
|
|
31
|
+
When ghost.post.publish is called with
|
|
32
|
+
| id | missing |
|
|
33
|
+
Then it fails with status 404
|
package/package.json
CHANGED
|
@@ -1,6 +1,66 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanoma-ai/connector-ghost",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Ghost Admin API operations for Sanoma workflows",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"blog",
|
|
7
|
+
"connector",
|
|
8
|
+
"ghost",
|
|
9
|
+
"sanoma"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://github.com/Sanoma-AI/sanoma/tree/main/connectors/ghost#readme",
|
|
12
|
+
"bugs": "https://github.com/Sanoma-AI/sanoma/issues",
|
|
13
|
+
"license": "FSL-1.1-ALv2",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/Sanoma-AI/sanoma.git",
|
|
17
|
+
"directory": "connectors/ghost"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"features",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"main": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"default": "./dist/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./fake": {
|
|
35
|
+
"types": "./dist/fake.d.ts",
|
|
36
|
+
"default": "./dist/fake.js"
|
|
37
|
+
},
|
|
38
|
+
"./driver": {
|
|
39
|
+
"types": "./dist/driver.d.ts",
|
|
40
|
+
"default": "./dist/driver.js"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"jose": "^6.2.12"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"msw": "^3.0.2",
|
|
51
|
+
"zod": "^4.6.5",
|
|
52
|
+
"@sanoma-ai/workflows": "0.2.0"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"@sanoma-ai/workflows": "^0.2.0",
|
|
56
|
+
"zod": "^4"
|
|
57
|
+
},
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=24"
|
|
60
|
+
},
|
|
61
|
+
"scripts": {
|
|
62
|
+
"build": "tsc -p tsconfig.build.json",
|
|
63
|
+
"clean": "rm -rf dist",
|
|
64
|
+
"watch": "tsc -p tsconfig.build.json --watch --preserveWatchOutput"
|
|
65
|
+
}
|
|
6
66
|
}
|