@synap-core/sdk 1.5.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/dist/testing.cjs CHANGED
@@ -180,6 +180,20 @@ async function pkceChallenge(verifier) {
180
180
  return base64url(new Uint8Array(digest));
181
181
  }
182
182
 
183
+ // src/testing/roles.ts
184
+ function desugarSeedRoles(row, at) {
185
+ if (!Array.isArray(row.seedRoles)) return row;
186
+ const { seedRoles, ...rest } = row;
187
+ const facets = Array.isArray(row.facets) ? row.facets : [];
188
+ 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 }));
189
+ for (const r of shorthand) {
190
+ if (facets.some((f) => f?.slug === r.slug && f.workspaceId === r.workspaceId)) {
191
+ 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`);
192
+ }
193
+ }
194
+ return { ...rest, facets: [...facets, ...shorthand] };
195
+ }
196
+
183
197
  // src/testing/auth-server.ts
184
198
  var MOCK_AUTH_ISSUER = "https://pod.mock-synap.test";
185
199
  var MOCK_GRANT = [
@@ -213,13 +227,27 @@ function createMockAuthServer(options = {}) {
213
227
  const writes = [];
214
228
  const proposals = [];
215
229
  const seed = options.seed ?? {};
216
- const entities = [...seed.entities ?? [], ...options.entities ?? []];
230
+ const entities = [...seed.entities ?? [], ...options.entities ?? []].map(
231
+ (e, i) => desugarSeedRoles(e, `entities[${i}]`)
232
+ );
217
233
  const sessions = [...seed.sessions ?? []];
218
234
  const documents = [...seed.documents ?? []];
219
235
  const relations = [...seed.relations ?? []];
220
236
  const templateOf = new Map((seed.spaces ?? []).filter((sp) => sp.templateSlug).map((sp) => [sp.id, sp.templateSlug]));
221
237
  const approvals = [];
222
238
  const seededAt = new Date(now()).toISOString();
239
+ const rolesOf = (e) => Array.isArray(e.facets) ? e.facets.filter((f) => typeof f?.slug === "string") : [];
240
+ function readableEntity(e, spaces, only) {
241
+ if (typeof e.workspaceId === "string" && spaces.has(e.workspaceId) && (!only || e.workspaceId === only)) return true;
242
+ return facetsOf(e, spaces, only ?? void 0).some((f) => e.workspaceId == null || !f.sharedRole);
243
+ }
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 facetRow = (f) => ({ facetId: f.facetId, slug: f.slug, properties: f.properties ?? {}, status: f.status ?? null });
246
+ const entityWire = (e) => {
247
+ const { facets: _facets, facetSlugs: _facetSlugs, ...wire } = e;
248
+ return wire;
249
+ };
250
+ const readSpaces = (scope) => new Set(grant.filter((g) => /^entity\.(.+\.)?read$/.test(g.permission) && scope.includes(g.permission)).map((g) => g.workspaceId));
223
251
  for (const p of seed.proposals ?? []) {
224
252
  proposals.push({
225
253
  proposalType: "create",
@@ -367,11 +395,12 @@ function createMockAuthServer(options = {}) {
367
395
  triage: { pending: false, acceptedAt: null, acceptedBy: null },
368
396
  ...s
369
397
  });
370
- function readSeeded(path, query) {
398
+ function readSeeded(path, query, scope) {
399
+ const spaces = readSpaces(scope);
371
400
  const conn = /^\/entities\/([^/]+)\/connections$/.exec(path);
372
401
  if (conn) {
373
402
  const id = decodeURIComponent(conn[1]);
374
- const ws = query.get("workspaceId");
403
+ const ws = query.get("workspaceId") ?? void 0;
375
404
  const limit = Math.min(Math.max(Number(query.get("limit") ?? 50) || 50, 1), 200);
376
405
  const inLens = (e) => !ws || e.workspaceId === ws || e.workspaceId == null;
377
406
  const connections = [];
@@ -380,10 +409,15 @@ function createMockAuthServer(options = {}) {
380
409
  const outgoing = r.sourceEntityId === id;
381
410
  const otherId = outgoing ? r.targetEntityId : r.sourceEntityId;
382
411
  const other = entities.find((e) => e.id === otherId);
383
- if (!other || !inLens(other)) continue;
412
+ if (!other || !inLens(other) || !readableEntity(other, spaces)) continue;
384
413
  connections.push({
385
414
  entityId: otherId,
386
- entity: { ...other, type: other.profileSlug ?? other.type ?? null, facetSlugs: other.facetSlugs ?? [] },
415
+ // relations.ts: the raw row plus its role slugs under the read's facet lens (a 1.5.0 seed's own facetSlugs otherwise).
416
+ entity: {
417
+ ...entityWire(other),
418
+ type: other.profileSlug ?? other.type ?? null,
419
+ facetSlugs: rolesOf(other).length > 0 ? facetsOf(other, spaces, ws).map((f) => f.slug) : other.facetSlugs ?? []
420
+ },
387
421
  label: r.type,
388
422
  direction: outgoing ? "outgoing" : "incoming",
389
423
  source: "graph",
@@ -416,7 +450,39 @@ function createMockAuthServer(options = {}) {
416
450
  const entity = /^\/entities\/([^/]+)$/.exec(path);
417
451
  if (entity) {
418
452
  const found = entities.find((e) => e.id === decodeURIComponent(entity[1]));
419
- return found ? json(200, found) : new Response(null, { status: 404 });
453
+ return found && readableEntity(found, spaces) ? json(200, entityWire(found)) : new Response(null, { status: 404 });
454
+ }
455
+ const roles = /^\/entities\/([^/]+)\/facets$/.exec(path);
456
+ if (roles) {
457
+ const found = entities.find((e) => e.id === decodeURIComponent(roles[1]));
458
+ if (!found || !readableEntity(found, spaces)) return new Response(null, { status: 404 });
459
+ return json(200, {
460
+ facets: facetsOf(found, spaces).map((f) => ({
461
+ facet: {
462
+ id: f.facetId,
463
+ entityId: found.id,
464
+ profileId: `profile_${f.slug}`,
465
+ userId: found.userId ?? user.id,
466
+ // A stamped facet answers its space; a shared role's facet is stored pod-wide.
467
+ workspaceId: f.sharedRole ? null : f.workspaceId,
468
+ contextEntityId: null,
469
+ status: f.status ?? null,
470
+ statusCategory: null,
471
+ properties: f.properties ?? {},
472
+ metadata: {},
473
+ createdByKind: null,
474
+ createdByUserId: null,
475
+ agentUserId: null,
476
+ sourceProposalId: null,
477
+ correlationId: null,
478
+ createdAt: seededAt,
479
+ updatedAt: seededAt,
480
+ deletedAt: null
481
+ },
482
+ profile: { id: `profile_${f.slug}`, slug: f.slug, profileKind: "role" },
483
+ effectiveProperties: []
484
+ }))
485
+ });
420
486
  }
421
487
  const doc = /^\/documents\/([^/]+)$/.exec(path);
422
488
  if (doc) {
@@ -482,12 +548,27 @@ function createMockAuthServer(options = {}) {
482
548
  }
483
549
  if (method === "GET" && path === "/users/me") return json(200, user);
484
550
  if (method === "GET" && path === "/entities") {
485
- const ws = query.get("workspaceId");
551
+ const ws = query.get("workspaceId") || void 0;
486
552
  const kind = query.get("profileSlug");
553
+ const role = query.get("facetSlug");
554
+ const podWide = query.get("includePodWide") !== "false";
555
+ const withFacets = query.get("includeFacets") === "true";
487
556
  const offset = Number(query.get("offset") ?? 0) || 0;
488
557
  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));
558
+ const spaces = readSpaces(row.scope);
559
+ const rows = entities.filter((e) => {
560
+ if (!readableEntity(e, spaces, ws)) return false;
561
+ if (ws && !podWide && e.workspaceId !== ws && !facetsOf(e, spaces, ws).some((f) => !f.sharedRole)) return false;
562
+ const slugs = facetsOf(e, spaces, ws).map((f) => f.slug);
563
+ return (!kind || e.profileSlug === kind || slugs.includes(kind)) && (!role || slugs.includes(role));
564
+ });
565
+ return json(
566
+ 200,
567
+ rows.slice(offset, offset + limit).map((e) => {
568
+ const facets = withFacets ? facetsOf(e, spaces, ws).map(facetRow) : [];
569
+ return facets.length > 0 ? { ...entityWire(e), facetSlugs: facets.map((f) => f.slug), facets } : entityWire(e);
570
+ })
571
+ );
491
572
  }
492
573
  if (method === "GET" && path === "/proposals") {
493
574
  const readable = new Set(grant.filter((g) => g.permission === "proposal.read" && row.scope.includes(g.permission)).map((g) => g.workspaceId));
@@ -502,7 +583,7 @@ function createMockAuthServer(options = {}) {
502
583
  return json(200, { proposals: page, limit, offset, total: rows.length, hasMore: offset + page.length < rows.length });
503
584
  }
504
585
  if (method === "GET") {
505
- const read = readSeeded(path, query);
586
+ const read = readSeeded(path, query, row.scope);
506
587
  if (read) return read;
507
588
  }
508
589
  const approve = method === "POST" ? /^\/proposals\/([^/]+)\/approve$/.exec(path) : null;
@@ -520,6 +601,11 @@ function createMockAuthServer(options = {}) {
520
601
  approvals.push(p.id);
521
602
  return json(200, { success: true, proposalId: p.id, proposal: { ...p } });
522
603
  }
604
+ const patched = method === "PATCH" ? /^\/entities\/([^/]+)$/.exec(path) : null;
605
+ if (patched) {
606
+ const target = entities.find((e) => e.id === decodeURIComponent(patched[1]));
607
+ if (target && target.workspaceId == null) return json(403, { error: "grant does not allow" });
608
+ }
523
609
  if (method !== "GET") {
524
610
  writes.push({ method, path, body });
525
611
  const fields = body && typeof body === "object" ? body : {};
@@ -697,9 +783,36 @@ function parseMockPodSeed(input) {
697
783
  for (const field of REQUIRED[key]) {
698
784
  if (typeof row[field] !== "string") throw new TypeError(`seed.${key}[${i}].${field}: expected a string`);
699
785
  }
786
+ if (key === "entities") checkFacets(row.facets, `seed.entities[${i}].facets`);
700
787
  });
701
788
  }
702
- return JSON.parse(JSON.stringify(obj));
789
+ obj.entities?.forEach((row, i) => {
790
+ const roles = row.seedRoles;
791
+ if (roles === void 0) return;
792
+ if (!Array.isArray(roles)) throw new TypeError(`seed.entities[${i}].seedRoles: expected an array`);
793
+ roles.forEach((r, j) => {
794
+ for (const field of ["profileSlug", "workspaceId"]) {
795
+ if (!r || typeof r !== "object" || typeof r[field] !== "string") {
796
+ throw new TypeError(`seed.entities[${i}].seedRoles[${j}].${field}: expected a string`);
797
+ }
798
+ }
799
+ });
800
+ });
801
+ const out = JSON.parse(JSON.stringify(obj));
802
+ if (out.entities) out.entities = out.entities.map((row, i) => desugarSeedRoles(row, `seed.entities[${i}]`));
803
+ return out;
804
+ }
805
+ function checkFacets(facets, at) {
806
+ if (facets === void 0) return;
807
+ if (!Array.isArray(facets)) throw new TypeError(`${at}: expected an array`);
808
+ facets.forEach((f, j) => {
809
+ if (!f || typeof f !== "object" || Array.isArray(f)) throw new TypeError(`${at}[${j}]: expected an object`);
810
+ for (const field of ["facetId", "slug", "workspaceId"]) {
811
+ if (typeof f[field] !== "string") throw new TypeError(`${at}[${j}].${field}: expected a string`);
812
+ }
813
+ const shared = f.sharedRole;
814
+ if (shared !== void 0 && typeof shared !== "boolean") throw new TypeError(`${at}[${j}].sharedRole: expected a boolean`);
815
+ });
703
816
  }
704
817
 
705
818
  // src/connect/rules.ts