@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.
Files changed (107) hide show
  1. package/CHANGELOG.md +1126 -0
  2. package/README.md +94 -0
  3. package/dist/auth/auth.d.ts +243 -0
  4. package/dist/auth/hosted.d.ts +40 -0
  5. package/dist/auth/index.d.ts +21 -0
  6. package/dist/auth/operations.d.ts +144 -0
  7. package/dist/auth/ops.d.ts +99 -0
  8. package/dist/auth/session-client.d.ts +76 -0
  9. package/dist/auth-session.js +137 -0
  10. package/dist/auth.js +973 -92
  11. package/dist/chunk-IGNLWT3H.js +35 -0
  12. package/dist/chunk-OHYVKTGB.js +45 -0
  13. package/dist/chunk-TAL5H6VA.js +39 -0
  14. package/dist/client/platform-client.d.ts +29 -0
  15. package/dist/client.js +275 -37
  16. package/dist/codegen/block-registry.d.ts +13 -23
  17. package/dist/codegen/block-registry.js +3 -31
  18. package/dist/codegen/block-scaffold.d.ts +12 -25
  19. package/dist/codegen/block-scaffold.js +3 -41
  20. package/dist/codegen/index.d.ts +12 -61
  21. package/dist/codegen/index.js +7 -11
  22. package/dist/codegen/item-registry.d.ts +28 -0
  23. package/dist/codegen/item-registry.js +7 -0
  24. package/dist/codegen/item-scaffold.d.ts +29 -0
  25. package/dist/codegen/item-scaffold.js +7 -0
  26. package/dist/codegen/preset.d.ts +67 -0
  27. package/dist/codegen/shared.d.ts +36 -0
  28. package/dist/components/EditorProvider.d.ts +36 -0
  29. package/dist/content/content-renderer.d.ts +113 -0
  30. package/dist/content/index.d.ts +18 -0
  31. package/dist/content/richtext.d.ts +118 -0
  32. package/dist/content.js +227 -80
  33. package/dist/data/builtins.d.ts +506 -0
  34. package/dist/data/client.d.ts +166 -0
  35. package/dist/data/document.d.ts +251 -0
  36. package/dist/data/index.d.ts +73 -0
  37. package/dist/data/manifest.d.ts +424 -0
  38. package/dist/data/schema.d.ts +313 -0
  39. package/dist/data/selection.d.ts +524 -0
  40. package/dist/data/subjects.d.ts +221 -0
  41. package/dist/data/subscription-document.d.ts +75 -0
  42. package/dist/data/subscriptions.d.ts +181 -0
  43. package/dist/data/transport.d.ts +325 -0
  44. package/dist/data/ws-transport.d.ts +197 -0
  45. package/dist/data.js +1683 -0
  46. package/dist/graphql/cdn-route.d.ts +26 -0
  47. package/dist/graphql/client.d.ts +30 -0
  48. package/dist/graphql/content-client.d.ts +66 -0
  49. package/dist/graphql/foir-request.d.ts +47 -0
  50. package/dist/hooks/useAutoResize.d.ts +22 -0
  51. package/dist/hooks/useEditor.d.ts +16 -0
  52. package/dist/index.d.ts +10 -394
  53. package/dist/media/BlurhashCanvas.d.ts +10 -0
  54. package/dist/media/ProgressiveMedia.d.ts +56 -0
  55. package/dist/media/index.d.ts +18 -0
  56. package/dist/media/media.d.ts +139 -0
  57. package/dist/media/screen-capture.d.ts +81 -0
  58. package/dist/media/useProgressiveLoad.d.ts +23 -0
  59. package/dist/media.js +102 -0
  60. package/dist/next/cookieAdapter.d.ts +26 -0
  61. package/dist/next/index.d.ts +15 -0
  62. package/dist/next/middleware.d.ts +49 -0
  63. package/dist/next/session.d.ts +53 -0
  64. package/dist/next.js +763 -78
  65. package/dist/protocol/constants.d.ts +6 -0
  66. package/dist/protocol/messages.d.ts +155 -0
  67. package/dist/protocol/validation.d.ts +8 -0
  68. package/dist/push/index.d.ts +14 -0
  69. package/dist/push/operations.d.ts +22 -0
  70. package/dist/push/web-push.d.ts +39 -0
  71. package/dist/push.js +892 -78
  72. package/dist/react/FoirFeedback.d.ts +62 -0
  73. package/dist/react/FoirProvider.d.ts +116 -0
  74. package/dist/react/OAuthButtons.d.ts +20 -0
  75. package/dist/react/SignIn.d.ts +22 -0
  76. package/dist/react/UserButton.d.ts +13 -0
  77. package/dist/react/feedback-notes.d.ts +101 -0
  78. package/dist/react/index.d.ts +25 -0
  79. package/dist/react/localStorageAdapter.d.ts +10 -0
  80. package/dist/react.js +1373 -91
  81. package/dist/{realtime.d.ts → realtime/index.d.ts} +35 -16
  82. package/dist/realtime.js +1 -1
  83. package/dist/server/callback-client.d.ts +92 -0
  84. package/dist/server/events-client.d.ts +279 -0
  85. package/dist/server/index.d.ts +12 -0
  86. package/dist/server/operation-types.d.ts +134 -0
  87. package/dist/server/require-scoped-token.d.ts +76 -0
  88. package/dist/server/server-client.d.ts +34 -0
  89. package/dist/server/types.d.ts +54 -0
  90. package/dist/server/verify-scoped-token.d.ts +100 -0
  91. package/dist/server/webhook-helpers.d.ts +36 -0
  92. package/dist/server.js +298 -0
  93. package/dist/transport/foir-http.d.ts +234 -0
  94. package/dist/transport/persisted-query.d.ts +68 -0
  95. package/dist/transport/retry.d.ts +61 -0
  96. package/dist/types/index.d.ts +124 -0
  97. package/package.json +35 -13
  98. package/dist/auth.d.ts +0 -382
  99. package/dist/chunk-ADO5FJNM.js +0 -30
  100. package/dist/client.d.ts +0 -135
  101. package/dist/content.d.ts +0 -96
  102. package/dist/media.d.ts +0 -234
  103. package/dist/next.d.ts +0 -178
  104. package/dist/push.d.ts +0 -109
  105. package/dist/react.d.ts +0 -319
  106. package/dist/server.d.ts +0 -534
  107. 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
- /** Namespace-specific id. For `record`: `{modelKey}:{recordId}:{variantId}`. */
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
+ }