@githolon/dsl 0.7.1 → 0.8.0
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/package.json +1 -1
- package/src/aggregate.ts +19 -16
- package/src/build_package.ts +7 -0
- package/src/codegen_dart.ts +9 -0
- package/src/codegen_ts.ts +12 -10
- package/src/compile_package_main.ts +10 -0
- package/src/directive.ts +15 -18
- package/src/engine_entry.ts +8 -2
- package/src/framework/encryption_identity.ts +3 -3
- package/src/framework/genesis.ts +88 -0
- package/src/framework/impure_capability.ts +3 -3
- package/src/framework/relations.ts +28 -4
- package/src/framework/signing_identity.ts +97 -0
- package/src/framework/workspace_invariant.ts +5 -3
- package/src/framework/workspaces.ts +158 -103
- package/src/index.ts +72 -18
- package/src/manifest.ts +0 -6
- package/src/relation_schema.ts +298 -0
- package/src/relation_sugar.ts +324 -0
- package/src/usd.ts +142 -7
- package/src/usd_layers.ts +53 -0
- package/src/framework/rbac.ts +0 -418
package/src/usd.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { createHash } from "node:crypto";
|
|
|
30
30
|
import {
|
|
31
31
|
canonicalJson,
|
|
32
32
|
domainManifest,
|
|
33
|
+
type CanonicalManifest,
|
|
33
34
|
type CanonicalProjectionReturn,
|
|
34
35
|
} from "./manifest.js";
|
|
35
36
|
import {
|
|
@@ -39,6 +40,7 @@ import {
|
|
|
39
40
|
} from "./compose.js";
|
|
40
41
|
import type { WireSchema, WireRefMode } from "./wire.js";
|
|
41
42
|
import type { DomainModule } from "./codegen_dart.js";
|
|
43
|
+
import { relationType, user, key, anyUser } from "./relation_schema.js";
|
|
42
44
|
|
|
43
45
|
/** The referential marker of a directive prim (kernel `RefMode` wire string). */
|
|
44
46
|
export type RefMode = WireRefMode;
|
|
@@ -188,6 +190,37 @@ export interface UsdLayer {
|
|
|
188
190
|
/** Declared workspace invariants `{id, on}` the gate executes, sorted by id. */
|
|
189
191
|
readonly workspaceInvariants?: { id: string; on: string }[];
|
|
190
192
|
};
|
|
193
|
+
/**
|
|
194
|
+
* THE NATIVE ZANZIBAR RELATION SCHEMA (P1 — the userset-rewrite law as PURE DATA). Present IFF the
|
|
195
|
+
* layer declares ≥1 `relationType`: the law's EXPLICIT opt-in to the recursive Check/Expand gate.
|
|
196
|
+
* The Rust `RewriteEngine` READS this AST (per-type relation rewrites + fail-closed type restrictions
|
|
197
|
+
* + the schema-declared depth/fanout caps); the JS lump never runs it. OMITTED for relation-free law
|
|
198
|
+
* (byte-identical hash — the exact `nomosInvariantGate` era discipline; pre-feature chains replay
|
|
199
|
+
* green, the relation gate inert). Keys SORTED for byte-determinism (the layered flatten reproduces
|
|
200
|
+
* these bytes; see usd_layers.ts). Relation refs are NAMES in P1; the #58 stable-id swap is the
|
|
201
|
+
* localized follow-up.
|
|
202
|
+
*/
|
|
203
|
+
readonly nomosRelations?: {
|
|
204
|
+
readonly v: 1;
|
|
205
|
+
/** typeName → { relations: {relName → {rewrite, allowed}}, caps? } — keys sorted. */
|
|
206
|
+
readonly types: Record<
|
|
207
|
+
string,
|
|
208
|
+
{
|
|
209
|
+
readonly relations: Record<string, { readonly rewrite: unknown; readonly allowed: unknown[] }>;
|
|
210
|
+
readonly caps?: { readonly maxDepth: number; readonly maxFanout: number };
|
|
211
|
+
}
|
|
212
|
+
>;
|
|
213
|
+
};
|
|
214
|
+
/**
|
|
215
|
+
* THE NATIVE ZANZIBAR WRITE-AUTHZ GATE (P4). Present IFF ≥1 directive declares `.requiresRelation`:
|
|
216
|
+
* the one gate (`executor::admit`) refuses an author who does not HOLD the relation the directive
|
|
217
|
+
* requires (`engine.check` over the folded tuple store). `requires` maps directiveId → {relation,
|
|
218
|
+
* object}. OMITTED for relation-gate-free law (the era rule — byte-identical hash). Keys sorted.
|
|
219
|
+
*/
|
|
220
|
+
readonly nomosRelationGate?: {
|
|
221
|
+
readonly v: 1;
|
|
222
|
+
readonly requires: Record<string, { readonly relation: string; readonly object: string }>;
|
|
223
|
+
};
|
|
191
224
|
/**
|
|
192
225
|
* STABLE IDENTIFIERS (#58 — names are labels, identity is minted). The layer's
|
|
193
226
|
* domain/aggregate/field sids + label lineages, carried verbatim from the canonical
|
|
@@ -214,15 +247,17 @@ export interface UsdLayer {
|
|
|
214
247
|
readonly directives: Record<string, string[]>;
|
|
215
248
|
};
|
|
216
249
|
/**
|
|
217
|
-
* THE READ-VISIBILITY GATE DECLARATION (
|
|
250
|
+
* THE READ-VISIBILITY GATE DECLARATION (the native-relations read gate).
|
|
218
251
|
* Present IFF ≥1 aggregate of this layer is declared PRIVATE (`visibility:
|
|
219
252
|
* requires(cap)`): the law's EXPLICIT opt-in to the githolon gating its own
|
|
220
253
|
* reads. The one wasm read boundary serves a read touching a private type ONLY to
|
|
221
|
-
* a principal that
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
254
|
+
* a principal that holds the read RELATION `cap` on the workspace-scope object —
|
|
255
|
+
* decided by the native `RewriteEngine.check` over this workspace's own folded
|
|
256
|
+
* relation tuples (the compiler ALSO synthesizes a `workspace#<cap>` relation into
|
|
257
|
+
* `nomosRelations`). OMITTED for all-public law (byte-identical hash — the EXACT
|
|
258
|
+
* `nomosReadGate`/`nomosInvariantGate` era discipline; every pre-feature chain
|
|
259
|
+
* serves reads exactly as before, and a public type is never gated). Keyed by
|
|
260
|
+
* aggregate TYPE name → required capability (= read relation).
|
|
226
261
|
*/
|
|
227
262
|
readonly nomosReadVisibility?: {
|
|
228
263
|
readonly v: 1;
|
|
@@ -285,7 +320,10 @@ function encodeModuleToPrims(layerPath: string, mod: DomainModule): UsdPrim[] {
|
|
|
285
320
|
path: `${layerPath}/${d.id}`,
|
|
286
321
|
target: d.target,
|
|
287
322
|
marker: d.marker,
|
|
288
|
-
|
|
323
|
+
// The legacy RBAC capability set is GONE (full-native teardown) — native write-authz is the
|
|
324
|
+
// `nomosRelationGate` era key (a directive's `.requires(relation)`), read by the one gate, not
|
|
325
|
+
// this prim field. Kept empty so the #137 layer-composition shape is unchanged.
|
|
326
|
+
requires: [],
|
|
289
327
|
// Carry the declared law boundary straight from the canonical manifest, which
|
|
290
328
|
// already applies omit-when-empty: `reads`/`emits` are present here ONLY when the
|
|
291
329
|
// directive declared them, so a boundary-free prim is byte-identical to before.
|
|
@@ -300,6 +338,75 @@ function encodeModuleToPrims(layerPath: string, mod: DomainModule): UsdPrim[] {
|
|
|
300
338
|
return sortPrims(prims);
|
|
301
339
|
}
|
|
302
340
|
|
|
341
|
+
/**
|
|
342
|
+
* Lower a layer's `relationType` decls into the canonical `nomosRelations` wire object (P1) —
|
|
343
|
+
* keys SORTED for byte-determinism, omit-when-empty (the era rule). Multiple decls for one type
|
|
344
|
+
* MERGE their relations (a relation-name collision is an author error); a caps conflict on one
|
|
345
|
+
* type is an error. Rewrite-child ORDER is authored (semantic); `allowed` is sorted (it is a set).
|
|
346
|
+
*/
|
|
347
|
+
function serializeRelationSchema(
|
|
348
|
+
decls: readonly import("./relation_schema.js").RelationTypeDecl[],
|
|
349
|
+
): UsdLayer["nomosRelations"] {
|
|
350
|
+
if (decls.length === 0) return undefined;
|
|
351
|
+
type AllowedW = { kind: string; type?: string; ns?: string; relation?: string };
|
|
352
|
+
const allowedKey = (a: AllowedW): string =>
|
|
353
|
+
a.kind === "direct" ? `direct:${a.type}` : a.kind === "wildcard" ? "wildcard" : `userset:${a.type}#${a.relation}`;
|
|
354
|
+
const byType = new Map<string, { relations: Map<string, { rewrite: unknown; allowed: unknown[] }>; caps?: { maxDepth: number; maxFanout: number } }>();
|
|
355
|
+
for (const d of decls) {
|
|
356
|
+
let entry = byType.get(d.type);
|
|
357
|
+
if (entry === undefined) {
|
|
358
|
+
entry = { relations: new Map(), ...(d.caps ? { caps: d.caps } : {}) };
|
|
359
|
+
byType.set(d.type, entry);
|
|
360
|
+
} else if (d.caps !== undefined) {
|
|
361
|
+
if (entry.caps !== undefined && (entry.caps.maxDepth !== d.caps.maxDepth || entry.caps.maxFanout !== d.caps.maxFanout)) {
|
|
362
|
+
throw new Error(`relationType("${d.type}"): conflicting caps across declarations`);
|
|
363
|
+
}
|
|
364
|
+
entry.caps = d.caps;
|
|
365
|
+
}
|
|
366
|
+
for (const [rel, def] of d.relations) {
|
|
367
|
+
if (entry.relations.has(rel)) throw new Error(`relationType("${d.type}"): relation "${rel}" declared in two relationType() blocks`);
|
|
368
|
+
const allowed = [...def.allowed].sort((x, y) => (allowedKey(x as AllowedW) < allowedKey(y as AllowedW) ? -1 : 1));
|
|
369
|
+
entry.relations.set(rel, { rewrite: def.rewrite, allowed });
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const types: Record<string, { relations: Record<string, { rewrite: unknown; allowed: unknown[] }>; caps?: { maxDepth: number; maxFanout: number } }> = {};
|
|
373
|
+
for (const type of [...byType.keys()].sort()) {
|
|
374
|
+
const entry = byType.get(type)!;
|
|
375
|
+
const relations: Record<string, { rewrite: unknown; allowed: unknown[] }> = {};
|
|
376
|
+
for (const rel of [...entry.relations.keys()].sort()) relations[rel] = entry.relations.get(rel)!;
|
|
377
|
+
types[type] = { relations, ...(entry.caps ? { caps: entry.caps } : {}) };
|
|
378
|
+
}
|
|
379
|
+
return { v: 1 as const, types };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Synthesize the NATIVE read relations a layer's private aggregates imply. Each distinct
|
|
384
|
+
* `requires(cap)` becomes ONE relation `cap` on the workspace-scope object, allowing the leaf
|
|
385
|
+
* subject classes a read principal can be (`user:<uid>`, the `key:<hash>` control-plane subject,
|
|
386
|
+
* and the `user:*` public wildcard). The read gate's `RewriteEngine.check(workspace:self, cap,
|
|
387
|
+
* Subject::parse(principal))` resolves THIS relation against the tuples a `relateSubject` grant
|
|
388
|
+
* folds — so a granted reader is admitted and a non-grantee fail-closes (no tuple). Pure-direct
|
|
389
|
+
* `this` rewrite (a flat membership relation — the read-cap is just "who was granted it").
|
|
390
|
+
*/
|
|
391
|
+
function synthesizeReadRelationTypes(
|
|
392
|
+
manifest: CanonicalManifest,
|
|
393
|
+
): import("./relation_schema.js").RelationTypeDecl[] {
|
|
394
|
+
const caps = new Set<string>();
|
|
395
|
+
for (const a of manifest.aggregates) {
|
|
396
|
+
if (a.requiresRead !== undefined) caps.add(a.requiresRead);
|
|
397
|
+
}
|
|
398
|
+
if (caps.size === 0) return [];
|
|
399
|
+
const builder = relationType("workspace");
|
|
400
|
+
// Sorted for byte-determinism (the serializer also sorts, but keep the source stable). PURE
|
|
401
|
+
// DIRECT (`rewrite` omitted ⇒ `null`, not `self()`): a flat membership relation — the read-cap is
|
|
402
|
+
// exactly "who was granted it". (`self()`/`Rewrite::This` inside a rewrite expands to an EMPTY
|
|
403
|
+
// leaf in v1, which would zero the encryption fan-out roster — the direct form is what Expand reads.)
|
|
404
|
+
for (const cap of [...caps].sort()) {
|
|
405
|
+
builder.relation(cap, { allow: [user(), key(), anyUser()] });
|
|
406
|
+
}
|
|
407
|
+
return [builder];
|
|
408
|
+
}
|
|
409
|
+
|
|
303
410
|
/**
|
|
304
411
|
* Encode each `{ path, module }` into a `UsdLayer` of prims under that path, returning
|
|
305
412
|
* the canonical `UsdDocument`: layers SORTED by path, prims SORTED by path.
|
|
@@ -323,6 +430,32 @@ export function emitUsd(
|
|
|
323
430
|
const wsInvariants = [...(l.module.workspaceInvariants ?? [])]
|
|
324
431
|
.map((w) => ({ id: w.id, on: w.on }))
|
|
325
432
|
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
433
|
+
// THE READ-VISIBILITY → NATIVE RELATION lowering (the one-model teardown): a private
|
|
434
|
+
// aggregate's `requires(cap)` IS a native read relation on the workspace-scope object. Each
|
|
435
|
+
// distinct read cap synthesizes a `relationType("workspace").relation(cap, { allow:[user, key,
|
|
436
|
+
// anyUser] })` so the ONE evaluator (`RewriteEngine.check`) resolves it at the read gate — the
|
|
437
|
+
// SAME relation a `relateSubject({ object:"workspace:self", relation:cap, ... })` grant folds
|
|
438
|
+
// into. The `nomosReadVisibility` key (type → cap) is RETAINED as the type-to-relation map; the
|
|
439
|
+
// synthesized schema is what makes that cap a checkable relation. SORTED + omit-when-empty.
|
|
440
|
+
const readRelationTypes = synthesizeReadRelationTypes(manifest);
|
|
441
|
+
// THE RELATION SCHEMA (P1): per-type rewrites lowered to the nomosRelations AST, UNIONED with the
|
|
442
|
+
// synthesized read relations. SORTED keys (byte-determinism — the layered flatten reproduces these
|
|
443
|
+
// bytes); omit-when-empty (era rule). A read-cap name colliding with an authored `workspace`
|
|
444
|
+
// relation is an author error (caught in the merge).
|
|
445
|
+
const nomosRelations = serializeRelationSchema([
|
|
446
|
+
...(l.module.relationTypes ?? []),
|
|
447
|
+
...readRelationTypes,
|
|
448
|
+
]);
|
|
449
|
+
// THE RELATION GATE (P4): directives declaring `.requiresRelation` → directiveId → {relation,
|
|
450
|
+
// object}. SORTED keys; omit-when-empty (era rule — a relation-gate-free layer is byte-identical).
|
|
451
|
+
const relationGateReq: Record<string, { relation: string; object: string }> = {};
|
|
452
|
+
for (const d of [...l.module.directives].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))) {
|
|
453
|
+
if (d.requiresRelationGate !== undefined) {
|
|
454
|
+
relationGateReq[d.id] = { relation: d.requiresRelationGate.relation, object: d.requiresRelationGate.object };
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
const nomosRelationGate =
|
|
458
|
+
Object.keys(relationGateReq).length > 0 ? { v: 1 as const, requires: relationGateReq } : undefined;
|
|
326
459
|
// THE CAPTURED-READ GATE DECLARATION (#58): collected from the canonical
|
|
327
460
|
// manifest's per-directive `capturedReads` (omit-when-empty — read-free law's
|
|
328
461
|
// USD is byte-identical apart from the always-on stableIds key).
|
|
@@ -380,6 +513,8 @@ export function emitUsd(
|
|
|
380
513
|
},
|
|
381
514
|
}
|
|
382
515
|
: {}),
|
|
516
|
+
...(nomosRelations !== undefined ? { nomosRelations } : {}),
|
|
517
|
+
...(nomosRelationGate !== undefined ? { nomosRelationGate } : {}),
|
|
383
518
|
...(Object.keys(visAggregates).length > 0
|
|
384
519
|
? { nomosReadVisibility: { v: 1 as const, aggregates: visAggregates } }
|
|
385
520
|
: {}),
|
package/src/usd_layers.ts
CHANGED
|
@@ -365,6 +365,12 @@ export function usdaLayerText(
|
|
|
365
365
|
...(layer.nomosEncryption !== undefined
|
|
366
366
|
? [` custom string nomos:encryptionHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosEncryption)))}`]
|
|
367
367
|
: []),
|
|
368
|
+
...(layer.nomosRelations !== undefined
|
|
369
|
+
? [` custom string nomos:relationsHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosRelations)))}`]
|
|
370
|
+
: []),
|
|
371
|
+
...(layer.nomosRelationGate !== undefined
|
|
372
|
+
? [` custom string nomos:relationGateHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosRelationGate)))}`]
|
|
373
|
+
: []),
|
|
368
374
|
``,
|
|
369
375
|
...body,
|
|
370
376
|
` }`,
|
|
@@ -549,6 +555,35 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
|
|
|
549
555
|
encryption = { v: 1, aggregates: aggregatesRec };
|
|
550
556
|
}
|
|
551
557
|
}
|
|
558
|
+
// THE RELATION SCHEMA (P1): per-type relation defs merged later-wins across the stack,
|
|
559
|
+
// SORTED keys (matching the composed emission). Omit-when-empty (the nomosRelations era rule).
|
|
560
|
+
let relations: UsdLayer["nomosRelations"];
|
|
561
|
+
{
|
|
562
|
+
const t = new Map<string, NonNullable<UsdLayer["nomosRelations"]>["types"][string]>();
|
|
563
|
+
for (const l of stack) {
|
|
564
|
+
if (l.nomosRelations === undefined) continue;
|
|
565
|
+
for (const [type, spec] of Object.entries(l.nomosRelations.types)) t.set(type, spec);
|
|
566
|
+
}
|
|
567
|
+
if (t.size > 0) {
|
|
568
|
+
const typesRec: NonNullable<UsdLayer["nomosRelations"]>["types"] = {};
|
|
569
|
+
for (const k of [...t.keys()].sort()) typesRec[k] = t.get(k)!;
|
|
570
|
+
relations = { v: 1, types: typesRec };
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
// THE RELATION GATE (P4): directiveId → {relation, object} merged later-wins, SORTED. Omit-when-empty.
|
|
574
|
+
let relationGate: UsdLayer["nomosRelationGate"];
|
|
575
|
+
{
|
|
576
|
+
const r = new Map<string, { relation: string; object: string }>();
|
|
577
|
+
for (const l of stack) {
|
|
578
|
+
if (l.nomosRelationGate === undefined) continue;
|
|
579
|
+
for (const [dir, spec] of Object.entries(l.nomosRelationGate.requires)) r.set(dir, spec);
|
|
580
|
+
}
|
|
581
|
+
if (r.size > 0) {
|
|
582
|
+
const requiresRec: Record<string, { relation: string; object: string }> = {};
|
|
583
|
+
for (const k of [...r.keys()].sort()) requiresRec[k] = r.get(k)!;
|
|
584
|
+
relationGate = { v: 1, requires: requiresRec };
|
|
585
|
+
}
|
|
586
|
+
}
|
|
552
587
|
return {
|
|
553
588
|
path,
|
|
554
589
|
prims: [...prims.keys()].sort().map((p) => prims.get(p)!),
|
|
@@ -559,6 +594,8 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
|
|
|
559
594
|
...(readGate !== undefined ? { nomosReadGate: readGate } : {}),
|
|
560
595
|
...(readVisibility !== undefined ? { nomosReadVisibility: readVisibility } : {}),
|
|
561
596
|
...(encryption !== undefined ? { nomosEncryption: encryption } : {}),
|
|
597
|
+
...(relations !== undefined ? { nomosRelations: relations } : {}),
|
|
598
|
+
...(relationGate !== undefined ? { nomosRelationGate: relationGate } : {}),
|
|
562
599
|
};
|
|
563
600
|
});
|
|
564
601
|
|
|
@@ -680,6 +717,8 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
680
717
|
const readGateByDomain = new Map<string, UsdLayer["nomosReadGate"]>();
|
|
681
718
|
const readVisibilityByDomain = new Map<string, UsdLayer["nomosReadVisibility"]>();
|
|
682
719
|
const encryptionByDomain = new Map<string, UsdLayer["nomosEncryption"]>();
|
|
720
|
+
const relationsByDomain = new Map<string, UsdLayer["nomosRelations"]>();
|
|
721
|
+
const relationGateByDomain = new Map<string, UsdLayer["nomosRelationGate"]>();
|
|
683
722
|
for (const p of prims) {
|
|
684
723
|
if (p.attrs.get("nomos:kind") === "domain") {
|
|
685
724
|
const domain = attrString(p, "nomos:domain");
|
|
@@ -703,6 +742,16 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
703
742
|
if (typeof encHex === "string") {
|
|
704
743
|
encryptionByDomain.set(domain, JSON.parse(unhexUtf8(encHex)) as UsdLayer["nomosEncryption"]);
|
|
705
744
|
}
|
|
745
|
+
// THE RELATION SCHEMA (P1): the hex-carried per-type relation rewrite map.
|
|
746
|
+
const relHex = p.attrs.get("nomos:relationsHex");
|
|
747
|
+
if (typeof relHex === "string") {
|
|
748
|
+
relationsByDomain.set(domain, JSON.parse(unhexUtf8(relHex)) as UsdLayer["nomosRelations"]);
|
|
749
|
+
}
|
|
750
|
+
// THE RELATION GATE (P4): the hex-carried directiveId → {relation, object} map.
|
|
751
|
+
const relGateHex = p.attrs.get("nomos:relationGateHex");
|
|
752
|
+
if (typeof relGateHex === "string") {
|
|
753
|
+
relationGateByDomain.set(domain, JSON.parse(unhexUtf8(relGateHex)) as UsdLayer["nomosRelationGate"]);
|
|
754
|
+
}
|
|
706
755
|
}
|
|
707
756
|
}
|
|
708
757
|
|
|
@@ -794,6 +843,8 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
794
843
|
const rg = readGateByDomain.get(domain);
|
|
795
844
|
const rv = readVisibilityByDomain.get(domain);
|
|
796
845
|
const enc = encryptionByDomain.get(domain);
|
|
846
|
+
const rel = relationsByDomain.get(domain);
|
|
847
|
+
const relGate = relationGateByDomain.get(domain);
|
|
797
848
|
return {
|
|
798
849
|
path: `/Nomos/${domain}`,
|
|
799
850
|
prims: [...b.prims].sort(byPath),
|
|
@@ -804,6 +855,8 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
804
855
|
...(rg !== undefined ? { nomosReadGate: rg } : {}),
|
|
805
856
|
...(rv !== undefined ? { nomosReadVisibility: rv } : {}),
|
|
806
857
|
...(enc !== undefined ? { nomosEncryption: enc } : {}),
|
|
858
|
+
...(rel !== undefined ? { nomosRelations: rel } : {}),
|
|
859
|
+
...(relGate !== undefined ? { nomosRelationGate: relGate } : {}),
|
|
807
860
|
};
|
|
808
861
|
})
|
|
809
862
|
.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|