@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/CHANGELOG.md +82 -0
- package/README.md +16 -0
- package/dist/connect.cjs +9 -2
- package/dist/connect.cjs.map +1 -1
- package/dist/connect.d.cts +17 -1
- package/dist/connect.d.ts +17 -1
- package/dist/connect.js +9 -2
- package/dist/connect.js.map +1 -1
- package/dist/testing.cjs +124 -11
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +50 -1
- package/dist/testing.d.ts +50 -1
- package/dist/testing.js +124 -11
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
- package/src/connect/auth.ts +15 -2
- package/src/connect/client.ts +3 -0
- package/src/connect/hub-types.ts +17 -0
- package/src/connect/index.ts +1 -0
- package/src/testing/auth-server.ts +120 -12
- package/src/testing/roles.ts +27 -0
- package/src/testing/seed.ts +78 -2
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
|
-
|
|
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
|
|
490
|
-
|
|
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
|
-
|
|
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
|