@foir/sdk 0.5.0 → 0.8.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/CHANGELOG.md +1126 -0
- package/README.md +94 -0
- package/dist/auth/auth.d.ts +243 -0
- package/dist/auth/hosted.d.ts +40 -0
- package/dist/auth/index.d.ts +21 -0
- package/dist/auth/operations.d.ts +144 -0
- package/dist/auth/ops.d.ts +99 -0
- package/dist/auth/session-client.d.ts +76 -0
- package/dist/auth-session.js +137 -0
- package/dist/auth.js +973 -92
- package/dist/chunk-IGNLWT3H.js +35 -0
- package/dist/chunk-OHYVKTGB.js +45 -0
- package/dist/chunk-TAL5H6VA.js +39 -0
- package/dist/client/platform-client.d.ts +29 -0
- package/dist/client.js +275 -37
- package/dist/codegen/block-registry.d.ts +13 -23
- package/dist/codegen/block-registry.js +3 -31
- package/dist/codegen/block-scaffold.d.ts +12 -25
- package/dist/codegen/block-scaffold.js +3 -41
- package/dist/codegen/index.d.ts +12 -61
- package/dist/codegen/index.js +7 -11
- package/dist/codegen/item-registry.d.ts +28 -0
- package/dist/codegen/item-registry.js +7 -0
- package/dist/codegen/item-scaffold.d.ts +29 -0
- package/dist/codegen/item-scaffold.js +7 -0
- package/dist/codegen/preset.d.ts +67 -0
- package/dist/codegen/shared.d.ts +36 -0
- package/dist/components/EditorProvider.d.ts +36 -0
- package/dist/content/content-renderer.d.ts +113 -0
- package/dist/content/index.d.ts +18 -0
- package/dist/content/richtext.d.ts +118 -0
- package/dist/content.js +227 -80
- package/dist/data/builtins.d.ts +506 -0
- package/dist/data/client.d.ts +166 -0
- package/dist/data/document.d.ts +251 -0
- package/dist/data/index.d.ts +73 -0
- package/dist/data/manifest.d.ts +424 -0
- package/dist/data/schema.d.ts +313 -0
- package/dist/data/selection.d.ts +524 -0
- package/dist/data/subjects.d.ts +221 -0
- package/dist/data/subscription-document.d.ts +75 -0
- package/dist/data/subscriptions.d.ts +181 -0
- package/dist/data/transport.d.ts +325 -0
- package/dist/data/ws-transport.d.ts +197 -0
- package/dist/data.js +1683 -0
- package/dist/graphql/cdn-route.d.ts +26 -0
- package/dist/graphql/client.d.ts +30 -0
- package/dist/graphql/content-client.d.ts +66 -0
- package/dist/graphql/foir-request.d.ts +47 -0
- package/dist/hooks/useAutoResize.d.ts +22 -0
- package/dist/hooks/useEditor.d.ts +16 -0
- package/dist/index.d.ts +10 -394
- package/dist/media/BlurhashCanvas.d.ts +10 -0
- package/dist/media/ProgressiveMedia.d.ts +56 -0
- package/dist/media/index.d.ts +18 -0
- package/dist/media/media.d.ts +139 -0
- package/dist/media/screen-capture.d.ts +81 -0
- package/dist/media/useProgressiveLoad.d.ts +23 -0
- package/dist/media.js +102 -0
- package/dist/next/cookieAdapter.d.ts +26 -0
- package/dist/next/index.d.ts +15 -0
- package/dist/next/middleware.d.ts +49 -0
- package/dist/next/session.d.ts +53 -0
- package/dist/next.js +763 -78
- package/dist/protocol/constants.d.ts +6 -0
- package/dist/protocol/messages.d.ts +155 -0
- package/dist/protocol/validation.d.ts +8 -0
- package/dist/push/index.d.ts +14 -0
- package/dist/push/operations.d.ts +22 -0
- package/dist/push/web-push.d.ts +39 -0
- package/dist/push.js +892 -78
- package/dist/react/FoirFeedback.d.ts +62 -0
- package/dist/react/FoirProvider.d.ts +116 -0
- package/dist/react/OAuthButtons.d.ts +20 -0
- package/dist/react/SignIn.d.ts +22 -0
- package/dist/react/UserButton.d.ts +13 -0
- package/dist/react/feedback-notes.d.ts +101 -0
- package/dist/react/index.d.ts +25 -0
- package/dist/react/localStorageAdapter.d.ts +10 -0
- package/dist/react.js +1373 -91
- package/dist/{realtime.d.ts → realtime/index.d.ts} +35 -16
- package/dist/realtime.js +1 -1
- package/dist/server/callback-client.d.ts +92 -0
- package/dist/server/events-client.d.ts +279 -0
- package/dist/server/index.d.ts +12 -0
- package/dist/server/operation-types.d.ts +134 -0
- package/dist/server/require-scoped-token.d.ts +76 -0
- package/dist/server/server-client.d.ts +34 -0
- package/dist/server/types.d.ts +54 -0
- package/dist/server/verify-scoped-token.d.ts +100 -0
- package/dist/server/webhook-helpers.d.ts +36 -0
- package/dist/server.js +298 -0
- package/dist/transport/foir-http.d.ts +234 -0
- package/dist/transport/persisted-query.d.ts +68 -0
- package/dist/transport/retry.d.ts +61 -0
- package/dist/types/index.d.ts +124 -0
- package/package.json +35 -13
- package/dist/auth.d.ts +0 -382
- package/dist/chunk-ADO5FJNM.js +0 -30
- package/dist/client.d.ts +0 -135
- package/dist/content.d.ts +0 -96
- package/dist/media.d.ts +0 -234
- package/dist/next.d.ts +0 -178
- package/dist/push.d.ts +0 -109
- package/dist/react.d.ts +0 -319
- package/dist/server.d.ts +0 -534
- package/dist/shared-DkWlEd2Z.d.ts +0 -18
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import * as Y from 'yjs';
|
|
2
|
-
import { WebsocketProvider } from 'y-websocket';
|
|
3
|
-
|
|
4
1
|
/**
|
|
5
2
|
* @foir/sdk/realtime — optional thin web helper over y-websocket (EID-103).
|
|
6
3
|
*
|
|
@@ -22,22 +19,45 @@ import { WebsocketProvider } from 'y-websocket';
|
|
|
22
19
|
*
|
|
23
20
|
* Wire-protocol contract: `docs/realtime-collab-contract.md` (EID-117).
|
|
24
21
|
*/
|
|
25
|
-
|
|
22
|
+
import type * as Y from 'yjs';
|
|
23
|
+
import { WebsocketProvider } from 'y-websocket';
|
|
26
24
|
type Awareness = WebsocketProvider['awareness'];
|
|
27
|
-
interface RoomAddress {
|
|
25
|
+
export interface RoomAddress {
|
|
28
26
|
/** Room-type selector — e.g. `record`, `ephemeral`, `tilly_note`. */
|
|
29
27
|
namespace: string;
|
|
30
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Namespace-specific id. For `record`: the triple
|
|
30
|
+
* `{modelKey}:{recordId}:{variantKey}` (contract §2).
|
|
31
|
+
*
|
|
32
|
+
* The third segment is the selected variant's HUMAN KEY — `vip`, `default`,
|
|
33
|
+
* or the reserved `live` — **never a variant row's id**. `default` is the key
|
|
34
|
+
* for a record on no variant, and the segment is positional, so it cannot be
|
|
35
|
+
* omitted (`page:rec_1:` is a different room and an empty part is rejected
|
|
36
|
+
* with a 400).
|
|
37
|
+
*
|
|
38
|
+
* That segment is a KEY space because the platform compares it against the
|
|
39
|
+
* reserved token `live` to select an EPHEMERAL persistence policy (EID-385):
|
|
40
|
+
* live co-edit only — no `editing_state`, no autosave, doc destroyed on last
|
|
41
|
+
* leave. A cuid2 row id can never equal `live`, so sending an id puts that
|
|
42
|
+
* policy out of reach by construction. Worse, variants are themselves rows in
|
|
43
|
+
* `records`, so a variant id still passes the join gate: the connection
|
|
44
|
+
* succeeds and edits a DIFFERENT record, with nothing reported on either side
|
|
45
|
+
* (TTS-953).
|
|
46
|
+
*
|
|
47
|
+
* The address to send is the one the API advertises — the `collab` field on
|
|
48
|
+
* every public record type returns `{ enabled, wsUrl, roomName }`, where
|
|
49
|
+
* `roomName` is exactly this triple.
|
|
50
|
+
*/
|
|
31
51
|
id: string;
|
|
32
52
|
}
|
|
33
|
-
interface CollabPresence {
|
|
53
|
+
export interface CollabPresence {
|
|
34
54
|
id: string;
|
|
35
55
|
name: string;
|
|
36
56
|
/** Stable per-user colour; derived deterministically from `id` when omitted. */
|
|
37
57
|
color?: string;
|
|
38
58
|
[key: string]: unknown;
|
|
39
59
|
}
|
|
40
|
-
interface CollabPeer {
|
|
60
|
+
export interface CollabPeer {
|
|
41
61
|
/** The remote peer's y-awareness client id. */
|
|
42
62
|
clientId: number;
|
|
43
63
|
id: string;
|
|
@@ -45,7 +65,7 @@ interface CollabPeer {
|
|
|
45
65
|
color?: string;
|
|
46
66
|
[key: string]: unknown;
|
|
47
67
|
}
|
|
48
|
-
interface CreateCollabSessionOptions {
|
|
68
|
+
export interface CreateCollabSessionOptions {
|
|
49
69
|
/** Explicit realtime origin, e.g. `wss://realtime.foir.io`. Never derived from the API host. */
|
|
50
70
|
url: string;
|
|
51
71
|
/** Room address; built into the canonical bare `{namespace}/{id}` path. A raw string path is also accepted. */
|
|
@@ -65,7 +85,7 @@ interface CreateCollabSessionOptions {
|
|
|
65
85
|
/** WebSocket implementation for non-browser runtimes. */
|
|
66
86
|
WebSocketPolyfill?: typeof WebSocket;
|
|
67
87
|
}
|
|
68
|
-
type CollabEventMap = {
|
|
88
|
+
export type CollabEventMap = {
|
|
69
89
|
/** Connection status flipped. */
|
|
70
90
|
status: {
|
|
71
91
|
connected: boolean;
|
|
@@ -77,7 +97,7 @@ type CollabEventMap = {
|
|
|
77
97
|
/** A `getToken()` (or other internal) error surfaced. */
|
|
78
98
|
error: unknown;
|
|
79
99
|
};
|
|
80
|
-
interface CollabSession {
|
|
100
|
+
export interface CollabSession {
|
|
81
101
|
/** The underlying y-websocket provider (escape hatch). */
|
|
82
102
|
readonly provider: WebsocketProvider;
|
|
83
103
|
/** The y-awareness instance (escape hatch). */
|
|
@@ -99,13 +119,12 @@ interface CollabSession {
|
|
|
99
119
|
destroy(): void;
|
|
100
120
|
}
|
|
101
121
|
/** Deterministic per-id presence colour (stable across sessions and clients). */
|
|
102
|
-
declare function colorForId(id: string): string;
|
|
122
|
+
export declare function colorForId(id: string): string;
|
|
103
123
|
/** Build the canonical bare `{namespace}/{id}` room path (contract §2). */
|
|
104
|
-
declare function roomPath(room: RoomAddress | string): string;
|
|
124
|
+
export declare function roomPath(room: RoomAddress | string): string;
|
|
105
125
|
/**
|
|
106
126
|
* Create a collaboration session: a wrapped `y-websocket` provider that conforms
|
|
107
127
|
* to the Foir realtime contract and keeps its auth token fresh for reconnects.
|
|
108
128
|
*/
|
|
109
|
-
declare function createCollabSession(options: CreateCollabSessionOptions): CollabSession;
|
|
110
|
-
|
|
111
|
-
export { type CollabEventMap, type CollabPeer, type CollabPresence, type CollabSession, type CreateCollabSessionOptions, type RoomAddress, colorForId, createCollabSession, roomPath };
|
|
129
|
+
export declare function createCollabSession(options: CreateCollabSessionOptions): CollabSession;
|
|
130
|
+
export {};
|
package/dist/realtime.js
CHANGED
|
@@ -91,7 +91,7 @@ function createCollabSession(options) {
|
|
|
91
91
|
if (clientId === awareness.clientID) return;
|
|
92
92
|
const user = state["user"];
|
|
93
93
|
if (user && typeof user.id === "string") {
|
|
94
|
-
next.push({ clientId, id: user.id, name: user.name, color: user.color });
|
|
94
|
+
next.push({ ...user, clientId, id: user.id, name: user.name, color: user.color });
|
|
95
95
|
}
|
|
96
96
|
});
|
|
97
97
|
peers = next;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Callback client for extensions running async operations.
|
|
3
|
+
*
|
|
4
|
+
* The platform dispatches with `mode='async'` and includes a `callback`
|
|
5
|
+
* block in the operation payload. The extension acks 202, kicks the work
|
|
6
|
+
* off in its own durable runtime (CF Queue, BullMQ, etc.), and later
|
|
7
|
+
* calls back with one of:
|
|
8
|
+
*
|
|
9
|
+
* - `complete(result)` — operation succeeded; result conforms to the
|
|
10
|
+
* operation's `output_schema` (per-op typed via codegen).
|
|
11
|
+
* - `fail(code, message, opts?)` — operation failed; `retryable=true`
|
|
12
|
+
* pushes a DLQ entry.
|
|
13
|
+
* - `progress({pct, message, metadata})` — non-terminal update; the
|
|
14
|
+
* monotonic guard at the platform DB layer drops out-of-order packets.
|
|
15
|
+
* - `cancel()` — extension self-cancels (rare; admins normally drive
|
|
16
|
+
* cancel via the admin app's ConnectRPC path).
|
|
17
|
+
*
|
|
18
|
+
* Each call carries:
|
|
19
|
+
* - `Authorization: Bearer <callback.token>` — Ed25519 scoped token.
|
|
20
|
+
* - `X-Foir-Signature: sha256=<hex>` — HMAC of the raw request body
|
|
21
|
+
* keyed on the project's `operations.signing_secret`. The secret is
|
|
22
|
+
* out-of-band (CLI publishes it to the extension on push); the SDK
|
|
23
|
+
* only handles the HMAC computation.
|
|
24
|
+
*
|
|
25
|
+
* Both response body shape and content-type follow the GraphQL spec —
|
|
26
|
+
* extensions importing this client receive typed result data.
|
|
27
|
+
*/
|
|
28
|
+
import type { OperationCallback } from './operation-types.js';
|
|
29
|
+
/** Shape of a successful response from any callback mutation. */
|
|
30
|
+
export interface CallbackResult {
|
|
31
|
+
/**
|
|
32
|
+
* Status as seen by the platform AFTER the call. One of:
|
|
33
|
+
* pending|dispatched|running|completed|failed|cancelled|timed_out|progress_stale.
|
|
34
|
+
* Always set even on idempotent / cancelled calls.
|
|
35
|
+
*/
|
|
36
|
+
status: string;
|
|
37
|
+
/**
|
|
38
|
+
* True when the admin cancelled the execution while the extension was
|
|
39
|
+
* processing — the platform refused to overwrite the cancelled state.
|
|
40
|
+
* Extensions should stop work and (optionally) call `fail('CANCELLED')`.
|
|
41
|
+
*/
|
|
42
|
+
cancelled: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Progress mutations only: false when the monotonic guard rejected an
|
|
45
|
+
* out-of-order update. Extensions can use this to detect packet
|
|
46
|
+
* reordering without erroring; missing on complete/fail/cancel.
|
|
47
|
+
*/
|
|
48
|
+
applied?: boolean;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Per-operation callback client. `TResult` is the operation's typed result
|
|
52
|
+
* shape — supply the type generated by graphql-codegen against the
|
|
53
|
+
* api-public schema (`Complete{Op}ExecutionMutationVariables['result']`).
|
|
54
|
+
*/
|
|
55
|
+
export interface CallbackClient<TResult = unknown> {
|
|
56
|
+
complete(result: TResult): Promise<CallbackResult>;
|
|
57
|
+
fail(code: string, message: string, opts?: {
|
|
58
|
+
retryable?: boolean;
|
|
59
|
+
details?: Record<string, unknown>;
|
|
60
|
+
}): Promise<CallbackResult>;
|
|
61
|
+
/**
|
|
62
|
+
* Returns `{cancelled: true}` when the admin has cancelled — extension
|
|
63
|
+
* must stop work. `applied: false` means the monotonic guard dropped
|
|
64
|
+
* the update because pct was lower than the persisted value.
|
|
65
|
+
*/
|
|
66
|
+
progress(update: {
|
|
67
|
+
pct?: number;
|
|
68
|
+
message?: string;
|
|
69
|
+
metadata?: Record<string, unknown>;
|
|
70
|
+
}): Promise<CallbackResult>;
|
|
71
|
+
cancel(): Promise<CallbackResult>;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Construct a callback client from the dispatch payload's callback block
|
|
75
|
+
* plus the runtime-provided execution id and signing secret.
|
|
76
|
+
*
|
|
77
|
+
* `executionId` comes from the same operation payload that delivered
|
|
78
|
+
* `callback`; pass them together so the SDK doesn't have to re-parse the
|
|
79
|
+
* payload structure on every call.
|
|
80
|
+
*
|
|
81
|
+
* `signingSecret` lives on the extension (set as `FOIR_SIGNING_SECRET` or
|
|
82
|
+
* equivalent env var). `foir push` writes it there from the project's
|
|
83
|
+
* signing secret, so it's available before any callback runs.
|
|
84
|
+
*
|
|
85
|
+
* `fetch` is overridable for testability and to support runtimes (CF
|
|
86
|
+
* Workers, Node) where `fetch` may not be the global.
|
|
87
|
+
*/
|
|
88
|
+
export declare function createCallbackClient<TResult = unknown>(callback: OperationCallback, opts: {
|
|
89
|
+
executionId: string;
|
|
90
|
+
signingSecret: string;
|
|
91
|
+
fetch?: typeof fetch;
|
|
92
|
+
}): CallbackClient<TResult>;
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@foir/sdk/server`'s event-log client — cursor-resumable consumption of a
|
|
3
|
+
* project's durable event history (TTS-983, epic TTS-912).
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { createEventsClient } from '@foir/sdk/server';
|
|
7
|
+
*
|
|
8
|
+
* const events = createEventsClient({
|
|
9
|
+
* apiUrl: process.env.PLATFORM_API_URL!,
|
|
10
|
+
* apiKey: process.env.FOIR_SECRET_KEY!, // sk_…, holding events:list.project
|
|
11
|
+
* });
|
|
12
|
+
*
|
|
13
|
+
* const page = await events.list({ after: lastSeq, limit: 500 });
|
|
14
|
+
* for (const event of page.events) {
|
|
15
|
+
* await handle(event);
|
|
16
|
+
* lastSeq = event.seq; // durable position; store it
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* ---------------------------------------------------------------------------
|
|
21
|
+
* OPERATOR SURFACE. This is not a customer-facing capability.
|
|
22
|
+
* ---------------------------------------------------------------------------
|
|
23
|
+
*
|
|
24
|
+
* DECIDED 2026-08-04 (TTS-981). The event log is reachable by an `sk_` api key
|
|
25
|
+
* and by an admin session. It is NOT reachable by a customer token, there is no
|
|
26
|
+
* customer lane for it on api-public, and a `pk_` key can never be minted
|
|
27
|
+
* holding `events:list.project` at all.
|
|
28
|
+
*
|
|
29
|
+
* The reason is a predicate asymmetry rather than a policy preference.
|
|
30
|
+
* `events.view` is plain tenant+project containment; `record.view` carries
|
|
31
|
+
* owner, admin_owner, access_mode, grantee, comp_parent, read_borrow and
|
|
32
|
+
* slice_parent terms on top of it. A containment-only read over the log
|
|
33
|
+
* therefore describes rows whose underlying records the same caller could not
|
|
34
|
+
* read — and every row carries `actor`, naming which OTHER customer acted. In a
|
|
35
|
+
* project with owner confinement that is a per-record activity feed of other
|
|
36
|
+
* people's actions.
|
|
37
|
+
*
|
|
38
|
+
* Three consequences for the shape of this file, all deliberate:
|
|
39
|
+
*
|
|
40
|
+
* 1. **It lives on `@foir/sdk/server` and nowhere else.** Not `./data`, not
|
|
41
|
+
* `./client`, not `./react`. Those subpaths are the ones a storefront
|
|
42
|
+
* bundles, and `./data` in particular is routinely constructed with a `pk_`
|
|
43
|
+
* key in browser-reachable code. `src/server/__tests__/events-client.test.ts`
|
|
44
|
+
* walks the import graph of every published subpath and fails if this
|
|
45
|
+
* module becomes reachable from one that ships to a browser — because
|
|
46
|
+
* "we only documented it on the server" is not a property anything checks.
|
|
47
|
+
*
|
|
48
|
+
* 2. **A `pk_` key is refused at construction**, before a request is made. The
|
|
49
|
+
* server would refuse it anyway (403), but a 403 at 3am in a deploy log is
|
|
50
|
+
* a worse teacher than a thrown error naming the key type at the line that
|
|
51
|
+
* built the client.
|
|
52
|
+
*
|
|
53
|
+
* 3. **There is no `bearerToken` option, and an `Authorization` header is
|
|
54
|
+
* refused.** On api-public a request presenting `sk_` plus a customer
|
|
55
|
+
* bearer resolves the CUSTOMER as the authority, and `events` is stripped
|
|
56
|
+
* from every customer authority upstream — so attaching a signed-in
|
|
57
|
+
* shopper's token to an operator read turns a working call into a 403 that
|
|
58
|
+
* reads like a scope problem. Refusing it here says what actually happened.
|
|
59
|
+
*
|
|
60
|
+
* Widening this later is easy. Teaching customers a capability they do not have
|
|
61
|
+
* is a support burden that cannot be retracted.
|
|
62
|
+
*
|
|
63
|
+
* ---------------------------------------------------------------------------
|
|
64
|
+
* Why `GET /v1/events` and not the GraphQL `events` field
|
|
65
|
+
* ---------------------------------------------------------------------------
|
|
66
|
+
*
|
|
67
|
+
* Both exist and both call the same platform RPC with the same credential and
|
|
68
|
+
* the same scope requirement, so this is not an authorization difference. It is
|
|
69
|
+
* a caching one: the GraphQL lane sits behind api-public's response cache, and
|
|
70
|
+
* a cursor-paged log served from a cached body can hand a consumer a page it has
|
|
71
|
+
* already processed while `hasMore` says there is nothing left. The REST twin is
|
|
72
|
+
* deliberately not response-cached, which is the property a durable consumer
|
|
73
|
+
* needs. It also keeps this module `graphql`-free, like `@foir/sdk/data`.
|
|
74
|
+
*/
|
|
75
|
+
import { FoirRequestError } from '../transport/foir-http';
|
|
76
|
+
/** The path `GET /v1/events` is mounted on, inside api-public's api-key group. */
|
|
77
|
+
export declare const FOIR_EVENTS_PATH = "/v1/events";
|
|
78
|
+
/**
|
|
79
|
+
* The scope an `sk_` key must hold. Never mintable on a `pk_` — `events` is
|
|
80
|
+
* absent from the platform's `publicSafeScopeResources` — and never present in a
|
|
81
|
+
* customer's resolved authority.
|
|
82
|
+
*/
|
|
83
|
+
export declare const FOIR_EVENTS_SCOPE = "events:list.project";
|
|
84
|
+
/**
|
|
85
|
+
* The flat retention window, in days. Reported on every page as
|
|
86
|
+
* {@link FoirEventRetention.windowDays} too; this constant is the one to compare
|
|
87
|
+
* a hard-coded expectation against, and the two are pinned to each other by
|
|
88
|
+
* `oldestRetainedAt` in the tests.
|
|
89
|
+
*/
|
|
90
|
+
export declare const FOIR_EVENTS_RETENTION_DAYS = 90;
|
|
91
|
+
/** Who caused an event. `null` when the emit site attributed nobody. */
|
|
92
|
+
export interface FoirEventActor {
|
|
93
|
+
/** e.g. `"customer"`, `"admin"`, `"api_key"`, `"system"`. */
|
|
94
|
+
kind?: string;
|
|
95
|
+
/** e.g. `"customer:cus_123"`. */
|
|
96
|
+
ref?: string;
|
|
97
|
+
[key: string]: unknown;
|
|
98
|
+
}
|
|
99
|
+
/** One row of the durable event log. */
|
|
100
|
+
export interface FoirEvent {
|
|
101
|
+
id: string;
|
|
102
|
+
/**
|
|
103
|
+
* This event's position in the project's stream, assigned by the relay in
|
|
104
|
+
* COMMIT order — not at insert, and not by `created_at`.
|
|
105
|
+
*
|
|
106
|
+
* Store the highest one you have finished processing and pass it back as
|
|
107
|
+
* `after`. Gaps are normal: the stream carries events your filter did not
|
|
108
|
+
* match, and history ages out of the retention window. `seq` is for
|
|
109
|
+
* comparing, never for counting.
|
|
110
|
+
*/
|
|
111
|
+
seq: number;
|
|
112
|
+
/** The registry name, e.g. `"record.created"`. */
|
|
113
|
+
event: string;
|
|
114
|
+
/**
|
|
115
|
+
* The event's own flat keys, plus `state` and `stateSchema`.
|
|
116
|
+
*
|
|
117
|
+
* `state` is the resource AS OF the event — self-contained, not a pointer to
|
|
118
|
+
* current state — for the customer, session, operation and workspace-plane
|
|
119
|
+
* families. It is null for `record.*` events, deliberately and permanently:
|
|
120
|
+
* `events.view` is plain tenant/project containment while `record.view`
|
|
121
|
+
* carries owner, grant, borrow and per-field terms, so a copy of record data
|
|
122
|
+
* in the log would escape every one of them for the whole retention window.
|
|
123
|
+
*
|
|
124
|
+
* A record event therefore carries the `recordId` / `modelKey` pointer and no
|
|
125
|
+
* field content — read the record back under your own credentials.
|
|
126
|
+
*
|
|
127
|
+
* `stateSchema` versions the snapshot's shape.
|
|
128
|
+
*/
|
|
129
|
+
payload: Record<string, unknown>;
|
|
130
|
+
actor: FoirEventActor | null;
|
|
131
|
+
/** The operation-key history that led here. */
|
|
132
|
+
causationChain: string[];
|
|
133
|
+
/** RFC 3339, as sent. */
|
|
134
|
+
createdAt: string;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The retention window, reported on EVERY page rather than only when something
|
|
138
|
+
* has gone wrong — so a consumer can see how close its cursor is to the edge
|
|
139
|
+
* before it falls off, which is the difference between an alert and an incident.
|
|
140
|
+
*/
|
|
141
|
+
export interface FoirEventRetention {
|
|
142
|
+
/** The flat window in days. 90, no tiering. */
|
|
143
|
+
windowDays: number;
|
|
144
|
+
/** The oldest `createdAt` the API will serve. RFC 3339. */
|
|
145
|
+
oldestRetainedAt: string;
|
|
146
|
+
/**
|
|
147
|
+
* The lowest `seq` still inside the window for this project, or 0 when nothing
|
|
148
|
+
* is retained. Compare your stored cursor against it to see the runway left.
|
|
149
|
+
*/
|
|
150
|
+
oldestAvailableSeq: number;
|
|
151
|
+
}
|
|
152
|
+
export interface FoirEventPage {
|
|
153
|
+
/** The page, ordered by `seq` ascending. */
|
|
154
|
+
events: FoirEvent[];
|
|
155
|
+
/**
|
|
156
|
+
* The cursor to persist and resume from: the `seq` of the last event on this
|
|
157
|
+
* page, or the request's `after` when the page is empty. Never moves
|
|
158
|
+
* backwards, so persisting it unconditionally is safe.
|
|
159
|
+
*/
|
|
160
|
+
nextCursor: number;
|
|
161
|
+
/** True when more events are already available past `nextCursor`. */
|
|
162
|
+
hasMore: boolean;
|
|
163
|
+
retention: FoirEventRetention;
|
|
164
|
+
}
|
|
165
|
+
export interface FoirEventsListParams {
|
|
166
|
+
/**
|
|
167
|
+
* Exclusive cursor: return events with `seq > after`. Omit or pass 0 to start
|
|
168
|
+
* at the oldest event still inside the retention window.
|
|
169
|
+
*/
|
|
170
|
+
after?: number;
|
|
171
|
+
/** Page size. The server defaults to 100 and caps at 1000. */
|
|
172
|
+
limit?: number;
|
|
173
|
+
/**
|
|
174
|
+
* Filter by event name, e.g. `['record.created', 'record.updated']`. Omit for
|
|
175
|
+
* every event.
|
|
176
|
+
*/
|
|
177
|
+
events?: readonly string[];
|
|
178
|
+
/** Aborts the underlying request. */
|
|
179
|
+
signal?: AbortSignal;
|
|
180
|
+
}
|
|
181
|
+
export interface FoirEventsStreamParams extends FoirEventsListParams {
|
|
182
|
+
/**
|
|
183
|
+
* Called after each page is fetched and before its events are yielded, with
|
|
184
|
+
* the cursor that page ended on. Use it to checkpoint at page granularity
|
|
185
|
+
* instead of per event.
|
|
186
|
+
*/
|
|
187
|
+
onPage?: (page: FoirEventPage) => void | Promise<void>;
|
|
188
|
+
}
|
|
189
|
+
export interface FoirEventsClientConfig {
|
|
190
|
+
/**
|
|
191
|
+
* Foir public API origin, e.g. `https://api.foir.dev`. A full
|
|
192
|
+
* `https://api.foir.dev/graphql` is accepted too, since that is what
|
|
193
|
+
* `PLATFORM_API_URL` conventionally holds.
|
|
194
|
+
*/
|
|
195
|
+
apiUrl: string;
|
|
196
|
+
/**
|
|
197
|
+
* An `sk_` secret key holding `events:list.project`.
|
|
198
|
+
*
|
|
199
|
+
* A `pk_` key is refused here rather than at the server — see the operator
|
|
200
|
+
* note at the top of this file.
|
|
201
|
+
*/
|
|
202
|
+
apiKey: string;
|
|
203
|
+
/**
|
|
204
|
+
* Extra headers. An `Authorization` header is refused: a customer bearer
|
|
205
|
+
* flips api-public's resolved authority to the customer, and no customer
|
|
206
|
+
* authority can contain `events:list.project`.
|
|
207
|
+
*/
|
|
208
|
+
headers?: Readonly<Record<string, string>>;
|
|
209
|
+
/** `fetch` implementation. Defaults to `globalThis.fetch`. */
|
|
210
|
+
fetch?: typeof globalThis.fetch;
|
|
211
|
+
/**
|
|
212
|
+
* Merged into every `fetch` call. Applied after method/headers, so those
|
|
213
|
+
* cannot be clobbered by accident.
|
|
214
|
+
*/
|
|
215
|
+
requestInit?: Omit<RequestInit, 'method' | 'headers' | 'body'>;
|
|
216
|
+
}
|
|
217
|
+
export interface FoirEventsClient {
|
|
218
|
+
/** One page of the project's event history. */
|
|
219
|
+
list(params?: FoirEventsListParams): Promise<FoirEventPage>;
|
|
220
|
+
/**
|
|
221
|
+
* Every event from `after` to the current end of the stream, paging as it
|
|
222
|
+
* goes. Finishes when the server reports no more — it does NOT poll, so a
|
|
223
|
+
* long-running consumer wraps it in its own interval.
|
|
224
|
+
*
|
|
225
|
+
* Each yielded event carries its own `seq`, which is the cursor: a consumer
|
|
226
|
+
* that stores `event.seq` after processing resumes exactly where it stopped,
|
|
227
|
+
* including mid-page after a crash.
|
|
228
|
+
*/
|
|
229
|
+
stream(params?: FoirEventsStreamParams): AsyncGenerator<FoirEvent, void, void>;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* The cursor named a position older than the retention window, so events the
|
|
233
|
+
* consumer had not read have expired. HTTP 410 Gone.
|
|
234
|
+
*
|
|
235
|
+
* The server REFUSES rather than serving a shortened page, deliberately: a flag
|
|
236
|
+
* on a successful response makes silent data loss the default for every consumer
|
|
237
|
+
* that does not read the flag. Acknowledging a gap should be one deliberate line
|
|
238
|
+
* of code, not an accident.
|
|
239
|
+
*
|
|
240
|
+
* ```ts
|
|
241
|
+
* try {
|
|
242
|
+
* page = await events.list({ after: cursor });
|
|
243
|
+
* } catch (err) {
|
|
244
|
+
* if (err instanceof FoirEventCursorExpiredError) {
|
|
245
|
+
* await recordGap(cursor, err.resumeFrom);
|
|
246
|
+
* page = await events.list({ after: err.resumeFrom ?? 0 });
|
|
247
|
+
* } else throw err;
|
|
248
|
+
* }
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
export declare class FoirEventCursorExpiredError extends FoirRequestError {
|
|
252
|
+
/**
|
|
253
|
+
* The cursor to resume from, accepting the gap — parsed out of the server's
|
|
254
|
+
* message, which names it.
|
|
255
|
+
*
|
|
256
|
+
* `undefined` only if the message ever stops naming one, in which case
|
|
257
|
+
* restarting from 0 and de-duplicating on `id` is the safe fallback. It is
|
|
258
|
+
* optional for that reason and not because the server omits it today; both
|
|
259
|
+
* refusal messages carry it, and both are pinned as fixtures in the tests.
|
|
260
|
+
*/
|
|
261
|
+
readonly resumeFrom?: number;
|
|
262
|
+
/** The cursor that was rejected — whatever was passed as `after`. */
|
|
263
|
+
readonly requestedCursor?: number;
|
|
264
|
+
constructor(message: string, options: {
|
|
265
|
+
status: number;
|
|
266
|
+
resumeFrom?: number;
|
|
267
|
+
requestedCursor?: number;
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* True when the failure is a cursor that fell outside the retention window.
|
|
272
|
+
*
|
|
273
|
+
* Provided alongside the class because `instanceof` across two copies of the
|
|
274
|
+
* package — a monorepo with a hoisted and a nested `@foir/sdk` — is false even
|
|
275
|
+
* for the right error, and the SDK's other taxonomy helpers
|
|
276
|
+
* (`isFoirRateLimited`, `isFoirForbidden`) already set this precedent.
|
|
277
|
+
*/
|
|
278
|
+
export declare function isFoirEventCursorExpired(err: unknown): err is FoirEventCursorExpiredError;
|
|
279
|
+
export declare function createEventsClient(config: FoirEventsClientConfig): FoirEventsClient;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { createServerClient, ServerClientError } from './server-client';
|
|
2
|
+
export { verifyWebhookSignature } from './webhook-helpers';
|
|
3
|
+
export { verifyScopedToken, parseContextHeader, parseSubject, recordsReadModels, recordsWriteModels, type ScopedTokenClaims, type ScopedTokenSubject, type VerifyScopedTokenOptions, } from './verify-scoped-token';
|
|
4
|
+
export { requireScopedToken, ScopedTokenError, type RequireScopedTokenOptions, type ScopedTokenIngress, } from './require-scoped-token';
|
|
5
|
+
export type { ServerClient, ServerClientOptions, GraphQLResponse, EditorPlacement, } from './types';
|
|
6
|
+
export type { OperationPayload, OperationTrigger, OperationRecordContext, OperationContext, OperationResult, OperationCallback, } from './operation-types';
|
|
7
|
+
export { createCallbackClient } from './callback-client';
|
|
8
|
+
export type { CallbackClient, CallbackResult } from './callback-client';
|
|
9
|
+
export { createEventsClient, FoirEventCursorExpiredError, isFoirEventCursorExpired, FOIR_EVENTS_PATH, FOIR_EVENTS_SCOPE, FOIR_EVENTS_RETENTION_DAYS, } from './events-client';
|
|
10
|
+
export type { FoirEvent, FoirEventActor, FoirEventPage, FoirEventRetention, FoirEventsClient, FoirEventsClientConfig, FoirEventsListParams, FoirEventsStreamParams, } from './events-client';
|
|
11
|
+
export { FoirRequestError, isFoirForbidden, isFoirNotFound, isFoirRateLimited, isFoirUnauthenticated, } from '../transport/foir-http';
|
|
12
|
+
export type { FoirErrorCode, FoirErrorExtensions, FoirGraphQLErrorItem } from '../transport/foir-http';
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for platform operations.
|
|
3
|
+
*
|
|
4
|
+
* These types match the platform's operation execution payload structure.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Operation payload sent by the platform to extension endpoints.
|
|
8
|
+
*
|
|
9
|
+
* All operations receive this standardized payload structure regardless
|
|
10
|
+
* of how they're triggered (UI, API, lifecycle hook, schedule).
|
|
11
|
+
*/
|
|
12
|
+
export interface OperationPayload {
|
|
13
|
+
/** Execution id — present so the extension can include it in callbacks. */
|
|
14
|
+
executionId?: string;
|
|
15
|
+
/** Unique key identifying this operation (e.g., 'tilly-process-note') */
|
|
16
|
+
operationKey: string;
|
|
17
|
+
/** How this operation was triggered */
|
|
18
|
+
trigger: OperationTrigger;
|
|
19
|
+
/** Input data provided by the caller */
|
|
20
|
+
input: Record<string, unknown>;
|
|
21
|
+
/** Content/field value (for field-triggered operations) */
|
|
22
|
+
content: unknown;
|
|
23
|
+
/** Record context (if operation was triggered by a record event) */
|
|
24
|
+
record: OperationRecordContext | null;
|
|
25
|
+
/** Platform context (tenant, project, user, locale) */
|
|
26
|
+
context: OperationContext;
|
|
27
|
+
/** Extension credentials (auto-injected by platform) */
|
|
28
|
+
credentials?: Record<string, unknown>;
|
|
29
|
+
/** Extension config (auto-injected by platform) */
|
|
30
|
+
config?: Record<string, unknown>;
|
|
31
|
+
/**
|
|
32
|
+
* Async-callback dispatch metadata. Present iff the operation was
|
|
33
|
+
* dispatched in `async-callback` mode. Contains the scoped token, the
|
|
34
|
+
* api-public GraphQL endpoint, and the per-op typed mutation names the
|
|
35
|
+
* extension should call back to.
|
|
36
|
+
*
|
|
37
|
+
* Absent for SYNC operations — the extension returns its result in the
|
|
38
|
+
* HTTP response body and there's nothing to call back to.
|
|
39
|
+
*/
|
|
40
|
+
callback?: OperationCallback;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Callback context the platform hands the extension when it dispatches an
|
|
44
|
+
* async-callback operation. Everything the extension needs to call back is
|
|
45
|
+
* here — endpoint, auth token, signing secret reference (the secret itself
|
|
46
|
+
* lives on the extension, set out of band; the platform only sends the
|
|
47
|
+
* token), and the per-op mutation names so the extension doesn't need to
|
|
48
|
+
* derive them from the operation key.
|
|
49
|
+
*/
|
|
50
|
+
export interface OperationCallback {
|
|
51
|
+
/** api-public GraphQL endpoint, e.g. https://api.foir.dev/graphql */
|
|
52
|
+
gqlEndpoint: string;
|
|
53
|
+
/** Bearer token for the Authorization header (Ed25519, single execution scope) */
|
|
54
|
+
token: string;
|
|
55
|
+
/** RFC3339 expiry — extensions can short-circuit work past this. */
|
|
56
|
+
expiresAt: string;
|
|
57
|
+
/** Per-op typed mutation names the extension should call back with. */
|
|
58
|
+
mutations: {
|
|
59
|
+
complete: string;
|
|
60
|
+
fail: string;
|
|
61
|
+
progress: string;
|
|
62
|
+
cancel: string;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Trigger information describing how the operation was invoked.
|
|
67
|
+
*/
|
|
68
|
+
export interface OperationTrigger {
|
|
69
|
+
/** Trigger type */
|
|
70
|
+
type: 'ui' | 'api' | 'lifecycle' | 'field';
|
|
71
|
+
/** Field key (for field-triggered operations) */
|
|
72
|
+
fieldKey?: string;
|
|
73
|
+
/** Field type (for field-triggered operations) */
|
|
74
|
+
fieldType?: string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Record context provided when operation is triggered by a record event.
|
|
78
|
+
*/
|
|
79
|
+
export interface OperationRecordContext {
|
|
80
|
+
/** Record ID */
|
|
81
|
+
id: string;
|
|
82
|
+
/** Model key (e.g., 'tilly_note') */
|
|
83
|
+
modelKey: string;
|
|
84
|
+
/** Version ID (if versioned record) */
|
|
85
|
+
versionId?: string;
|
|
86
|
+
/** Record content/data */
|
|
87
|
+
data: Record<string, unknown>;
|
|
88
|
+
/** Record metadata */
|
|
89
|
+
metadata: {
|
|
90
|
+
/** Natural key (slug, handle, etc.) */
|
|
91
|
+
naturalKey?: string;
|
|
92
|
+
/** Additional metadata fields */
|
|
93
|
+
[key: string]: unknown;
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Platform context provided to all operations.
|
|
98
|
+
*/
|
|
99
|
+
export interface OperationContext {
|
|
100
|
+
/** Tenant ID */
|
|
101
|
+
tenantId: string;
|
|
102
|
+
/** Project ID */
|
|
103
|
+
projectId: string;
|
|
104
|
+
/** User ID (if user-triggered) */
|
|
105
|
+
userId?: string;
|
|
106
|
+
/** Customer ID (if customer-scoped operation) */
|
|
107
|
+
customerId?: string;
|
|
108
|
+
/** Locale code (if locale-specific) */
|
|
109
|
+
locale?: string;
|
|
110
|
+
/** Timestamp when operation was triggered */
|
|
111
|
+
timestamp: string;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Standard operation result shape.
|
|
115
|
+
*
|
|
116
|
+
* All operations should return this structure.
|
|
117
|
+
*/
|
|
118
|
+
export interface OperationResult {
|
|
119
|
+
/** Whether the operation succeeded */
|
|
120
|
+
success: boolean;
|
|
121
|
+
/** Operation result data (if successful) */
|
|
122
|
+
result?: unknown;
|
|
123
|
+
/** Error information (if failed) */
|
|
124
|
+
error?: {
|
|
125
|
+
/** Error code (e.g., 'MISSING_INPUT', 'PROCESSING_FAILED') */
|
|
126
|
+
code: string;
|
|
127
|
+
/** Human-readable error message */
|
|
128
|
+
message: string;
|
|
129
|
+
/** Additional error details */
|
|
130
|
+
details?: Record<string, unknown>;
|
|
131
|
+
};
|
|
132
|
+
/** Optional metadata (timing, cost, etc.) */
|
|
133
|
+
metadata?: Record<string, unknown>;
|
|
134
|
+
}
|