@synap-core/sdk 1.3.0 → 1.5.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 (51) hide show
  1. package/CHANGELOG.md +133 -0
  2. package/README.md +212 -12
  3. package/dist/{chunk-J7XHAAFJ.js → chunk-UK2U6SMA.js} +14 -2
  4. package/dist/chunk-UK2U6SMA.js.map +1 -0
  5. package/dist/{client-CVUT1Mcf.d.cts → client-B50KC2aJ.d.cts} +14 -9
  6. package/dist/{client-CVUT1Mcf.d.ts → client-B50KC2aJ.d.ts} +14 -9
  7. package/dist/connect.cjs +790 -161
  8. package/dist/connect.cjs.map +1 -1
  9. package/dist/connect.d.cts +610 -37
  10. package/dist/connect.d.ts +610 -37
  11. package/dist/connect.js +777 -162
  12. package/dist/connect.js.map +1 -1
  13. package/dist/public.cjs +9 -2
  14. package/dist/public.cjs.map +1 -1
  15. package/dist/public.d.cts +19 -4
  16. package/dist/public.d.ts +19 -4
  17. package/dist/public.js +8 -2
  18. package/dist/public.js.map +1 -1
  19. package/dist/testing.cjs +366 -17
  20. package/dist/testing.cjs.map +1 -1
  21. package/dist/testing.d.cts +190 -7
  22. package/dist/testing.d.ts +190 -7
  23. package/dist/testing.js +358 -17
  24. package/dist/testing.js.map +1 -1
  25. package/dist/{types-DpqPNaaa.d.cts → types-DZYNBCq7.d.cts} +47 -5
  26. package/dist/{types-DpqPNaaa.d.ts → types-DZYNBCq7.d.ts} +47 -5
  27. package/package.json +36 -12
  28. package/src/connect/auth.ts +314 -55
  29. package/src/connect/client.ts +472 -26
  30. package/src/connect/cloud.ts +210 -0
  31. package/src/connect/discovery.ts +79 -44
  32. package/src/connect/encoding.ts +1 -1
  33. package/src/connect/errors.ts +8 -1
  34. package/src/connect/flow.ts +80 -12
  35. package/src/connect/http.ts +42 -0
  36. package/src/connect/hub-types.ts +254 -0
  37. package/src/connect/index.ts +69 -23
  38. package/src/connect/rules.ts +18 -0
  39. package/src/connect/seal.ts +1 -1
  40. package/src/connect/store.ts +50 -8
  41. package/src/connect/types.ts +48 -4
  42. package/src/public/client.ts +14 -2
  43. package/src/public/safe-markdown.ts +13 -0
  44. package/src/public/share-model.ts +6 -2
  45. package/src/public/site-model.ts +5 -3
  46. package/src/testing/auth-server.ts +377 -20
  47. package/src/testing/cloud.ts +83 -0
  48. package/src/testing/fixtures.ts +26 -0
  49. package/src/testing/index.ts +10 -6
  50. package/src/testing/seed.ts +120 -0
  51. package/dist/chunk-J7XHAAFJ.js.map +0 -1
package/dist/testing.cjs CHANGED
@@ -20,10 +20,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/testing/index.ts
21
21
  var testing_exports = {};
