@synap-core/sdk 1.4.0 → 1.5.1
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 +215 -0
- package/README.md +152 -10
- package/dist/{chunk-J7XHAAFJ.js → chunk-UK2U6SMA.js} +14 -2
- package/dist/chunk-UK2U6SMA.js.map +1 -0
- package/dist/{client-C5sF20f1.d.cts → client-B50KC2aJ.d.cts} +14 -9
- package/dist/{client-C5sF20f1.d.ts → client-B50KC2aJ.d.ts} +14 -9
- package/dist/connect.cjs +419 -64
- package/dist/connect.cjs.map +1 -1
- package/dist/connect.d.cts +416 -32
- package/dist/connect.d.ts +416 -32
- package/dist/connect.js +414 -65
- package/dist/connect.js.map +1 -1
- package/dist/public.cjs +9 -2
- package/dist/public.cjs.map +1 -1
- package/dist/public.d.cts +19 -4
- package/dist/public.d.ts +19 -4
- package/dist/public.js +8 -2
- package/dist/public.js.map +1 -1
- package/dist/testing.cjs +350 -15
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +171 -27
- package/dist/testing.d.ts +171 -27
- package/dist/testing.js +346 -15
- package/dist/testing.js.map +1 -1
- package/dist/{types-C2KGNXEn.d.cts → types-DZYNBCq7.d.cts} +28 -5
- package/dist/{types-C2KGNXEn.d.ts → types-DZYNBCq7.d.ts} +28 -5
- package/package.json +30 -6
- package/src/connect/auth.ts +169 -27
- package/src/connect/client.ts +377 -34
- package/src/connect/cloud.ts +14 -10
- package/src/connect/discovery.ts +66 -32
- package/src/connect/encoding.ts +1 -1
- package/src/connect/errors.ts +7 -0
- package/src/connect/flow.ts +42 -14
- package/src/connect/http.ts +42 -0
- package/src/connect/hub-types.ts +271 -0
- package/src/connect/index.ts +61 -42
- package/src/connect/rules.ts +18 -0
- package/src/connect/store.ts +9 -5
- package/src/connect/types.ts +28 -4
- package/src/public/client.ts +14 -2
- package/src/public/safe-markdown.ts +13 -0
- package/src/public/share-model.ts +6 -2
- package/src/public/site-model.ts +5 -3
- package/src/testing/auth-server.ts +350 -17
- package/src/testing/cloud.ts +3 -4
- package/src/testing/fixtures.ts +26 -0
- package/src/testing/index.ts +8 -6
- package/src/testing/roles.ts +27 -0
- package/src/testing/seed.ts +196 -0
- package/dist/chunk-J7XHAAFJ.js.map +0 -1
package/dist/testing.cjs
CHANGED
|
@@ -27,6 +27,7 @@ __export(testing_exports, {
|
|
|
27
27
|
MOCK_FAILURE_TOKENS: () => MOCK_FAILURE_TOKENS,
|
|
28
28
|
MOCK_FORM: () => MOCK_FORM,
|
|
29
29
|
MOCK_GRANT: () => MOCK_GRANT,
|
|
30
|
+
MOCK_HTML_SHARE: () => MOCK_HTML_SHARE,
|
|
30
31
|
MOCK_POD_URL: () => MOCK_POD_URL,
|
|
31
32
|
MOCK_RETRY_AFTER_SEC: () => MOCK_RETRY_AFTER_SEC,
|
|
32
33
|
MOCK_SHARE: () => MOCK_SHARE,
|
|
@@ -35,7 +36,8 @@ __export(testing_exports, {
|
|
|
35
36
|
MOCK_USER: () => MOCK_USER,
|
|
36
37
|
createMockAuthServer: () => createMockAuthServer,
|
|
37
38
|
createMockCloud: () => createMockCloud,
|
|
38
|
-
createMockPod: () => createMockPod
|
|
39
|
+
createMockPod: () => createMockPod,
|
|
40
|
+
parseMockPodSeed: () => parseMockPodSeed
|
|
39
41
|
});
|
|
40
42
|
module.exports = __toCommonJS(testing_exports);
|
|
41
43
|
|
|
@@ -78,6 +80,26 @@ var MOCK_SHARE = {
|
|
|
78
80
|
},
|
|
79
81
|
publishedOn: "2026-09-27"
|
|
80
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
|
+
};
|
|
81
103
|
function publicId(prefix, n) {
|
|
82
104
|
return `p_${prefix}${String(n).padStart(16 - prefix.length, "0")}`;
|
|
83
105
|
}
|
|
@@ -158,6 +180,20 @@ async function pkceChallenge(verifier) {
|
|
|
158
180
|
return base64url(new Uint8Array(digest));
|
|
159
181
|
}
|
|
160
182
|
|
|
183
|
+
// src/testing/roles.ts
|
|
184
|
+
function desugarSeedRoles(row, at) {
|
|
185
|
+
if (!Array.isArray(row.seedRoles)) return row;
|
|
186
|
+
const { seedRoles, ...rest } = row;
|
|
187
|
+
const facets = Array.isArray(row.facets) ? row.facets : [];
|
|
188
|
+
const shorthand = seedRoles.filter((r) => typeof r?.profileSlug === "string" && typeof r.workspaceId === "string").map((r, i) => ({ facetId: `seedrole_${String(row.id)}_${i}`, slug: r.profileSlug, workspaceId: r.workspaceId }));
|
|
189
|
+
for (const r of shorthand) {
|
|
190
|
+
if (facets.some((f) => f?.slug === r.slug && f.workspaceId === r.workspaceId)) {
|
|
191
|
+
throw new TypeError(`${at} ("${String(row.id)}"): the role "${r.slug}" in ${r.workspaceId} is in both facets and seedRoles; keep it in facets only`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return { ...rest, facets: [...facets, ...shorthand] };
|
|
195
|
+
}
|
|
196
|
+
|
|
161
197
|
// src/testing/auth-server.ts
|
|
162
198
|
var MOCK_AUTH_ISSUER = "https://pod.mock-synap.test";
|
|
163
199
|
var MOCK_GRANT = [
|
|
@@ -190,7 +226,43 @@ function createMockAuthServer(options = {}) {
|
|
|
190
226
|
const requests = [];
|
|
191
227
|
const writes = [];
|
|
192
228
|
const proposals = [];
|
|
193
|
-
const
|
|
229
|
+
const seed = options.seed ?? {};
|
|
230
|
+
const entities = [...seed.entities ?? [], ...options.entities ?? []].map(
|
|
231
|
+
(e, i) => desugarSeedRoles(e, `entities[${i}]`)
|
|
232
|
+
);
|
|
233
|
+
const sessions = [...seed.sessions ?? []];
|
|
234
|
+
const documents = [...seed.documents ?? []];
|
|
235
|
+
const relations = [...seed.relations ?? []];
|
|
236
|
+
const templateOf = new Map((seed.spaces ?? []).filter((sp) => sp.templateSlug).map((sp) => [sp.id, sp.templateSlug]));
|
|
237
|
+
const approvals = [];
|
|
238
|
+
const seededAt = new Date(now()).toISOString();
|
|
239
|
+
const rolesOf = (e) => Array.isArray(e.facets) ? e.facets.filter((f) => typeof f?.slug === "string") : [];
|
|
240
|
+
function readableEntity(e, spaces, only) {
|
|
241
|
+
if (typeof e.workspaceId === "string" && spaces.has(e.workspaceId) && (!only || e.workspaceId === only)) return true;
|
|
242
|
+
return facetsOf(e, spaces, only ?? void 0).some((f) => e.workspaceId == null || !f.sharedRole);
|
|
243
|
+
}
|
|
244
|
+
const facetsOf = (e, spaces, lens) => rolesOf(e).filter((f) => typeof f.workspaceId === "string" && spaces.has(f.workspaceId) && (lens === void 0 || f.workspaceId === lens));
|
|
245
|
+
const facetRow = (f) => ({ facetId: f.facetId, slug: f.slug, properties: f.properties ?? {}, status: f.status ?? null });
|
|
246
|
+
const entityWire = (e) => {
|
|
247
|
+
const { facets: _facets, facetSlugs: _facetSlugs, ...wire } = e;
|
|
248
|
+
return wire;
|
|
249
|
+
};
|
|
250
|
+
const readSpaces = (scope) => new Set(grant.filter((g) => /^entity\.(.+\.)?read$/.test(g.permission) && scope.includes(g.permission)).map((g) => g.workspaceId));
|
|
251
|
+
for (const p of seed.proposals ?? []) {
|
|
252
|
+
proposals.push({
|
|
253
|
+
proposalType: "create",
|
|
254
|
+
targetType: "entity",
|
|
255
|
+
targetId: null,
|
|
256
|
+
status: "pending",
|
|
257
|
+
workspaceId: null,
|
|
258
|
+
createdAt: seededAt,
|
|
259
|
+
correlationId: null,
|
|
260
|
+
sessionId: null,
|
|
261
|
+
agentUserId: null,
|
|
262
|
+
...p
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
const writeMode = options.writeMode ?? (options.actsAs === "person" ? "applied" : "proposed");
|
|
194
266
|
const graceMs = (options.refreshGraceSec ?? 30) * 1e3;
|
|
195
267
|
const app = options.app === void 0 ? MOCK_APP : options.app;
|
|
196
268
|
let proposalSeq = 0;
|
|
@@ -232,9 +304,11 @@ function createMockAuthServer(options = {}) {
|
|
|
232
304
|
// As the pod: what the key has left, never an assumed hour.
|
|
233
305
|
expires_in: Math.max(0, Math.floor((row.expiresAt - now()) / 1e3)),
|
|
234
306
|
scope: scope.join(" "),
|
|
235
|
-
|
|
307
|
+
// Each grant row names its space's template when it came from one.
|
|
308
|
+
synap_grant: grant.filter((g) => scope.includes(g.permission)).map((g) => g.templateSlug || !templateOf.has(g.workspaceId) ? g : { ...g, templateSlug: templateOf.get(g.workspaceId) }),
|
|
236
309
|
synap_user: user,
|
|
237
|
-
...app ? { synap_app: app } : {}
|
|
310
|
+
...app ? { synap_app: app } : {},
|
|
311
|
+
...options.actsAs ? { synap_acts_as: options.actsAs } : {}
|
|
238
312
|
};
|
|
239
313
|
if (pair) {
|
|
240
314
|
body.refresh_token = pair.refreshToken;
|
|
@@ -297,6 +371,160 @@ function createMockAuthServer(options = {}) {
|
|
|
297
371
|
}
|
|
298
372
|
return oauthError("unsupported_grant_type", "Unsupported grant_type");
|
|
299
373
|
}
|
|
374
|
+
const sessionRow = (s) => ({
|
|
375
|
+
workspaceId: null,
|
|
376
|
+
projectId: null,
|
|
377
|
+
userId: user.id,
|
|
378
|
+
correlationId: null,
|
|
379
|
+
title: null,
|
|
380
|
+
goal: "",
|
|
381
|
+
status: "active",
|
|
382
|
+
templateId: null,
|
|
383
|
+
expectedOutputs: [],
|
|
384
|
+
channelId: null,
|
|
385
|
+
progress: null,
|
|
386
|
+
currentStage: null,
|
|
387
|
+
agentIds: [],
|
|
388
|
+
closedAt: null,
|
|
389
|
+
verificationReport: null,
|
|
390
|
+
metadata: {},
|
|
391
|
+
startedAt: seededAt,
|
|
392
|
+
createdAt: seededAt,
|
|
393
|
+
updatedAt: seededAt,
|
|
394
|
+
kind: "work",
|
|
395
|
+
triage: { pending: false, acceptedAt: null, acceptedBy: null },
|
|
396
|
+
...s
|
|
397
|
+
});
|
|
398
|
+
function readSeeded(path, query, scope) {
|
|
399
|
+
const spaces = readSpaces(scope);
|
|
400
|
+
const conn = /^\/entities\/([^/]+)\/connections$/.exec(path);
|
|
401
|
+
if (conn) {
|
|
402
|
+
const id = decodeURIComponent(conn[1]);
|
|
403
|
+
const ws = query.get("workspaceId") ?? void 0;
|
|
404
|
+
const limit = Math.min(Math.max(Number(query.get("limit") ?? 50) || 50, 1), 200);
|
|
405
|
+
const inLens = (e) => !ws || e.workspaceId === ws || e.workspaceId == null;
|
|
406
|
+
const connections = [];
|
|
407
|
+
const graph = relations.filter((r) => r.sourceEntityId === id || r.targetEntityId === id).sort((a, b) => String(b.createdAt ?? "").localeCompare(String(a.createdAt ?? ""))).slice(0, limit);
|
|
408
|
+
for (const r of graph) {
|
|
409
|
+
const outgoing = r.sourceEntityId === id;
|
|
410
|
+
const otherId = outgoing ? r.targetEntityId : r.sourceEntityId;
|
|
411
|
+
const other = entities.find((e) => e.id === otherId);
|
|
412
|
+
if (!other || !inLens(other) || !readableEntity(other, spaces)) continue;
|
|
413
|
+
connections.push({
|
|
414
|
+
entityId: otherId,
|
|
415
|
+
// relations.ts: the raw row plus its role slugs under the read's facet lens (a 1.5.0 seed's own facetSlugs otherwise).
|
|
416
|
+
entity: {
|
|
417
|
+
...entityWire(other),
|
|
418
|
+
type: other.profileSlug ?? other.type ?? null,
|
|
419
|
+
facetSlugs: rolesOf(other).length > 0 ? facetsOf(other, spaces, ws).map((f) => f.slug) : other.facetSlugs ?? []
|
|
420
|
+
},
|
|
421
|
+
label: r.type,
|
|
422
|
+
direction: outgoing ? "outgoing" : "incoming",
|
|
423
|
+
source: "graph",
|
|
424
|
+
relationId: r.id,
|
|
425
|
+
relationType: r.type,
|
|
426
|
+
createdAt: r.createdAt ?? null
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
for (const s of sessions.filter((x) => x.subjectEntityId === id).slice(0, limit)) {
|
|
430
|
+
const row = sessionRow(s);
|
|
431
|
+
connections.push({
|
|
432
|
+
entityId: s.id,
|
|
433
|
+
entity: null,
|
|
434
|
+
label: row.title || row.goal,
|
|
435
|
+
direction: "incoming",
|
|
436
|
+
source: "focus_session",
|
|
437
|
+
focusSessionId: s.id,
|
|
438
|
+
focusSessionGoal: row.goal,
|
|
439
|
+
focusSessionStatus: row.status,
|
|
440
|
+
focusSessionWorkspaceId: row.workspaceId,
|
|
441
|
+
createdAt: row.createdAt
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
const n = (source) => connections.filter((c) => c.source === source).length;
|
|
445
|
+
return json(200, {
|
|
446
|
+
connections,
|
|
447
|
+
counts: { total: connections.length, graph: n("graph"), structural: n("property"), threads: n("thread"), contextChannels: n("context_channel"), focusSessions: n("focus_session") }
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
const entity = /^\/entities\/([^/]+)$/.exec(path);
|
|
451
|
+
if (entity) {
|
|
452
|
+
const found = entities.find((e) => e.id === decodeURIComponent(entity[1]));
|
|
453
|
+
return found && readableEntity(found, spaces) ? json(200, entityWire(found)) : new Response(null, { status: 404 });
|
|
454
|
+
}
|
|
455
|
+
const roles = /^\/entities\/([^/]+)\/facets$/.exec(path);
|
|
456
|
+
if (roles) {
|
|
457
|
+
const found = entities.find((e) => e.id === decodeURIComponent(roles[1]));
|
|
458
|
+
if (!found || !readableEntity(found, spaces)) return new Response(null, { status: 404 });
|
|
459
|
+
return json(200, {
|
|
460
|
+
facets: facetsOf(found, spaces).map((f) => ({
|
|
461
|
+
facet: {
|
|
462
|
+
id: f.facetId,
|
|
463
|
+
entityId: found.id,
|
|
464
|
+
profileId: `profile_${f.slug}`,
|
|
465
|
+
userId: found.userId ?? user.id,
|
|
466
|
+
// A stamped facet answers its space; a shared role's facet is stored pod-wide.
|
|
467
|
+
workspaceId: f.sharedRole ? null : f.workspaceId,
|
|
468
|
+
contextEntityId: null,
|
|
469
|
+
status: f.status ?? null,
|
|
470
|
+
statusCategory: null,
|
|
471
|
+
properties: f.properties ?? {},
|
|
472
|
+
metadata: {},
|
|
473
|
+
createdByKind: null,
|
|
474
|
+
createdByUserId: null,
|
|
475
|
+
agentUserId: null,
|
|
476
|
+
sourceProposalId: null,
|
|
477
|
+
correlationId: null,
|
|
478
|
+
createdAt: seededAt,
|
|
479
|
+
updatedAt: seededAt,
|
|
480
|
+
deletedAt: null
|
|
481
|
+
},
|
|
482
|
+
profile: { id: `profile_${f.slug}`, slug: f.slug, profileKind: "role" },
|
|
483
|
+
effectiveProperties: []
|
|
484
|
+
}))
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
const doc = /^\/documents\/([^/]+)$/.exec(path);
|
|
488
|
+
if (doc) {
|
|
489
|
+
const format = query.get("format") ?? "raw";
|
|
490
|
+
if (format !== "raw" && format !== "readable") return json(400, { error: "format must be raw or readable" });
|
|
491
|
+
const d = documents.find((x) => x.id === decodeURIComponent(doc[1]));
|
|
492
|
+
if (!d) return json(404, { error: "Document not found" });
|
|
493
|
+
return json(200, {
|
|
494
|
+
document: {
|
|
495
|
+
id: d.id,
|
|
496
|
+
title: d.title,
|
|
497
|
+
type: d.type ?? "markdown",
|
|
498
|
+
language: d.language ?? null,
|
|
499
|
+
workspaceId: d.workspaceId ?? null,
|
|
500
|
+
format,
|
|
501
|
+
content: d.content,
|
|
502
|
+
revision: d.revision ?? 1,
|
|
503
|
+
version: d.version ?? 1,
|
|
504
|
+
sections: [],
|
|
505
|
+
diagnostics: [],
|
|
506
|
+
updatedAt: d.updatedAt ?? seededAt,
|
|
507
|
+
createdAt: d.createdAt ?? seededAt
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
if (path === "/focus-sessions") {
|
|
512
|
+
const ws = query.get("workspaceId");
|
|
513
|
+
const project = query.get("projectId");
|
|
514
|
+
if (!ws && !project) return json(400, { error: "workspaceId or projectId is required" });
|
|
515
|
+
const status = query.get("status") ?? "all";
|
|
516
|
+
const lens = query.get("lens") ?? "all";
|
|
517
|
+
const kind = query.get("kind") ?? "all";
|
|
518
|
+
const subject = query.get("subjectEntityId");
|
|
519
|
+
const limit = Math.min(Math.max(Number(query.get("limit") ?? 20) || 20, 1), 50);
|
|
520
|
+
const rows = sessions.map(sessionRow).filter((r) => (!ws || r.workspaceId === ws) && (!project || r.projectId === project)).filter((r) => status === "all" || r.status === status).filter((r) => {
|
|
521
|
+
const pending = r.triage?.pending === true;
|
|
522
|
+
return lens === "triage" ? pending : lens === "default" ? !pending : true;
|
|
523
|
+
}).filter((r) => kind === "all" || r.kind === kind).filter((r) => !subject || r.subjectEntityId === subject).sort((a, b) => String(b.startedAt).localeCompare(String(a.startedAt)));
|
|
524
|
+
return json(200, rows.slice(0, limit));
|
|
525
|
+
}
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
300
528
|
function hub(method, path, query, headers, body) {
|
|
301
529
|
const failure2 = options.failures?.[path];
|
|
302
530
|
if (failure2 === "unreachable") return "reject";
|
|
@@ -320,12 +548,27 @@ function createMockAuthServer(options = {}) {
|
|
|
320
548
|
}
|
|
321
549
|
if (method === "GET" && path === "/users/me") return json(200, user);
|
|
322
550
|
if (method === "GET" && path === "/entities") {
|
|
323
|
-
const ws = query.get("workspaceId");
|
|
551
|
+
const ws = query.get("workspaceId") || void 0;
|
|
324
552
|
const kind = query.get("profileSlug");
|
|
553
|
+
const role = query.get("facetSlug");
|
|
554
|
+
const podWide = query.get("includePodWide") !== "false";
|
|
555
|
+
const withFacets = query.get("includeFacets") === "true";
|
|
325
556
|
const offset = Number(query.get("offset") ?? 0) || 0;
|
|
326
557
|
const limit = Number(query.get("limit") ?? 50) || 50;
|
|
327
|
-
const
|
|
328
|
-
|
|
558
|
+
const spaces = readSpaces(row.scope);
|
|
559
|
+
const rows = entities.filter((e) => {
|
|
560
|
+
if (!readableEntity(e, spaces, ws)) return false;
|
|
561
|
+
if (ws && !podWide && e.workspaceId !== ws && !facetsOf(e, spaces, ws).some((f) => !f.sharedRole)) return false;
|
|
562
|
+
const slugs = facetsOf(e, spaces, ws).map((f) => f.slug);
|
|
563
|
+
return (!kind || e.profileSlug === kind || slugs.includes(kind)) && (!role || slugs.includes(role));
|
|
564
|
+
});
|
|
565
|
+
return json(
|
|
566
|
+
200,
|
|
567
|
+
rows.slice(offset, offset + limit).map((e) => {
|
|
568
|
+
const facets = withFacets ? facetsOf(e, spaces, ws).map(facetRow) : [];
|
|
569
|
+
return facets.length > 0 ? { ...entityWire(e), facetSlugs: facets.map((f) => f.slug), facets } : entityWire(e);
|
|
570
|
+
})
|
|
571
|
+
);
|
|
329
572
|
}
|
|
330
573
|
if (method === "GET" && path === "/proposals") {
|
|
331
574
|
const readable = new Set(grant.filter((g) => g.permission === "proposal.read" && row.scope.includes(g.permission)).map((g) => g.workspaceId));
|
|
@@ -333,15 +576,41 @@ function createMockAuthServer(options = {}) {
|
|
|
333
576
|
const ws = query.get("workspaceId");
|
|
334
577
|
const limit = Math.min(Math.max(Number(query.get("limit") ?? 50) || 50, 1), 200);
|
|
335
578
|
const offset = Number(query.get("offset") ?? 0) || 0;
|
|
336
|
-
const
|
|
579
|
+
const idsRaw = query.getAll("proposalIds");
|
|
580
|
+
const ids = idsRaw.length > 0 ? new Set(idsRaw.flatMap((v) => v.split(",").map((x) => x.trim()).filter(Boolean))) : null;
|
|
581
|
+
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();
|
|
337
582
|
const page = rows.slice(offset, offset + limit);
|
|
338
583
|
return json(200, { proposals: page, limit, offset, total: rows.length, hasMore: offset + page.length < rows.length });
|
|
339
584
|
}
|
|
585
|
+
if (method === "GET") {
|
|
586
|
+
const read = readSeeded(path, query, row.scope);
|
|
587
|
+
if (read) return read;
|
|
588
|
+
}
|
|
589
|
+
const approve = method === "POST" ? /^\/proposals\/([^/]+)\/approve$/.exec(path) : null;
|
|
590
|
+
if (approve) {
|
|
591
|
+
if (options.actsAs !== "person") {
|
|
592
|
+
return json(403, { error: "An agent credential cannot approve proposals \u2014 approve is human review. Approve from a human session." });
|
|
593
|
+
}
|
|
594
|
+
const p = proposals.find((x) => x.id === decodeURIComponent(approve[1]));
|
|
595
|
+
if (!p) return json(404, { error: "Proposal not found" });
|
|
596
|
+
if (!grant.some((g) => g.permission === "proposal.approve" && row.scope.includes(g.permission) && g.workspaceId === p.workspaceId)) {
|
|
597
|
+
return json(403, { error: "You may not approve changes here" });
|
|
598
|
+
}
|
|
599
|
+
if (p.status !== "pending") return json(409, { error: `The proposal is ${p.status}, not pending` });
|
|
600
|
+
p.status = "approved";
|
|
601
|
+
approvals.push(p.id);
|
|
602
|
+
return json(200, { success: true, proposalId: p.id, proposal: { ...p } });
|
|
603
|
+
}
|
|
604
|
+
const patched = method === "PATCH" ? /^\/entities\/([^/]+)$/.exec(path) : null;
|
|
605
|
+
if (patched) {
|
|
606
|
+
const target = entities.find((e) => e.id === decodeURIComponent(patched[1]));
|
|
607
|
+
if (target && target.workspaceId == null) return json(403, { error: "grant does not allow" });
|
|
608
|
+
}
|
|
340
609
|
if (method !== "GET") {
|
|
341
610
|
writes.push({ method, path, body });
|
|
342
611
|
const fields = body && typeof body === "object" ? body : {};
|
|
343
612
|
const workspaceId = typeof fields.workspaceId === "string" ? fields.workspaceId : null;
|
|
344
|
-
if (
|
|
613
|
+
if (writeMode === "proposed") {
|
|
345
614
|
const proposalId = `prop_mock_${++proposalSeq}`;
|
|
346
615
|
proposals.push({
|
|
347
616
|
id: proposalId,
|
|
@@ -416,6 +685,7 @@ function createMockAuthServer(options = {}) {
|
|
|
416
685
|
requests,
|
|
417
686
|
writes,
|
|
418
687
|
proposals,
|
|
688
|
+
approvals,
|
|
419
689
|
settleProposal(id, status) {
|
|
420
690
|
const p = proposals.find((x) => x.id === id);
|
|
421
691
|
if (p) p.status = status;
|
|
@@ -474,6 +744,9 @@ function createMockAuthServer(options = {}) {
|
|
|
474
744
|
for (const row of access.values()) row.revoked = true;
|
|
475
745
|
for (const row of refreshTokens.values()) row.revoked = true;
|
|
476
746
|
},
|
|
747
|
+
revokeAccessKeys() {
|
|
748
|
+
for (const row of access.values()) row.revoked = true;
|
|
749
|
+
},
|
|
477
750
|
liveRefreshTokens() {
|
|
478
751
|
return [...refreshTokens].filter(([, r]) => !r.used && !r.revoked).map(([t]) => t);
|
|
479
752
|
},
|
|
@@ -484,10 +757,70 @@ function createMockAuthServer(options = {}) {
|
|
|
484
757
|
};
|
|
485
758
|
}
|
|
486
759
|
|
|
760
|
+
// src/testing/seed.ts
|
|
761
|
+
var KEYS = ["entities", "sessions", "documents", "relations", "proposals", "spaces"];
|
|
762
|
+
var REQUIRED = {
|
|
763
|
+
entities: ["id"],
|
|
764
|
+
sessions: ["id"],
|
|
765
|
+
documents: ["id", "title", "content"],
|
|
766
|
+
relations: ["id", "sourceEntityId", "targetEntityId", "type"],
|
|
767
|
+
proposals: ["id"],
|
|
768
|
+
spaces: ["id", "name"]
|
|
769
|
+
};
|
|
770
|
+
function parseMockPodSeed(input) {
|
|
771
|
+
const value = typeof input === "string" ? JSON.parse(input) : input;
|
|
772
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError("seed: expected an object");
|
|
773
|
+
const obj = value;
|
|
774
|
+
for (const key of Object.keys(obj)) {
|
|
775
|
+
if (!KEYS.includes(key)) throw new TypeError(`seed.${key}: unknown collection (expected ${KEYS.join(", ")})`);
|
|
776
|
+
}
|
|
777
|
+
for (const key of KEYS) {
|
|
778
|
+
const rows = obj[key];
|
|
779
|
+
if (rows === void 0) continue;
|
|
780
|
+
if (!Array.isArray(rows)) throw new TypeError(`seed.${key}: expected an array`);
|
|
781
|
+
rows.forEach((row, i) => {
|
|
782
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) throw new TypeError(`seed.${key}[${i}]: expected an object`);
|
|
783
|
+
for (const field of REQUIRED[key]) {
|
|
784
|
+
if (typeof row[field] !== "string") throw new TypeError(`seed.${key}[${i}].${field}: expected a string`);
|
|
785
|
+
}
|
|
786
|
+
if (key === "entities") checkFacets(row.facets, `seed.entities[${i}].facets`);
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
obj.entities?.forEach((row, i) => {
|
|
790
|
+
const roles = row.seedRoles;
|
|
791
|
+
if (roles === void 0) return;
|
|
792
|
+
if (!Array.isArray(roles)) throw new TypeError(`seed.entities[${i}].seedRoles: expected an array`);
|
|
793
|
+
roles.forEach((r, j) => {
|
|
794
|
+
for (const field of ["profileSlug", "workspaceId"]) {
|
|
795
|
+
if (!r || typeof r !== "object" || typeof r[field] !== "string") {
|
|
796
|
+
throw new TypeError(`seed.entities[${i}].seedRoles[${j}].${field}: expected a string`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
});
|
|
801
|
+
const out = JSON.parse(JSON.stringify(obj));
|
|
802
|
+
if (out.entities) out.entities = out.entities.map((row, i) => desugarSeedRoles(row, `seed.entities[${i}]`));
|
|
803
|
+
return out;
|
|
804
|
+
}
|
|
805
|
+
function checkFacets(facets, at) {
|
|
806
|
+
if (facets === void 0) return;
|
|
807
|
+
if (!Array.isArray(facets)) throw new TypeError(`${at}: expected an array`);
|
|
808
|
+
facets.forEach((f, j) => {
|
|
809
|
+
if (!f || typeof f !== "object" || Array.isArray(f)) throw new TypeError(`${at}[${j}]: expected an object`);
|
|
810
|
+
for (const field of ["facetId", "slug", "workspaceId"]) {
|
|
811
|
+
if (typeof f[field] !== "string") throw new TypeError(`${at}[${j}].${field}: expected a string`);
|
|
812
|
+
}
|
|
813
|
+
const shared = f.sharedRole;
|
|
814
|
+
if (shared !== void 0 && typeof shared !== "boolean") throw new TypeError(`${at}[${j}].sharedRole: expected a boolean`);
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// src/connect/rules.ts
|
|
819
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
820
|
+
var CLOUD_STATE_RE = /^[A-Za-z0-9._~-]{16,256}$/;
|
|
821
|
+
|
|
487
822
|
// src/testing/cloud.ts
|
|
488
823
|
var MOCK_CLOUD_URL = "https://cp.mock-synap.test";
|
|
489
|
-
var STATE_RE = /^[A-Za-z0-9._~-]{16,256}$/;
|
|
490
|
-
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
491
824
|
function createMockCloud(options = {}) {
|
|
492
825
|
const base = (options.url ?? MOCK_CLOUD_URL).replace(/\/+$/, "");
|
|
493
826
|
const requests = [];
|
|
@@ -514,7 +847,7 @@ function createMockCloud(options = {}) {
|
|
|
514
847
|
const clientId = url.searchParams.get("client_id");
|
|
515
848
|
const state = url.searchParams.get("state");
|
|
516
849
|
if (!clientId || !cimdUrlAccepted(clientId)) throw new Error("Chooser refused: client_id must be an https CIMD URL");
|
|
517
|
-
if (!state || !
|
|
850
|
+
if (!state || !CLOUD_STATE_RE.test(state)) throw new Error("Chooser refused: state is missing or malformed");
|
|
518
851
|
if (clientId !== metadata.client_id) throw new Error("Chooser refused: the client metadata is not this client_id's");
|
|
519
852
|
return new URL(metadata.initiate_login_uri);
|
|
520
853
|
}
|
|
@@ -532,7 +865,7 @@ function createMockCloud(options = {}) {
|
|
|
532
865
|
|
|
533
866
|
// src/testing/index.ts
|
|
534
867
|
var MOCK_POD_URL = "https://mock.synap.invalid";
|
|
535
|
-
var MOCK_TOKENS = { form: "demo-form", share: "demo", site: "demo-site" };
|
|
868
|
+
var MOCK_TOKENS = { form: "demo-form", share: "demo", htmlShare: "demo-html", site: "demo-site" };
|
|
536
869
|
var MOCK_RETRY_AFTER_SEC = 30;
|
|
537
870
|
var MOCK_FAILURE_TOKENS = {
|
|
538
871
|
/** 429 with no `Retry-After` (what the pod's public doors send today). */
|
|
@@ -712,7 +1045,7 @@ function invalidKeys(form, body) {
|
|
|
712
1045
|
}
|
|
713
1046
|
function createMockPod(options = {}) {
|
|
714
1047
|
const forms = options.forms ?? { [MOCK_TOKENS.form]: MOCK_FORM };
|
|
715
|
-
const shares = options.shares ?? { [MOCK_TOKENS.share]: MOCK_SHARE };
|
|
1048
|
+
const shares = options.shares ?? { [MOCK_TOKENS.share]: MOCK_SHARE, [MOCK_TOKENS.htmlShare]: MOCK_HTML_SHARE };
|
|
716
1049
|
const sites = options.sites ?? { [MOCK_TOKENS.site]: MOCK_SITE };
|
|
717
1050
|
const requests = [];
|
|
718
1051
|
const submissions = [];
|
|
@@ -785,6 +1118,7 @@ function createMockPod(options = {}) {
|
|
|
785
1118
|
MOCK_FAILURE_TOKENS,
|
|
786
1119
|
MOCK_FORM,
|
|
787
1120
|
MOCK_GRANT,
|
|
1121
|
+
MOCK_HTML_SHARE,
|
|
788
1122
|
MOCK_POD_URL,
|
|
789
1123
|
MOCK_RETRY_AFTER_SEC,
|
|
790
1124
|
MOCK_SHARE,
|
|
@@ -793,6 +1127,7 @@ function createMockPod(options = {}) {
|
|
|
793
1127
|
MOCK_USER,
|
|
794
1128
|
createMockAuthServer,
|
|
795
1129
|
createMockCloud,
|
|
796
|
-
createMockPod
|
|
1130
|
+
createMockPod,
|
|
1131
|
+
parseMockPodSeed
|
|
797
1132
|
});
|
|
798
1133
|
//# sourceMappingURL=testing.cjs.map
|