@synap-core/sdk 1.1.0 → 1.3.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 (63) hide show
  1. package/README.md +138 -6
  2. package/dist/chunk-J7XHAAFJ.js +79 -0
  3. package/dist/chunk-J7XHAAFJ.js.map +1 -0
  4. package/dist/client-CVUT1Mcf.d.cts +253 -0
  5. package/dist/client-CVUT1Mcf.d.ts +253 -0
  6. package/dist/connect.browser.cjs +22 -0
  7. package/dist/connect.browser.cjs.map +1 -0
  8. package/dist/connect.browser.d.cts +2 -0
  9. package/dist/connect.browser.d.ts +2 -0
  10. package/dist/connect.browser.js +5 -0
  11. package/dist/connect.browser.js.map +1 -0
  12. package/dist/connect.cjs +1206 -0
  13. package/dist/connect.cjs.map +1 -0
  14. package/dist/connect.d.cts +442 -0
  15. package/dist/connect.d.ts +442 -0
  16. package/dist/connect.js +1092 -0
  17. package/dist/connect.js.map +1 -0
  18. package/dist/index.cjs +48 -6
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.cts +118 -7
  21. package/dist/index.d.ts +118 -7
  22. package/dist/index.js +41 -2
  23. package/dist/index.js.map +1 -1
  24. package/dist/public.cjs +627 -0
  25. package/dist/public.cjs.map +1 -0
  26. package/dist/public.d.cts +228 -0
  27. package/dist/public.d.ts +228 -0
  28. package/dist/public.js +578 -0
  29. package/dist/public.js.map +1 -0
  30. package/dist/testing.cjs +671 -0
  31. package/dist/testing.cjs.map +1 -0
  32. package/dist/testing.d.cts +181 -0
  33. package/dist/testing.d.ts +181 -0
  34. package/dist/testing.js +614 -0
  35. package/dist/testing.js.map +1 -0
  36. package/dist/types-DpqPNaaa.d.cts +76 -0
  37. package/dist/types-DpqPNaaa.d.ts +76 -0
  38. package/package.json +16 -3
  39. package/src/client.ts +24 -7
  40. package/src/connect/auth.ts +405 -0
  41. package/src/connect/browser.ts +10 -0
  42. package/src/connect/client.ts +193 -0
  43. package/src/connect/discovery.ts +262 -0
  44. package/src/connect/encoding.ts +93 -0
  45. package/src/connect/errors.ts +115 -0
  46. package/src/connect/flow.ts +449 -0
  47. package/src/connect/guard.ts +25 -0
  48. package/src/connect/index.ts +59 -0
  49. package/src/connect/seal.ts +119 -0
  50. package/src/connect/store.ts +151 -0
  51. package/src/connect/types.ts +80 -0
  52. package/src/index.ts +13 -0
  53. package/src/public/client.ts +579 -0
  54. package/src/public/form-model.ts +210 -0
  55. package/src/public/index.ts +12 -0
  56. package/src/public/safe-markdown.ts +114 -0
  57. package/src/public/share-model.ts +47 -0
  58. package/src/public/site-model.ts +217 -0
  59. package/src/templates.ts +122 -0
  60. package/src/testing/auth-server.ts +380 -0
  61. package/src/testing/fixtures.ts +132 -0
  62. package/src/testing/index.ts +359 -0
  63. package/src/public.ts +0 -262
