@synap-core/sdk 1.4.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 (50) hide show
  1. package/CHANGELOG.md +133 -0
  2. package/README.md +136 -10
  3. package/dist/{chunk-J7XHAAFJ.js → chunk-UK2U6SMA.js} +14 -2
  4. package/dist/chunk-UK2U6SMA.js.map +1 -0
  5. package/dist/{client-C5sF20f1.d.cts → client-B50KC2aJ.d.cts} +14 -9
  6. package/dist/{client-C5sF20f1.d.ts → client-B50KC2aJ.d.ts} +14 -9
  7. package/dist/connect.cjs +412 -64
  8. package/dist/connect.cjs.map +1 -1
  9. package/dist/connect.d.cts +400 -32
  10. package/dist/connect.d.ts +400 -32
  11. package/dist/connect.js +407 -65
  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 +234 -12
  20. package/dist/testing.cjs.map +1 -1
  21. package/dist/testing.d.cts +122 -27
  22. package/dist/testing.d.ts +122 -27
  23. package/dist/testing.js +230 -12
  24. package/dist/testing.js.map +1 -1
  25. package/dist/{types-C2KGNXEn.d.cts → types-DZYNBCq7.d.cts} +28 -5
  26. package/dist/{types-C2KGNXEn.d.ts → types-DZYNBCq7.d.ts} +28 -5
  27. package/package.json +30 -6
  28. package/src/connect/auth.ts +156 -27
  29. package/src/connect/client.ts +374 -34
  30. package/src/connect/cloud.ts +14 -10
  31. package/src/connect/discovery.ts +66 -32
  32. package/src/connect/encoding.ts +1 -1
  33. package/src/connect/errors.ts +7 -0
  34. package/src/connect/flow.ts +42 -14
  35. package/src/connect/http.ts +42 -0
  36. package/src/connect/hub-types.ts +254 -0
  37. package/src/connect/index.ts +60 -42
  38. package/src/connect/rules.ts +18 -0
  39. package/src/connect/store.ts +9 -5
  40. package/src/connect/types.ts +28 -4
  41. package/src/public/client.ts +14 -2
  42. package/src/public/safe-markdown.ts +13 -0
  43. package/src/public/share-model.ts +6 -2
  44. package/src/public/site-model.ts +5 -3
  45. package/src/testing/auth-server.ts +239 -14
  46. package/src/testing/cloud.ts +3 -4
  47. package/src/testing/fixtures.ts +26 -0
  48. package/src/testing/index.ts +8 -6
  49. package/src/testing/seed.ts +120 -0
  50. package/dist/chunk-J7XHAAFJ.js.map +0 -1
package/dist/testing.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import {
2
+ CLOUD_STATE_RE,
3
+ LOOPBACK_HOSTS,
2
4
  pkceChallenge,
3
5
  randomToken
4
- } from "./chunk-J7XHAAFJ.js";
6
+ } from "./chunk-UK2U6SMA.js";
5
7
 
6
8
  // src/testing/fixtures.ts
7
9
  var MOCK_FORM = {
@@ -42,6 +44,26 @@ var MOCK_SHARE = {
42
44
  },
43
45
  publishedOn: "2026-09-27"
44
46
  };
47
+ var MOCK_HTML_SHARE = {
48
+ resourceType: "entity",
49
+ title: "Estimate for Northwind",
50
+ properties: { total: "4 800 \u20AC", validUntil: "2026-11-30" },
51
+ body: {
52
+ format: "html",
53
+ content: [
54
+ "<!doctype html>",
55
+ '<html lang="en"><head><meta charset="utf-8"><title>Estimate</title>',
56
+ "<style>body{font-family:Georgia,serif;max-width:40rem;margin:2rem auto;line-height:1.5}td{padding:.25rem .75rem}</style>",
57
+ "</head><body>",
58
+ "<h1>Estimate for Northwind</h1>",
59
+ "<table><tr><td>Discovery workshop</td><td>1 200 \u20AC</td></tr><tr><td>Build</td><td>3 600 \u20AC</td></tr></table>",
60
+ "<p><strong>Total: 4 800 \u20AC</strong></p>",
61
+ "<script>document.body.textContent = 'scripts never run in a sandboxed frame'</script>",
62
+ "</body></html>"
63
+ ].join("")
64
+ },
65
+ publishedOn: "2026-10-22"
66
+ };
45
67
  function publicId(prefix, n) {
46
68
  return `p_${prefix}${String(n).padStart(16 - prefix.length, "0")}`;
47
69
  }
