@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.
- package/CHANGELOG.md +325 -0
- package/README.md +63 -0
- package/dist/auth/auth.d.ts +140 -0
- package/dist/auth/hosted.d.ts +40 -0
- package/dist/auth/index.d.ts +15 -0
- package/dist/auth/operations.d.ts +123 -0
- package/dist/auth/session-client.d.ts +76 -0
- package/dist/auth-session.js +137 -0
- package/dist/auth.js +238 -26
- package/dist/client/platform-client.d.ts +29 -0
- package/dist/client.js +290 -32
- package/dist/codegen/block-registry.d.ts +4 -8
- package/dist/codegen/block-scaffold.d.ts +4 -8
- package/dist/codegen/index.d.ts +8 -62
- package/dist/codegen/preset.d.ts +60 -0
- package/dist/codegen/shared.d.ts +25 -0
- package/dist/components/EditorProvider.d.ts +36 -0
- package/dist/content/content-renderer.d.ts +47 -0
- package/dist/content/index.d.ts +12 -0
- package/dist/content/richtext.d.ts +70 -0
- package/dist/data/builtins.d.ts +296 -0
- package/dist/data/client.d.ts +94 -0
- package/dist/data/index.d.ts +54 -0
- package/dist/data/manifest.d.ts +161 -0
- package/dist/data/schema.d.ts +243 -0
- package/dist/data/selection.d.ts +330 -0
- package/dist/data/transport.d.ts +107 -0
- package/dist/data.js +309 -0
- package/dist/graphql/cdn-route.d.ts +42 -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 +14 -0
- package/dist/media/media.d.ts +139 -0
- package/dist/media/useProgressiveLoad.d.ts +23 -0
- package/dist/media.js +5 -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 +230 -26
- 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 +230 -26
- 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/index.d.ts +19 -0
- package/dist/react/localStorageAdapter.d.ts +10 -0
- package/dist/react.js +230 -26
- package/dist/{realtime.d.ts → realtime/index.d.ts} +35 -16
- package/dist/server/callback-client.d.ts +92 -0
- package/dist/server/index.d.ts +8 -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/transport/foir-http.d.ts +217 -0
- package/dist/transport/retry.d.ts +61 -0
- package/dist/types/index.d.ts +124 -0
- package/package.json +23 -13
- package/dist/auth.d.ts +0 -382
- package/dist/client.d.ts +0 -83
- 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
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
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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 (
|
|
103
|
+
if (input.draftChannel) {
|
|
46
104
|
headers["X-Foir-Schema-Channel"] = "draft";
|
|
47
105
|
}
|
|
48
|
-
if (
|
|
49
|
-
headers["Authorization"] = `Bearer ${
|
|
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 (
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
109
|
+
if (input.extra) {
|
|
110
|
+
for (const [key, value] of Object.entries(input.extra)) {
|
|
111
|
+
headers[key] = value;
|
|
112
|
+
}
|
|
65
113
|
}
|
|
66
|
-
|
|
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
|
-
/**
|
|
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
|
+
}
|