@foir/sdk 0.4.0 → 0.7.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 (82) hide show
  1. package/CHANGELOG.md +325 -0
  2. package/README.md +63 -0
  3. package/dist/auth/auth.d.ts +140 -0
  4. package/dist/auth/hosted.d.ts +40 -0
  5. package/dist/auth/index.d.ts +15 -0
  6. package/dist/auth/operations.d.ts +123 -0
  7. package/dist/auth/session-client.d.ts +76 -0
  8. package/dist/auth-session.js +137 -0
  9. package/dist/auth.js +238 -26
  10. package/dist/client/platform-client.d.ts +29 -0
  11. package/dist/client.js +290 -32
  12. package/dist/codegen/block-registry.d.ts +4 -8
  13. package/dist/codegen/block-scaffold.d.ts +4 -8
  14. package/dist/codegen/index.d.ts +8 -62
  15. package/dist/codegen/preset.d.ts +60 -0
  16. package/dist/codegen/shared.d.ts +25 -0
  17. package/dist/components/EditorProvider.d.ts +36 -0
  18. package/dist/content/content-renderer.d.ts +47 -0
  19. package/dist/content/index.d.ts +12 -0
  20. package/dist/content/richtext.d.ts +70 -0
  21. package/dist/data/builtins.d.ts +296 -0
  22. package/dist/data/client.d.ts +94 -0
  23. package/dist/data/index.d.ts +54 -0
  24. package/dist/data/manifest.d.ts +161 -0
  25. package/dist/data/schema.d.ts +243 -0
  26. package/dist/data/selection.d.ts +330 -0
  27. package/dist/data/transport.d.ts +107 -0
  28. package/dist/data.js +309 -0
  29. package/dist/graphql/cdn-route.d.ts +42 -0
  30. package/dist/graphql/client.d.ts +30 -0
  31. package/dist/graphql/content-client.d.ts +66 -0
  32. package/dist/graphql/foir-request.d.ts +47 -0
  33. package/dist/hooks/useAutoResize.d.ts +22 -0
  34. package/dist/hooks/useEditor.d.ts +16 -0
  35. package/dist/index.d.ts +10 -394
  36. package/dist/media/BlurhashCanvas.d.ts +10 -0
  37. package/dist/media/ProgressiveMedia.d.ts +56 -0
  38. package/dist/media/index.d.ts +14 -0
  39. package/dist/media/media.d.ts +139 -0
  40. package/dist/media/useProgressiveLoad.d.ts +23 -0
  41. package/dist/media.js +5 -0
  42. package/dist/next/cookieAdapter.d.ts +26 -0
  43. package/dist/next/index.d.ts +15 -0
  44. package/dist/next/middleware.d.ts +49 -0
  45. package/dist/next/session.d.ts +53 -0
  46. package/dist/next.js +230 -26
  47. package/dist/protocol/constants.d.ts +6 -0
  48. package/dist/protocol/messages.d.ts +155 -0
  49. package/dist/protocol/validation.d.ts +8 -0
  50. package/dist/push/index.d.ts +14 -0
  51. package/dist/push/operations.d.ts +22 -0
  52. package/dist/push/web-push.d.ts +39 -0
  53. package/dist/push.js +230 -26
  54. package/dist/react/FoirProvider.d.ts +116 -0
  55. package/dist/react/OAuthButtons.d.ts +20 -0
  56. package/dist/react/SignIn.d.ts +22 -0
  57. package/dist/react/UserButton.d.ts +13 -0
  58. package/dist/react/index.d.ts +19 -0
  59. package/dist/react/localStorageAdapter.d.ts +10 -0
  60. package/dist/react.js +230 -26
  61. package/dist/{realtime.d.ts → realtime/index.d.ts} +35 -16
  62. package/dist/server/callback-client.d.ts +92 -0
  63. package/dist/server/index.d.ts +8 -0
  64. package/dist/server/operation-types.d.ts +134 -0
  65. package/dist/server/require-scoped-token.d.ts +76 -0
  66. package/dist/server/server-client.d.ts +34 -0
  67. package/dist/server/types.d.ts +54 -0
  68. package/dist/server/verify-scoped-token.d.ts +100 -0
  69. package/dist/server/webhook-helpers.d.ts +36 -0
  70. package/dist/transport/foir-http.d.ts +217 -0
  71. package/dist/transport/retry.d.ts +61 -0
  72. package/dist/types/index.d.ts +124 -0
  73. package/package.json +23 -13
  74. package/dist/auth.d.ts +0 -382
  75. package/dist/client.d.ts +0 -83
  76. package/dist/content.d.ts +0 -96
  77. package/dist/media.d.ts +0 -234
  78. package/dist/next.d.ts +0 -178
  79. package/dist/push.d.ts +0 -109
  80. package/dist/react.d.ts +0 -319
  81. package/dist/server.d.ts +0 -534
  82. package/dist/shared-DkWlEd2Z.d.ts +0 -18
