@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.
- package/CHANGELOG.md +133 -0
- package/README.md +212 -12
- package/dist/{chunk-J7XHAAFJ.js → chunk-UK2U6SMA.js} +14 -2
- package/dist/chunk-UK2U6SMA.js.map +1 -0
- package/dist/{client-CVUT1Mcf.d.cts → client-B50KC2aJ.d.cts} +14 -9
- package/dist/{client-CVUT1Mcf.d.ts → client-B50KC2aJ.d.ts} +14 -9
- package/dist/connect.cjs +790 -161
- package/dist/connect.cjs.map +1 -1
- package/dist/connect.d.cts +610 -37
- package/dist/connect.d.ts +610 -37
- package/dist/connect.js +777 -162
- 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 +366 -17
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +190 -7
- package/dist/testing.d.ts +190 -7
- package/dist/testing.js +358 -17
- package/dist/testing.js.map +1 -1
- package/dist/{types-DpqPNaaa.d.cts → types-DZYNBCq7.d.cts} +47 -5
- package/dist/{types-DpqPNaaa.d.ts → types-DZYNBCq7.d.ts} +47 -5
- package/package.json +36 -12
- package/src/connect/auth.ts +314 -55
- package/src/connect/client.ts +472 -26
- package/src/connect/cloud.ts +210 -0
- package/src/connect/discovery.ts +79 -44
- package/src/connect/encoding.ts +1 -1
- package/src/connect/errors.ts +8 -1
- package/src/connect/flow.ts +80 -12
- package/src/connect/http.ts +42 -0
- package/src/connect/hub-types.ts +254 -0
- package/src/connect/index.ts +69 -23
- package/src/connect/rules.ts +18 -0
- package/src/connect/seal.ts +1 -1
- package/src/connect/store.ts +50 -8
- package/src/connect/types.ts +48 -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 +377 -20
- package/src/testing/cloud.ts +83 -0
- package/src/testing/fixtures.ts +26 -0
- package/src/testing/index.ts +10 -6
- package/src/testing/seed.ts +120 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
223
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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")
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1016
|
+
createMockCloud,
|
|
1017
|
+
createMockPod,
|
|
1018
|
+
parseMockPodSeed
|
|
670
1019
|
});
|
|
671
1020
|
//# sourceMappingURL=testing.cjs.map
|