@synap-core/sdk 1.5.1 → 1.5.2
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/AGENTS.md +250 -0
- package/CHANGELOG.md +83 -17
- package/README.md +3 -3
- package/dist/connect.cjs +29 -5
- package/dist/connect.cjs.map +1 -1
- package/dist/connect.d.cts +39 -3
- package/dist/connect.d.ts +39 -3
- package/dist/connect.js +29 -5
- package/dist/connect.js.map +1 -1
- package/dist/testing.cjs +68 -9
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +19 -1
- package/dist/testing.d.ts +19 -1
- package/dist/testing.js +68 -9
- package/dist/testing.js.map +1 -1
- package/package.json +10 -9
- package/src/connect/auth.ts +14 -4
- package/src/connect/client.ts +55 -2
- package/src/connect/flow.ts +23 -2
- package/src/connect/index.ts +1 -0
- package/src/testing/auth-server.ts +118 -14
package/dist/testing.cjs
CHANGED
|
@@ -200,6 +200,7 @@ var MOCK_GRANT = [
|
|
|
200
200
|
{ permission: "entity.post.read", workspaceId: "ws_mock_studio", workspaceName: "Studio" },
|
|
201
201
|
{ permission: "entity.post.create", workspaceId: "ws_mock_studio", workspaceName: "Studio" }
|
|
202
202
|
];
|
|
203
|
+
var STEP_UP_MESSAGE = "Confirm it's you before changing who or what can reach this pod.";
|
|
203
204
|
var MOCK_USER = { id: "user_mock_ada", name: "Ada Lovelace" };
|
|
204
205
|
var MOCK_APP = { id: "app_mock_studio", manageUrl: "https://pod-admin.mock-synap.test/apps/app_mock_studio" };
|
|
205
206
|
var MOCK_APP_AGENT_ID = "agent_mock_app";
|
|
@@ -238,16 +239,32 @@ function createMockAuthServer(options = {}) {
|
|
|
238
239
|
const seededAt = new Date(now()).toISOString();
|
|
239
240
|
const rolesOf = (e) => Array.isArray(e.facets) ? e.facets.filter((f) => typeof f?.slug === "string") : [];
|
|
240
241
|
function readableEntity(e, spaces, only) {
|
|
241
|
-
if (typeof e.workspaceId === "string" &&
|
|
242
|
+
if (typeof e.workspaceId === "string" && kindReadable(e, spaces, e.workspaceId) && (!only || e.workspaceId === only)) return true;
|
|
242
243
|
return facetsOf(e, spaces, only ?? void 0).some((f) => e.workspaceId == null || !f.sharedRole);
|
|
243
244
|
}
|
|
244
|
-
const facetsOf = (e, spaces, lens) => rolesOf(e).filter((f) => typeof f.workspaceId === "string" && spaces
|
|
245
|
+
const facetsOf = (e, spaces, lens) => rolesOf(e).filter((f) => typeof f.workspaceId === "string" && kindReadable(e, spaces, f.workspaceId) && (lens === void 0 || f.workspaceId === lens));
|
|
245
246
|
const facetRow = (f) => ({ facetId: f.facetId, slug: f.slug, properties: f.properties ?? {}, status: f.status ?? null });
|
|
246
247
|
const entityWire = (e) => {
|
|
247
248
|
const { facets: _facets, facetSlugs: _facetSlugs, ...wire } = e;
|
|
248
249
|
return wire;
|
|
249
250
|
};
|
|
250
|
-
const readSpaces = (scope) =>
|
|
251
|
+
const readSpaces = (scope) => {
|
|
252
|
+
const out = /* @__PURE__ */ new Map();
|
|
253
|
+
for (const g of grant) {
|
|
254
|
+
const m = /^entity\.(?:(.+)\.)?read$/.exec(g.permission);
|
|
255
|
+
if (!m || !scope.includes(g.permission)) continue;
|
|
256
|
+
const kind = m[1];
|
|
257
|
+
if (kind === void 0 || kind === "*") out.set(g.workspaceId, null);
|
|
258
|
+
else if (out.get(g.workspaceId) !== null) out.set(g.workspaceId, (out.get(g.workspaceId) ?? /* @__PURE__ */ new Set()).add(kind));
|
|
259
|
+
else continue;
|
|
260
|
+
}
|
|
261
|
+
return out;
|
|
262
|
+
};
|
|
263
|
+
const kindReadable = (e, spaces, space) => {
|
|
264
|
+
if (!spaces.has(space)) return false;
|
|
265
|
+
const kinds = spaces.get(space);
|
|
266
|
+
return kinds === null || kinds === void 0 ? kinds === null : kinds.has(String(e.profileSlug ?? e.type ?? ""));
|
|
267
|
+
};
|
|
251
268
|
for (const p of seed.proposals ?? []) {
|
|
252
269
|
proposals.push({
|
|
253
270
|
proposalType: "create",
|
|
@@ -262,7 +279,9 @@ function createMockAuthServer(options = {}) {
|
|
|
262
279
|
...p
|
|
263
280
|
});
|
|
264
281
|
}
|
|
265
|
-
|
|
282
|
+
let actsAs = options.actsAs;
|
|
283
|
+
const writeMode = () => options.writeMode ?? (actsAs === "person" ? "applied" : "proposed");
|
|
284
|
+
let deviceFresh = !options.requireStepUp;
|
|
266
285
|
const graceMs = (options.refreshGraceSec ?? 30) * 1e3;
|
|
267
286
|
const app = options.app === void 0 ? MOCK_APP : options.app;
|
|
268
287
|
let proposalSeq = 0;
|
|
@@ -308,7 +327,7 @@ function createMockAuthServer(options = {}) {
|
|
|
308
327
|
synap_grant: grant.filter((g) => scope.includes(g.permission)).map((g) => g.templateSlug || !templateOf.has(g.workspaceId) ? g : { ...g, templateSlug: templateOf.get(g.workspaceId) }),
|
|
309
328
|
synap_user: user,
|
|
310
329
|
...app ? { synap_app: app } : {},
|
|
311
|
-
...
|
|
330
|
+
...actsAs ? { synap_acts_as: actsAs } : {}
|
|
312
331
|
};
|
|
313
332
|
if (pair) {
|
|
314
333
|
body.refresh_token = pair.refreshToken;
|
|
@@ -334,6 +353,21 @@ function createMockAuthServer(options = {}) {
|
|
|
334
353
|
if (form.resource !== void 0 && row.resource && form.resource !== row.resource) {
|
|
335
354
|
return oauthError("invalid_target", "resource does not match the authorization request");
|
|
336
355
|
}
|
|
356
|
+
if (actsAs === "person" && row.stepUp !== Boolean(form.synap_device_refresh_token)) {
|
|
357
|
+
return oauthError(
|
|
358
|
+
"invalid_grant",
|
|
359
|
+
row.stepUp ? "This code re-confirms a device: send its refresh token as synap_device_refresh_token" : "This code signs a new device in; a re-confirmation asks with prompt=login"
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
if (row.stepUp) {
|
|
363
|
+
const device = refreshTokens.get(form.synap_device_refresh_token);
|
|
364
|
+
if (!device || device.clientId !== row.clientId || device.used || device.revoked) {
|
|
365
|
+
return oauthError("invalid_grant", "Invalid, expired, revoked or already-used refresh token");
|
|
366
|
+
}
|
|
367
|
+
device.used = true;
|
|
368
|
+
deviceFresh = true;
|
|
369
|
+
return json(200, tokenBody(row.scope, row.clientId, device.family));
|
|
370
|
+
}
|
|
337
371
|
for (const r of refreshTokens.values()) if (r.clientId === row.clientId) revokeFamily(r.family);
|
|
338
372
|
return json(200, tokenBody(row.scope, row.clientId, randomToken()));
|
|
339
373
|
}
|
|
@@ -410,6 +444,8 @@ function createMockAuthServer(options = {}) {
|
|
|
410
444
|
const otherId = outgoing ? r.targetEntityId : r.sourceEntityId;
|
|
411
445
|
const other = entities.find((e) => e.id === otherId);
|
|
412
446
|
if (!other || !inLens(other) || !readableEntity(other, spaces)) continue;
|
|
447
|
+
const self = entities.find((e) => e.id === id);
|
|
448
|
+
if (self && self.workspaceId == null && other.workspaceId == null) continue;
|
|
413
449
|
connections.push({
|
|
414
450
|
entityId: otherId,
|
|
415
451
|
// relations.ts: the raw row plus its role slugs under the read's facet lens (a 1.5.0 seed's own facetSlugs otherwise).
|
|
@@ -538,6 +574,12 @@ function createMockAuthServer(options = {}) {
|
|
|
538
574
|
return json(403, { error: "Forbidden" });
|
|
539
575
|
case "door_unsupported":
|
|
540
576
|
return json(403, { error: "This door is not available to a space-scoped key", code: "GRANT_DOOR_UNSUPPORTED" });
|
|
577
|
+
case "step_up_required":
|
|
578
|
+
return json(403, { error: STEP_UP_MESSAGE });
|
|
579
|
+
case "email_unverified":
|
|
580
|
+
return json(409, { error: "Add and verify an email on your Synap account to connect a pod.", code: "CP_ACCOUNT_EMAIL_REQUIRED", recovery: { action: "update_control_plane_profile" } });
|
|
581
|
+
case "origin_insecure":
|
|
582
|
+
return json(409, { error: "This application must be served over HTTPS to connect to a Pod.", code: "FEDERATED_APPLICATION_ORIGIN_INSECURE" });
|
|
541
583
|
case "server":
|
|
542
584
|
return json(500, { error: "Internal error" });
|
|
543
585
|
case "unavailable":
|
|
@@ -588,9 +630,10 @@ function createMockAuthServer(options = {}) {
|
|
|
588
630
|
}
|
|
589
631
|
const approve = method === "POST" ? /^\/proposals\/([^/]+)\/approve$/.exec(path) : null;
|
|
590
632
|
if (approve) {
|
|
591
|
-
if (
|
|
633
|
+
if (actsAs !== "person") {
|
|
592
634
|
return json(403, { error: "An agent credential cannot approve proposals \u2014 approve is human review. Approve from a human session." });
|
|
593
635
|
}
|
|
636
|
+
if (!deviceFresh) return json(403, { error: STEP_UP_MESSAGE });
|
|
594
637
|
const p = proposals.find((x) => x.id === decodeURIComponent(approve[1]));
|
|
595
638
|
if (!p) return json(404, { error: "Proposal not found" });
|
|
596
639
|
if (!grant.some((g) => g.permission === "proposal.approve" && row.scope.includes(g.permission) && g.workspaceId === p.workspaceId)) {
|
|
@@ -601,16 +644,26 @@ function createMockAuthServer(options = {}) {
|
|
|
601
644
|
approvals.push(p.id);
|
|
602
645
|
return json(200, { success: true, proposalId: p.id, proposal: { ...p } });
|
|
603
646
|
}
|
|
647
|
+
const attach = method === "POST" ? /^\/entities\/([^/]+)\/facets$/.exec(path) : null;
|
|
648
|
+
const facetPatch = method === "PATCH" ? /^\/facets\/([^/]+)$/.exec(path) : null;
|
|
649
|
+
if (attach) return json(403, { error: "grant does not allow" });
|
|
650
|
+
if (facetPatch) {
|
|
651
|
+
const fid = decodeURIComponent(facetPatch[1]);
|
|
652
|
+
const parent = entities.find((e) => rolesOf(e).some((f) => f.facetId === fid));
|
|
653
|
+
if (parent && parent.workspaceId == null) return json(403, { error: "grant does not allow" });
|
|
654
|
+
}
|
|
604
655
|
const patched = method === "PATCH" ? /^\/entities\/([^/]+)$/.exec(path) : null;
|
|
605
656
|
if (patched) {
|
|
606
657
|
const target = entities.find((e) => e.id === decodeURIComponent(patched[1]));
|
|
607
|
-
if (target && target.workspaceId == null)
|
|
658
|
+
if (target && target.workspaceId == null) {
|
|
659
|
+
return typeof target.userId === "string" && target.userId !== user.id ? new Response(null, { status: 404 }) : json(403, { error: "grant does not allow" });
|
|
660
|
+
}
|
|
608
661
|
}
|
|
609
662
|
if (method !== "GET") {
|
|
610
663
|
writes.push({ method, path, body });
|
|
611
664
|
const fields = body && typeof body === "object" ? body : {};
|
|
612
665
|
const workspaceId = typeof fields.workspaceId === "string" ? fields.workspaceId : null;
|
|
613
|
-
if (writeMode === "proposed") {
|
|
666
|
+
if (writeMode() === "proposed") {
|
|
614
667
|
const proposalId = `prop_mock_${++proposalSeq}`;
|
|
615
668
|
proposals.push({
|
|
616
669
|
id: proposalId,
|
|
@@ -727,9 +780,15 @@ function createMockAuthServer(options = {}) {
|
|
|
727
780
|
if (scope.length === 0) return finish({ error: "invalid_scope" });
|
|
728
781
|
if (decision.deny) return finish({ error: "access_denied" });
|
|
729
782
|
const code = randomToken();
|
|
730
|
-
codes.set(code, { clientId, redirectUri, challenge: q.get("code_challenge"), resource, scope });
|
|
783
|
+
codes.set(code, { clientId, redirectUri, challenge: q.get("code_challenge"), resource, scope, stepUp: actsAs === "person" && q.get("prompt") === "login" });
|
|
731
784
|
return finish({ code });
|
|
732
785
|
},
|
|
786
|
+
setActsAs(v) {
|
|
787
|
+
actsAs = v ?? void 0;
|
|
788
|
+
},
|
|
789
|
+
expireStepUp() {
|
|
790
|
+
deviceFresh = false;
|
|
791
|
+
},
|
|
733
792
|
consumeRefreshToken(t) {
|
|
734
793
|
const row = refreshTokens.get(t);
|
|
735
794
|
if (row) row.used = true;
|