package/dist/react.js CHANGED
@@ -12,10 +12,18 @@ import {
12
12
  } from "react";
13
13
 
14
14
  // src/graphql/foir-request.ts
15
+ import { print as print2 } from "graphql";
16
+
17
+ // src/graphql/cdn-route.ts
15
18
  import { print } from "graphql";
19
+
20
+ // src/transport/foir-http.ts
16
21
  function isFoirPreview(env) {
17
22
  return env.FOIR_PREVIEW === "true";
18
23
  }
24
+ function resolveFoirApiKey(env) {
25
+ return isFoirPreview(env) ? env.PLATFORM_PREVIEW_API_KEY ?? env.PLATFORM_API_KEY : env.PLATFORM_API_KEY;
26
+ }
19
27
  var FoirRequestError = class extends Error {
20
28
  /** HTTP status code, when the request failed at the HTTP layer. */
21
29
  status;
@@ -29,41 +37,83 @@ var FoirRequestError = class extends Error {
29
37
  this.status = options?.status;
30
38
  this.graphQLErrors = options?.graphQLErrors;
31
39
  }
40
+ /**
41
+ * The code of the first error, if the response carried one.
42
+ *
43
+ * A getter rather than a stored field so it stays correct for an error
44
+ * constructed anywhere, including by a caller in a test.
45
+ */
46
+ get code() {
47
+ return this.graphQLErrors?.[0]?.extensions?.code;
48
+ }
49
+ /** The correlation id, if the response carried one. */
50
+ get requestId() {
51
+ return this.graphQLErrors?.[0]?.extensions?.requestId;
52
+ }
53
+ /**
54
+ * Whether retrying this request could succeed.
55
+ *
56
+ * The 429 is the one that has to be in here and is easy to leave out: it sits
57
+ * among the 4xx statuses that are otherwise permanent, so a policy written as
58
+ * "retry 5xx, never retry 4xx" excludes exactly the failure that clears on its
59
+ * own. A storefront wrote that policy, its key saturated, and every read
60
+ * failed on the first attempt for an hour.
61
+ */
62
+ get retryable() {
63
+ if (this.status === void 0) return true;
64
+ if (this.status === 429 || this.code === "RATE_LIMITED") return true;
65
+ if (this.status >= 500) return true;
66
+ return this.code === "UNAVAILABLE" || this.code === "TIMEOUT";
67
+ }
68
+ /**
69
+ * How long to wait before retrying, in milliseconds, when the server said.
70
+ * Undefined when it did not, which means the caller picks its own backoff.
71
+ */
72
+ get retryAfterMs() {
73
+ const seconds = this.graphQLErrors?.[0]?.extensions?.retryAfter;
74
+ return typeof seconds === "number" && seconds > 0 ? seconds * 1e3 : void 0;
75
+ }
32
76
  };