22
22
  __export(testing_exports, {
23
+ MOCK_APP: () => MOCK_APP,
24
+ MOCK_APP_AGENT_ID: () => MOCK_APP_AGENT_ID,
23
25
  MOCK_AUTH_ISSUER: () => MOCK_AUTH_ISSUER,
26
+ MOCK_CLOUD_URL: () => MOCK_CLOUD_URL,
24
27
  MOCK_FAILURE_TOKENS: () => MOCK_FAILURE_TOKENS,
25
28
  MOCK_FORM: () => MOCK_FORM,
26
29
  MOCK_GRANT: () => MOCK_GRANT,
30
+ MOCK_HTML_SHARE: () => MOCK_HTML_SHARE,
27
31
  MOCK_POD_URL: () => MOCK_POD_URL,
28
32
  MOCK_RETRY_AFTER_SEC: () => MOCK_RETRY_AFTER_SEC,
29
33
  MOCK_SHARE: () => MOCK_SHARE,
@@ -31,7 +35,9 @@ __export(testing_exports, {
31
35
  MOCK_TOKENS: () => MOCK_TOKENS,
32
36
  MOCK_USER: () => MOCK_USER,
33
37
  createMockAuthServer: () => createMockAuthServer,
34
- createMockPod: () => createMockPod
38
+ createMockCloud: () => createMockCloud,
39
+ createMockPod: () => createMockPod,
40
+ parseMockPodSeed: () => parseMockPodSeed
35
41
  });
36
42
  module.exports = __toCommonJS(testing_exports);
37
43
 
@@ -74,6 +80,26 @@ var MOCK_SHARE = {
74
80
  },
75
81
  publishedOn: "2026-09-27"
76
82
  };
83
+ var MOCK_HTML_SHARE = {
84
+ resourceType: "entity",
85
+ title: "Estimate for Northwind",
86
+ properties: { total: "4 800 \u20AC", validUntil: "2026-11-30" },
87
+ body: {
88
+ format: "html",
89
+ content: [
90
+ "<!doctype html>",
91
+ '<html lang="en"><head><meta charset="utf-8"><title>Estimate</title>',
92
+ "<style>body{font-family:Georgia,serif;max-width:40rem;margin:2rem auto;line-height:1.5}td{padding:.25rem .75rem}</style>",
93
+ "</head><body>",
94
+ "<h1>Estimate for Northwind</h1>",
95
+ "<table><tr><td>Discovery workshop</td><td>1 200 \u20AC</td></tr><tr><td>Build</td><td>3 600 \u20AC</td></tr></table>",
96
+ "<p><strong>Total: 4 800 \u20AC</strong></p>",
97
+ "<script>document.body.textContent = 'scripts never run in a sandboxed frame'</script>",
98
+ "</body></html>"
99
+ ].join("")
100
+ },
101
+ publishedOn: "2026-10-22"
102
+ };
77
103
  function publicId(prefix, n) {
78
104
  return `p_${prefix}${String(n).padStart(16 - prefix.length, "0")}`;
79
105
  }
@@ -161,6 +187,8 @@ var MOCK_GRANT = [
161
187
  { permission: "entity.post.create", workspaceId: "ws_mock_studio", workspaceName: "Studio" }
162
188
  ];
163
189
  var MOCK_USER = { id: "user_mock_ada", name: "Ada Lovelace" };
190
+ var MOCK_APP = { id: "app_mock_studio", manageUrl: "https://pod-admin.mock-synap.test/apps/app_mock_studio" };
191
+ var MOCK_APP_AGENT_ID = "agent_mock_app";
164
192
  function json(status, body, headers = {}) {
165
193
  return new Response(JSON.stringify(body), {
166
194
  status,
@@ -183,7 +211,34 @@ function createMockAuthServer(options = {}) {
183
211
  const refreshTokens = /* @__PURE__ */ new Map();
184
212
  const requests = [];
185
213
  const writes = [];
214
+ const proposals = [];
215
+ const seed = options.seed ?? {};
216
+ const entities = [...seed.entities ?? [], ...options.entities ?? []];
217
+ const sessions = [...seed.sessions ?? []];
218
+ const documents = [...seed.documents ?? []];
219
+ const relations = [...seed.relations ?? []];
220
+ const templateOf = new Map((seed.spaces ?? []).filter((sp) => sp.templateSlug).map((sp) => [sp.id, sp.templateSlug]));
221
+ const approvals = [];
222
+ const seededAt = new Date(now()).toISOString();
223
+ for (const p of seed.proposals ?? []) {
224
+ proposals.push({
225
+ proposalType: "create",
226
+ targetType: "entity",
227
+ targetId: null,
228
+ status: "pending",
229
+ workspaceId: null,
230
+ createdAt: seededAt,
231
+ correlationId: null,
232
+ sessionId: null,
233
+ agentUserId: null,
234
+ ...p
235
+ });
236
+ }
237
+ const writeMode = options.writeMode ?? (options.actsAs === "person" ? "applied" : "proposed");
238
+ const graceMs = (options.refreshGraceSec ?? 30) * 1e3;
239
+ const app = options.app === void 0 ? MOCK_APP : options.app;
186
240
  let proposalSeq = 0;
241
+ let graceAnswers = 0;
187
242
  const metadata = () => {
188
243
  const base = {
189
244
  issuer,
@@ -211,18 +266,26 @@ function createMockAuthServer(options = {}) {
211
266
  for (const r of refreshTokens.values()) if (r.family === family) r.revoked = true;
212
267
  for (const a of access.values()) if (a.family === family) a.revoked = true;
213
268
  };
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 });
269
+ const tokenBody = (scope, clientId, family, pair) => {
270
+ const accessToken = pair?.accessToken ?? `synap_hub_mock_${randomToken()}`;
271
+ if (!pair) access.set(accessToken, { expiresAt: now() + accessTtl * 1e3, revoked: false, family: refresh ? family : null, scope });
272
+ const row = access.get(accessToken);
217
273
  const body = {
218
274
  access_token: accessToken,
219
275
  token_type: "Bearer",
220
- expires_in: accessTtl,
276
+ // As the pod: what the key has left, never an assumed hour.
277
+ expires_in: Math.max(0, Math.floor((row.expiresAt - now()) / 1e3)),
221
278
  scope: scope.join(" "),
222
- synap_grant: grant.filter((g) => scope.includes(g.permission)),
223
- synap_user: user
279
+ // Each grant row names its space's template when it came from one.
280
+ synap_grant: grant.filter((g) => scope.includes(g.permission)).map((g) => g.templateSlug || !templateOf.has(g.workspaceId) ? g : { ...g, templateSlug: templateOf.get(g.workspaceId) }),
281
+ synap_user: user,
282
+ ...app ? { synap_app: app } : {},
283
+ ...options.actsAs ? { synap_acts_as: options.actsAs } : {}
224
284
  };
225
- if (refresh) {
285
+ if (pair) {
286
+ body.refresh_token = pair.refreshToken;
287
+ if (options.advertiseRefreshExpiry) body.refresh_token_expires_in = Math.floor((refreshTokens.get(pair.refreshToken).idleUntil - now()) / 1e3);
288
+ } else if (refresh) {
226
289
  const refreshToken = randomToken();
227
290
  refreshTokens.set(refreshToken, { clientId, family, idleUntil: now() + refreshIdle * 1e3, used: false, revoked: false, scope });
228
291
  body.refresh_token = refreshToken;
@@ -254,6 +317,13 @@ function createMockAuthServer(options = {}) {
254
317
  return oauthError("invalid_grant", "Invalid, expired, revoked or already-used refresh token");
255
318
  }
256
319
  if (row.used) {
320
+ const successor = row.successor ? refreshTokens.get(row.successor) : void 0;
321
+ const key = row.pairAccess ? access.get(row.pairAccess) : void 0;
322
+ const inGrace = graceMs > 0 && row.usedAt !== void 0 && now() - row.usedAt < graceMs;
323
+ if (inGrace && successor && !successor.used && !successor.revoked && key && !key.revoked && key.expiresAt > now()) {
324
+ graceAnswers++;
325
+ return json(200, tokenBody(row.scope, row.clientId, row.family, { accessToken: row.pairAccess, refreshToken: row.successor }));
326
+ }
257
327
  reuses++;
258
328
  revokeFamily(row.family);
259
329
  return oauthError("invalid_grant", "Invalid, expired, revoked or already-used refresh token");
@@ -264,11 +334,132 @@ function createMockAuthServer(options = {}) {
264
334
  return oauthError("invalid_grant", "Invalid, expired, revoked or already-used refresh token");
265
335
  }
266
336
  row.used = true;
267
- return json(200, tokenBody(row.scope, row.clientId, row.family));
337
+ row.usedAt = now();
338
+ for (const a of access.values()) if (a.family === row.family) a.revoked = true;
339
+ const body = tokenBody(row.scope, row.clientId, row.family);
340
+ row.successor = body.refresh_token;
341
+ row.pairAccess = body.access_token;
342
+ return json(200, body);
268
343
  }
269
344
  return oauthError("unsupported_grant_type", "Unsupported grant_type");
270
345
  }
271
- function hub(method, path, headers, body) {
346
+ const sessionRow = (s) => ({
347
+ workspaceId: null,
348
+ projectId: null,
349
+ userId: user.id,
350
+ correlationId: null,
351
+ title: null,
352
+ goal: "",
353
+ status: "active",
354
+ templateId: null,
355
+ expectedOutputs: [],
356
+ channelId: null,
357
+ progress: null,
358
+ currentStage: null,
359
+ agentIds: [],
360
+ closedAt: null,
361
+ verificationReport: null,
362
+ metadata: {},
363
+ startedAt: seededAt,
364
+ createdAt: seededAt,
365
+ updatedAt: seededAt,
366
+ kind: "work",
367
+ triage: { pending: false, acceptedAt: null, acceptedBy: null },
368
+ ...s
369
+ });
370
+ function readSeeded(path, query) {
371
+ const conn = /^\/entities\/([^/]+)\/connections$/.exec(path);
372
+ if (conn) {
373
+ const id = decodeURIComponent(conn[1]);
374
+ const ws = query.get("workspaceId");
375
+ const limit = Math.min(Math.max(Number(query.get("limit") ?? 50) || 50, 1), 200);
376
+ const inLens = (e) => !ws || e.workspaceId === ws || e.workspaceId == null;
377
+ const connections = [];
378
+ const graph = relations.filter((r) => r.sourceEntityId === id || r.targetEntityId === id).sort((a, b) => String(b.createdAt ?? "").localeCompare(String(a.createdAt ?? ""))).slice(0, limit);
379
+ for (const r of graph) {
380
+ const outgoing = r.sourceEntityId === id;
381
+ const otherId = outgoing ? r.targetEntityId : r.sourceEntityId;
382
+ const other = entities.find((e) => e.id === otherId);
383
+ if (!other || !inLens(other)) continue;
384
+ connections.push({
385
+ entityId: otherId,
386
+ entity: { ...other, type: other.profileSlug ?? other.type ?? null, facetSlugs: other.facetSlugs ?? [] },
387
+ label: r.type,
388
+ direction: outgoing ? "outgoing" : "incoming",
389
+ source: "graph",
390
+ relationId: r.id,
391
+ relationType: r.type,
392
+ createdAt: r.createdAt ?? null
393
+ });
394
+ }
395
+ for (const s of sessions.filter((x) => x.subjectEntityId === id).slice(0, limit)) {
396
+ const row = sessionRow(s);
397
+ connections.push({
398
+ entityId: s.id,
399
+ entity: null,
400
+ label: row.title || row.goal,
401
+ direction: "incoming",
402
+ source: "focus_session",
403
+ focusSessionId: s.id,
404
+ focusSessionGoal: row.goal,
405
+ focusSessionStatus: row.status,
406
+ focusSessionWorkspaceId: row.workspaceId,
407
+ createdAt: row.createdAt
408
+ });
409
+ }
410
+ const n = (source) => connections.filter((c) => c.source === source).length;
411
+ return json(200, {
412
+ connections,
413
+ counts: { total: connections.length, graph: n("graph"), structural: n("property"), threads: n("thread"), contextChannels: n("context_channel"), focusSessions: n("focus_session") }
414
+ });
415
+ }
416
+ const entity = /^\/entities\/([^/]+)$/.exec(path);
417
+ if (entity) {
418
+ const found = entities.find((e) => e.id === decodeURIComponent(entity[1]));
419
+ return found ? json(200, found) : new Response(null, { status: 404 });
420
+ }
421
+ const doc = /^\/documents\/([^/]+)$/.exec(path);
422
+ if (doc) {
423
+ const format = query.get("format") ?? "raw";
424
+ if (format !== "raw" && format !== "readable") return json(400, { error: "format must be raw or readable" });
425
+ const d = documents.find((x) => x.id === decodeURIComponent(doc[1]));
426
+ if (!d) return json(404, { error: "Document not found" });
427
+ return json(200, {
428
+ document: {
429
+ id: d.id,
430
+ title: d.title,
431
+ type: d.type ?? "markdown",
432
+ language: d.language ?? null,
433
+ workspaceId: d.workspaceId ?? null,
434
+ format,
435
+ content: d.content,
436
+ revision: d.revision ?? 1,
437
+ version: d.version ?? 1,
438
+ sections: [],
439
+ diagnostics: [],
440
+ updatedAt: d.updatedAt ?? seededAt,
441
+ createdAt: d.createdAt ?? seededAt
442
+ }
443
+ });
444
+ }
445
+ if (path === "/focus-sessions") {
446
+ const ws = query.get("workspaceId");
447
+ const project = query.get("projectId");
448
+ if (!ws && !project) return json(400, { error: "workspaceId or projectId is required" });
449
+ const status = query.get("status") ?? "all";
450
+ const lens = query.get("lens") ?? "all";
451
+ const kind = query.get("kind") ?? "all";
452
+ const subject = query.get("subjectEntityId");
453
+ const limit = Math.min(Math.max(Number(query.get("limit") ?? 20) || 20, 1), 50);
454
+ const rows = sessions.map(sessionRow).filter((r) => (!ws || r.workspaceId === ws) && (!project || r.projectId === project)).filter((r) => status === "all" || r.status === status).filter((r) => {
455
+ const pending = r.triage?.pending === true;
456
+ return lens === "triage" ? pending : lens === "default" ? !pending : true;
457
+ }).filter((r) => kind === "all" || r.kind === kind).filter((r) => !subject || r.subjectEntityId === subject).sort((a, b) => String(b.startedAt).localeCompare(String(a.startedAt)));
458
+ return json(200, rows.slice(0, limit));
459
+ }
460
+ return null;
461
+ }
462
+ function hub(method, path, query, headers, body) {
272
463
  const failure2 = options.failures?.[path];
273
464
  if (failure2 === "unreachable") return "reject";
274
465
  const bearer = /^Bearer (.+)$/.exec(headers.authorization ?? "")?.[1];
@@ -290,11 +481,64 @@ function createMockAuthServer(options = {}) {
290
481
  return json(403, { error: "This door is not available to a space-scoped key", code: "GRANT_DOOR_UNSUPPORTED" });
291
482
  }
292
483
  if (method === "GET" && path === "/users/me") return json(200, user);
293
- if (method === "GET" && path === "/entities") return json(200, { entities: [], total: 0 });
484
+ if (method === "GET" && path === "/entities") {
485
+ const ws = query.get("workspaceId");
486
+ const kind = query.get("profileSlug");
487
+ const offset = Number(query.get("offset") ?? 0) || 0;
488
+ const limit = Number(query.get("limit") ?? 50) || 50;
489
+ const rows = entities.filter((e) => (!ws || e.workspaceId === ws) && (!kind || e.profileSlug === kind));
490
+ return json(200, rows.slice(offset, offset + limit));
491
+ }
492
+ if (method === "GET" && path === "/proposals") {
493
+ const readable = new Set(grant.filter((g) => g.permission === "proposal.read" && row.scope.includes(g.permission)).map((g) => g.workspaceId));
494
+ const status = query.get("status") ?? "pending";
495
+ const ws = query.get("workspaceId");
496
+ const limit = Math.min(Math.max(Number(query.get("limit") ?? 50) || 50, 1), 200);
497
+ const offset = Number(query.get("offset") ?? 0) || 0;
498
+ const idsRaw = query.getAll("proposalIds");
499
+ const ids = idsRaw.length > 0 ? new Set(idsRaw.flatMap((v) => v.split(",").map((x) => x.trim()).filter(Boolean))) : null;
500
+ const rows = proposals.filter((p) => p.workspaceId !== null && readable.has(p.workspaceId)).filter((p) => (status === "all" || p.status === status) && (!ws || p.workspaceId === ws)).filter((p) => !ids || ids.has(p.id)).reverse();
501
+ const page = rows.slice(offset, offset + limit);
502
+ return json(200, { proposals: page, limit, offset, total: rows.length, hasMore: offset + page.length < rows.length });
503
+ }
504
+ if (method === "GET") {
505
+ const read = readSeeded(path, query);
506
+ if (read) return read;
507
+ }
508
+ const approve = method === "POST" ? /^\/proposals\/([^/]+)\/approve$/.exec(path) : null;
509
+ if (approve) {
510
+ if (options.actsAs !== "person") {
511
+ return json(403, { error: "An agent credential cannot approve proposals \u2014 approve is human review. Approve from a human session." });
512
+ }
513
+ const p = proposals.find((x) => x.id === decodeURIComponent(approve[1]));
514
+ if (!p) return json(404, { error: "Proposal not found" });
515
+ if (!grant.some((g) => g.permission === "proposal.approve" && row.scope.includes(g.permission) && g.workspaceId === p.workspaceId)) {
516
+ return json(403, { error: "You may not approve changes here" });
517
+ }
518
+ if (p.status !== "pending") return json(409, { error: `The proposal is ${p.status}, not pending` });
519
+ p.status = "approved";
520
+ approvals.push(p.id);
521
+ return json(200, { success: true, proposalId: p.id, proposal: { ...p } });
522
+ }
294
523
  if (method !== "GET") {
295
524
  writes.push({ method, path, body });
296
- if ((options.writeMode ?? "proposed") === "proposed") {
525
+ const fields = body && typeof body === "object" ? body : {};
526
+ const workspaceId = typeof fields.workspaceId === "string" ? fields.workspaceId : null;
527
+ if (writeMode === "proposed") {
297
528
  const proposalId = `prop_mock_${++proposalSeq}`;
529
+ proposals.push({
530
+ id: proposalId,
531
+ proposalType: method === "POST" ? "create" : method === "DELETE" ? "delete" : "update",
532
+ targetType: path.split("/")[1]?.replace(/s$/, "") ?? "entity",
533
+ targetId: null,
534
+ status: "pending",
535
+ workspaceId,
536
+ createdAt: new Date(now()).toISOString(),
537
+ correlationId: null,
538
+ sessionId: null,
539
+ agentUserId: MOCK_APP_AGENT_ID,
540
+ ...typeof fields.title === "string" ? { summary: `Create "${fields.title}"` } : {}
541
+ });
298
542
  return json(200, {
299
543
  status: "proposed",
300
544
  message: "Sent for review",
@@ -302,7 +546,9 @@ function createMockAuthServer(options = {}) {
302
546
  reviewUrl: `${issuer}/open/${proposalId}`
303
547
  });
304
548
  }
305
- return json(200, { status: "created", id: `ent_mock_${writes.length}` });
549
+ const id = `ent_mock_${writes.length}`;
550
+ if (method === "POST" && path === "/entities") entities.push({ id, ...fields });
551
+ return json(200, { status: "created", id });
306
552
  }
307
553
  return json(404, { error: "Not found" });
308
554
  }
@@ -341,7 +587,7 @@ function createMockAuthServer(options = {}) {
341
587
  } catch {
342
588
  body = rawBody;
343
589
  }
344
- const res = hub(method, hubPath, headers, body);
590
+ const res = hub(method, hubPath, url.searchParams, headers, body);
345
591
  if (res === "reject") throw new TypeError("Failed to fetch: mock pod unreachable");
346
592
  return res;
347
593
  }
@@ -352,6 +598,21 @@ function createMockAuthServer(options = {}) {
352
598
  fetch: mockFetch,
353
599
  requests,
354
600
  writes,
601
+ proposals,
602
+ approvals,
603
+ settleProposal(id, status) {
604
+ const p = proposals.find((x) => x.id === id);
605
+ if (p) p.status = status;
606
+ },
607
+ async redeemElsewhere(t) {
608
+ const row = refreshTokens.get(t);
609
+ const res = await token({ grant_type: "refresh_token", refresh_token: t, client_id: row?.clientId ?? "" });
610
+ if (res.status !== 200) throw new Error(`redeemElsewhere: the pod answered ${res.status}`);
611
+ return await res.json();
612
+ },
613
+ get graceAnswers() {
614
+ return graceAnswers;
615
+ },
355
616
  authorize(authorizeUrl, decision = {}) {
356
617
  const url = new URL(authorizeUrl);
357
618
  if (`${url.origin}${url.pathname}` !== `${issuer}/authorize`) throw new Error(`Not this pod's /authorize: ${authorizeUrl}`);
@@ -397,6 +658,9 @@ function createMockAuthServer(options = {}) {
397
658
  for (const row of access.values()) row.revoked = true;
398
659
  for (const row of refreshTokens.values()) row.revoked = true;
399
660
  },
661
+ revokeAccessKeys() {
662
+ for (const row of access.values()) row.revoked = true;
663
+ },
400
664
  liveRefreshTokens() {
401
665
  return [...refreshTokens].filter(([, r]) => !r.used && !r.revoked).map(([t]) => t);
402
666
  },
@@ -407,9 +671,88 @@ function createMockAuthServer(options = {}) {
407
671
  };
408
672
  }
409
673
 
674
+ // src/testing/seed.ts
675
+ var KEYS = ["entities", "sessions", "documents", "relations", "proposals", "spaces"];
676
+ var REQUIRED = {
677
+ entities: ["id"],
678
+ sessions: ["id"],
679
+ documents: ["id", "title", "content"],
680
+ relations: ["id", "sourceEntityId", "targetEntityId", "type"],
681
+ proposals: ["id"],
682
+ spaces: ["id", "name"]
683
+ };
684
+ function parseMockPodSeed(input) {
685
+ const value = typeof input === "string" ? JSON.parse(input) : input;
686
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError("seed: expected an object");
687
+ const obj = value;
688
+ for (const key of Object.keys(obj)) {
689
+ if (!KEYS.includes(key)) throw new TypeError(`seed.${key}: unknown collection (expected ${KEYS.join(", ")})`);
690
+ }
691
+ for (const key of KEYS) {
692
+ const rows = obj[key];
693
+ if (rows === void 0) continue;
694
+ if (!Array.isArray(rows)) throw new TypeError(`seed.${key}: expected an array`);
695
+ rows.forEach((row, i) => {
696
+ if (!row || typeof row !== "object" || Array.isArray(row)) throw new TypeError(`seed.${key}[${i}]: expected an object`);
697
+ for (const field of REQUIRED[key]) {
698
+ if (typeof row[field] !== "string") throw new TypeError(`seed.${key}[${i}].${field}: expected a string`);
699
+ }
700
+ });
701
+ }
702
+ return JSON.parse(JSON.stringify(obj));
703
+ }
704
+
705
+ // src/connect/rules.ts
706
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
707
+ var CLOUD_STATE_RE = /^[A-Za-z0-9._~-]{16,256}$/;
708
+
709
+ // src/testing/cloud.ts
710
+ var MOCK_CLOUD_URL = "https://cp.mock-synap.test";
711
+ function createMockCloud(options = {}) {
712
+ const base = (options.url ?? MOCK_CLOUD_URL).replace(/\/+$/, "");
713
+ const requests = [];
714
+ return {
715
+ url: base,
716
+ requests,
717
+ choose(location, { pod, metadata }) {
718
+ const back = check(location, metadata);
719
+ back.searchParams.set("iss", pod);
720
+ back.searchParams.set("state", new URL(location).searchParams.get("state"));
721
+ return back.href;
722
+ },
723
+ cancel(location, { metadata }) {
724
+ const back = check(location, metadata);
725
+ back.searchParams.set("state", new URL(location).searchParams.get("state"));
726
+ back.searchParams.set("error", "access_denied");
727
+ return back.href;
728
+ }
729
+ };
730
+ function check(location, metadata) {
731
+ requests.push(location);
732
+ const url = new URL(location);
733
+ if (`${url.origin}${url.pathname}` !== `${base}/connect/pod`) throw new Error(`Not this CP's /connect/pod: ${location}`);
734
+ const clientId = url.searchParams.get("client_id");
735
+ const state = url.searchParams.get("state");
736
+ if (!clientId || !cimdUrlAccepted(clientId)) throw new Error("Chooser refused: client_id must be an https CIMD URL");
737
+ if (!state || !CLOUD_STATE_RE.test(state)) throw new Error("Chooser refused: state is missing or malformed");
738
+ if (clientId !== metadata.client_id) throw new Error("Chooser refused: the client metadata is not this client_id's");
739
+ return new URL(metadata.initiate_login_uri);
740
+ }
741
+ function cimdUrlAccepted(clientId) {
742
+ let url;
743
+ try {
744
+ url = new URL(clientId);
745
+ } catch {
746
+ return false;
747
+ }
748
+ if (url.protocol === "https:") return true;
749
+ return url.protocol === "http:" && options.allowInsecureLoopback === true && LOOPBACK_HOSTS.has(url.hostname);
750
+ }
751
+ }
752
+
410
753
  // src/testing/index.ts
411
754
  var MOCK_POD_URL = "https://mock.synap.invalid";
412
- var MOCK_TOKENS = { form: "demo-form", share: "demo", site: "demo-site" };
755
+ var MOCK_TOKENS = { form: "demo-form", share: "demo", htmlShare: "demo-html", site: "demo-site" };
413
756
  var MOCK_RETRY_AFTER_SEC = 30;
414
757
  var MOCK_FAILURE_TOKENS = {
415
758
  /** 429 with no `Retry-After` (what the pod's public doors send today). */
@@ -589,7 +932,7 @@ function invalidKeys(form, body) {
589
932
  }
590
933
  function createMockPod(options = {}) {
591
934
  const forms = options.forms ?? { [MOCK_TOKENS.form]: MOCK_FORM };
592
- const shares = options.shares ?? { [MOCK_TOKENS.share]: MOCK_SHARE };
935
+ const shares = options.shares ?? { [MOCK_TOKENS.share]: MOCK_SHARE, [MOCK_TOKENS.htmlShare]: MOCK_HTML_SHARE };
593
936
  const sites = options.sites ?? { [MOCK_TOKENS.site]: MOCK_SITE };
594
937
  const requests = [];
595
938
  const submissions = [];
@@ -655,10 +998,14 @@ function createMockPod(options = {}) {
655
998
  }
656
999
  // Annotate the CommonJS export names for ESM import in node:
657
1000
  0 && (module.exports = {
1001
+ MOCK_APP,
1002
+ MOCK_APP_AGENT_ID,
658
1003
  MOCK_AUTH_ISSUER,
1004
+ MOCK_CLOUD_URL,
659
1005
  MOCK_FAILURE_TOKENS,
660
1006
  MOCK_FORM,
661
1007
  MOCK_GRANT,
1008
+ MOCK_HTML_SHARE,
662
1009
  MOCK_POD_URL,
663
1010
  MOCK_RETRY_AFTER_SEC,
664
1011
  MOCK_SHARE,
@@ -666,6 +1013,8 @@ function createMockPod(options = {}) {
666
1013
  MOCK_TOKENS,
667
1014
  MOCK_USER,
668
1015
  createMockAuthServer,
669
- createMockPod
1016
+ createMockCloud,
1017
+ createMockPod,
1018
+ parseMockPodSeed
670
1019
  });
671
1020
  //# sourceMappingURL=testing.cjs.map