@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/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" && spaces.has(e.workspaceId) && (!only || e.workspaceId === only)) return true;
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.has(f.workspaceId) && (lens === void 0 || f.workspaceId === lens));
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) => new Set(grant.filter((g) => /^entity\.(.+\.)?read$/.test(g.permission) && scope.includes(g.permission)).map((g) => g.workspaceId));
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
- const writeMode = options.writeMode ?? (options.actsAs === "person" ? "applied" : "proposed");
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
- ...options.actsAs ? { synap_acts_as: options.actsAs } : {}
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 (options.actsAs !== "person") {
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) return json(403, { error: "grant does not allow" });
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;