33
- async function foirRequest(env, document, variables, options) {
34
- const endpoint = env.PLATFORM_API_URL;
35
- const apiKey = isFoirPreview(env) ? env.PLATFORM_PREVIEW_API_KEY ?? env.PLATFORM_API_KEY : env.PLATFORM_API_KEY;
36
- if (!endpoint || !apiKey) {
37
- throw new FoirRequestError(
38
- "[foir] PLATFORM_API_URL or PLATFORM_API_KEY is not set \u2014 add them to .env"
39
- );
77
+ function readFoirRateLimit(headers) {
78
+ const raw = {
79
+ limit: headers.get("X-RateLimit-Limit"),
80
+ remaining: headers.get("X-RateLimit-Remaining"),
81
+ resetAt: headers.get("X-RateLimit-Reset")
82
+ };
83
+ if (raw.limit === null || raw.remaining === null || raw.resetAt === null) {
84
+ return void 0;
40
85
  }
86
+ const limit = Number(raw.limit);
87
+ const remaining = Number(raw.remaining);
88
+ const resetAt = Number(raw.resetAt);
89
+ if (!Number.isFinite(limit) || !Number.isFinite(remaining) || !Number.isFinite(resetAt)) {
90
+ return void 0;
91
+ }
92
+ return { limit, remaining, resetAt };
93
+ }
94
+ function readFoirRetryAfterMs(headers) {
95
+ const seconds = Number(headers.get("Retry-After"));
96
+ return Number.isFinite(seconds) && seconds > 0 ? seconds * 1e3 : void 0;
97
+ }
98
+ function buildFoirHeaders(input) {
41
99
  const headers = {
42
100
  "Content-Type": "application/json",
43
- "x-api-key": apiKey
101
+ "x-api-key": input.apiKey
44
102
  };
45
- if (isFoirPreview(env)) {
103
+ if (input.draftChannel) {
46
104
  headers["X-Foir-Schema-Channel"] = "draft";
47
105
  }
48
- if (options?.bearerToken) {
49
- headers["Authorization"] = `Bearer ${options.bearerToken}`;
50
- }
51
- let res;
52
- try {
53
- res = await fetch(endpoint, {
54
- method: "POST",
55
- headers,
56
- body: JSON.stringify({ query: print(document), variables })
57
- });
58
- } catch (err) {
59
- throw new FoirRequestError("[foir] network error", { cause: err });
106
+ if (input.bearerToken) {
107
+ headers["Authorization"] = `Bearer ${input.bearerToken}`;
60
108
  }
61
- if (!res.ok) {
62
- throw new FoirRequestError(`[foir] HTTP ${res.status} ${res.statusText}`, {
63
- status: res.status
64
- });
109
+ if (input.extra) {
110
+ for (const [key, value] of Object.entries(input.extra)) {
111
+ headers[key] = value;
112
+ }
65
113
  }
66
- const json = await res.json();
114
+ return headers;
115
+ }
116
+ function readFoirEnvelope(json) {
67
117
  if (json.errors?.length) {
68
118
  const msg = json.errors.map((e) => e.message).join("; ");
69
119
  if (json.data) {
@@ -80,6 +130,150 @@ async function foirRequest(env, document, variables, options) {
80
130
  return json.data;
81
131
  }
82
132
 
133
+ // src/graphql/cdn-route.ts
134
+ var CDN_PATH = "/cdn/graphql";
135
+ function isCdnEligible(env, documentText, bearerToken) {
136
+ if (bearerToken) return false;
137
+ if (isFoirPreview(env)) return false;
138
+ const apiKey = resolveFoirApiKey(env);
139
+ if (!apiKey?.startsWith("pk_")) return false;
140
+ return !/\bmutation\b/.test(documentText);
141
+ }
142
+ function buildCdnUrl(endpoint, apiKey, hash, variables) {
143
+ const url = new URL(endpoint);
144
+ url.pathname = url.pathname.replace(/\/graphql\/?$/, "") + CDN_PATH;
145
+ url.searchParams.set("key", apiKey);
146
+ url.searchParams.set("extensions", JSON.stringify(persistedQueryExtension(hash)));
147
+ if (variables && Object.keys(variables).length > 0) {
148
+ url.searchParams.set("variables", JSON.stringify(variables));
149
+ }
150
+ return url.toString();
151
+ }
152
+ function persistedQueryExtension(hash) {
153
+ return { persistedQuery: { version: 1, sha256Hash: hash } };
154
+ }
155
+ async function documentHash(documentText) {
156
+ const digest = await crypto.subtle.digest(
157
+ "SHA-256",
158
+ new TextEncoder().encode(documentText)
159
+ );
160
+ return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
161
+ }
162
+ var PERSISTED_QUERY_NOT_FOUND = "PERSISTED_QUERY_NOT_FOUND";
163
+ var NOT_SHAREABLE = "NOT_SHAREABLE";
164
+
165
+ // src/graphql/foir-request.ts
166
+ async function foirRequest(env, document, variables, options) {
167
+ const endpoint = env.PLATFORM_API_URL;
168
+ const apiKey = resolveFoirApiKey(env);
169
+ if (!endpoint || !apiKey) {
170
+ throw new FoirRequestError(
171
+ "[foir] PLATFORM_API_URL or PLATFORM_API_KEY is not set \u2014 add them to .env"
172
+ );
173
+ }
174
+ const headers = buildFoirHeaders({
175
+ apiKey,
176
+ draftChannel: isFoirPreview(env),
177
+ bearerToken: options?.bearerToken
178
+ });
179
+ const documentText = print2(document);
180
+ let res;
181
+ try {
182
+ if (isCdnEligible(env, documentText, options?.bearerToken)) {
183
+ const hash = await documentHash(documentText);
184
+ let shared = await fetch(buildCdnUrl(endpoint, apiKey, hash, variables), {
185
+ method: "GET",
186
+ headers
187
+ });
188
+ if (await isPersistedQueryMiss(shared)) {
189
+ shared = await fetch(buildCdnUrl(endpoint, apiKey, hash, variables), {
190
+ method: "POST",
191
+ headers,
192
+ body: JSON.stringify({
193
+ query: documentText,
194
+ variables,
195
+ extensions: persistedQueryExtension(hash)
196
+ })
197
+ });
198
+ }
199
+ if (await isSharedRefusal(shared)) {
200
+ res = await fetch(endpoint, {
201
+ method: "POST",
202
+ headers,
203
+ body: JSON.stringify({ query: documentText, variables })
204
+ });
205
+ } else {
206
+ res = shared;
207
+ }
208
+ } else {
209
+ res = await fetch(endpoint, {
210
+ method: "POST",
211
+ headers,
212
+ body: JSON.stringify({ query: documentText, variables })
213
+ });
214
+ }
215
+ } catch (err) {
216
+ throw new FoirRequestError("[foir] network error", { cause: err });
217
+ }
218
+ options?.onRateLimit?.(readFoirRateLimit(res.headers));
219
+ if (!res.ok) {
220
+ const errors = await readErrorEnvelope(res);
221
+ const first = errors?.[0]?.message;
222
+ throw new FoirRequestError(
223
+ first ? `[foir] HTTP ${res.status} ${res.statusText}: ${first}` : `[foir] HTTP ${res.status} ${res.statusText}`,
224
+ { status: res.status, graphQLErrors: errors }
225
+ );
226
+ }
227
+ const json = await res.json();
228
+ return readFoirEnvelope(json);
229
+ }
230
+ async function readErrorEnvelope(res) {
231
+ let errors;
232
+ try {
233
+ const body = await res.json();
234
+ errors = body.errors;
235
+ } catch {
236
+ errors = void 0;
237
+ }
238
+ const headerRetryMs = readFoirRetryAfterMs(res.headers);
239
+ if (headerRetryMs === void 0) return errors;
240
+ const seconds = Math.round(headerRetryMs / 1e3);
241
+ if (!errors?.length) {
242
+ return [
243
+ {
244
+ message: res.statusText || `HTTP ${res.status}`,
245
+ extensions: { code: "RATE_LIMITED", retryAfter: seconds }
246
+ }
247
+ ];
248
+ }
249
+ if (errors[0].extensions?.retryAfter === void 0) {
250
+ errors[0] = {
251
+ ...errors[0],
252
+ extensions: { ...errors[0].extensions, retryAfter: seconds }
253
+ };
254
+ }
255
+ return errors;
256
+ }
257
+ async function isSharedRefusal(res) {
258
+ if (res.ok) return false;
259
+ try {
260
+ const body = await res.clone().json();
261
+ return body.errors?.some((e) => e.extensions?.code === NOT_SHAREABLE) ?? false;
262
+ } catch {
263
+ return false;
264
+ }
265
+ }
266
+ async function isPersistedQueryMiss(res) {
267
+ try {
268
+ const body = await res.clone().json();
269
+ return body.errors?.some(
270
+ (e) => e.extensions?.code === PERSISTED_QUERY_NOT_FOUND
271
+ ) ?? false;
272
+ } catch {
273
+ return false;
274
+ }
275
+ }
276
+
83
277
  // src/auth/operations.ts
84
278
  import { parse } from "graphql";
85
279
  var AUTH_RESULT_FIELDS = "token refreshToken customerId email";
@@ -128,6 +322,16 @@ var AuthProvidersDocument = parse(
128
322
  authProviders { key name type isDefault }
129
323
  }`
130
324
  );
325
+ var CustomerJoinWaitlistDocument = parse(
326
+ `mutation CustomerJoinWaitlist($email: String!) {
327
+ customerJoinWaitlist(email: $email) { id email status }
328
+ }`
329
+ );
330
+ var CustomerSignupContextDocument = parse(
331
+ `query CustomerSignupContext {
332
+ customerSignupContext { signupMode }
333
+ }`
334
+ );
131
335
 
132
336
  // src/auth/auth.ts
133
337
  var TOKEN_KEY = "foir:token";
@@ -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 {};
@@ -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,8 @@
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';
@@ -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
+ }