@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.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-
|
|
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
|
}
|
|
@@ -98,6 +120,20 @@ var MOCK_SITE = {
|
|
|
98
120
|
}
|
|
99
121
|
};
|
|
100
122
|
|
|
123
|
+
// src/testing/roles.ts
|
|
124
|
+
function desugarSeedRoles(row, at) {
|
|
125
|
+
if (!Array.isArray(row.seedRoles)) return row;
|
|
126
|
+
const { seedRoles, ...rest } = row;
|
|
127
|
+
const facets = Array.isArray(row.facets) ? row.facets : [];
|
|
128
|
+
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 }));
|
|
129
|
+
for (const r of shorthand) {
|
|
130
|
+
if (facets.some((f) => f?.slug === r.slug && f.workspaceId === r.workspaceId)) {
|
|
131
|
+
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`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return { ...rest, facets: [...facets, ...shorthand] };
|
|
135
|
+
}
|
|
136
|
+
|
|
101
137
|
// src/testing/auth-server.ts
|
|
102
138
|
var MOCK_AUTH_ISSUER = "https://pod.mock-synap.test";
|
|
103
139
|
var MOCK_GRANT = [
|
|
@@ -130,7 +166,43 @@ function createMockAuthServer(options = {}) {
|
|
|
130
166
|
const requests = [];
|
|
131
167
|
const writes = [];
|
|
132
168
|
const proposals = [];
|
|
133
|
-
const
|
|
169
|
+
const seed = options.seed ?? {};
|
|
170
|
+
const entities = [...seed.entities ?? [], ...options.entities ?? []].map(
|
|
171
|
+
(e, i) => desugarSeedRoles(e, `entities[${i}]`)
|
|
172
|
+
);
|
|
173
|
+
const sessions = [...seed.sessions ?? []];
|
|
174
|
+
const documents = [...seed.documents ?? []];
|
|
175
|
+
const relations = [...seed.relations ?? []];
|
|
176
|
+
const templateOf = new Map((seed.spaces ?? []).filter((sp) => sp.templateSlug).map((sp) => [sp.id, sp.templateSlug]));
|
|
177
|
+
const approvals = [];
|
|
178
|
+
const seededAt = new Date(now()).toISOString();
|
|
179
|
+
const rolesOf = (e) => Array.isArray(e.facets) ? e.facets.filter((f) => typeof f?.slug === "string") : [];
|
|
180
|
+
function readableEntity(e, spaces, only) {
|
|
181
|
+
if (typeof e.workspaceId === "string" && spaces.has(e.workspaceId) && (!only || e.workspaceId === only)) return true;
|
|
182
|
+
return facetsOf(e, spaces, only ?? void 0).some((f) => e.workspaceId == null || !f.sharedRole);
|
|
183
|
+
}
|
|
184
|
+
const facetsOf = (e, spaces, lens) => rolesOf(e).filter((f) => typeof f.workspaceId === "string" && spaces.has(f.workspaceId) && (lens === void 0 || f.workspaceId === lens));
|
|
185
|
+
const facetRow = (f) => ({ facetId: f.facetId, slug: f.slug, properties: f.properties ?? {}, status: f.status ?? null });
|
|
186
|
+
const entityWire = (e) => {
|
|
187
|
+
const { facets: _facets, facetSlugs: _facetSlugs, ...wire } = e;
|
|
188
|
+
return wire;
|
|
189
|
+
};
|
|
190
|
+
const readSpaces = (scope) => new Set(grant.filter((g) => /^entity\.(.+\.)?read$/.test(g.permission) && scope.includes(g.permission)).map((g) => g.workspaceId));
|
|
191
|
+
for (const p of seed.proposals ?? []) {
|
|
192
|
+
proposals.push({
|
|
193
|
+
proposalType: "create",
|
|
194
|
+
targetType: "entity",
|
|
195
|
+
targetId: null,
|
|
196
|
+
status: "pending",
|
|
197
|
+
workspaceId: null,
|
|
198
|
+
createdAt: seededAt,
|
|
199
|
+
correlationId: null,
|
|
200
|
+
sessionId: null,
|
|
201
|
+
agentUserId: null,
|
|
202
|
+
...p
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
const writeMode = options.writeMode ?? (options.actsAs === "person" ? "applied" : "proposed");
|
|
134
206
|
const graceMs = (options.refreshGraceSec ?? 30) * 1e3;
|
|
135
207
|
const app = options.app === void 0 ? MOCK_APP : options.app;
|
|
136
208
|
let proposalSeq = 0;
|
|
@@ -172,9 +244,11 @@ function createMockAuthServer(options = {}) {
|
|
|
172
244
|
// As the pod: what the key has left, never an assumed hour.
|
|
173
245
|
expires_in: Math.max(0, Math.floor((row.expiresAt - now()) / 1e3)),
|
|
174
246
|
scope: scope.join(" "),
|
|
175
|
-
|
|
247
|
+
// Each grant row names its space's template when it came from one.
|
|
248
|
+
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
249
|
synap_user: user,
|
|
177
|
-
...app ? { synap_app: app } : {}
|
|
250
|
+
...app ? { synap_app: app } : {},
|
|
251
|
+
...options.actsAs ? { synap_acts_as: options.actsAs } : {}
|
|
178
252
|
};
|
|
179
253
|
if (pair) {
|
|
180
254
|
body.refresh_token = pair.refreshToken;
|
|
@@ -237,6 +311,160 @@ function createMockAuthServer(options = {}) {
|
|
|
237
311
|
}
|
|
238
312
|
return oauthError("unsupported_grant_type", "Unsupported grant_type");
|
|
239
313
|
}
|
|
314
|
+
const sessionRow = (s) => ({
|
|
315
|
+
workspaceId: null,
|
|
316
|
+
projectId: null,
|
|
317
|
+
userId: user.id,
|
|
318
|
+
correlationId: null,
|
|
319
|
+
title: null,
|
|
320
|
+
goal: "",
|
|
321
|
+
status: "active",
|
|
322
|
+
templateId: null,
|
|
323
|
+
expectedOutputs: [],
|
|
324
|
+
channelId: null,
|
|
325
|
+
progress: null,
|
|
326
|
+
currentStage: null,
|
|
327
|
+
agentIds: [],
|
|
328
|
+
closedAt: null,
|
|
329
|
+
verificationReport: null,
|
|
330
|
+
metadata: {},
|
|
331
|
+
startedAt: seededAt,
|
|
332
|
+
createdAt: seededAt,
|
|
333
|
+
updatedAt: seededAt,
|
|
334
|
+
kind: "work",
|
|
335
|
+
triage: { pending: false, acceptedAt: null, acceptedBy: null },
|
|
336
|
+
...s
|
|
337
|
+
});
|
|
338
|
+
function readSeeded(path, query, scope) {
|
|
339
|
+
const spaces = readSpaces(scope);
|
|
340
|
+
const conn = /^\/entities\/([^/]+)\/connections$/.exec(path);
|
|
341
|
+
if (conn) {
|
|
342
|
+
const id = decodeURIComponent(conn[1]);
|
|
343
|
+
const ws = query.get("workspaceId") ?? void 0;
|
|
344
|
+
const limit = Math.min(Math.max(Number(query.get("limit") ?? 50) || 50, 1), 200);
|
|
345
|
+
const inLens = (e) => !ws || e.workspaceId === ws || e.workspaceId == null;
|
|
346
|
+
const connections = [];
|
|
347
|
+
const graph = relations.filter((r) => r.sourceEntityId === id || r.targetEntityId === id).sort((a, b) => String(b.createdAt ?? "").localeCompare(String(a.createdAt ?? ""))).slice(0, limit);
|
|
348
|
+
for (const r of graph) {
|
|
349
|
+
const outgoing = r.sourceEntityId === id;
|
|
350
|
+
const otherId = outgoing ? r.targetEntityId : r.sourceEntityId;
|
|
351
|
+
const other = entities.find((e) => e.id === otherId);
|
|
352
|
+
if (!other || !inLens(other) || !readableEntity(other, spaces)) continue;
|
|
353
|
+
connections.push({
|
|
354
|
+
entityId: otherId,
|
|
355
|
+
// relations.ts: the raw row plus its role slugs under the read's facet lens (a 1.5.0 seed's own facetSlugs otherwise).
|
|
356
|
+
entity: {
|
|
357
|
+
...entityWire(other),
|
|
358
|
+
type: other.profileSlug ?? other.type ?? null,
|
|
359
|
+
facetSlugs: rolesOf(other).length > 0 ? facetsOf(other, spaces, ws).map((f) => f.slug) : other.facetSlugs ?? []
|
|
360
|
+
},
|
|
361
|
+
label: r.type,
|
|
362
|
+
direction: outgoing ? "outgoing" : "incoming",
|
|
363
|
+
source: "graph",
|
|
364
|
+
relationId: r.id,
|
|
365
|
+
relationType: r.type,
|
|
366
|
+
createdAt: r.createdAt ?? null
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
for (const s of sessions.filter((x) => x.subjectEntityId === id).slice(0, limit)) {
|
|
370
|
+
const row = sessionRow(s);
|
|
371
|
+
connections.push({
|
|
372
|
+
entityId: s.id,
|
|
373
|
+
entity: null,
|
|
374
|
+
label: row.title || row.goal,
|
|
375
|
+
direction: "incoming",
|
|
376
|
+
source: "focus_session",
|
|
377
|
+
focusSessionId: s.id,
|
|
378
|
+
focusSessionGoal: row.goal,
|
|
379
|
+
focusSessionStatus: row.status,
|
|
380
|
+
focusSessionWorkspaceId: row.workspaceId,
|
|
381
|
+
createdAt: row.createdAt
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
const n = (source) => connections.filter((c) => c.source === source).length;
|
|
385
|
+
return json(200, {
|
|
386
|
+
connections,
|
|
387
|
+
counts: { total: connections.length, graph: n("graph"), structural: n("property"), threads: n("thread"), contextChannels: n("context_channel"), focusSessions: n("focus_session") }
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
const entity = /^\/entities\/([^/]+)$/.exec(path);
|
|
391
|
+
if (entity) {
|
|
392
|
+
const found = entities.find((e) => e.id === decodeURIComponent(entity[1]));
|
|
393
|
+
return found && readableEntity(found, spaces) ? json(200, entityWire(found)) : new Response(null, { status: 404 });
|
|
394
|
+
}
|
|
395
|
+
const roles = /^\/entities\/([^/]+)\/facets$/.exec(path);
|
|
396
|
+
if (roles) {
|
|
397
|
+
const found = entities.find((e) => e.id === decodeURIComponent(roles[1]));
|
|
398
|
+
if (!found || !readableEntity(found, spaces)) return new Response(null, { status: 404 });
|
|
399
|
+
return json(200, {
|
|
400
|
+
facets: facetsOf(found, spaces).map((f) => ({
|
|
401
|
+
facet: {
|
|
402
|
+
id: f.facetId,
|
|
403
|
+
entityId: found.id,
|
|
404
|
+
profileId: `profile_${f.slug}`,
|
|
405
|
+
userId: found.userId ?? user.id,
|
|
406
|
+
// A stamped facet answers its space; a shared role's facet is stored pod-wide.
|
|
407
|
+
workspaceId: f.sharedRole ? null : f.workspaceId,
|
|
408
|
+
contextEntityId: null,
|
|
409
|
+
status: f.status ?? null,
|
|
410
|
+
statusCategory: null,
|
|
411
|
+
properties: f.properties ?? {},
|
|
412
|
+
metadata: {},
|
|
413
|
+
createdByKind: null,
|
|
414
|
+
createdByUserId: null,
|
|
415
|
+
agentUserId: null,
|
|
416
|
+
sourceProposalId: null,
|
|
417
|
+
correlationId: null,
|
|
418
|
+
createdAt: seededAt,
|
|
419
|
+
updatedAt: seededAt,
|
|
420
|
+
deletedAt: null
|
|
421
|
+
},
|
|
422
|
+
profile: { id: `profile_${f.slug}`, slug: f.slug, profileKind: "role" },
|
|
423
|
+
effectiveProperties: []
|
|
424
|
+
}))
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
const doc = /^\/documents\/([^/]+)$/.exec(path);
|
|
428
|
+
if (doc) {
|
|
429
|
+
const format = query.get("format") ?? "raw";
|
|
430
|
+
if (format !== "raw" && format !== "readable") return json(400, { error: "format must be raw or readable" });
|
|
431
|
+
const d = documents.find((x) => x.id === decodeURIComponent(doc[1]));
|
|
432
|
+
if (!d) return json(404, { error: "Document not found" });
|
|
433
|
+
return json(200, {
|
|
434
|
+
document: {
|
|
435
|
+
id: d.id,
|
|
436
|
+
title: d.title,
|
|
437
|
+
type: d.type ?? "markdown",
|
|
438
|
+
language: d.language ?? null,
|
|
439
|
+
workspaceId: d.workspaceId ?? null,
|
|
440
|
+
format,
|
|
441
|
+
content: d.content,
|
|
442
|
+
revision: d.revision ?? 1,
|
|
443
|
+
version: d.version ?? 1,
|
|
444
|
+
sections: [],
|
|
445
|
+
diagnostics: [],
|
|
446
|
+
updatedAt: d.updatedAt ?? seededAt,
|
|
447
|
+
createdAt: d.createdAt ?? seededAt
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
if (path === "/focus-sessions") {
|
|
452
|
+
const ws = query.get("workspaceId");
|
|
453
|
+
const project = query.get("projectId");
|
|
454
|
+
if (!ws && !project) return json(400, { error: "workspaceId or projectId is required" });
|
|
455
|
+
const status = query.get("status") ?? "all";
|
|
456
|
+
const lens = query.get("lens") ?? "all";
|
|
457
|
+
const kind = query.get("kind") ?? "all";
|
|
458
|
+
const subject = query.get("subjectEntityId");
|
|
459
|
+
const limit = Math.min(Math.max(Number(query.get("limit") ?? 20) || 20, 1), 50);
|
|
460
|
+
const rows = sessions.map(sessionRow).filter((r) => (!ws || r.workspaceId === ws) && (!project || r.projectId === project)).filter((r) => status === "all" || r.status === status).filter((r) => {
|
|
461
|
+
const pending = r.triage?.pending === true;
|
|
462
|
+
return lens === "triage" ? pending : lens === "default" ? !pending : true;
|
|
463
|
+
}).filter((r) => kind === "all" || r.kind === kind).filter((r) => !subject || r.subjectEntityId === subject).sort((a, b) => String(b.startedAt).localeCompare(String(a.startedAt)));
|
|
464
|
+
return json(200, rows.slice(0, limit));
|
|
465
|
+
}
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
240
468
|
function hub(method, path, query, headers, body) {
|
|
241
469
|
const failure2 = options.failures?.[path];
|
|
242
470
|
if (failure2 === "unreachable") return "reject";
|
|
@@ -260,12 +488,27 @@ function createMockAuthServer(options = {}) {
|
|
|
260
488
|
}
|
|
261
489
|
if (method === "GET" && path === "/users/me") return json(200, user);
|
|
262
490
|
if (method === "GET" && path === "/entities") {
|
|
263
|
-
const ws = query.get("workspaceId");
|
|
491
|
+
const ws = query.get("workspaceId") || void 0;
|
|
264
492
|
const kind = query.get("profileSlug");
|
|
493
|
+
const role = query.get("facetSlug");
|
|
494
|
+
const podWide = query.get("includePodWide") !== "false";
|
|
495
|
+
const withFacets = query.get("includeFacets") === "true";
|
|
265
496
|
const offset = Number(query.get("offset") ?? 0) || 0;
|
|
266
497
|
const limit = Number(query.get("limit") ?? 50) || 50;
|
|
267
|
-
const
|
|
268
|
-
|
|
498
|
+
const spaces = readSpaces(row.scope);
|
|
499
|
+
const rows = entities.filter((e) => {
|
|
500
|
+
if (!readableEntity(e, spaces, ws)) return false;
|
|
501
|
+
if (ws && !podWide && e.workspaceId !== ws && !facetsOf(e, spaces, ws).some((f) => !f.sharedRole)) return false;
|
|
502
|
+
const slugs = facetsOf(e, spaces, ws).map((f) => f.slug);
|
|
503
|
+
return (!kind || e.profileSlug === kind || slugs.includes(kind)) && (!role || slugs.includes(role));
|
|
504
|
+
});
|
|
505
|
+
return json(
|
|
506
|
+
200,
|
|
507
|
+
rows.slice(offset, offset + limit).map((e) => {
|
|
508
|
+
const facets = withFacets ? facetsOf(e, spaces, ws).map(facetRow) : [];
|
|
509
|
+
return facets.length > 0 ? { ...entityWire(e), facetSlugs: facets.map((f) => f.slug), facets } : entityWire(e);
|
|
510
|
+
})
|
|
511
|
+
);
|
|
269
512
|
}
|
|
270
513
|
if (method === "GET" && path === "/proposals") {
|
|
271
514
|
const readable = new Set(grant.filter((g) => g.permission === "proposal.read" && row.scope.includes(g.permission)).map((g) => g.workspaceId));
|
|
@@ -273,15 +516,41 @@ function createMockAuthServer(options = {}) {
|
|
|
273
516
|
const ws = query.get("workspaceId");
|
|
274
517
|
const limit = Math.min(Math.max(Number(query.get("limit") ?? 50) || 50, 1), 200);
|
|
275
518
|
const offset = Number(query.get("offset") ?? 0) || 0;
|
|
276
|
-
const
|
|
519
|
+
const idsRaw = query.getAll("proposalIds");
|
|
520
|
+
const ids = idsRaw.length > 0 ? new Set(idsRaw.flatMap((v) => v.split(",").map((x) => x.trim()).filter(Boolean))) : null;
|
|
521
|
+
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
522
|
const page = rows.slice(offset, offset + limit);
|
|
278
523
|
return json(200, { proposals: page, limit, offset, total: rows.length, hasMore: offset + page.length < rows.length });
|
|
279
524
|
}
|
|
525
|
+
if (method === "GET") {
|
|
526
|
+
const read = readSeeded(path, query, row.scope);
|
|
527
|
+
if (read) return read;
|
|
528
|
+
}
|
|
529
|
+
const approve = method === "POST" ? /^\/proposals\/([^/]+)\/approve$/.exec(path) : null;
|
|
530
|
+
if (approve) {
|
|
531
|
+
if (options.actsAs !== "person") {
|
|
532
|
+
return json(403, { error: "An agent credential cannot approve proposals \u2014 approve is human review. Approve from a human session." });
|
|
533
|
+
}
|
|
534
|
+
const p = proposals.find((x) => x.id === decodeURIComponent(approve[1]));
|
|
535
|
+
if (!p) return json(404, { error: "Proposal not found" });
|
|
536
|
+
if (!grant.some((g) => g.permission === "proposal.approve" && row.scope.includes(g.permission) && g.workspaceId === p.workspaceId)) {
|
|
537
|
+
return json(403, { error: "You may not approve changes here" });
|
|
538
|
+
}
|
|
539
|
+
if (p.status !== "pending") return json(409, { error: `The proposal is ${p.status}, not pending` });
|
|
540
|
+
p.status = "approved";
|
|
541
|
+
approvals.push(p.id);
|
|
542
|
+
return json(200, { success: true, proposalId: p.id, proposal: { ...p } });
|
|
543
|
+
}
|
|
544
|
+
const patched = method === "PATCH" ? /^\/entities\/([^/]+)$/.exec(path) : null;
|
|
545
|
+
if (patched) {
|
|
546
|
+
const target = entities.find((e) => e.id === decodeURIComponent(patched[1]));
|
|
547
|
+
if (target && target.workspaceId == null) return json(403, { error: "grant does not allow" });
|
|
548
|
+
}
|
|
280
549
|
if (method !== "GET") {
|
|
281
550
|
writes.push({ method, path, body });
|
|
282
551
|
const fields = body && typeof body === "object" ? body : {};
|
|
283
552
|
const workspaceId = typeof fields.workspaceId === "string" ? fields.workspaceId : null;
|
|
284
|
-
if (
|
|
553
|
+
if (writeMode === "proposed") {
|
|
285
554
|
const proposalId = `prop_mock_${++proposalSeq}`;
|
|
286
555
|
proposals.push({
|
|
287
556
|
id: proposalId,
|
|
@@ -356,6 +625,7 @@ function createMockAuthServer(options = {}) {
|
|
|
356
625
|
requests,
|
|
357
626
|
writes,
|
|
358
627
|
proposals,
|
|
628
|
+
approvals,
|
|
359
629
|
settleProposal(id, status) {
|
|
360
630
|
const p = proposals.find((x) => x.id === id);
|
|
361
631
|
if (p) p.status = status;
|
|
@@ -414,6 +684,9 @@ function createMockAuthServer(options = {}) {
|
|
|
414
684
|
for (const row of access.values()) row.revoked = true;
|
|
415
685
|
for (const row of refreshTokens.values()) row.revoked = true;
|
|
416
686
|
},
|
|
687
|
+
revokeAccessKeys() {
|
|
688
|
+
for (const row of access.values()) row.revoked = true;
|
|
689
|
+
},
|
|
417
690
|
liveRefreshTokens() {
|
|
418
691
|
return [...refreshTokens].filter(([, r]) => !r.used && !r.revoked).map(([t]) => t);
|
|
419
692
|
},
|
|
@@ -424,10 +697,66 @@ function createMockAuthServer(options = {}) {
|
|
|
424
697
|
};
|
|
425
698
|
}
|
|
426
699
|
|
|
700
|
+
// src/testing/seed.ts
|
|
701
|
+
var KEYS = ["entities", "sessions", "documents", "relations", "proposals", "spaces"];
|
|
702
|
+
var REQUIRED = {
|
|
703
|
+
entities: ["id"],
|
|
704
|
+
sessions: ["id"],
|
|
705
|
+
documents: ["id", "title", "content"],
|
|
706
|
+
relations: ["id", "sourceEntityId", "targetEntityId", "type"],
|
|
707
|
+
proposals: ["id"],
|
|
708
|
+
spaces: ["id", "name"]
|
|
709
|
+
};
|
|
710
|
+
function parseMockPodSeed(input) {
|
|
711
|
+
const value = typeof input === "string" ? JSON.parse(input) : input;
|
|
712
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError("seed: expected an object");
|
|
713
|
+
const obj = value;
|
|
714
|
+
for (const key of Object.keys(obj)) {
|
|
715
|
+
if (!KEYS.includes(key)) throw new TypeError(`seed.${key}: unknown collection (expected ${KEYS.join(", ")})`);
|
|
716
|
+
}
|
|
717
|
+
for (const key of KEYS) {
|
|
718
|
+
const rows = obj[key];
|
|
719
|
+
if (rows === void 0) continue;
|
|
720
|
+
if (!Array.isArray(rows)) throw new TypeError(`seed.${key}: expected an array`);
|
|
721
|
+
rows.forEach((row, i) => {
|
|
722
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) throw new TypeError(`seed.${key}[${i}]: expected an object`);
|
|
723
|
+
for (const field of REQUIRED[key]) {
|
|
724
|
+
if (typeof row[field] !== "string") throw new TypeError(`seed.${key}[${i}].${field}: expected a string`);
|
|
725
|
+
}
|
|
726
|
+
if (key === "entities") checkFacets(row.facets, `seed.entities[${i}].facets`);
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
obj.entities?.forEach((row, i) => {
|
|
730
|
+
const roles = row.seedRoles;
|
|
731
|
+
if (roles === void 0) return;
|
|
732
|
+
if (!Array.isArray(roles)) throw new TypeError(`seed.entities[${i}].seedRoles: expected an array`);
|
|
733
|
+
roles.forEach((r, j) => {
|
|
734
|
+
for (const field of ["profileSlug", "workspaceId"]) {
|
|
735
|
+
if (!r || typeof r !== "object" || typeof r[field] !== "string") {
|
|
736
|
+
throw new TypeError(`seed.entities[${i}].seedRoles[${j}].${field}: expected a string`);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
|
+
const out = JSON.parse(JSON.stringify(obj));
|
|
742
|
+
if (out.entities) out.entities = out.entities.map((row, i) => desugarSeedRoles(row, `seed.entities[${i}]`));
|
|
743
|
+
return out;
|
|
744
|
+
}
|
|
745
|
+
function checkFacets(facets, at) {
|
|
746
|
+
if (facets === void 0) return;
|
|
747
|
+
if (!Array.isArray(facets)) throw new TypeError(`${at}: expected an array`);
|
|
748
|
+
facets.forEach((f, j) => {
|
|
749
|
+
if (!f || typeof f !== "object" || Array.isArray(f)) throw new TypeError(`${at}[${j}]: expected an object`);
|
|
750
|
+
for (const field of ["facetId", "slug", "workspaceId"]) {
|
|
751
|
+
if (typeof f[field] !== "string") throw new TypeError(`${at}[${j}].${field}: expected a string`);
|
|
752
|
+
}
|
|
753
|
+
const shared = f.sharedRole;
|
|
754
|
+
if (shared !== void 0 && typeof shared !== "boolean") throw new TypeError(`${at}[${j}].sharedRole: expected a boolean`);
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
|
|
427
758
|
// src/testing/cloud.ts
|
|
428
759
|
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
760
|
function createMockCloud(options = {}) {
|
|
432
761
|
const base = (options.url ?? MOCK_CLOUD_URL).replace(/\/+$/, "");
|
|
433
762
|
const requests = [];
|
|
@@ -454,7 +783,7 @@ function createMockCloud(options = {}) {
|
|
|
454
783
|
const clientId = url.searchParams.get("client_id");
|
|
455
784
|
const state = url.searchParams.get("state");
|
|
456
785
|
if (!clientId || !cimdUrlAccepted(clientId)) throw new Error("Chooser refused: client_id must be an https CIMD URL");
|
|
457
|
-
if (!state || !
|
|
786
|
+
if (!state || !CLOUD_STATE_RE.test(state)) throw new Error("Chooser refused: state is missing or malformed");
|
|
458
787
|
if (clientId !== metadata.client_id) throw new Error("Chooser refused: the client metadata is not this client_id's");
|
|
459
788
|
return new URL(metadata.initiate_login_uri);
|
|
460
789
|
}
|
|
@@ -472,7 +801,7 @@ function createMockCloud(options = {}) {
|
|
|
472
801
|
|
|
473
802
|
// src/testing/index.ts
|
|
474
803
|
var MOCK_POD_URL = "https://mock.synap.invalid";
|
|
475
|
-
var MOCK_TOKENS = { form: "demo-form", share: "demo", site: "demo-site" };
|
|
804
|
+
var MOCK_TOKENS = { form: "demo-form", share: "demo", htmlShare: "demo-html", site: "demo-site" };
|
|
476
805
|
var MOCK_RETRY_AFTER_SEC = 30;
|
|
477
806
|
var MOCK_FAILURE_TOKENS = {
|
|
478
807
|
/** 429 with no `Retry-After` (what the pod's public doors send today). */
|
|
@@ -652,7 +981,7 @@ function invalidKeys(form, body) {
|
|
|
652
981
|
}
|
|
653
982
|
function createMockPod(options = {}) {
|
|
654
983
|
const forms = options.forms ?? { [MOCK_TOKENS.form]: MOCK_FORM };
|
|
655
|
-
const shares = options.shares ?? { [MOCK_TOKENS.share]: MOCK_SHARE };
|
|
984
|
+
const shares = options.shares ?? { [MOCK_TOKENS.share]: MOCK_SHARE, [MOCK_TOKENS.htmlShare]: MOCK_HTML_SHARE };
|
|
656
985
|
const sites = options.sites ?? { [MOCK_TOKENS.site]: MOCK_SITE };
|
|
657
986
|
const requests = [];
|
|
658
987
|
const submissions = [];
|
|
@@ -724,6 +1053,7 @@ export {
|
|
|
724
1053
|
MOCK_FAILURE_TOKENS,
|
|
725
1054
|
MOCK_FORM,
|
|
726
1055
|
MOCK_GRANT,
|
|
1056
|
+
MOCK_HTML_SHARE,
|
|
727
1057
|
MOCK_POD_URL,
|
|
728
1058
|
MOCK_RETRY_AFTER_SEC,
|
|
729
1059
|
MOCK_SHARE,
|
|
@@ -732,6 +1062,7 @@ export {
|
|
|
732
1062
|
MOCK_USER,
|
|
733
1063
|
createMockAuthServer,
|
|
734
1064
|
createMockCloud,
|
|
735
|
-
createMockPod
|
|
1065
|
+
createMockPod,
|
|
1066
|
+
parseMockPodSeed
|
|
736
1067
|
};
|
|
737
1068
|
//# sourceMappingURL=testing.js.map
|