@synap-core/sdk 1.5.0 → 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 +148 -0
- package/README.md +17 -1
- package/dist/connect.cjs +38 -7
- package/dist/connect.cjs.map +1 -1
- package/dist/connect.d.cts +55 -3
- package/dist/connect.d.ts +55 -3
- package/dist/connect.js +38 -7
- package/dist/connect.js.map +1 -1
- package/dist/testing.cjs +188 -16
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +69 -2
- package/dist/testing.d.ts +69 -2
- package/dist/testing.js +188 -16
- package/dist/testing.js.map +1 -1
- package/package.json +10 -9
- package/src/connect/auth.ts +29 -6
- package/src/connect/client.ts +58 -2
- package/src/connect/flow.ts +23 -2
- package/src/connect/hub-types.ts +17 -0
- package/src/connect/index.ts +2 -0
- package/src/testing/auth-server.ts +230 -18
- package/src/testing/roles.ts +27 -0
- package/src/testing/seed.ts +78 -2
package/dist/testing.cjs
CHANGED
|
@@ -180,12 +180,27 @@ 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 = [
|
|
186
200
|
{ permission: "entity.post.read", workspaceId: "ws_mock_studio", workspaceName: "Studio" },
|
|
187
201
|
{ permission: "entity.post.create", workspaceId: "ws_mock_studio", workspaceName: "Studio" }
|
|
188
202
|
];
|
|
203
|
+
var STEP_UP_MESSAGE = "Confirm it's you before changing who or what can reach this pod.";
|
|
189
204
|
var MOCK_USER = { id: "user_mock_ada", name: "Ada Lovelace" };
|
|
190
205
|
var MOCK_APP = { id: "app_mock_studio", manageUrl: "https://pod-admin.mock-synap.test/apps/app_mock_studio" };
|
|
191
206
|
var MOCK_APP_AGENT_ID = "agent_mock_app";
|
|
@@ -213,13 +228,43 @@ function createMockAuthServer(options = {}) {
|
|
|
213
228
|
const writes = [];
|
|
214
229
|
const proposals = [];
|
|
215
230
|
const seed = options.seed ?? {};
|
|
216
|
-
const entities = [...seed.entities ?? [], ...options.entities ?? []]
|
|
231
|
+
const entities = [...seed.entities ?? [], ...options.entities ?? []].map(
|
|
232
|
+
(e, i) => desugarSeedRoles(e, `entities[${i}]`)
|
|
233
|
+
);
|
|
217
234
|
const sessions = [...seed.sessions ?? []];
|
|
218
235
|
const documents = [...seed.documents ?? []];
|
|
219
236
|
const relations = [...seed.relations ?? []];
|
|
220
237
|
const templateOf = new Map((seed.spaces ?? []).filter((sp) => sp.templateSlug).map((sp) => [sp.id, sp.templateSlug]));
|
|
221
238
|
const approvals = [];
|
|
222
239
|
const seededAt = new Date(now()).toISOString();
|
|
240
|
+
const rolesOf = (e) => Array.isArray(e.facets) ? e.facets.filter((f) => typeof f?.slug === "string") : [];
|
|
241
|
+
function readableEntity(e, spaces, only) {
|
|
242
|
+
if (typeof e.workspaceId === "string" && kindReadable(e, spaces, e.workspaceId) && (!only || e.workspaceId === only)) return true;
|
|
243
|
+
return facetsOf(e, spaces, only ?? void 0).some((f) => e.workspaceId == null || !f.sharedRole);
|
|
244
|
+
}
|
|
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));
|
|
246
|
+
const facetRow = (f) => ({ facetId: f.facetId, slug: f.slug, properties: f.properties ?? {}, status: f.status ?? null });
|
|
247
|
+
const entityWire = (e) => {
|
|
248
|
+
const { facets: _facets, facetSlugs: _facetSlugs, ...wire } = e;
|
|
249
|
+
return wire;
|
|
250
|
+
};
|
|
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
|
+
};
|
|
223
268
|
for (const p of seed.proposals ?? []) {
|
|
224
269
|
proposals.push({
|
|
225
270
|
proposalType: "create",
|
|
@@ -234,7 +279,9 @@ function createMockAuthServer(options = {}) {
|
|
|
234
279
|
...p
|
|
235
280
|
});
|
|
236
281
|
}
|
|
237
|
-
|
|
282
|
+
let actsAs = options.actsAs;
|
|
283
|
+
const writeMode = () => options.writeMode ?? (actsAs === "person" ? "applied" : "proposed");
|
|
284
|
+
let deviceFresh = !options.requireStepUp;
|
|
238
285
|
const graceMs = (options.refreshGraceSec ?? 30) * 1e3;
|
|
239
286
|
const app = options.app === void 0 ? MOCK_APP : options.app;
|
|
240
287
|
let proposalSeq = 0;
|
|
@@ -280,7 +327,7 @@ function createMockAuthServer(options = {}) {
|
|
|
280
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) }),
|
|
281
328
|
synap_user: user,
|
|
282
329
|
...app ? { synap_app: app } : {},
|
|
283
|
-
...
|
|
330
|
+
...actsAs ? { synap_acts_as: actsAs } : {}
|
|
284
331
|
};
|
|
285
332
|
if (pair) {
|
|
286
333
|
body.refresh_token = pair.refreshToken;
|
|
@@ -306,6 +353,21 @@ function createMockAuthServer(options = {}) {
|
|
|
306
353
|
if (form.resource !== void 0 && row.resource && form.resource !== row.resource) {
|
|
307
354
|
return oauthError("invalid_target", "resource does not match the authorization request");
|
|
308
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
|
+
}
|
|
309
371
|
for (const r of refreshTokens.values()) if (r.clientId === row.clientId) revokeFamily(r.family);
|
|
310
372
|
return json(200, tokenBody(row.scope, row.clientId, randomToken()));
|
|
311
373
|
}
|
|
@@ -367,11 +429,12 @@ function createMockAuthServer(options = {}) {
|
|
|
367
429
|
triage: { pending: false, acceptedAt: null, acceptedBy: null },
|
|
368
430
|
...s
|
|
369
431
|
});
|
|
370
|
-
function readSeeded(path, query) {
|
|
432
|
+
function readSeeded(path, query, scope) {
|
|
433
|
+
const spaces = readSpaces(scope);
|
|
371
434
|
const conn = /^\/entities\/([^/]+)\/connections$/.exec(path);
|
|
372
435
|
if (conn) {
|
|
373
436
|
const id = decodeURIComponent(conn[1]);
|
|
374
|
-
const ws = query.get("workspaceId");
|
|
437
|
+
const ws = query.get("workspaceId") ?? void 0;
|
|
375
438
|
const limit = Math.min(Math.max(Number(query.get("limit") ?? 50) || 50, 1), 200);
|
|
376
439
|
const inLens = (e) => !ws || e.workspaceId === ws || e.workspaceId == null;
|
|
377
440
|
const connections = [];
|
|
@@ -380,10 +443,17 @@ function createMockAuthServer(options = {}) {
|
|
|
380
443
|
const outgoing = r.sourceEntityId === id;
|
|
381
444
|
const otherId = outgoing ? r.targetEntityId : r.sourceEntityId;
|
|
382
445
|
const other = entities.find((e) => e.id === otherId);
|
|
383
|
-
if (!other || !inLens(other)) continue;
|
|
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;
|
|
384
449
|
connections.push({
|
|
385
450
|
entityId: otherId,
|
|
386
|
-
|
|
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).
|
|
452
|
+
entity: {
|
|
453
|
+
...entityWire(other),
|
|
454
|
+
type: other.profileSlug ?? other.type ?? null,
|
|
455
|
+
facetSlugs: rolesOf(other).length > 0 ? facetsOf(other, spaces, ws).map((f) => f.slug) : other.facetSlugs ?? []
|
|
456
|
+
},
|
|
387
457
|
label: r.type,
|
|
388
458
|
direction: outgoing ? "outgoing" : "incoming",
|
|
389
459
|
source: "graph",
|
|
@@ -416,7 +486,39 @@ function createMockAuthServer(options = {}) {
|
|
|
416
486
|
const entity = /^\/entities\/([^/]+)$/.exec(path);
|
|
417
487
|
if (entity) {
|
|
418
488
|
const found = entities.find((e) => e.id === decodeURIComponent(entity[1]));
|
|
419
|
-
return found ? json(200, found) : new Response(null, { status: 404 });
|
|
489
|
+
return found && readableEntity(found, spaces) ? json(200, entityWire(found)) : new Response(null, { status: 404 });
|
|
490
|
+
}
|
|
491
|
+
const roles = /^\/entities\/([^/]+)\/facets$/.exec(path);
|
|
492
|
+
if (roles) {
|
|
493
|
+
const found = entities.find((e) => e.id === decodeURIComponent(roles[1]));
|
|
494
|
+
if (!found || !readableEntity(found, spaces)) return new Response(null, { status: 404 });
|
|
495
|
+
return json(200, {
|
|
496
|
+
facets: facetsOf(found, spaces).map((f) => ({
|
|
497
|
+
facet: {
|
|
498
|
+
id: f.facetId,
|
|
499
|
+
entityId: found.id,
|
|
500
|
+
profileId: `profile_${f.slug}`,
|
|
501
|
+
userId: found.userId ?? user.id,
|
|
502
|
+
// A stamped facet answers its space; a shared role's facet is stored pod-wide.
|
|
503
|
+
workspaceId: f.sharedRole ? null : f.workspaceId,
|
|
504
|
+
contextEntityId: null,
|
|
505
|
+
status: f.status ?? null,
|
|
506
|
+
statusCategory: null,
|
|
507
|
+
properties: f.properties ?? {},
|
|
508
|
+
metadata: {},
|
|
509
|
+
createdByKind: null,
|
|
510
|
+
createdByUserId: null,
|
|
511
|
+
agentUserId: null,
|
|
512
|
+
sourceProposalId: null,
|
|
513
|
+
correlationId: null,
|
|
514
|
+
createdAt: seededAt,
|
|
515
|
+
updatedAt: seededAt,
|
|
516
|
+
deletedAt: null
|
|
517
|
+
},
|
|
518
|
+
profile: { id: `profile_${f.slug}`, slug: f.slug, profileKind: "role" },
|
|
519
|
+
effectiveProperties: []
|
|
520
|
+
}))
|
|
521
|
+
});
|
|
420
522
|
}
|
|
421
523
|
const doc = /^\/documents\/([^/]+)$/.exec(path);
|
|
422
524
|
if (doc) {
|
|
@@ -472,6 +574,12 @@ function createMockAuthServer(options = {}) {
|
|
|
472
574
|
return json(403, { error: "Forbidden" });
|
|
473
575
|
case "door_unsupported":
|
|
474
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" });
|
|
475
583
|
case "server":
|
|
476
584
|
return json(500, { error: "Internal error" });
|
|
477
585
|
case "unavailable":
|
|
@@ -482,12 +590,27 @@ function createMockAuthServer(options = {}) {
|
|
|
482
590
|
}
|
|
483
591
|
if (method === "GET" && path === "/users/me") return json(200, user);
|
|
484
592
|
if (method === "GET" && path === "/entities") {
|
|
485
|
-
const ws = query.get("workspaceId");
|
|
593
|
+
const ws = query.get("workspaceId") || void 0;
|
|
486
594
|
const kind = query.get("profileSlug");
|
|
595
|
+
const role = query.get("facetSlug");
|
|
596
|
+
const podWide = query.get("includePodWide") !== "false";
|
|
597
|
+
const withFacets = query.get("includeFacets") === "true";
|
|
487
598
|
const offset = Number(query.get("offset") ?? 0) || 0;
|
|
488
599
|
const limit = Number(query.get("limit") ?? 50) || 50;
|
|
489
|
-
const
|
|
490
|
-
|
|
600
|
+
const spaces = readSpaces(row.scope);
|
|
601
|
+
const rows = entities.filter((e) => {
|
|
602
|
+
if (!readableEntity(e, spaces, ws)) return false;
|
|
603
|
+
if (ws && !podWide && e.workspaceId !== ws && !facetsOf(e, spaces, ws).some((f) => !f.sharedRole)) return false;
|
|
604
|
+
const slugs = facetsOf(e, spaces, ws).map((f) => f.slug);
|
|
605
|
+
return (!kind || e.profileSlug === kind || slugs.includes(kind)) && (!role || slugs.includes(role));
|
|
606
|
+
});
|
|
607
|
+
return json(
|
|
608
|
+
200,
|
|
609
|
+
rows.slice(offset, offset + limit).map((e) => {
|
|
610
|
+
const facets = withFacets ? facetsOf(e, spaces, ws).map(facetRow) : [];
|
|
611
|
+
return facets.length > 0 ? { ...entityWire(e), facetSlugs: facets.map((f) => f.slug), facets } : entityWire(e);
|
|
612
|
+
})
|
|
613
|
+
);
|
|
491
614
|
}
|
|
492
615
|
if (method === "GET" && path === "/proposals") {
|
|
493
616
|
const readable = new Set(grant.filter((g) => g.permission === "proposal.read" && row.scope.includes(g.permission)).map((g) => g.workspaceId));
|
|
@@ -502,14 +625,15 @@ function createMockAuthServer(options = {}) {
|
|
|
502
625
|
return json(200, { proposals: page, limit, offset, total: rows.length, hasMore: offset + page.length < rows.length });
|
|
503
626
|
}
|
|
504
627
|
if (method === "GET") {
|
|
505
|
-
const read = readSeeded(path, query);
|
|
628
|
+
const read = readSeeded(path, query, row.scope);
|
|
506
629
|
if (read) return read;
|
|
507
630
|
}
|
|
508
631
|
const approve = method === "POST" ? /^\/proposals\/([^/]+)\/approve$/.exec(path) : null;
|
|
509
632
|
if (approve) {
|
|
510
|
-
if (
|
|
633
|
+
if (actsAs !== "person") {
|
|
511
634
|
return json(403, { error: "An agent credential cannot approve proposals \u2014 approve is human review. Approve from a human session." });
|
|
512
635
|
}
|
|
636
|
+
if (!deviceFresh) return json(403, { error: STEP_UP_MESSAGE });
|
|
513
637
|
const p = proposals.find((x) => x.id === decodeURIComponent(approve[1]));
|
|
514
638
|
if (!p) return json(404, { error: "Proposal not found" });
|
|
515
639
|
if (!grant.some((g) => g.permission === "proposal.approve" && row.scope.includes(g.permission) && g.workspaceId === p.workspaceId)) {
|
|
@@ -520,11 +644,26 @@ function createMockAuthServer(options = {}) {
|
|
|
520
644
|
approvals.push(p.id);
|
|
521
645
|
return json(200, { success: true, proposalId: p.id, proposal: { ...p } });
|
|
522
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
|
+
}
|
|
655
|
+
const patched = method === "PATCH" ? /^\/entities\/([^/]+)$/.exec(path) : null;
|
|
656
|
+
if (patched) {
|
|
657
|
+
const target = entities.find((e) => e.id === decodeURIComponent(patched[1]));
|
|
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
|
+
}
|
|
661
|
+
}
|
|
523
662
|
if (method !== "GET") {
|
|
524
663
|
writes.push({ method, path, body });
|
|
525
664
|
const fields = body && typeof body === "object" ? body : {};
|
|
526
665
|
const workspaceId = typeof fields.workspaceId === "string" ? fields.workspaceId : null;
|
|
527
|
-
if (writeMode === "proposed") {
|
|
666
|
+
if (writeMode() === "proposed") {
|
|
528
667
|
const proposalId = `prop_mock_${++proposalSeq}`;
|
|
529
668
|
proposals.push({
|
|
530
669
|
id: proposalId,
|
|
@@ -641,9 +780,15 @@ function createMockAuthServer(options = {}) {
|
|
|
641
780
|
if (scope.length === 0) return finish({ error: "invalid_scope" });
|
|
642
781
|
if (decision.deny) return finish({ error: "access_denied" });
|
|
643
782
|
const code = randomToken();
|
|
644
|
-
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" });
|
|
645
784
|
return finish({ code });
|
|
646
785
|
},
|
|
786
|
+
setActsAs(v) {
|
|
787
|
+
actsAs = v ?? void 0;
|
|
788
|
+
},
|
|
789
|
+
expireStepUp() {
|
|
790
|
+
deviceFresh = false;
|
|
791
|
+
},
|
|
647
792
|
consumeRefreshToken(t) {
|
|
648
793
|
const row = refreshTokens.get(t);
|
|
649
794
|
if (row) row.used = true;
|
|
@@ -697,9 +842,36 @@ function parseMockPodSeed(input) {
|
|
|
697
842
|
for (const field of REQUIRED[key]) {
|
|
698
843
|
if (typeof row[field] !== "string") throw new TypeError(`seed.${key}[${i}].${field}: expected a string`);
|
|
699
844
|
}
|
|
845
|
+
if (key === "entities") checkFacets(row.facets, `seed.entities[${i}].facets`);
|
|
700
846
|
});
|
|
701
847
|
}
|
|
702
|
-
|
|
848
|
+
obj.entities?.forEach((row, i) => {
|
|
849
|
+
const roles = row.seedRoles;
|
|
850
|
+
if (roles === void 0) return;
|
|
851
|
+
if (!Array.isArray(roles)) throw new TypeError(`seed.entities[${i}].seedRoles: expected an array`);
|
|
852
|
+
roles.forEach((r, j) => {
|
|
853
|
+
for (const field of ["profileSlug", "workspaceId"]) {
|
|
854
|
+
if (!r || typeof r !== "object" || typeof r[field] !== "string") {
|
|
855
|
+
throw new TypeError(`seed.entities[${i}].seedRoles[${j}].${field}: expected a string`);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
});
|
|
860
|
+
const out = JSON.parse(JSON.stringify(obj));
|
|
861
|
+
if (out.entities) out.entities = out.entities.map((row, i) => desugarSeedRoles(row, `seed.entities[${i}]`));
|
|
862
|
+
return out;
|
|
863
|
+
}
|
|
864
|
+
function checkFacets(facets, at) {
|
|
865
|
+
if (facets === void 0) return;
|
|
866
|
+
if (!Array.isArray(facets)) throw new TypeError(`${at}: expected an array`);
|
|
867
|
+
facets.forEach((f, j) => {
|
|
868
|
+
if (!f || typeof f !== "object" || Array.isArray(f)) throw new TypeError(`${at}[${j}]: expected an object`);
|
|
869
|
+
for (const field of ["facetId", "slug", "workspaceId"]) {
|
|
870
|
+
if (typeof f[field] !== "string") throw new TypeError(`${at}[${j}].${field}: expected a string`);
|
|
871
|
+
}
|
|
872
|
+
const shared = f.sharedRole;
|
|
873
|
+
if (shared !== void 0 && typeof shared !== "boolean") throw new TypeError(`${at}[${j}].sharedRole: expected a boolean`);
|
|
874
|
+
});
|
|
703
875
|
}
|
|
704
876
|
|
|
705
877
|
// src/connect/rules.ts
|