@@ -0,0 +1,671 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/testing/index.ts
21
+ var testing_exports = {};
22
+ __export(testing_exports, {
23
+ MOCK_AUTH_ISSUER: () => MOCK_AUTH_ISSUER,
24
+ MOCK_FAILURE_TOKENS: () => MOCK_FAILURE_TOKENS,
25
+ MOCK_FORM: () => MOCK_FORM,
26
+ MOCK_GRANT: () => MOCK_GRANT,
27
+ MOCK_POD_URL: () => MOCK_POD_URL,
28
+ MOCK_RETRY_AFTER_SEC: () => MOCK_RETRY_AFTER_SEC,
29
+ MOCK_SHARE: () => MOCK_SHARE,
30
+ MOCK_SITE: () => MOCK_SITE,
31
+ MOCK_TOKENS: () => MOCK_TOKENS,
32
+ MOCK_USER: () => MOCK_USER,
33
+ createMockAuthServer: () => createMockAuthServer,
34
+ createMockPod: () => createMockPod
35
+ });
36
+ module.exports = __toCommonJS(testing_exports);
37
+
38
+ // src/testing/fixtures.ts
39
+ var MOCK_FORM = {
40
+ title: "Get in touch",
41
+ fields: [
42
+ { key: "name", label: "Name", type: "text", required: true, constraints: { maxLength: 120 } },
43
+ { key: "email", label: "Email", type: "email", required: true },
44
+ {
45
+ key: "topic",
46
+ label: "Topic",
47
+ type: "enum",
48
+ constraints: { enum: ["Sales", "Support", "Partnership"] }
49
+ },
50
+ { key: "message", label: "Message", type: "richtext", help: "A few lines is plenty." }
51
+ ],
52
+ successMessage: "Thanks \u2014 we received your message.",
53
+ honeypotField: "hp",
54
+ ticket: "mock-ticket",
55
+ minSubmitMs: 0,
56
+ captcha: { required: false }
57
+ };
58
+ var MOCK_SHARE = {
59
+ resourceType: "entity",
60
+ title: "Launch notes",
61
+ properties: { status: "open", owner: "Ada", featured: true },
62
+ body: {
63
+ format: "markdown",
64
+ content: [
65
+ "# Launch notes",
66
+ "",
67
+ "This page is a **published snapshot** from a Synap pod.",
68
+ "",
69
+ "- Read the [Synap docs](https://synap.live)",
70
+ "- Links other than http, https and mailto render as plain text: [bad](javascript:alert(1))",
71
+ "",
72
+ "<script>raw HTML is shown as text, never run</script>"
73
+ ].join("\n")
74
+ },
75
+ publishedOn: "2026-09-27"
76
+ };
77
+ function publicId(prefix, n) {
78
+ return `p_${prefix}${String(n).padStart(16 - prefix.length, "0")}`;
79
+ }
80
+ function slugify(s) {
81
+ return s.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 96);
82
+ }
83
+ var PAPER_TITLES = [
84
+ "Sparse graphs at scale",
85
+ "Learning to rank, revisited",
86
+ "A note on causal tracing",
87
+ "Small models, long contexts",
88
+ "Evaluating retrieval honestly",
89
+ "Robust tokenisation",
90
+ "Graphs that forget",
91
+ "Calibration under shift",
92
+ "Latent space cartography",
93
+ "Proofs as programs, again",
94
+ "Cheap uncertainty",
95
+ "R\xE9sum\xE9 of a decade"
96
+ ];
97
+ var VENUES = ["NeurIPS", "ICML", "ACL", "ICLR"];
98
+ var PAPERS = PAPER_TITLES.map((title, i) => ({
99
+ collection: "paper",
100
+ publicId: publicId("paper", i + 1),
101
+ slug: slugify(title),
102
+ title,
103
+ properties: {
104
+ year: 2026 - i % 5,
105
+ venue: VENUES[i % VENUES.length],
106
+ open: i % 3 !== 0
107
+ },
108
+ publishedOn: `2026-09-${String(28 - i).padStart(2, "0")}`,
109
+ body: i % 4 === 3 ? null : { format: "markdown", content: `# ${title}
110
+
111
+ Abstract of **${title}**.` }
112
+ }));
113
+ var PEOPLE = ["Ada Lovelace", "Alan Turing", "Grace Hopper"].map((name, i) => ({
114
+ collection: "person",
115
+ publicId: publicId("person", i + 1),
116
+ slug: null,
117
+ title: name,
118
+ properties: { name, role: i === 0 ? "Lead" : "Researcher" },
119
+ publishedOn: "2026-09-01",
120
+ body: null
121
+ }));
122
+ var MOCK_SITE = {
123
+ title: "Demo lab",
124
+ description: "What the lab publishes",
125
+ publishedOn: "2026-09-01",
126
+ collections: {
127
+ paper: { sort: ["-year"], pageSize: 5, addressing: "slug", records: PAPERS },
128
+ person: { sort: ["name"], pageSize: 20, addressing: "publicId", records: PEOPLE },
129
+ event: { sort: [], pageSize: 10, addressing: "slug", records: [] }
130
+ }
131
+ };
132
+
133
+ // src/connect/encoding.ts
134
+ var encoder = new TextEncoder();
135
+ var decoder = new TextDecoder();
136
+ function utf8(text) {
137
+ return encoder.encode(text);
138
+ }
139
+ function base64url(bytes) {
140
+ let binary = "";
141
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
142
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
143
+ }
144
+ function randomBytes(length) {
145
+ const out = new Uint8Array(length);
146
+ crypto.getRandomValues(out);
147
+ return out;
148
+ }
149
+ function randomToken() {
150
+ return base64url(randomBytes(32));
151
+ }
152
+ async function pkceChallenge(verifier) {
153
+ const digest = await crypto.subtle.digest("SHA-256", utf8(verifier));
154
+ return base64url(new Uint8Array(digest));
155
+ }
156
+
157
+ // src/testing/auth-server.ts
158
+ var MOCK_AUTH_ISSUER = "https://pod.mock-synap.test";
159
+ var MOCK_GRANT = [
160
+ { permission: "entity.post.read", workspaceId: "ws_mock_studio", workspaceName: "Studio" },
161
+ { permission: "entity.post.create", workspaceId: "ws_mock_studio", workspaceName: "Studio" }
162
+ ];
163
+ var MOCK_USER = { id: "user_mock_ada", name: "Ada Lovelace" };
164
+ function json(status, body, headers = {}) {
165
+ return new Response(JSON.stringify(body), {
166
+ status,
167
+ headers: { "Content-Type": "application/json", "Cache-Control": "no-store", ...headers }
168
+ });
169
+ }
170
+ function oauthError(error, description, status = 400) {
171
+ return json(status, { error, error_description: description });
172
+ }
173
+ function createMockAuthServer(options = {}) {
174
+ const issuer = options.issuer ?? MOCK_AUTH_ISSUER;
175
+ const now = options.now ?? Date.now;
176
+ const refresh = options.refresh ?? true;
177
+ const accessTtl = options.accessTtlSec ?? 3600;
178
+ const refreshIdle = options.refreshIdleSec ?? 30 * 24 * 3600;
179
+ const grant = options.grant ?? MOCK_GRANT;
180
+ const user = options.user ?? MOCK_USER;
181
+ const codes = /* @__PURE__ */ new Map();
182
+ const access = /* @__PURE__ */ new Map();
183
+ const refreshTokens = /* @__PURE__ */ new Map();
184
+ const requests = [];
185
+ const writes = [];
186
+ let proposalSeq = 0;
187
+ const metadata = () => {
188
+ const base = {
189
+ issuer,
190
+ authorization_endpoint: `${issuer}/authorize`,
191
+ token_endpoint: `${issuer}/token`,
192
+ registration_endpoint: `${issuer}/register`,
193
+ revocation_endpoint: `${issuer}/revoke`,
194
+ revocation_endpoint_auth_methods_supported: ["none"],
195
+ response_types_supported: ["code"],
196
+ grant_types_supported: refresh ? ["authorization_code", "refresh_token"] : ["authorization_code"],
197
+ code_challenge_methods_supported: ["S256"],
198
+ token_endpoint_auth_methods_supported: ["none"],
199
+ authorization_response_iss_parameter_supported: true,
200
+ client_id_metadata_document_supported: true
201
+ };
202
+ for (const [k, v] of Object.entries(options.metadata ?? {})) {
203
+ if (v === void 0) delete base[k];
204
+ else base[k] = v;
205
+ }
206
+ return base;
207
+ };
208
+ let loseNextRace = false;
209
+ let reuses = 0;
210
+ const revokeFamily = (family) => {
211
+ for (const r of refreshTokens.values()) if (r.family === family) r.revoked = true;
212
+ for (const a of access.values()) if (a.family === family) a.revoked = true;
213
+ };
214
+ const tokenBody = (scope, clientId, family) => {
215
+ const accessToken = `synap_hub_mock_${randomToken()}`;
216
+ access.set(accessToken, { expiresAt: now() + accessTtl * 1e3, revoked: false, family: refresh ? family : null });
217
+ const body = {
218
+ access_token: accessToken,
219
+ token_type: "Bearer",
220
+ expires_in: accessTtl,
221
+ scope: scope.join(" "),
222
+ synap_grant: grant.filter((g) => scope.includes(g.permission)),
223
+ synap_user: user
224
+ };
225
+ if (refresh) {
226
+ const refreshToken = randomToken();
227
+ refreshTokens.set(refreshToken, { clientId, family, idleUntil: now() + refreshIdle * 1e3, used: false, revoked: false, scope });
228
+ body.refresh_token = refreshToken;
229
+ if (options.advertiseRefreshExpiry) body.refresh_token_expires_in = refreshIdle;
230
+ }
231
+ return body;
232
+ };
233
+ async function token(form) {
234
+ if (form.grant_type === "authorization_code") {
235
+ const row = form.code ? codes.get(form.code) : void 0;
236
+ if (form.code) codes.delete(form.code);
237
+ if (!row) return oauthError("invalid_grant", "Invalid, expired, or already-redeemed authorization code");
238
+ if (row.clientId !== form.client_id) return oauthError("invalid_grant", "client_id does not match the code");
239
+ if (row.redirectUri !== form.redirect_uri) return oauthError("invalid_grant", "redirect_uri mismatch");
240
+ if (!form.code_verifier || await pkceChallenge(form.code_verifier) !== row.challenge) {
241
+ return oauthError("invalid_grant", "PKCE verification failed");
242
+ }
243
+ if (form.resource !== void 0 && row.resource && form.resource !== row.resource) {
244
+ return oauthError("invalid_target", "resource does not match the authorization request");
245
+ }
246
+ for (const r of refreshTokens.values()) if (r.clientId === row.clientId) revokeFamily(r.family);
247
+ return json(200, tokenBody(row.scope, row.clientId, randomToken()));
248
+ }
249
+ if (form.grant_type === "refresh_token") {
250
+ if (!refresh) return oauthError("unsupported_grant_type", 'Only grant_type "authorization_code" is supported');
251
+ if (!form.refresh_token || !form.client_id) return oauthError("invalid_request", "refresh_token and client_id are required");
252
+ const row = refreshTokens.get(form.refresh_token);
253
+ if (!row || row.clientId !== form.client_id || row.revoked) {
254
+ return oauthError("invalid_grant", "Invalid, expired, revoked or already-used refresh token");
255
+ }
256
+ if (row.used) {
257
+ reuses++;
258
+ revokeFamily(row.family);
259
+ return oauthError("invalid_grant", "Invalid, expired, revoked or already-used refresh token");
260
+ }
261
+ if (row.idleUntil <= now()) return oauthError("invalid_grant", "Invalid, expired, revoked or already-used refresh token");
262
+ if (loseNextRace) {
263
+ loseNextRace = false;
264
+ return oauthError("invalid_grant", "Invalid, expired, revoked or already-used refresh token");
265
+ }
266
+ row.used = true;
267
+ return json(200, tokenBody(row.scope, row.clientId, row.family));
268
+ }
269
+ return oauthError("unsupported_grant_type", "Unsupported grant_type");
270
+ }
271
+ function hub(method, path, headers, body) {
272
+ const failure2 = options.failures?.[path];
273
+ if (failure2 === "unreachable") return "reject";
274
+ const bearer = /^Bearer (.+)$/.exec(headers.authorization ?? "")?.[1];
275
+ const row = bearer ? access.get(bearer) : void 0;
276
+ if (!row || row.revoked || row.expiresAt <= now()) return json(401, { error: "Unauthorized" });
277
+ switch (failure2) {
278
+ case "rate_limited":
279
+ return json(429, { error: "Too many requests" }, { "Retry-After": "30" });
280
+ case "forbidden":
281
+ return json(403, { error: "Forbidden" });
282
+ case "door_unsupported":
283
+ return json(403, { error: "This door is not available to a space-scoped key", code: "GRANT_DOOR_UNSUPPORTED" });
284
+ case "server":
285
+ return json(500, { error: "Internal error" });
286
+ case "unavailable":
287
+ return json(503, { error: "Unavailable" });
288
+ }
289
+ if (path.startsWith("/knowledge/") || path === "/entities/retrieve" || path === "/memory/search") {
290
+ return json(403, { error: "This door is not available to a space-scoped key", code: "GRANT_DOOR_UNSUPPORTED" });
291
+ }
292
+ if (method === "GET" && path === "/users/me") return json(200, user);
293
+ if (method === "GET" && path === "/entities") return json(200, { entities: [], total: 0 });
294
+ if (method !== "GET") {
295
+ writes.push({ method, path, body });
296
+ if ((options.writeMode ?? "proposed") === "proposed") {
297
+ const proposalId = `prop_mock_${++proposalSeq}`;
298
+ return json(200, {
299
+ status: "proposed",
300
+ message: "Sent for review",
301
+ proposalId,
302
+ reviewUrl: `${issuer}/open/${proposalId}`
303
+ });
304
+ }
305
+ return json(200, { status: "created", id: `ent_mock_${writes.length}` });
306
+ }
307
+ return json(404, { error: "Not found" });
308
+ }
309
+ const mockFetch = async (input, init) => {
310
+ const request = typeof input === "object" && "method" in input ? input : null;
311
+ const url = new URL(typeof input === "string" ? input : input instanceof URL ? input.href : input.url);
312
+ const method = (init?.method ?? request?.method ?? "GET").toUpperCase();
313
+ const headers = {};
314
+ new Headers(init?.headers ?? request?.headers).forEach((value, name) => {
315
+ headers[name.toLowerCase()] = value;
316
+ });
317
+ const rawBody = typeof init?.body === "string" ? init.body : request ? await request.clone().text() : "";
318
+ const isForm = (headers["content-type"] ?? "").includes("application/x-www-form-urlencoded");
319
+ const form = isForm ? Object.fromEntries(new URLSearchParams(rawBody)) : null;
320
+ requests.push({ method, url: url.href, headers, form });
321
+ if (url.origin !== new URL(issuer).origin) throw new TypeError(`Failed to fetch: ${url.origin} is not the mock pod`);
322
+ const path = url.pathname;
323
+ if (method === "GET" && path === "/.well-known/oauth-authorization-server") return json(200, metadata());
324
+ if (method === "POST" && path === "/token") return token(form ?? {});
325
+ if (method === "POST" && path === "/revoke") {
326
+ const t = form?.token ?? "";
327
+ const r = refreshTokens.get(t);
328
+ if (r && form?.client_id && form.client_id !== r.clientId) {
329
+ return oauthError("unauthorized_client", "The token was not issued to this client");
330
+ }
331
+ if (r) revokeFamily(r.family);
332
+ const a = access.get(t);
333
+ if (a) a.revoked = true;
334
+ return new Response(null, { status: 200 });
335
+ }
336
+ const hubPath = /^\/api\/hub(\/.*)$/.exec(path)?.[1];
337
+ if (hubPath) {
338
+ let body = void 0;
339
+ try {
340
+ body = rawBody ? JSON.parse(rawBody) : void 0;
341
+ } catch {
342
+ body = rawBody;
343
+ }
344
+ const res = hub(method, hubPath, headers, body);
345
+ if (res === "reject") throw new TypeError("Failed to fetch: mock pod unreachable");
346
+ return res;
347
+ }
348
+ return json(404, { error: "Not found" });
349
+ };
350
+ return {
351
+ issuer,
352
+ fetch: mockFetch,
353
+ requests,
354
+ writes,
355
+ authorize(authorizeUrl, decision = {}) {
356
+ const url = new URL(authorizeUrl);
357
+ if (`${url.origin}${url.pathname}` !== `${issuer}/authorize`) throw new Error(`Not this pod's /authorize: ${authorizeUrl}`);
358
+ const q = url.searchParams;
359
+ const redirectUri = q.get("redirect_uri");
360
+ const clientId = q.get("client_id");
361
+ if (!clientId || !redirectUri) throw new Error("Authorization request rejected: client_id and redirect_uri are required");
362
+ const answer = new URL(redirectUri);
363
+ const state = decision.state ?? q.get("state");
364
+ const finish = (fields) => {
365
+ for (const [k, v] of Object.entries(fields)) answer.searchParams.set(k, v);
366
+ if (state !== null) answer.searchParams.set("state", state);
367
+ const iss = decision.iss === void 0 ? issuer : decision.iss;
368
+ if (iss !== null) answer.searchParams.set("iss", iss);
369
+ return answer.href;
370
+ };
371
+ if (q.get("response_type") !== "code") return finish({ error: "unsupported_response_type" });
372
+ if (q.get("code_challenge_method") !== "S256" || !q.get("code_challenge")) {
373
+ return finish({ error: "invalid_request", error_description: "PKCE S256 is required" });
374
+ }
375
+ const resource = q.get("resource");
376
+ if (resource !== null && resource !== `${issuer}/api/hub` && resource !== `${issuer}/mcp`) {
377
+ return finish({ error: "invalid_target" });
378
+ }
379
+ const scope = (q.get("scope") ?? "").split(/\s+/).filter(Boolean);
380
+ if (scope.length === 0) return finish({ error: "invalid_scope" });
381
+ if (decision.deny) return finish({ error: "access_denied" });
382
+ const code = randomToken();
383
+ codes.set(code, { clientId, redirectUri, challenge: q.get("code_challenge"), resource, scope });
384
+ return finish({ code });
385
+ },
386
+ consumeRefreshToken(t) {
387
+ const row = refreshTokens.get(t);
388
+ if (row) row.used = true;
389
+ },
390
+ loseNextRefreshRace() {
391
+ loseNextRace = true;
392
+ },
393
+ get reuses() {
394
+ return reuses;
395
+ },
396
+ revokeAll() {
397
+ for (const row of access.values()) row.revoked = true;
398
+ for (const row of refreshTokens.values()) row.revoked = true;
399
+ },
400
+ liveRefreshTokens() {
401
+ return [...refreshTokens].filter(([, r]) => !r.used && !r.revoked).map(([t]) => t);
402
+ },
403
+ reset() {
404
+ requests.length = 0;
405
+ writes.length = 0;
406
+ }
407
+ };
408
+ }
409
+
410
+ // src/testing/index.ts
411
+ var MOCK_POD_URL = "https://mock.synap.invalid";
412
+ var MOCK_TOKENS = { form: "demo-form", share: "demo", site: "demo-site" };
413
+ var MOCK_RETRY_AFTER_SEC = 30;
414
+ var MOCK_FAILURE_TOKENS = {
415
+ /** 429 with no `Retry-After` (what the pod's public doors send today). */
416
+ rateLimited: "mock-rate-limited",
417
+ /** 429 with `Retry-After: 30`. */
418
+ rateLimitedRetryAfter: "mock-rate-limited-retry-after",
419
+ /** 403 `public_doors_disabled`: the owner switched public pages off. */
420
+ doorsDisabled: "mock-doors-disabled",
421
+ /** 500. */
422
+ serverError: "mock-server-error",
423
+ /** 503 (a read: `pod_unreachable` / `server`; a submit: `unavailable`). */
424
+ unavailable: "mock-unavailable",
425
+ /** No answer: the fetch rejects (`network` / `pod_unreachable`). */
426
+ unreachable: "mock-unreachable"
427
+ };
428
+ var NOT_FOUND = { error: "Not found" };
429
+ var DOORS_DISABLED = {
430
+ error: "The owner has not turned on public pages and forms for this workspace.",
431
+ code: "public_doors_disabled"
432
+ };
433
+ var SAFELISTED = /* @__PURE__ */ new Set(["accept", "accept-language", "content-language", "content-type"]);
434
+ var PREFLIGHT_ALLOWED = /* @__PURE__ */ new Set(["content-type"]);
435
+ var PUBLIC_ID_RE = /^p_[A-Za-z0-9]{16}$/;
436
+ var SLUG_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
437
+ var SLUG_MAX_LENGTH = 96;
438
+ var DIGITS = /^\d{1,9}$/;
439
+ var SORT_TERM = /^(-?)([A-Za-z][A-Za-z0-9_]{0,63})$/;
440
+ var MAX_OFFSET = 1e4;
441
+ var MAX_LIMIT = 100;
442
+ var MAX_SORT_KEYS = 3;
443
+ var PUBLISHED_AT = "publishedAt";
444
+ function json2(status, body, headers = {}) {
445
+ return new Response(JSON.stringify(body), {
446
+ status,
447
+ headers: { "Content-Type": "application/json", "Cache-Control": "no-cache", ...headers }
448
+ });
449
+ }
450
+ function weakEtag(value) {
451
+ let h = 2166136261;
452
+ for (const ch of JSON.stringify(value)) {
453
+ h ^= ch.charCodeAt(0);
454
+ h = Math.imul(h, 16777619) >>> 0;
455
+ }
456
+ return `W/"${h.toString(16).padStart(8, "0")}"`;
457
+ }
458
+ function etagMatches(header, etag) {
459
+ if (!header) return false;
460
+ if (header.trim() === "*") return true;
461
+ const strip = (t) => t.trim().replace(/^W\//, "");
462
+ return header.split(",").some((t) => strip(t) === strip(etag));
463
+ }
464
+ function failure(token) {
465
+ switch (token) {
466
+ case MOCK_FAILURE_TOKENS.rateLimited:
467
+ return json2(429, { error: "Too many requests", retryAfter: "5 minutes" });
468
+ case MOCK_FAILURE_TOKENS.rateLimitedRetryAfter:
469
+ return json2(429, { error: "Too many requests" }, { "Retry-After": String(MOCK_RETRY_AFTER_SEC) });
470
+ case MOCK_FAILURE_TOKENS.doorsDisabled:
471
+ return json2(403, DOORS_DISABLED);
472
+ case MOCK_FAILURE_TOKENS.serverError:
473
+ return json2(500, { error: "Internal error" });
474
+ case MOCK_FAILURE_TOKENS.unavailable:
475
+ return json2(503, { received: false });
476
+ case MOCK_FAILURE_TOKENS.unreachable:
477
+ return "reject";
478
+ default:
479
+ return null;
480
+ }
481
+ }
482
+ function summary(record) {
483
+ const { body: _body, ...rest } = record;
484
+ return rest;
485
+ }
486
+ function manifest(site) {
487
+ return {
488
+ ...site.title !== void 0 ? { title: site.title } : {},
489
+ ...site.description !== void 0 ? { description: site.description } : {},
490
+ publishedOn: site.publishedOn,
491
+ collections: Object.keys(site.collections).sort().map((key) => {
492
+ const c = site.collections[key];
493
+ return { key, count: c.records.length, sort: [...c.sort], pageSize: c.pageSize, addressing: c.addressing };
494
+ })
495
+ };
496
+ }
497
+ function parseQuery(params, c) {
498
+ for (const name of params.keys()) {
499
+ if (name === "embed") return "embed is not available";
500
+ if (name === "ref" || name.startsWith("ref[")) return "ref filter is not available";
501
+ }
502
+ const one = (name) => {
503
+ const all = params.getAll(name);
504
+ return all.length === 0 ? void 0 : all.length === 1 ? all[0] : null;
505
+ };
506
+ const rawOffset = one("offset");
507
+ if (rawOffset === null || rawOffset !== void 0 && !DIGITS.test(rawOffset)) return "invalid offset";
508
+ const offset = rawOffset === void 0 ? 0 : Number(rawOffset);
509
+ if (offset > MAX_OFFSET) return "offset too large";
510
+ const rawLimit = one("limit");
511
+ if (rawLimit === null || rawLimit !== void 0 && !DIGITS.test(rawLimit)) return "invalid limit";
512
+ const limit = Math.min(MAX_LIMIT, Math.max(1, rawLimit === void 0 ? c.pageSize : Number(rawLimit)));
513
+ const rawSort = one("sort");
514
+ if (rawSort === null) return "invalid sort";
515
+ const terms = rawSort !== void 0 ? rawSort.split(",") : c.sort.length > 0 ? [...c.sort] : [`-${PUBLISHED_AT}`];
516
+ if (terms.length > MAX_SORT_KEYS) return "invalid sort";
517
+ const allowed = /* @__PURE__ */ new Set([PUBLISHED_AT, ...c.sort.map((s) => s.replace(/^-/, ""))]);
518
+ const sort = [];
519
+ for (const term of terms) {
520
+ const m = SORT_TERM.exec(term);
521
+ if (!m || !allowed.has(m[2]) || sort.some((s) => s.key === m[2])) return "invalid sort";
522
+ sort.push({ key: m[2], desc: m[1] === "-" });
523
+ }
524
+ return { offset, limit, sort };
525
+ }
526
+ function compareRecords(sort) {
527
+ const rank = (v) => typeof v === "string" ? 0 : typeof v === "number" ? 1 : 2;
528
+ const valueOf = (r, key) => key === PUBLISHED_AT ? r.publishedOn : r.properties[key];
529
+ return (a, b) => {
530
+ for (const { key, desc } of sort) {
531
+ const va = valueOf(a, key);
532
+ const vb = valueOf(b, key);
533
+ if (va === void 0 || vb === void 0) {
534
+ if (va !== vb) return va === void 0 ? 1 : -1;
535
+ continue;
536
+ }
537
+ let d = rank(va) - rank(vb);
538
+ if (d === 0) d = va < vb ? -1 : va > vb ? 1 : 0;
539
+ if (d !== 0) return desc ? -d : d;
540
+ }
541
+ if (a.publishedOn !== b.publishedOn) return a.publishedOn < b.publishedOn ? 1 : -1;
542
+ return a.publicId < b.publicId ? -1 : a.publicId > b.publicId ? 1 : 0;
543
+ };
544
+ }
545
+ function cached(view, ifNoneMatch) {
546
+ const etag = weakEtag(view);
547
+ if (etagMatches(ifNoneMatch, etag)) {
548
+ return new Response(null, { status: 304, headers: { ETag: etag, "Cache-Control": "no-cache" } });
549
+ }
550
+ return json2(200, view, { ETag: etag });
551
+ }
552
+ function siteDoor(site, rest, params, ifNoneMatch) {
553
+ if (rest.length === 0) return cached(manifest(site), ifNoneMatch);
554
+ if (rest[0] !== "collections" || rest.length > 3 || !Object.hasOwn(site.collections, rest[1])) {
555
+ return json2(404, NOT_FOUND);
556
+ }
557
+ const c = site.collections[rest[1]];
558
+ if (rest.length === 2) {
559
+ const q = parseQuery(params, c);
560
+ if (typeof q === "string") return json2(400, { error: q });
561
+ const sorted = [...c.records].sort(compareRecords(q.sort));
562
+ const page = {
563
+ items: sorted.slice(q.offset, q.offset + q.limit).map(summary),
564
+ total: sorted.length,
565
+ offset: q.offset,
566
+ limit: q.limit
567
+ };
568
+ return cached(page, ifNoneMatch);
569
+ }
570
+ const address = rest[2];
571
+ const byPublicId = PUBLIC_ID_RE.test(address);
572
+ if (!byPublicId && !(address.length <= SLUG_MAX_LENGTH && SLUG_RE.test(address))) return json2(404, NOT_FOUND);
573
+ const record = c.records.find((r) => byPublicId ? r.publicId === address : r.slug === address);
574
+ if (!record) return json2(404, NOT_FOUND);
575
+ if (params.has("embed")) return json2(400, { error: "embed is not available" });
576
+ return cached(record, ifNoneMatch);
577
+ }
578
+ function invalidKeys(form, body) {
579
+ const fields = body?.fields;
580
+ if (typeof fields !== "object" || fields === null) return [];
581
+ const answers = fields;
582
+ return form.fields.filter((f) => {
583
+ const v = answers[f.key];
584
+ if (v === void 0 || v === "" || v === null) return f.required === true;
585
+ if (f.type === "boolean") return typeof v !== "boolean" || f.required === true && v !== true;
586
+ const allowed = f.constraints?.enum;
587
+ return allowed !== void 0 && !allowed.includes(String(v));
588
+ }).map((f) => f.key);
589
+ }
590
+ function createMockPod(options = {}) {
591
+ const forms = options.forms ?? { [MOCK_TOKENS.form]: MOCK_FORM };
592
+ const shares = options.shares ?? { [MOCK_TOKENS.share]: MOCK_SHARE };
593
+ const sites = options.sites ?? { [MOCK_TOKENS.site]: MOCK_SITE };
594
+ const requests = [];
595
+ const submissions = [];
596
+ const mockFetch = async (input, init) => {
597
+ const request = typeof input === "object" && "method" in input ? input : null;
598
+ const url = new URL(typeof input === "string" ? input : input instanceof URL ? input.href : input.url);
599
+ const method = (init?.method ?? request?.method ?? "GET").toUpperCase();
600
+ const headers = {};
601
+ new Headers(init?.headers ?? request?.headers).forEach((value, name) => {
602
+ headers[name.toLowerCase()] = value;
603
+ });
604
+ const credentials = init?.credentials ?? request?.credentials;
605
+ requests.push({ method, url: url.href, headers, credentials });
606
+ if (credentials === "include") throw new TypeError("Failed to fetch: credentialed request to a public door");
607
+ if (options.browser) {
608
+ const preflighted = Object.keys(headers).filter((h) => !SAFELISTED.has(h) && !PREFLIGHT_ALLOWED.has(h));
609
+ if (preflighted.length > 0) throw new TypeError(`Failed to fetch: preflight refuses ${preflighted.join(", ")}`);
610
+ }
611
+ const m = url.pathname.match(/^\/api\/hub\/public\/(forms|shares|sites)\/(.+)$/);
612
+ if (!m) return json2(404, NOT_FOUND);
613
+ let segments;
614
+ try {
615
+ segments = m[2].split("/").map(decodeURIComponent);
616
+ } catch {
617
+ return json2(404, NOT_FOUND);
618
+ }
619
+ const [token, ...rest] = segments;
620
+ const failed = failure(token);
621
+ if (failed === "reject") throw new TypeError("Failed to fetch: mock pod unreachable");
622
+ if (failed) return failed;
623
+ const door = m[1];
624
+ if (door === "forms" && method === "POST" && rest.length === 0) {
625
+ let body = null;
626
+ try {
627
+ body = JSON.parse(typeof init?.body === "string" ? init.body : "null");
628
+ } catch {
629
+ body = null;
630
+ }
631
+ const form = Object.hasOwn(forms, token) ? forms[token] : void 0;
632
+ const invalid = form ? invalidKeys(form, body) : [];
633
+ const status = invalid.length > 0 ? 422 : 202;
634
+ submissions.push({ token, body, status });
635
+ return status === 422 ? json2(422, { received: false, invalid }) : json2(202, { received: true });
636
+ }
637
+ if (method !== "GET") return json2(404, NOT_FOUND);
638
+ if (door === "forms" && rest.length === 0 && Object.hasOwn(forms, token)) return json2(200, forms[token]);
639
+ if (door === "shares" && rest.length === 0 && Object.hasOwn(shares, token)) return json2(200, shares[token]);
640
+ if (door === "sites" && Object.hasOwn(sites, token)) {
641
+ return siteDoor(sites[token], rest, url.searchParams, headers["if-none-match"] ?? null);
642
+ }
643
+ return json2(404, NOT_FOUND);
644
+ };
645
+ return {
646
+ podUrl: MOCK_POD_URL,
647
+ fetch: mockFetch,
648
+ requests,
649
+ submissions,
650
+ reset() {
651
+ requests.length = 0;
652
+ submissions.length = 0;
653
+ }
654
+ };
655
+ }
656
+ // Annotate the CommonJS export names for ESM import in node:
657
+ 0 && (module.exports = {
658
+ MOCK_AUTH_ISSUER,
659
+ MOCK_FAILURE_TOKENS,
660
+ MOCK_FORM,
661
+ MOCK_GRANT,
662
+ MOCK_POD_URL,
663
+ MOCK_RETRY_AFTER_SEC,
664
+ MOCK_SHARE,
665
+ MOCK_SITE,
666
+ MOCK_TOKENS,
667
+ MOCK_USER,
668
+ createMockAuthServer,
669
+ createMockPod
670
+ });
671
+ //# sourceMappingURL=testing.cjs.map