@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.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
|
}
|
|
@@ -105,6 +127,8 @@ var MOCK_GRANT = [
|
|
|
105
127
|
{ permission: "entity.post.create", workspaceId: "ws_mock_studio", workspaceName: "Studio" }
|
|
106
128
|
];
|
|
107
129
|
var MOCK_USER = { id: "user_mock_ada", name: "Ada Lovelace" };
|
|
130
|
+
var MOCK_APP = { id: "app_mock_studio", manageUrl: "https://pod-admin.mock-synap.test/apps/app_mock_studio" };
|
|
131
|
+
var MOCK_APP_AGENT_ID = "agent_mock_app";
|
|
108
132
|
function json(status, body, headers = {}) {
|
|
109
133
|
return new Response(JSON.stringify(body), {
|
|
110
134
|
status,
|
|
@@ -127,7 +151,34 @@ function createMockAuthServer(options = {}) {
|
|
|
127
151
|
const refreshTokens = /* @__PURE__ */ new Map();
|
|
128
152
|
const requests = [];
|
|
129
153
|
const writes = [];
|
|
154
|
+
const proposals = [];
|
|
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");
|
|
178
|
+
const graceMs = (options.refreshGraceSec ?? 30) * 1e3;
|
|
179
|
+
const app = options.app === void 0 ? MOCK_APP : options.app;
|
|
130
180
|
let proposalSeq = 0;
|
|
181
|
+
let graceAnswers = 0;
|
|
131
182
|
const metadata = () => {
|
|
132
183
|
const base = {
|
|
133
184
|
issuer,
|
|
@@ -155,18 +206,26 @@ function createMockAuthServer(options = {}) {
|
|
|
155
206
|
for (const r of refreshTokens.values()) if (r.family === family) r.revoked = true;
|
|
156
207
|
for (const a of access.values()) if (a.family === family) a.revoked = true;
|
|
157
208
|
};
|
|
158
|
-
const tokenBody = (scope, clientId, family) => {
|
|
159
|
-
const accessToken = `synap_hub_mock_${randomToken()}`;
|
|
160
|
-
access.set(accessToken, { expiresAt: now() + accessTtl * 1e3, revoked: false, family: refresh ? family : null });
|
|
209
|
+
const tokenBody = (scope, clientId, family, pair) => {
|
|
210
|
+
const accessToken = pair?.accessToken ?? `synap_hub_mock_${randomToken()}`;
|
|
211
|
+
if (!pair) access.set(accessToken, { expiresAt: now() + accessTtl * 1e3, revoked: false, family: refresh ? family : null, scope });
|
|
212
|
+
const row = access.get(accessToken);
|
|
161
213
|
const body = {
|
|
162
214
|
access_token: accessToken,
|
|
163
215
|
token_type: "Bearer",
|
|
164
|
-
|
|
216
|
+
// As the pod: what the key has left, never an assumed hour.
|
|
217
|
+
expires_in: Math.max(0, Math.floor((row.expiresAt - now()) / 1e3)),
|
|
165
218
|
scope: scope.join(" "),
|
|
166
|
-
|
|
167
|
-
|
|
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) }),
|
|
221
|
+
synap_user: user,
|
|
222
|
+
...app ? { synap_app: app } : {},
|
|
223
|
+
...options.actsAs ? { synap_acts_as: options.actsAs } : {}
|
|
168
224
|
};
|
|
169
|
-
if (
|
|
225
|
+
if (pair) {
|
|
226
|
+
body.refresh_token = pair.refreshToken;
|
|
227
|
+
if (options.advertiseRefreshExpiry) body.refresh_token_expires_in = Math.floor((refreshTokens.get(pair.refreshToken).idleUntil - now()) / 1e3);
|
|
228
|
+
} else if (refresh) {
|
|
170
229
|
const refreshToken = randomToken();
|
|
171
230
|
refreshTokens.set(refreshToken, { clientId, family, idleUntil: now() + refreshIdle * 1e3, used: false, revoked: false, scope });
|
|
172
231
|
body.refresh_token = refreshToken;
|
|
@@ -198,6 +257,13 @@ function createMockAuthServer(options = {}) {
|
|
|
198
257
|
return oauthError("invalid_grant", "Invalid, expired, revoked or already-used refresh token");
|
|
199
258
|
}
|
|
200
259
|
if (row.used) {
|
|
260
|
+
const successor = row.successor ? refreshTokens.get(row.successor) : void 0;
|
|
261
|
+
const key = row.pairAccess ? access.get(row.pairAccess) : void 0;
|
|
262
|
+
const inGrace = graceMs > 0 && row.usedAt !== void 0 && now() - row.usedAt < graceMs;
|
|
263
|
+
if (inGrace && successor && !successor.used && !successor.revoked && key && !key.revoked && key.expiresAt > now()) {
|
|
264
|
+
graceAnswers++;
|
|
265
|
+
return json(200, tokenBody(row.scope, row.clientId, row.family, { accessToken: row.pairAccess, refreshToken: row.successor }));
|
|
266
|
+
}
|
|
201
267
|
reuses++;
|
|
202
268
|
revokeFamily(row.family);
|
|
203
269
|
return oauthError("invalid_grant", "Invalid, expired, revoked or already-used refresh token");
|
|
@@ -208,11 +274,132 @@ function createMockAuthServer(options = {}) {
|
|
|
208
274
|
return oauthError("invalid_grant", "Invalid, expired, revoked or already-used refresh token");
|
|
209
275
|
}
|
|
210
276
|
row.used = true;
|
|
211
|
-
|
|
277
|
+
row.usedAt = now();
|
|
278
|
+
for (const a of access.values()) if (a.family === row.family) a.revoked = true;
|
|
279
|
+
const body = tokenBody(row.scope, row.clientId, row.family);
|
|
280
|
+
row.successor = body.refresh_token;
|
|
281
|
+
row.pairAccess = body.access_token;
|
|
282
|
+
return json(200, body);
|
|
212
283
|
}
|
|
213
284
|
return oauthError("unsupported_grant_type", "Unsupported grant_type");
|
|
214
285
|
}
|
|
215
|
-
|
|
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
|
+
}
|
|
402
|
+
function hub(method, path, query, headers, body) {
|
|
216
403
|
const failure2 = options.failures?.[path];
|
|
217
404
|
if (failure2 === "unreachable") return "reject";
|
|
218
405
|
const bearer = /^Bearer (.+)$/.exec(headers.authorization ?? "")?.[1];
|
|
@@ -234,11 +421,64 @@ function createMockAuthServer(options = {}) {
|
|
|
234
421
|
return json(403, { error: "This door is not available to a space-scoped key", code: "GRANT_DOOR_UNSUPPORTED" });
|
|
235
422
|
}
|
|
236
423
|
if (method === "GET" && path === "/users/me") return json(200, user);
|
|
237
|
-
if (method === "GET" && path === "/entities")
|
|
424
|
+
if (method === "GET" && path === "/entities") {
|
|
425
|
+
const ws = query.get("workspaceId");
|
|
426
|
+
const kind = query.get("profileSlug");
|
|
427
|
+
const offset = Number(query.get("offset") ?? 0) || 0;
|
|
428
|
+
const limit = Number(query.get("limit") ?? 50) || 50;
|
|
429
|
+
const rows = entities.filter((e) => (!ws || e.workspaceId === ws) && (!kind || e.profileSlug === kind));
|
|
430
|
+
return json(200, rows.slice(offset, offset + limit));
|
|
431
|
+
}
|
|
432
|
+
if (method === "GET" && path === "/proposals") {
|
|
433
|
+
const readable = new Set(grant.filter((g) => g.permission === "proposal.read" && row.scope.includes(g.permission)).map((g) => g.workspaceId));
|
|
434
|
+
const status = query.get("status") ?? "pending";
|
|
435
|
+
const ws = query.get("workspaceId");
|
|
436
|
+
const limit = Math.min(Math.max(Number(query.get("limit") ?? 50) || 50, 1), 200);
|
|
437
|
+
const offset = Number(query.get("offset") ?? 0) || 0;
|
|
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();
|
|
441
|
+
const page = rows.slice(offset, offset + limit);
|
|
442
|
+
return json(200, { proposals: page, limit, offset, total: rows.length, hasMore: offset + page.length < rows.length });
|
|
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
|
+
}
|
|
238
463
|
if (method !== "GET") {
|
|
239
464
|
writes.push({ method, path, body });
|
|
240
|
-
|
|
465
|
+
const fields = body && typeof body === "object" ? body : {};
|
|
466
|
+
const workspaceId = typeof fields.workspaceId === "string" ? fields.workspaceId : null;
|
|
467
|
+
if (writeMode === "proposed") {
|
|
241
468
|
const proposalId = `prop_mock_${++proposalSeq}`;
|
|
469
|
+
proposals.push({
|
|
470
|
+
id: proposalId,
|
|
471
|
+
proposalType: method === "POST" ? "create" : method === "DELETE" ? "delete" : "update",
|
|
472
|
+
targetType: path.split("/")[1]?.replace(/s$/, "") ?? "entity",
|
|
473
|
+
targetId: null,
|
|
474
|
+
status: "pending",
|
|
475
|
+
workspaceId,
|
|
476
|
+
createdAt: new Date(now()).toISOString(),
|
|
477
|
+
correlationId: null,
|
|
478
|
+
sessionId: null,
|
|
479
|
+
agentUserId: MOCK_APP_AGENT_ID,
|
|
480
|
+
...typeof fields.title === "string" ? { summary: `Create "${fields.title}"` } : {}
|
|
481
|
+
});
|
|
242
482
|
return json(200, {
|
|
243
483
|
status: "proposed",
|
|
244
484
|
message: "Sent for review",
|
|
@@ -246,7 +486,9 @@ function createMockAuthServer(options = {}) {
|
|
|
246
486
|
reviewUrl: `${issuer}/open/${proposalId}`
|
|
247
487
|
});
|
|
248
488
|
}
|
|
249
|
-
|
|
489
|
+
const id = `ent_mock_${writes.length}`;
|
|
490
|
+
if (method === "POST" && path === "/entities") entities.push({ id, ...fields });
|
|
491
|
+
return json(200, { status: "created", id });
|
|
250
492
|
}
|
|
251
493
|
return json(404, { error: "Not found" });
|
|
252
494
|
}
|
|
@@ -285,7 +527,7 @@ function createMockAuthServer(options = {}) {
|
|
|
285
527
|
} catch {
|
|
286
528
|
body = rawBody;
|
|
287
529
|
}
|
|
288
|
-
const res = hub(method, hubPath, headers, body);
|
|
530
|
+
const res = hub(method, hubPath, url.searchParams, headers, body);
|
|
289
531
|
if (res === "reject") throw new TypeError("Failed to fetch: mock pod unreachable");
|
|
290
532
|
return res;
|
|
291
533
|
}
|
|
@@ -296,6 +538,21 @@ function createMockAuthServer(options = {}) {
|
|
|
296
538
|
fetch: mockFetch,
|
|
297
539
|
requests,
|
|
298
540
|
writes,
|
|
541
|
+
proposals,
|
|
542
|
+
approvals,
|
|
543
|
+
settleProposal(id, status) {
|
|
544
|
+
const p = proposals.find((x) => x.id === id);
|
|
545
|
+
if (p) p.status = status;
|
|
546
|
+
},
|
|
547
|
+
async redeemElsewhere(t) {
|
|
548
|
+
const row = refreshTokens.get(t);
|
|
549
|
+
const res = await token({ grant_type: "refresh_token", refresh_token: t, client_id: row?.clientId ?? "" });
|
|
550
|
+
if (res.status !== 200) throw new Error(`redeemElsewhere: the pod answered ${res.status}`);
|
|
551
|
+
return await res.json();
|
|
552
|
+
},
|
|
553
|
+
get graceAnswers() {
|
|
554
|
+
return graceAnswers;
|
|
555
|
+
},
|
|
299
556
|
authorize(authorizeUrl, decision = {}) {
|
|
300
557
|
const url = new URL(authorizeUrl);
|
|
301
558
|
if (`${url.origin}${url.pathname}` !== `${issuer}/authorize`) throw new Error(`Not this pod's /authorize: ${authorizeUrl}`);
|
|
@@ -341,6 +598,9 @@ function createMockAuthServer(options = {}) {
|
|
|
341
598
|
for (const row of access.values()) row.revoked = true;
|
|
342
599
|
for (const row of refreshTokens.values()) row.revoked = true;
|
|
343
600
|
},
|
|
601
|
+
revokeAccessKeys() {
|
|
602
|
+
for (const row of access.values()) row.revoked = true;
|
|
603
|
+
},
|
|
344
604
|
liveRefreshTokens() {
|
|
345
605
|
return [...refreshTokens].filter(([, r]) => !r.used && !r.revoked).map(([t]) => t);
|
|
346
606
|
},
|
|
@@ -351,9 +611,84 @@ function createMockAuthServer(options = {}) {
|
|
|
351
611
|
};
|
|
352
612
|
}
|
|
353
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
|
+
|
|
645
|
+
// src/testing/cloud.ts
|
|
646
|
+
var MOCK_CLOUD_URL = "https://cp.mock-synap.test";
|
|
647
|
+
function createMockCloud(options = {}) {
|
|
648
|
+
const base = (options.url ?? MOCK_CLOUD_URL).replace(/\/+$/, "");
|
|
649
|
+
const requests = [];
|
|
650
|
+
return {
|
|
651
|
+
url: base,
|
|
652
|
+
requests,
|
|
653
|
+
choose(location, { pod, metadata }) {
|
|
654
|
+
const back = check(location, metadata);
|
|
655
|
+
back.searchParams.set("iss", pod);
|
|
656
|
+
back.searchParams.set("state", new URL(location).searchParams.get("state"));
|
|
657
|
+
return back.href;
|
|
658
|
+
},
|
|
659
|
+
cancel(location, { metadata }) {
|
|
660
|
+
const back = check(location, metadata);
|
|
661
|
+
back.searchParams.set("state", new URL(location).searchParams.get("state"));
|
|
662
|
+
back.searchParams.set("error", "access_denied");
|
|
663
|
+
return back.href;
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
function check(location, metadata) {
|
|
667
|
+
requests.push(location);
|
|
668
|
+
const url = new URL(location);
|
|
669
|
+
if (`${url.origin}${url.pathname}` !== `${base}/connect/pod`) throw new Error(`Not this CP's /connect/pod: ${location}`);
|
|
670
|
+
const clientId = url.searchParams.get("client_id");
|
|
671
|
+
const state = url.searchParams.get("state");
|
|
672
|
+
if (!clientId || !cimdUrlAccepted(clientId)) throw new Error("Chooser refused: client_id must be an https CIMD URL");
|
|
673
|
+
if (!state || !CLOUD_STATE_RE.test(state)) throw new Error("Chooser refused: state is missing or malformed");
|
|
674
|
+
if (clientId !== metadata.client_id) throw new Error("Chooser refused: the client metadata is not this client_id's");
|
|
675
|
+
return new URL(metadata.initiate_login_uri);
|
|
676
|
+
}
|
|
677
|
+
function cimdUrlAccepted(clientId) {
|
|
678
|
+
let url;
|
|
679
|
+
try {
|
|
680
|
+
url = new URL(clientId);
|
|
681
|
+
} catch {
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
if (url.protocol === "https:") return true;
|
|
685
|
+
return url.protocol === "http:" && options.allowInsecureLoopback === true && LOOPBACK_HOSTS.has(url.hostname);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
354
689
|
// src/testing/index.ts
|
|
355
690
|
var MOCK_POD_URL = "https://mock.synap.invalid";
|
|
356
|
-
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" };
|
|
357
692
|
var MOCK_RETRY_AFTER_SEC = 30;
|
|
358
693
|
var MOCK_FAILURE_TOKENS = {
|
|
359
694
|
/** 429 with no `Retry-After` (what the pod's public doors send today). */
|
|
@@ -533,7 +868,7 @@ function invalidKeys(form, body) {
|
|
|
533
868
|
}
|
|
534
869
|
function createMockPod(options = {}) {
|
|
535
870
|
const forms = options.forms ?? { [MOCK_TOKENS.form]: MOCK_FORM };
|
|
536
|
-
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 };
|
|
537
872
|
const sites = options.sites ?? { [MOCK_TOKENS.site]: MOCK_SITE };
|
|
538
873
|
const requests = [];
|
|
539
874
|
const submissions = [];
|
|
@@ -598,10 +933,14 @@ function createMockPod(options = {}) {
|
|
|
598
933
|
};
|
|
599
934
|
}
|
|
600
935
|
export {
|
|
936
|
+
MOCK_APP,
|
|
937
|
+
MOCK_APP_AGENT_ID,
|
|
601
938
|
MOCK_AUTH_ISSUER,
|
|
939
|
+
MOCK_CLOUD_URL,
|
|
602
940
|
MOCK_FAILURE_TOKENS,
|
|
603
941
|
MOCK_FORM,
|
|
604
942
|
MOCK_GRANT,
|
|
943
|
+
MOCK_HTML_SHARE,
|
|
605
944
|
MOCK_POD_URL,
|
|
606
945
|
MOCK_RETRY_AFTER_SEC,
|
|
607
946
|
MOCK_SHARE,
|
|
@@ -609,6 +948,8 @@ export {
|
|
|
609
948
|
MOCK_TOKENS,
|
|
610
949
|
MOCK_USER,
|
|
611
950
|
createMockAuthServer,
|
|
612
|
-
|
|
951
|
+
createMockCloud,
|
|
952
|
+
createMockPod,
|
|
953
|
+
parseMockPodSeed
|
|
613
954
|
};
|
|
614
955
|
//# sourceMappingURL=testing.js.map
|