@forumone/throughline-integrations 0.7.0 → 0.8.1
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/CHANGELOG.md +69 -0
- package/README.md +29 -2
- package/dist/admin/SyncButton.d.ts +27 -0
- package/dist/admin/SyncButton.d.ts.map +1 -0
- package/dist/admin/SyncButton.js +134 -0
- package/dist/admin/SyncButton.js.map +1 -0
- package/dist/admin/sync-client.d.ts +83 -0
- package/dist/admin/sync-client.d.ts.map +1 -0
- package/dist/admin/sync-client.js +121 -0
- package/dist/admin/sync-client.js.map +1 -0
- package/dist/collection.d.ts +9 -2
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +74 -2
- package/dist/collection.js.map +1 -1
- package/dist/endpoints/sync.d.ts +23 -0
- package/dist/endpoints/sync.d.ts.map +1 -0
- package/dist/endpoints/sync.js +85 -0
- package/dist/endpoints/sync.js.map +1 -0
- package/dist/exports/client.d.ts +12 -0
- package/dist/exports/client.d.ts.map +1 -0
- package/dist/exports/client.js +10 -0
- package/dist/exports/client.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/plugin.d.ts +7 -3
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +16 -4
- package/dist/plugin.js.map +1 -1
- package/dist/sync/manual-sync.d.ts +64 -0
- package/dist/sync/manual-sync.d.ts.map +1 -0
- package/dist/sync/manual-sync.js +88 -0
- package/dist/sync/manual-sync.js.map +1 -0
- package/dist/tools/trigger-sync.d.ts.map +1 -1
- package/dist/tools/trigger-sync.js +13 -30
- package/dist/tools/trigger-sync.js.map +1 -1
- package/package.json +19 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,74 @@
|
|
|
1
1
|
# @forumone/throughline-integrations
|
|
2
2
|
|
|
3
|
+
## 0.8.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 45724ee: The credentials come out of an editor's read of an integration
|
|
8
|
+
|
|
9
|
+
`integrations` grants document read to an admin _or_ an editor, and it should:
|
|
10
|
+
the reason an editor opens this collection is to see whether last night's sync
|
|
11
|
+
ran. The collection's docblock has always said so — "editors can read instance
|
|
12
|
+
status" — but a document read hands over the whole document, and `config` is one
|
|
13
|
+
JSON column holding whatever an integration needs to authenticate. A HubSpot
|
|
14
|
+
private app token, a webhook signing secret, a target URL. So _status_ was a
|
|
15
|
+
description of the intent, not of the code, and every editor could read every
|
|
16
|
+
credential from `GET /api/<slug>` and from the admin screen.
|
|
17
|
+
|
|
18
|
+
`config` now carries its own `read` / `create` / `update`, admin-only. An
|
|
19
|
+
editor's read returns the row without it: `name`, `enabled`, `lastSyncAt`,
|
|
20
|
+
`lastSyncStatus` and `lastError` are untouched, which is the whole of what the
|
|
21
|
+
status view and the `list_integrations` / `get_integration_status` tools project
|
|
22
|
+
anyway.
|
|
23
|
+
|
|
24
|
+
Nothing that needs the value loses it. Every reader of `config` goes through the
|
|
25
|
+
Local API — `loadInstances` here, and a host's sync and form endpoints — which
|
|
26
|
+
overrides access. The MCP tools never emitted the field.
|
|
27
|
+
|
|
28
|
+
`create` and `update` are redundant today, since the collection's own create and
|
|
29
|
+
update are already admin-only. They are there for the day document write is
|
|
30
|
+
widened so an editor can correct a status, and are the reason that day does not
|
|
31
|
+
also hand over the credential.
|
|
32
|
+
|
|
33
|
+
A `configFields` entry cannot do this itself: those drive the admin UI, and the
|
|
34
|
+
schema has one `json` column behind them, so there is no per-key field for
|
|
35
|
+
Payload to gate. Anything secret in `config` is protected by this rule or not at
|
|
36
|
+
all.
|
|
37
|
+
|
|
38
|
+
## 0.8.0
|
|
39
|
+
|
|
40
|
+
### Minor Changes
|
|
41
|
+
|
|
42
|
+
- 3ca0453: Sync now: a button on the integrations document, and the endpoint behind it
|
|
43
|
+
|
|
44
|
+
Manual sync was complete except for any way to reach it. The
|
|
45
|
+
`integration/manual-sync` event existed, every integration handled it, the run
|
|
46
|
+
wrote `lastSyncAt` / `lastSyncStatus` / `lastError` and an audit row — and the
|
|
47
|
+
only ways in were a `trigger_sync` MCP call, which needs a minted API key and a
|
|
48
|
+
JSON-RPC round trip, or hand-sending the event in the Inngest dashboard. Neither
|
|
49
|
+
is available to the operator who has just rotated a token or fixed a record
|
|
50
|
+
upstream and is looking at the instance in the admin, where the next scheduled
|
|
51
|
+
run may be an hour away.
|
|
52
|
+
- **`requestManualSync()`** is now the one definition of what a trigger means:
|
|
53
|
+
the instance exists, the instance is enabled, this event shape. The MCP tool
|
|
54
|
+
calls it, and so does the new endpoint, so the two cannot drift.
|
|
55
|
+
- **`POST /api/<integrations>/:id/sync`** — session-cookie auth, admin only,
|
|
56
|
+
`202` when queued, `404` / `409` / `502` for an unknown id, a disabled
|
|
57
|
+
instance, and an Inngest that would not take the event.
|
|
58
|
+
- **A Sync now button** in the document sidebar, above `lastSyncAt`. It says the
|
|
59
|
+
run was queued rather than implying a result it does not have, then watches
|
|
60
|
+
`lastSyncAt` for two minutes and reports the outcome when it moves.
|
|
61
|
+
|
|
62
|
+
An unreachable Inngest used to escape `trigger_sync` as an unhandled rejection
|
|
63
|
+
and reach the caller as a generic tool failure. It is now a refusal that says
|
|
64
|
+
nothing was queued — the distinction a button depends on, since a sync that was
|
|
65
|
+
never requested otherwise looks exactly like one that has not finished.
|
|
66
|
+
|
|
67
|
+
This is the package's first admin component, so hosts must run
|
|
68
|
+
`payload generate:importmap` after upgrading; a stale import map 500s the admin
|
|
69
|
+
screen. `react` and `@payloadcms/ui` are optional peers, needed only by
|
|
70
|
+
`@forumone/throughline-integrations/client`.
|
|
71
|
+
|
|
3
72
|
## 0.7.0
|
|
4
73
|
|
|
5
74
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ Plugin architecture for connecting Throughline-powered Payload sites to external
|
|
|
7
7
|
- **`Integration` contract** — id, name, description, configFields, validateConfig, subscribes, createFunctions, mcpTools (optional), healthcheck. Every Salesforce / Mailchimp / Slack / etc. integration uses this exact shape.
|
|
8
8
|
- **`IntegrationRegistry`** — process-local, per-plugin-init store keyed by integration id. Rejects duplicates synchronously.
|
|
9
9
|
- **Integrations collection** — `name`, `integrationType`, `enabled`, `config` (json), and read-only `lastSyncAt` / `lastSyncStatus` / `lastError`. Admin-only writes; admin/editor reads.
|
|
10
|
+
- **A Sync now button**, in the document's sidebar beside those status fields, and the `POST /api/<slug>/:id/sync` endpoint behind it. See below.
|
|
10
11
|
- **Five MCP tools**, handed to the host's collector at `onInit` and served by `@payloadcms/plugin-mcp` on one `/api/mcp`. Pass `mcpTools` or they reach nobody:
|
|
11
12
|
|
|
12
13
|
| Tool | Use it for | Access |
|
|
@@ -19,6 +20,32 @@ Plugin architecture for connecting Throughline-powered Payload sites to external
|
|
|
19
20
|
|
|
20
21
|
- **Webhook integration** — generic outbound HTTPS POST with HMAC-SHA256 signing, configurable event filter, retries (5x), timeout, and a HEAD-based healthcheck. RFC 4231 known-answer test vectors pin the wire format so refactoring can never silently break receivers.
|
|
21
22
|
|
|
23
|
+
## Triggering a sync from the admin
|
|
24
|
+
|
|
25
|
+
Everything behind a manual sync — the `integration/manual-sync` event, a handler on every integration, the status fields, the audit rows — was reachable only over MCP or by hand-sending an event in the Inngest dashboard. Neither is available to the person who has just fixed a record in the upstream system and wants it on the site before the next cron.
|
|
26
|
+
|
|
27
|
+
So the collection ships one admin component: **Sync now**, in the sidebar, above `lastSyncAt`. It POSTs to a collection endpoint:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
POST /api/integrations/:id/sync { "reason": "optional" }
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
authenticated by the Payload session cookie — no API key in the operator's path — and admin-only, matching `trigger_sync`. The endpoint and the tool both call `requestManualSync()`, which is the single definition of what a trigger checks and what event it sends; neither re-implements the rules.
|
|
34
|
+
|
|
35
|
+
| Answer | Means |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `202` | Queued. The run has **not** happened yet. |
|
|
38
|
+
| `403` | Not an admin. |
|
|
39
|
+
| `404` | No instance with that id. |
|
|
40
|
+
| `409` | The instance is disabled. Enable it first. |
|
|
41
|
+
| `502` | Inngest would not take the event — nothing was queued. |
|
|
42
|
+
|
|
43
|
+
`202`, not `200`: it fires an event and returns. The button says the run was queued, then watches `lastSyncAt` for two minutes and reports the outcome when it moves — against the value the endpoint returned rather than what the page last rendered, so a cron run that lands mid-wait is not mistaken for this one. Giving up watching is not failing, and the copy says so.
|
|
44
|
+
|
|
45
|
+
`requestManualSync` and `createSyncEndpoint` are exported, so a host with its own screen can reuse either.
|
|
46
|
+
|
|
47
|
+
Because this is the package's first admin component, hosts must run `payload generate:importmap` after upgrading. A stale import map 500s the admin screen.
|
|
48
|
+
|
|
22
49
|
## Why this is separate from the other server packages
|
|
23
50
|
|
|
24
51
|
The other server packages do one job well. This is a **framework within the framework**: a contract for integration modules plus tooling to register, configure, observe, and trigger them. Every real client engagement needs integrations; building them ad-hoc produces unmaintainable tangle. This package keeps the surface area bounded as the integration count grows.
|
|
@@ -29,7 +56,7 @@ The other server packages do one job well. This is a **framework within the fram
|
|
|
29
56
|
pnpm add @forumone/throughline-integrations
|
|
30
57
|
```
|
|
31
58
|
|
|
32
|
-
Peers: `payload@^3.0.0`, `inngest@^4.0.0`. Required runtime peer: `@forumone/throughline-core` (audit log).
|
|
59
|
+
Peers: `payload@^3.0.0`, `inngest@^4.0.0`. Required runtime peer: `@forumone/throughline-core` (audit log). `react` and `@payloadcms/ui` are optional peers, needed only to render the Sync now button — a host that never loads `@forumone/throughline-integrations/client` needs neither.
|
|
33
60
|
|
|
34
61
|
## Usage
|
|
35
62
|
|
|
@@ -71,7 +98,7 @@ Integration `createFunctions` returns Inngest functions, but **this plugin does
|
|
|
71
98
|
|
|
72
99
|
A prompt-injection attacker could otherwise convince Claude to retarget a webhook to attacker-controlled infrastructure or rotate the signing secret. Claude can _trigger_ and _observe_ integrations conversationally, but configuration changes are deliberate human actions in the Payload admin.
|
|
73
100
|
|
|
74
|
-
This asymmetry is intentional and is why `trigger_sync` is admin-only too — manual triggering writes to an external system, even if it doesn't change configuration.
|
|
101
|
+
This asymmetry is intentional and is why `trigger_sync` is admin-only too — manual triggering writes to an external system, even if it doesn't change configuration. The Sync now button and its endpoint apply the same rule, and the button does not render for a non-admin.
|
|
75
102
|
|
|
76
103
|
## Webhook details
|
|
77
104
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface ThroughlineSyncButtonProps {
|
|
3
|
+
/** Slug the integrations collection is mounted at. Injected via `clientProps`. */
|
|
4
|
+
collectionSlug?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* "Sync now", in the sidebar beside the status fields it moves.
|
|
8
|
+
*
|
|
9
|
+
* Everything behind this button already existed — the `integration/manual-sync`
|
|
10
|
+
* event, a handler on every integration, the status fields, the audit rows —
|
|
11
|
+
* and reaching it meant an MCP round trip with a minted API key or a hand-sent
|
|
12
|
+
* event in the Inngest dashboard. Neither is available to the person who has
|
|
13
|
+
* just fixed a job posting and wants to see it on the site before the next
|
|
14
|
+
* hourly cron.
|
|
15
|
+
*
|
|
16
|
+
* It fires an event; it does not wait for the sync. So the button says the run
|
|
17
|
+
* was *queued*, then watches `lastSyncAt` for the two minutes a sync normally
|
|
18
|
+
* takes and reports the outcome when it moves. Stopping watching is not the
|
|
19
|
+
* same as failing, and the copy says so — the run continues either way.
|
|
20
|
+
*
|
|
21
|
+
* The form is deliberately untouched. Writing the new status into form state
|
|
22
|
+
* would put server values into a document the editor may not have saved, so
|
|
23
|
+
* the result is rendered here instead and the sidebar fields catch up on the
|
|
24
|
+
* next load.
|
|
25
|
+
*/
|
|
26
|
+
export declare function SyncButton(props?: ThroughlineSyncButtonProps): React.ReactNode;
|
|
27
|
+
//# sourceMappingURL=SyncButton.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SyncButton.d.ts","sourceRoot":"","sources":["../../src/admin/SyncButton.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAmD,MAAM,OAAO,CAAA;AAWvE,MAAM,WAAW,0BAA0B;IACzC,kFAAkF;IAClF,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAQD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,UAAU,CAAC,KAAK,GAAE,0BAA+B,GAAG,KAAK,CAAC,SAAS,CAyHlF"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { Button, FieldLabel, toast, useAuth, useConfig, useDocumentInfo } from '@payloadcms/ui';
|
|
5
|
+
import { describeSyncOutcome, fetchSyncStatus, formatSyncTime, syncHasFinished, triggerSync, } from './sync-client.js';
|
|
6
|
+
/** How long to watch for the run to finish before saying so and stopping. */
|
|
7
|
+
const POLL_INTERVAL_MS = 3_000;
|
|
8
|
+
const POLL_TIMEOUT_MS = 120_000;
|
|
9
|
+
/**
|
|
10
|
+
* "Sync now", in the sidebar beside the status fields it moves.
|
|
11
|
+
*
|
|
12
|
+
* Everything behind this button already existed — the `integration/manual-sync`
|
|
13
|
+
* event, a handler on every integration, the status fields, the audit rows —
|
|
14
|
+
* and reaching it meant an MCP round trip with a minted API key or a hand-sent
|
|
15
|
+
* event in the Inngest dashboard. Neither is available to the person who has
|
|
16
|
+
* just fixed a job posting and wants to see it on the site before the next
|
|
17
|
+
* hourly cron.
|
|
18
|
+
*
|
|
19
|
+
* It fires an event; it does not wait for the sync. So the button says the run
|
|
20
|
+
* was *queued*, then watches `lastSyncAt` for the two minutes a sync normally
|
|
21
|
+
* takes and reports the outcome when it moves. Stopping watching is not the
|
|
22
|
+
* same as failing, and the copy says so — the run continues either way.
|
|
23
|
+
*
|
|
24
|
+
* The form is deliberately untouched. Writing the new status into form state
|
|
25
|
+
* would put server values into a document the editor may not have saved, so
|
|
26
|
+
* the result is rendered here instead and the sidebar fields catch up on the
|
|
27
|
+
* next load.
|
|
28
|
+
*/
|
|
29
|
+
export function SyncButton(props = {}) {
|
|
30
|
+
const collectionSlug = props.collectionSlug ?? 'integrations';
|
|
31
|
+
const { id, data } = useDocumentInfo();
|
|
32
|
+
const { config } = useConfig();
|
|
33
|
+
const { user } = useAuth();
|
|
34
|
+
const [phase, setPhase] = useState('idle');
|
|
35
|
+
const [baseline, setBaseline] = useState(null);
|
|
36
|
+
const [outcome, setOutcome] = useState(null);
|
|
37
|
+
const [gaveUpWaiting, setGaveUpWaiting] = useState(false);
|
|
38
|
+
const { api } = config.routes;
|
|
39
|
+
const serverURL = config.serverURL ?? '';
|
|
40
|
+
const instanceName = typeof data?.['name'] === 'string' ? data['name'] : undefined;
|
|
41
|
+
const enabled = data?.['enabled'] === true;
|
|
42
|
+
const roles = user?.['roles'];
|
|
43
|
+
const isAdmin = Array.isArray(roles) && roles.includes('admin');
|
|
44
|
+
// Read inside the poll effect, which must not restart every time the name
|
|
45
|
+
// changes — a restarted effect is a restarted timeout.
|
|
46
|
+
const instanceNameRef = useRef(instanceName);
|
|
47
|
+
instanceNameRef.current = instanceName;
|
|
48
|
+
const start = useCallback(async () => {
|
|
49
|
+
if (id === undefined || id === null || phase !== 'idle')
|
|
50
|
+
return;
|
|
51
|
+
setPhase('triggering');
|
|
52
|
+
setOutcome(null);
|
|
53
|
+
setGaveUpWaiting(false);
|
|
54
|
+
const result = await triggerSync({ serverURL, apiRoute: api, collectionSlug, id });
|
|
55
|
+
if (!result.ok) {
|
|
56
|
+
toast.error(result.message);
|
|
57
|
+
setPhase('idle');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
toast.success(result.body.message ?? 'Sync queued.');
|
|
61
|
+
setBaseline(result.body.lastSyncAt ?? null);
|
|
62
|
+
setPhase('waiting');
|
|
63
|
+
}, [api, collectionSlug, id, phase, serverURL]);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (phase !== 'waiting' || id === undefined || id === null)
|
|
66
|
+
return;
|
|
67
|
+
const controller = new AbortController();
|
|
68
|
+
let timer;
|
|
69
|
+
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
70
|
+
const poll = async () => {
|
|
71
|
+
const status = await fetchSyncStatus({
|
|
72
|
+
serverURL,
|
|
73
|
+
apiRoute: api,
|
|
74
|
+
collectionSlug,
|
|
75
|
+
id,
|
|
76
|
+
signal: controller.signal,
|
|
77
|
+
});
|
|
78
|
+
if (controller.signal.aborted)
|
|
79
|
+
return;
|
|
80
|
+
if (status && syncHasFinished(baseline, status)) {
|
|
81
|
+
setOutcome(status);
|
|
82
|
+
setPhase('idle');
|
|
83
|
+
const described = describeSyncOutcome(status, instanceNameRef.current);
|
|
84
|
+
const options = described.description ? { description: described.description } : {};
|
|
85
|
+
if (described.severity === 'success')
|
|
86
|
+
toast.success(described.title, options);
|
|
87
|
+
else if (described.severity === 'warning')
|
|
88
|
+
toast.warning(described.title, options);
|
|
89
|
+
else
|
|
90
|
+
toast.error(described.title, { ...options, duration: 10_000 });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (Date.now() >= deadline) {
|
|
94
|
+
setGaveUpWaiting(true);
|
|
95
|
+
setPhase('idle');
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
timer = setTimeout(() => void poll(), POLL_INTERVAL_MS);
|
|
99
|
+
};
|
|
100
|
+
timer = setTimeout(() => void poll(), POLL_INTERVAL_MS);
|
|
101
|
+
return () => {
|
|
102
|
+
controller.abort();
|
|
103
|
+
if (timer)
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
};
|
|
106
|
+
}, [api, baseline, collectionSlug, id, phase, serverURL]);
|
|
107
|
+
// Create view: no instance to sync, and no id to sync it by.
|
|
108
|
+
if (id === undefined || id === null)
|
|
109
|
+
return null;
|
|
110
|
+
// The endpoint refuses a non-admin anyway; not rendering the control is how
|
|
111
|
+
// an editor finds that out without pressing it.
|
|
112
|
+
if (!isAdmin)
|
|
113
|
+
return null;
|
|
114
|
+
const label = phase === 'idle' ? 'Sync now' : phase === 'triggering' ? 'Queueing…' : 'Syncing…';
|
|
115
|
+
return (_jsxs("div", { className: "field-type throughline-integration-sync", children: [_jsx(FieldLabel, { as: "span", label: "Manual sync" }), _jsx(Button, { buttonStyle: "secondary", disabled: !enabled || phase !== 'idle', onClick: () => {
|
|
116
|
+
void start();
|
|
117
|
+
}, size: "small", type: "button", ...(enabled ? {} : { tooltip: 'Enable this integration first.' }), children: label }), _jsx("p", { "aria-live": "polite", className: "field-description", children: state({ enabled, gaveUpWaiting, outcome, phase }) })] }));
|
|
118
|
+
}
|
|
119
|
+
/** The line under the button. Pure, so the copy can be read in one place. */
|
|
120
|
+
function state(args) {
|
|
121
|
+
if (!args.enabled)
|
|
122
|
+
return 'Disabled integrations cannot be synced. Enable and save first.';
|
|
123
|
+
if (args.phase === 'triggering')
|
|
124
|
+
return 'Asking the queue to run this integration…';
|
|
125
|
+
if (args.phase === 'waiting')
|
|
126
|
+
return 'Queued. Watching for the run to finish.';
|
|
127
|
+
if (args.gaveUpWaiting) {
|
|
128
|
+
return 'Still running after two minutes. It has not failed — reload to see the result.';
|
|
129
|
+
}
|
|
130
|
+
if (args.outcome)
|
|
131
|
+
return `Finished at ${formatSyncTime(args.outcome.lastSyncAt)}.`;
|
|
132
|
+
return 'Runs this integration now, instead of waiting for its schedule.';
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=SyncButton.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SyncButton.js","sourceRoot":"","sources":["../../src/admin/SyncButton.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side helpers behind the admin's Sync now button. No JSX here, so the
|
|
3
|
+
* request handling and the message formatting can be unit tested without a
|
|
4
|
+
* form, a document or a browser.
|
|
5
|
+
*/
|
|
6
|
+
export type SyncStatusValue = 'never-run' | 'success' | 'partial' | 'failed';
|
|
7
|
+
export interface TriggerSyncBody {
|
|
8
|
+
ok?: boolean;
|
|
9
|
+
triggered?: {
|
|
10
|
+
instanceId: string;
|
|
11
|
+
instanceName: string;
|
|
12
|
+
type: string;
|
|
13
|
+
};
|
|
14
|
+
/** The instance's `lastSyncAt` *before* the run. The baseline to poll against. */
|
|
15
|
+
lastSyncAt?: string | null;
|
|
16
|
+
message?: string;
|
|
17
|
+
error?: string;
|
|
18
|
+
code?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface TriggerSyncArgs {
|
|
21
|
+
serverURL: string;
|
|
22
|
+
apiRoute: string;
|
|
23
|
+
collectionSlug: string;
|
|
24
|
+
id: number | string;
|
|
25
|
+
reason?: string;
|
|
26
|
+
}
|
|
27
|
+
export type TriggerSyncResult = {
|
|
28
|
+
ok: true;
|
|
29
|
+
body: TriggerSyncBody;
|
|
30
|
+
} | {
|
|
31
|
+
ok: false;
|
|
32
|
+
message: string;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* POSTs to the collection's sync endpoint using the browser's session cookie.
|
|
36
|
+
*
|
|
37
|
+
* Every refusal — not found, disabled, Inngest unreachable — comes back as a
|
|
38
|
+
* non-2xx with a message, so all of them land in `ok: false` and get shown.
|
|
39
|
+
* A button that appears to work when nothing was queued is the failure this
|
|
40
|
+
* exists to avoid.
|
|
41
|
+
*/
|
|
42
|
+
export declare function triggerSync(args: TriggerSyncArgs): Promise<TriggerSyncResult>;
|
|
43
|
+
export interface SyncStatus {
|
|
44
|
+
lastSyncAt: string | null;
|
|
45
|
+
lastSyncStatus: SyncStatusValue | null;
|
|
46
|
+
lastError: string | null;
|
|
47
|
+
}
|
|
48
|
+
export interface FetchSyncStatusArgs {
|
|
49
|
+
serverURL: string;
|
|
50
|
+
apiRoute: string;
|
|
51
|
+
collectionSlug: string;
|
|
52
|
+
id: number | string;
|
|
53
|
+
signal?: AbortSignal;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Reads the three status fields off the instance. Returns null on any failure,
|
|
57
|
+
* because this is called on a timer and a single missed poll is not something
|
|
58
|
+
* to tell anyone about — the caller keeps waiting.
|
|
59
|
+
*/
|
|
60
|
+
export declare function fetchSyncStatus(args: FetchSyncStatusArgs): Promise<null | SyncStatus>;
|
|
61
|
+
/**
|
|
62
|
+
* True once the instance has recorded a run that started after the trigger.
|
|
63
|
+
*
|
|
64
|
+
* Compared against the `lastSyncAt` the endpoint returned rather than against
|
|
65
|
+
* whatever the page last rendered: the sidebar can be minutes stale, and a
|
|
66
|
+
* document opened during a cron run would otherwise report that run's result
|
|
67
|
+
* as this one's.
|
|
68
|
+
*/
|
|
69
|
+
export declare function syncHasFinished(baseline: null | string, current: null | SyncStatus): boolean;
|
|
70
|
+
export interface SyncOutcome {
|
|
71
|
+
severity: 'error' | 'success' | 'warning';
|
|
72
|
+
title: string;
|
|
73
|
+
description?: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* How a finished run reads. `partial` is deliberately a warning rather than a
|
|
77
|
+
* success: some of what was asked for did not happen, and the whole reason
|
|
78
|
+
* somebody pressed this button is to find out whether their change landed.
|
|
79
|
+
*/
|
|
80
|
+
export declare function describeSyncOutcome(status: SyncStatus, instanceName?: string): SyncOutcome;
|
|
81
|
+
/** `2026-08-29T14:03:00.000Z` as something an operator reads at a glance. */
|
|
82
|
+
export declare function formatSyncTime(value: null | string): string;
|
|
83
|
+
//# sourceMappingURL=sync-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-client.d.ts","sourceRoot":"","sources":["../../src/admin/sync-client.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,MAAM,eAAe,GAAG,WAAW,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAA;AAE5E,MAAM,WAAW,eAAe;IAC9B,EAAE,CAAC,EAAE,OAAO,CAAA;IACZ,SAAS,CAAC,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IACtE,kFAAkF;IAClF,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,cAAc,EAAE,MAAM,CAAA;IACtB,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;IACnB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,MAAM,iBAAiB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,eAAe,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAEpG;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA2BnF;AAED,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,cAAc,EAAE,eAAe,GAAG,IAAI,CAAA;IACtC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,cAAc,EAAE,MAAM,CAAA;IACtB,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;IACnB,MAAM,CAAC,EAAE,WAAW,CAAA;CACrB;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,GAAG,UAAU,CAAC,CAkB3F;AAMD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,IAAI,GAAG,MAAM,EAAE,OAAO,EAAE,IAAI,GAAG,UAAU,GAAG,OAAO,CAI5F;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CAAA;IACzC,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,UAAU,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,WAAW,CAwB1F;AAED,6EAA6E;AAC7E,wBAAgB,cAAc,CAAC,KAAK,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,CAK3D"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side helpers behind the admin's Sync now button. No JSX here, so the
|
|
3
|
+
* request handling and the message formatting can be unit tested without a
|
|
4
|
+
* form, a document or a browser.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* POSTs to the collection's sync endpoint using the browser's session cookie.
|
|
8
|
+
*
|
|
9
|
+
* Every refusal — not found, disabled, Inngest unreachable — comes back as a
|
|
10
|
+
* non-2xx with a message, so all of them land in `ok: false` and get shown.
|
|
11
|
+
* A button that appears to work when nothing was queued is the failure this
|
|
12
|
+
* exists to avoid.
|
|
13
|
+
*/
|
|
14
|
+
export async function triggerSync(args) {
|
|
15
|
+
const url = `${args.serverURL}${args.apiRoute}/${args.collectionSlug}/${args.id}/sync`;
|
|
16
|
+
let response;
|
|
17
|
+
try {
|
|
18
|
+
response = await fetch(url, {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
credentials: 'include',
|
|
21
|
+
headers: { 'content-type': 'application/json' },
|
|
22
|
+
body: JSON.stringify(args.reason ? { reason: args.reason } : {}),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return { ok: false, message: 'Could not reach the server to queue the sync.' };
|
|
27
|
+
}
|
|
28
|
+
let body;
|
|
29
|
+
try {
|
|
30
|
+
body = (await response.json());
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return { ok: false, message: `The server returned ${response.status}.` };
|
|
34
|
+
}
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
return { ok: false, message: body.error ?? `The server returned ${response.status}.` };
|
|
37
|
+
}
|
|
38
|
+
return { ok: true, body };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Reads the three status fields off the instance. Returns null on any failure,
|
|
42
|
+
* because this is called on a timer and a single missed poll is not something
|
|
43
|
+
* to tell anyone about — the caller keeps waiting.
|
|
44
|
+
*/
|
|
45
|
+
export async function fetchSyncStatus(args) {
|
|
46
|
+
const url = `${args.serverURL}${args.apiRoute}/${args.collectionSlug}/${args.id}?depth=0`;
|
|
47
|
+
try {
|
|
48
|
+
const response = await fetch(url, {
|
|
49
|
+
credentials: 'include',
|
|
50
|
+
...(args.signal ? { signal: args.signal } : {}),
|
|
51
|
+
});
|
|
52
|
+
if (!response.ok)
|
|
53
|
+
return null;
|
|
54
|
+
const doc = (await response.json());
|
|
55
|
+
return {
|
|
56
|
+
lastSyncAt: typeof doc['lastSyncAt'] === 'string' ? doc['lastSyncAt'] : null,
|
|
57
|
+
lastSyncStatus: isSyncStatusValue(doc['lastSyncStatus']) ? doc['lastSyncStatus'] : null,
|
|
58
|
+
lastError: typeof doc['lastError'] === 'string' ? doc['lastError'] : null,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function isSyncStatusValue(value) {
|
|
66
|
+
return value === 'never-run' || value === 'success' || value === 'partial' || value === 'failed';
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* True once the instance has recorded a run that started after the trigger.
|
|
70
|
+
*
|
|
71
|
+
* Compared against the `lastSyncAt` the endpoint returned rather than against
|
|
72
|
+
* whatever the page last rendered: the sidebar can be minutes stale, and a
|
|
73
|
+
* document opened during a cron run would otherwise report that run's result
|
|
74
|
+
* as this one's.
|
|
75
|
+
*/
|
|
76
|
+
export function syncHasFinished(baseline, current) {
|
|
77
|
+
if (!current)
|
|
78
|
+
return false;
|
|
79
|
+
if (current.lastSyncAt === null)
|
|
80
|
+
return false;
|
|
81
|
+
return current.lastSyncAt !== baseline;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* How a finished run reads. `partial` is deliberately a warning rather than a
|
|
85
|
+
* success: some of what was asked for did not happen, and the whole reason
|
|
86
|
+
* somebody pressed this button is to find out whether their change landed.
|
|
87
|
+
*/
|
|
88
|
+
export function describeSyncOutcome(status, instanceName) {
|
|
89
|
+
const subject = instanceName ? `"${instanceName}"` : 'The integration';
|
|
90
|
+
const description = status.lastError ?? undefined;
|
|
91
|
+
switch (status.lastSyncStatus) {
|
|
92
|
+
case 'success':
|
|
93
|
+
return { severity: 'success', title: `${subject} synced.` };
|
|
94
|
+
case 'partial':
|
|
95
|
+
return {
|
|
96
|
+
severity: 'warning',
|
|
97
|
+
title: `${subject} synced, with problems.`,
|
|
98
|
+
...(description ? { description } : {}),
|
|
99
|
+
};
|
|
100
|
+
case 'failed':
|
|
101
|
+
return {
|
|
102
|
+
severity: 'error',
|
|
103
|
+
title: `${subject} failed to sync.`,
|
|
104
|
+
...(description ? { description } : {}),
|
|
105
|
+
};
|
|
106
|
+
default:
|
|
107
|
+
// A run finished — `lastSyncAt` moved — without leaving a status. Nothing
|
|
108
|
+
// to celebrate and nothing to blame; say what is known.
|
|
109
|
+
return { severity: 'warning', title: `${subject} ran, but reported no status.` };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** `2026-08-29T14:03:00.000Z` as something an operator reads at a glance. */
|
|
113
|
+
export function formatSyncTime(value) {
|
|
114
|
+
if (!value)
|
|
115
|
+
return 'Never';
|
|
116
|
+
const date = new Date(value);
|
|
117
|
+
if (Number.isNaN(date.getTime()))
|
|
118
|
+
return 'Unknown';
|
|
119
|
+
return date.toLocaleString();
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=sync-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-client.js","sourceRoot":"","sources":["../../src/admin/sync-client.ts"],"names":[],"mappings":"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"}
|
package/dist/collection.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import type { CollectionConfig } from 'payload';
|
|
1
|
+
import type { CollectionConfig, Endpoint } from 'payload';
|
|
2
2
|
import type { IntegrationRegistry } from './registry.js';
|
|
3
3
|
export interface CreateIntegrationsCollectionOptions {
|
|
4
4
|
slug?: string;
|
|
5
5
|
registry: IntegrationRegistry;
|
|
6
|
+
/** Collection endpoints to mount, e.g. the manual-sync trigger. */
|
|
7
|
+
endpoints?: Endpoint[];
|
|
6
8
|
}
|
|
7
9
|
/**
|
|
8
10
|
* Collection that persists per-instance integration configuration. Configuration
|
|
@@ -10,7 +12,12 @@ export interface CreateIntegrationsCollectionOptions {
|
|
|
10
12
|
* would let an attacker re-target webhook URLs or rotate signing secrets.
|
|
11
13
|
*
|
|
12
14
|
* Editors can read instance status (used by the listing/status MCP tools);
|
|
13
|
-
* write operations are reserved for admins editing in the Payload UI.
|
|
15
|
+
* write operations are reserved for admins editing in the Payload UI. *Status*
|
|
16
|
+
* is the operative word: `config` carries its own admin-only field access, so
|
|
17
|
+
* an editor's read returns the row without the credentials in it.
|
|
18
|
+
*
|
|
19
|
+
* The one control on the document that is not a field is the sidebar's Sync
|
|
20
|
+
* now button, which POSTs to the `:id/sync` endpoint passed in `endpoints`.
|
|
14
21
|
*/
|
|
15
22
|
export declare function createIntegrationsCollection(options: CreateIntegrationsCollectionOptions): CollectionConfig;
|
|
16
23
|
//# sourceMappingURL=collection.d.ts.map
|
package/dist/collection.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../src/collection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAU,gBAAgB,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../src/collection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAU,gBAAgB,EAAE,QAAQ,EAAe,MAAM,SAAS,CAAA;AAC9E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA;AASxD,MAAM,WAAW,mCAAmC;IAClD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,mBAAmB,CAAA;IAC7B,mEAAmE;IACnE,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAA;CACvB;AAsBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,mCAAmC,GAC3C,gBAAgB,CAiKlB"}
|