@@ -130,7 +152,29 @@ function createMockAuthServer(options = {}) {
130
152
  const requests = [];
131
153
  const writes = [];
132
154
  const proposals = [];
133
- const entities = [...options.entities ?? []];
155
+ const seed = options.seed ?? {};
156
+ const entities = [...seed.entities ?? [], ...options.entities ?? []];
157
+ const sessions = [...seed.sessions ?? []];
158
+ const documents = [...seed.documents ?? []];
159
+ const relations = [...seed.relations ?? []];
160
+ const templateOf = new Map((seed.spaces ?? []).filter((sp) => sp.templateSlug).map((sp) => [sp.id, sp.templateSlug]));
161
+ const approvals = [];
162
+ const seededAt = new Date(now()).toISOString();
163
+ for (const p of seed.proposals ?? []) {
164
+ proposals.push({
165
+ proposalType: "create",
166
+ targetType: "entity",
167
+ targetId: null,
168
+ status: "pending",
169
+ workspaceId: null,
170
+ createdAt: seededAt,
171
+ correlationId: null,
172
+ sessionId: null,
173
+ agentUserId: null,
174
+ ...p
175
+ });
176
+ }
177
+ const writeMode = options.writeMode ?? (options.actsAs === "person" ? "applied" : "proposed");
134
178
  const graceMs = (options.refreshGraceSec ?? 30) * 1e3;
135
179
  const app = options.app === void 0 ? MOCK_APP : options.app;
136
180
  let proposalSeq = 0;
@@ -172,9 +216,11 @@ function createMockAuthServer(options = {}) {
172
216
  // As the pod: what the key has left, never an assumed hour.
173
217
  expires_in: Math.max(0, Math.floor((row.expiresAt - now()) / 1e3)),
174
218
  scope: scope.join(" "),
175
- synap_grant: grant.filter((g) => scope.includes(g.permission)),
219
+ // Each grant row names its space's template when it came from one.
220
+ synap_grant: grant.filter((g) => scope.includes(g.permission)).map((g) => g.templateSlug || !templateOf.has(g.workspaceId) ? g : { ...g, templateSlug: templateOf.get(g.workspaceId) }),
176
221
  synap_user: user,
177
- ...app ? { synap_app: app } : {}
222
+ ...app ? { synap_app: app } : {},
223
+ ...options.actsAs ? { synap_acts_as: options.actsAs } : {}
178
224
  };
179
225
  if (pair) {
180
226
  body.refresh_token = pair.refreshToken;
@@ -237,6 +283,122 @@ function createMockAuthServer(options = {}) {
237
283
  }
238
284
  return oauthError("unsupported_grant_type", "Unsupported grant_type");
239
285
  }
286
+ const sessionRow = (s) => ({
287
+ workspaceId: null,
288
+ projectId: null,
289
+ userId: user.id,
290
+ correlationId: null,
291
+ title: null,
292
+ goal: "",
293
+ status: "active",
294
+ templateId: null,
295
+ expectedOutputs: [],
296
+ channelId: null,
297
+ progress: null,
298
+ currentStage: null,
299
+ agentIds: [],
300
+ closedAt: null,
301
+ verificationReport: null,
302
+ metadata: {},
303
+ startedAt: seededAt,
304
+ createdAt: seededAt,
305
+ updatedAt: seededAt,
306
+ kind: "work",
307
+ triage: { pending: false, acceptedAt: null, acceptedBy: null },
308
+ ...s
309
+ });
310
+ function readSeeded(path, query) {
311
+ const conn = /^\/entities\/([^/]+)\/connections$/.exec(path);
312
+ if (conn) {
313
+ const id = decodeURIComponent(conn[1]);
314
+ const ws = query.get("workspaceId");
315
+ const limit = Math.min(Math.max(Number(query.get("limit") ?? 50) || 50, 1), 200);
316
+ const inLens = (e) => !ws || e.workspaceId === ws || e.workspaceId == null;
317
+ const connections = [];
318
+ const graph = relations.filter((r) => r.sourceEntityId === id || r.targetEntityId === id).sort((a, b) => String(b.createdAt ?? "").localeCompare(String(a.createdAt ?? ""))).slice(0, limit);
319
+ for (const r of graph) {
320
+ const outgoing = r.sourceEntityId === id;
321
+ const otherId = outgoing ? r.targetEntityId : r.sourceEntityId;
322
+ const other = entities.find((e) => e.id === otherId);
323
+ if (!other || !inLens(other)) continue;
324
+ connections.push({
325
+ entityId: otherId,
326
+ entity: { ...other, type: other.profileSlug ?? other.type ?? null, facetSlugs: other.facetSlugs ?? [] },
327
+ label: r.type,
328
+ direction: outgoing ? "outgoing" : "incoming",
329
+ source: "graph",
330
+ relationId: r.id,
331
+ relationType: r.type,
332
+ createdAt: r.createdAt ?? null
333
+ });
334
+ }
335
+ for (const s of sessions.filter((x) => x.subjectEntityId === id).slice(0, limit)) {
336
+ const row = sessionRow(s);
337
+ connections.push({
338
+ entityId: s.id,
339
+ entity: null,
340
+ label: row.title || row.goal,
341
+ direction: "incoming",
342
+ source: "focus_session",
343
+ focusSessionId: s.id,
344
+ focusSessionGoal: row.goal,
345
+ focusSessionStatus: row.status,
346
+ focusSessionWorkspaceId: row.workspaceId,
347
+ createdAt: row.createdAt
348
+ });
349
+ }
350
+ const n = (source) => connections.filter((c) => c.source === source).length;
351
+ return json(200, {
352
+ connections,
353
+ counts: { total: connections.length, graph: n("graph"), structural: n("property"), threads: n("thread"), contextChannels: n("context_channel"), focusSessions: n("focus_session") }
354
+ });
355
+ }
356
+ const entity = /^\/entities\/([^/]+)$/.exec(path);
357
+ if (entity) {
358
+ const found = entities.find((e) => e.id === decodeURIComponent(entity[1]));
359
+ return found ? json(200, found) : new Response(null, { status: 404 });
360
+ }
361
+ const doc = /^\/documents\/([^/]+)$/.exec(path);
362
+ if (doc) {
363
+ const format = query.get("format") ?? "raw";
364
+ if (format !== "raw" && format !== "readable") return json(400, { error: "format must be raw or readable" });
365
+ const d = documents.find((x) => x.id === decodeURIComponent(doc[1]));
366
+ if (!d) return json(404, { error: "Document not found" });
367
+ return json(200, {
368
+ document: {
369
+ id: d.id,
370
+ title: d.title,
371
+ type: d.type ?? "markdown",
372
+ language: d.language ?? null,
373
+ workspaceId: d.workspaceId ?? null,
374
+ format,
375
+ content: d.content,
376
+ revision: d.revision ?? 1,
377
+ version: d.version ?? 1,
378
+ sections: [],
379
+ diagnostics: [],
380
+ updatedAt: d.updatedAt ?? seededAt,
381
+ createdAt: d.createdAt ?? seededAt
382
+ }
383
+ });
384
+ }
385
+ if (path === "/focus-sessions") {
386
+ const ws = query.get("workspaceId");
387
+ const project = query.get("projectId");
388
+ if (!ws && !project) return json(400, { error: "workspaceId or projectId is required" });
389
+ const status = query.get("status") ?? "all";
390
+ const lens = query.get("lens") ?? "all";
391
+ const kind = query.get("kind") ?? "all";
392
+ const subject = query.get("subjectEntityId");
393
+ const limit = Math.min(Math.max(Number(query.get("limit") ?? 20) || 20, 1), 50);
394
+ const rows = sessions.map(sessionRow).filter((r) => (!ws || r.workspaceId === ws) && (!project || r.projectId === project)).filter((r) => status === "all" || r.status === status).filter((r) => {
395
+ const pending = r.triage?.pending === true;
396
+ return lens === "triage" ? pending : lens === "default" ? !pending : true;
397
+ }).filter((r) => kind === "all" || r.kind === kind).filter((r) => !subject || r.subjectEntityId === subject).sort((a, b) => String(b.startedAt).localeCompare(String(a.startedAt)));
398
+ return json(200, rows.slice(0, limit));
399
+ }
400
+ return null;
401
+ }
240
402
  function hub(method, path, query, headers, body) {
241
403
  const failure2 = options.failures?.[path];
242
404
  if (failure2 === "unreachable") return "reject";
@@ -273,15 +435,36 @@ function createMockAuthServer(options = {}) {
273
435
  const ws = query.get("workspaceId");
274
436
  const limit = Math.min(Math.max(Number(query.get("limit") ?? 50) || 50, 1), 200);
275
437
  const offset = Number(query.get("offset") ?? 0) || 0;
276
- const rows = proposals.filter((p) => p.workspaceId !== null && readable.has(p.workspaceId)).filter((p) => (status === "all" || p.status === status) && (!ws || p.workspaceId === ws)).reverse();
438
+ const idsRaw = query.getAll("proposalIds");
439
+ const ids = idsRaw.length > 0 ? new Set(idsRaw.flatMap((v) => v.split(",").map((x) => x.trim()).filter(Boolean))) : null;
440
+ 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();
277
441
  const page = rows.slice(offset, offset + limit);
278
442
  return json(200, { proposals: page, limit, offset, total: rows.length, hasMore: offset + page.length < rows.length });
279
443
  }
444
+ if (method === "GET") {
445
+ const read = readSeeded(path, query);
446
+ if (read) return read;
447
+ }
448
+ const approve = method === "POST" ? /^\/proposals\/([^/]+)\/approve$/.exec(path) : null;
449
+ if (approve) {
450
+ if (options.actsAs !== "person") {
451
+ return json(403, { error: "An agent credential cannot approve proposals \u2014 approve is human review. Approve from a human session." });
452
+ }
453
+ const p = proposals.find((x) => x.id === decodeURIComponent(approve[1]));
454
+ if (!p) return json(404, { error: "Proposal not found" });
455
+ if (!grant.some((g) => g.permission === "proposal.approve" && row.scope.includes(g.permission) && g.workspaceId === p.workspaceId)) {
456
+ return json(403, { error: "You may not approve changes here" });
457
+ }
458
+ if (p.status !== "pending") return json(409, { error: `The proposal is ${p.status}, not pending` });
459
+ p.status = "approved";
460
+ approvals.push(p.id);
461
+ return json(200, { success: true, proposalId: p.id, proposal: { ...p } });
462
+ }
280
463
  if (method !== "GET") {
281
464
  writes.push({ method, path, body });
282
465
  const fields = body && typeof body === "object" ? body : {};
283
466
  const workspaceId = typeof fields.workspaceId === "string" ? fields.workspaceId : null;
284
- if ((options.writeMode ?? "proposed") === "proposed") {
467
+ if (writeMode === "proposed") {
285
468
  const proposalId = `prop_mock_${++proposalSeq}`;
286
469
  proposals.push({
287
470
  id: proposalId,
@@ -356,6 +539,7 @@ function createMockAuthServer(options = {}) {
356
539
  requests,
357
540
  writes,
358
541
  proposals,
542
+ approvals,
359
543
  settleProposal(id, status) {
360
544
  const p = proposals.find((x) => x.id === id);
361
545
  if (p) p.status = status;
@@ -414,6 +598,9 @@ function createMockAuthServer(options = {}) {
414
598
  for (const row of access.values()) row.revoked = true;
415
599
  for (const row of refreshTokens.values()) row.revoked = true;
416
600
  },
601
+ revokeAccessKeys() {
602
+ for (const row of access.values()) row.revoked = true;
603
+ },
417
604
  liveRefreshTokens() {
418
605
  return [...refreshTokens].filter(([, r]) => !r.used && !r.revoked).map(([t]) => t);
419
606
  },
@@ -424,10 +611,39 @@ function createMockAuthServer(options = {}) {
424
611
  };
425
612
  }
426
613
 
614
+ // src/testing/seed.ts
615
+ var KEYS = ["entities", "sessions", "documents", "relations", "proposals", "spaces"];
616
+ var REQUIRED = {
617
+ entities: ["id"],
618
+ sessions: ["id"],
619
+ documents: ["id", "title", "content"],
620
+ relations: ["id", "sourceEntityId", "targetEntityId", "type"],
621
+ proposals: ["id"],
622
+ spaces: ["id", "name"]
623
+ };
624
+ function parseMockPodSeed(input) {
625
+ const value = typeof input === "string" ? JSON.parse(input) : input;
626
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError("seed: expected an object");
627
+ const obj = value;
628
+ for (const key of Object.keys(obj)) {
629
+ if (!KEYS.includes(key)) throw new TypeError(`seed.${key}: unknown collection (expected ${KEYS.join(", ")})`);
630
+ }
631
+ for (const key of KEYS) {
632
+ const rows = obj[key];
633
+ if (rows === void 0) continue;
634
+ if (!Array.isArray(rows)) throw new TypeError(`seed.${key}: expected an array`);
635
+ rows.forEach((row, i) => {
636
+ if (!row || typeof row !== "object" || Array.isArray(row)) throw new TypeError(`seed.${key}[${i}]: expected an object`);
637
+ for (const field of REQUIRED[key]) {
638
+ if (typeof row[field] !== "string") throw new TypeError(`seed.${key}[${i}].${field}: expected a string`);
639
+ }
640
+ });
641
+ }
642
+ return JSON.parse(JSON.stringify(obj));
643
+ }
644
+
427
645
  // src/testing/cloud.ts
428
646
  var MOCK_CLOUD_URL = "https://cp.mock-synap.test";
429
- var STATE_RE = /^[A-Za-z0-9._~-]{16,256}$/;
430
- var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
431
647
  function createMockCloud(options = {}) {
432
648
  const base = (options.url ?? MOCK_CLOUD_URL).replace(/\/+$/, "");
433
649
  const requests = [];
@@ -454,7 +670,7 @@ function createMockCloud(options = {}) {
454
670
  const clientId = url.searchParams.get("client_id");
455
671
  const state = url.searchParams.get("state");
456
672
  if (!clientId || !cimdUrlAccepted(clientId)) throw new Error("Chooser refused: client_id must be an https CIMD URL");
457
- if (!state || !STATE_RE.test(state)) throw new Error("Chooser refused: state is missing or malformed");
673
+ if (!state || !CLOUD_STATE_RE.test(state)) throw new Error("Chooser refused: state is missing or malformed");
458
674
  if (clientId !== metadata.client_id) throw new Error("Chooser refused: the client metadata is not this client_id's");
459
675
  return new URL(metadata.initiate_login_uri);
460
676
  }
@@ -472,7 +688,7 @@ function createMockCloud(options = {}) {
472
688
 
473
689
  // src/testing/index.ts
474
690
  var MOCK_POD_URL = "https://mock.synap.invalid";
475
- var MOCK_TOKENS = { form: "demo-form", share: "demo", site: "demo-site" };
691
+ var MOCK_TOKENS = { form: "demo-form", share: "demo", htmlShare: "demo-html", site: "demo-site" };
476
692
  var MOCK_RETRY_AFTER_SEC = 30;
477
693
  var MOCK_FAILURE_TOKENS = {
478
694
  /** 429 with no `Retry-After` (what the pod's public doors send today). */
@@ -652,7 +868,7 @@ function invalidKeys(form, body) {
652
868
  }
653
869
  function createMockPod(options = {}) {
654
870
  const forms = options.forms ?? { [MOCK_TOKENS.form]: MOCK_FORM };
655
- const shares = options.shares ?? { [MOCK_TOKENS.share]: MOCK_SHARE };
871
+ const shares = options.shares ?? { [MOCK_TOKENS.share]: MOCK_SHARE, [MOCK_TOKENS.htmlShare]: MOCK_HTML_SHARE };
656
872
  const sites = options.sites ?? { [MOCK_TOKENS.site]: MOCK_SITE };
657
873
  const requests = [];
658
874
  const submissions = [];
@@ -724,6 +940,7 @@ export {
724
940
  MOCK_FAILURE_TOKENS,
725
941
  MOCK_FORM,
726
942
  MOCK_GRANT,
943
+ MOCK_HTML_SHARE,
727
944
  MOCK_POD_URL,
728
945
  MOCK_RETRY_AFTER_SEC,
729
946
  MOCK_SHARE,
@@ -732,6 +949,7 @@ export {
732
949
  MOCK_USER,
733
950
  createMockAuthServer,
734
951
  createMockCloud,
735
- createMockPod
952
+ createMockPod,
953
+ parseMockPodSeed
736
954
  };
737
955
  //# sourceMappingURL=testing.js.map