@githolon/dsl 0.101.6 → 0.102.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/dart/CHANGELOG.md +13 -0
- package/dart/lib/nomos_client.dart +3 -52
- package/dart/lib/src/dev_cloud.dart +5 -0
- package/dart/lib/src/home.dart +10 -15
- package/dart/lib/src/nms_conformance_runtime.dart +12 -0
- package/dart/lib/src/nms_generated_runtime.dart +29 -0
- package/dart/lib/src/nms_test_environment.dart +8 -0
- package/dart/lib/src/node_runner_transport.dart +56 -18
- package/dart/lib/src/provider.dart +5 -1
- package/dart/lib/src/record_origin.dart +55 -0
- package/dart/lib/src/runner/node-entry.mjs +9105 -0
- package/dart/lib/src/runner/offline-install.mjs +4729 -0
- package/dart/lib/src/session_bound.dart +23 -10
- package/dart/lib/src/source_trust.dart +51 -0
- package/dart/lib/src/test_harness.dart +490 -58
- package/dart/lib/src/webview_bridge.dart +167 -58
- package/dart/lib/src/workspace_name.dart +7 -1
- package/dart/pubspec.lock +3 -3
- package/dart/pubspec.yaml +4 -2
- package/dart/test/acquire_session_credentials_test.dart +34 -17
- package/dart/test/acquire_session_test.dart +109 -24
- package/dart/test/adv_realm_interference_test.dart +227 -99
- package/dart/test/bundle_version_pairing_test.dart +1 -1
- package/dart/test/cloud_bound_topology_test.dart +1 -1
- package/dart/test/cluster_attested_test.dart +52 -35
- package/dart/test/cluster_births_test.dart +5 -2
- package/dart/test/cluster_device_keyed_acceptance_test.dart +5 -2
- package/dart/test/cluster_local_autosync_test.dart +5 -2
- package/dart/test/cluster_scale_test.dart +5 -2
- package/dart/test/compat_test.dart +1 -1
- package/dart/test/dlq_session_surface_test.dart +2 -1
- package/dart/test/errors_test.dart +1 -1
- package/dart/test/first_app_gauntlet_test.dart +78 -51
- package/dart/test/four_layer_nested_topology_test.dart +1 -1
- package/dart/test/harness_phase_timeout_test.dart +1 -1
- package/dart/test/home_parity_test.dart +9 -5
- package/dart/test/local_sync_inert_test.dart +22 -14
- package/dart/test/node_runner_transport_test.dart +9 -5
- package/dart/test/null_slot_test.dart +1 -1
- package/dart/test/parent_signer_recovery_test.dart +5 -2
- package/dart/test/schema_validation_test.dart +1 -1
- package/dart/test/sealing_client_test.dart +2 -1
- package/dart/test/session_bound_test.dart +92 -119
- package/dart/test/stale_bundle_test.dart +1 -1
- package/dart/test/test_harness_test.dart +77 -22
- package/dart/test/vo_read_typing_test.dart +32 -14
- package/dart/test/watch_after_offer_stall_test.dart +2 -1
- package/dart/test/webview_bridge_test.dart +17 -4
- package/dart/test/wire_test.dart +1 -1
- package/dart_types/CHANGELOG.md +8 -0
- package/dart_types/lib/nomos_types.dart +7 -6
- package/dart_types/lib/src/activity.dart +23 -13
- package/dart_types/lib/src/foreign_ref.dart +61 -112
- package/dart_types/lib/src/invitation.dart +96 -0
- package/dart_types/lib/src/nomos_ref.dart +97 -59
- package/dart_types/pubspec.yaml +1 -1
- package/dart_types/test/surfaced_test.dart +125 -19
- package/package.json +7 -3
- package/runtime/generated-application-host.js +8851 -0
- package/src/activity.ts +2 -2
- package/src/aggregate.ts +24 -1
- package/src/application_composition.ts +282 -0
- package/src/application_topology.ts +151 -0
- package/src/attested_core.ts +2 -2
- package/src/authoring_context.ts +9 -0
- package/src/birth_meter_ensure.ts +1 -1
- package/src/births.ts +53 -5
- package/src/build_package.ts +79 -71
- package/src/business_birth.ts +93 -10
- package/src/clear_directive.ts +3 -1
- package/src/codegen_dart.ts +369 -420
- package/src/codegen_dart_application.ts +1508 -0
- package/src/codegen_dart_client.ts +156 -95
- package/src/codegen_dart_scenario.ts +299 -0
- package/src/codegen_openapi.ts +8 -3
- package/src/codegen_proof.ts +1113 -1137
- package/src/codegen_provider_dart.ts +12 -1
- package/src/codegen_ts.ts +1176 -496
- package/src/compat_package_main.ts +22 -10
- package/src/compile_package_main.ts +1536 -337
- package/src/contract.ts +10 -4
- package/src/dart_barrel_guard.ts +5 -1
- package/src/describe_model.ts +13 -5
- package/src/directive.ts +244 -5
- package/src/engine_entry.ts +56 -13
- package/src/engine_javascript.ts +31 -0
- package/src/evolve_preview.ts +3 -12
- package/src/framework/application_home.ts +24 -10
- package/src/framework/capability.ts +15 -1
- package/src/framework/domain_lifecycle.ts +8 -0
- package/src/framework/framework_aggregates.ts +2 -2
- package/src/framework/genesis.ts +11 -1
- package/src/framework/home.ts +178 -75
- package/src/framework/impure_capability.ts +24 -7
- package/src/framework/relations.ts +13 -5
- package/src/framework/workspaces.ts +13 -12
- package/src/generated_client_names.ts +42 -3
- package/src/idof_lint.ts +65 -13
- package/src/index.ts +46 -12
- package/src/intent.ts +587 -0
- package/src/law_check.ts +64 -16
- package/src/precondition.ts +16 -4
- package/src/read_projection_namespace.ts +2 -7
- package/src/recipient.ts +33 -0
- package/src/relation_schema.ts +93 -0
- package/src/secondary_ensure.ts +11 -2
- package/src/shape_compat.ts +11 -1
- package/src/usd.ts +30 -8
- package/src/usd_layers.ts +447 -246
- package/src/usd_state.ts +1 -1
- package/src/value_object.ts +49 -13
- package/src/wire_encode.ts +124 -26
- package/src/workspace_sharing.ts +269 -0
- package/src/zod_introspection.ts +84 -0
- package/dart/example/nomos_secure_key_store.dart +0 -82
- package/dart/example/nomos_webview_transport.dart +0 -63
- package/dart/lib/nomos_headless.dart +0 -50
- package/dart/lib/testing.dart +0 -14
- package/dart/test/co2_product_gauntlet_test.dart +0 -244
- package/src/framework/identity.ts +0 -537
package/src/evolve_preview.ts
CHANGED
|
@@ -36,6 +36,7 @@ import { domainInterface } from "./shape_compat.js";
|
|
|
36
36
|
import type { StableIds } from "./stable_ids_types.js";
|
|
37
37
|
import type { DomainModule } from "./codegen_dart.js";
|
|
38
38
|
import type { WireSchema } from "./wire.js";
|
|
39
|
+
import { parseUsdaDocument } from "./usd_layers.js";
|
|
39
40
|
|
|
40
41
|
// ── the normalized law shape both sides of the diff speak ──────────────────────────
|
|
41
42
|
|
|
@@ -142,20 +143,10 @@ export interface DeployJson {
|
|
|
142
143
|
packageUsda?: string;
|
|
143
144
|
}
|
|
144
145
|
|
|
145
|
-
/**
|
|
146
|
-
* the SAME substring contract executor/src/law58.rs::usd_json_from_package uses). */
|
|
146
|
+
/** Reconstruct the effective law directly from a native `.package.usda` stage. */
|
|
147
147
|
export function usdJsonFromPackage(bundle: string): unknown | undefined {
|
|
148
|
-
const key = 'nomos:usdJsonHex = "';
|
|
149
|
-
const start = bundle.indexOf(key);
|
|
150
|
-
if (start < 0) return undefined;
|
|
151
|
-
const from = start + key.length;
|
|
152
|
-
const end = bundle.indexOf('"', from);
|
|
153
|
-
if (end < 0) return undefined;
|
|
154
|
-
const hex = bundle.slice(from, end);
|
|
155
|
-
if (hex.length % 2 !== 0) return undefined;
|
|
156
148
|
try {
|
|
157
|
-
|
|
158
|
-
return JSON.parse(bytes.toString("utf8"));
|
|
149
|
+
return parseUsdaDocument(bundle);
|
|
159
150
|
} catch {
|
|
160
151
|
return undefined;
|
|
161
152
|
}
|
|
@@ -22,17 +22,22 @@ import {
|
|
|
22
22
|
import { GenesisWarrant, seedGenesis } from "./genesis.js";
|
|
23
23
|
import {
|
|
24
24
|
BirthAttestation,
|
|
25
|
-
|
|
25
|
+
ReceivingGroup,
|
|
26
|
+
ReceivingGroupPolicy,
|
|
26
27
|
SharedWithMe,
|
|
27
28
|
birthAttestation,
|
|
28
|
-
|
|
29
|
-
homeNameBindsOwner,
|
|
30
|
-
homeRelations,
|
|
29
|
+
birthReceivingGroupForDomain,
|
|
31
30
|
parentAttestedBirth,
|
|
32
31
|
pruneShare,
|
|
32
|
+
receivingGroupNameBindsPrincipal,
|
|
33
|
+
receivingGroupAdditionalDeviceFollowsPolicy,
|
|
34
|
+
receivingGroupRejectsKeylessAdditionalDevice,
|
|
35
|
+
receivingGroupRelations,
|
|
36
|
+
receivingGroupRevocationFollowsPolicy,
|
|
33
37
|
recordShare,
|
|
34
38
|
seedBirthAttestation,
|
|
35
|
-
|
|
39
|
+
seedReceivingGroup,
|
|
40
|
+
seedReceivingGroupPolicy,
|
|
36
41
|
sharedByWorkspace,
|
|
37
42
|
} from "./home.js";
|
|
38
43
|
import { RelationTuple, relationTuplesByObject } from "./relations.js";
|
|
@@ -46,6 +51,8 @@ import {
|
|
|
46
51
|
enrollSignerSelf,
|
|
47
52
|
enrollSignerSelfRequiresVerified,
|
|
48
53
|
identityRelations,
|
|
54
|
+
revokeSigner,
|
|
55
|
+
revokeSignerEpochStrictlyIncreases,
|
|
49
56
|
signerDelegationByUser,
|
|
50
57
|
signerEpochCurrent,
|
|
51
58
|
signerEpochStrictlyIncreases,
|
|
@@ -63,11 +70,16 @@ const directive = <P>(value: Directive<P>): Internal<Directive<P>> => internal(v
|
|
|
63
70
|
const query = (value: QueryDecl): Internal<QueryDecl> => internal(value);
|
|
64
71
|
|
|
65
72
|
// Parent primitive + the law its born home uses.
|
|
66
|
-
export const
|
|
67
|
-
export const
|
|
68
|
-
export const
|
|
69
|
-
export const
|
|
70
|
-
export const
|
|
73
|
+
export const __nomos_birthReceivingGroup = directive(birthReceivingGroupForDomain(APPLICATION_HOME_DOMAIN));
|
|
74
|
+
export const __nomos_ReceivingGroup = aggregate(ReceivingGroup);
|
|
75
|
+
export const __nomos_seedReceivingGroup = directive(seedReceivingGroup);
|
|
76
|
+
export const __nomos_ReceivingGroupPolicy = aggregate(ReceivingGroupPolicy);
|
|
77
|
+
export const __nomos_seedReceivingGroupPolicy = directive(seedReceivingGroupPolicy);
|
|
78
|
+
export const __nomos_receivingGroupNameBindsPrincipal = internal(receivingGroupNameBindsPrincipal);
|
|
79
|
+
export const __nomos_receivingGroupRejectsKeylessAdditionalDevice = internal(receivingGroupRejectsKeylessAdditionalDevice);
|
|
80
|
+
export const __nomos_receivingGroupAdditionalDeviceFollowsPolicy = internal(receivingGroupAdditionalDeviceFollowsPolicy);
|
|
81
|
+
export const __nomos_receivingGroupRevocationFollowsPolicy = internal(receivingGroupRevocationFollowsPolicy);
|
|
82
|
+
export const __nomos_receivingGroupRelations = internal(receivingGroupRelations);
|
|
71
83
|
|
|
72
84
|
// Subject-side discovery/share custody.
|
|
73
85
|
export const __nomos_SharedWithMe = aggregate(SharedWithMe);
|
|
@@ -89,6 +101,8 @@ export const __nomos_enrollSigner = directive(enrollSigner);
|
|
|
89
101
|
export const __nomos_enrollFirstDevice = directive(enrollFirstDevice);
|
|
90
102
|
export const __nomos_enrollSignerSelf = directive(enrollSignerSelf);
|
|
91
103
|
export const __nomos_enrollSignerSelfRequiresVerified = internal(enrollSignerSelfRequiresVerified);
|
|
104
|
+
export const __nomos_revokeSigner = directive(revokeSigner);
|
|
105
|
+
export const __nomos_revokeSignerEpochStrictlyIncreases = internal(revokeSignerEpochStrictlyIncreases);
|
|
92
106
|
export const __nomos_SignerEpoch = aggregate(SignerEpoch);
|
|
93
107
|
export const __nomos_signerEpochCurrent = query(signerEpochCurrent);
|
|
94
108
|
export const __nomos_advanceSignerEpoch = directive(advanceSignerEpoch);
|
|
@@ -175,6 +175,17 @@ export function capability<
|
|
|
175
175
|
Object.entries(receipt).map(([k, f]) => [k, f.zod]),
|
|
176
176
|
) as { [K in keyof Rec]: Rec[K]["zod"] };
|
|
177
177
|
const receiptEntries = Object.entries(receipt) as [string, Field][];
|
|
178
|
+
const decodeReceipt = (resultRef: string): Record<string, unknown> => {
|
|
179
|
+
const decoded = JSON.parse(resultRef) as unknown;
|
|
180
|
+
if (decoded === null || typeof decoded !== "object" || Array.isArray(decoded)) {
|
|
181
|
+
throw new Error("a typed capability receipt must encode a JSON object");
|
|
182
|
+
}
|
|
183
|
+
const values = decoded as Record<string, unknown>;
|
|
184
|
+
for (const [k, f] of receiptEntries) {
|
|
185
|
+
if (values[k] !== undefined) f.zod.parse(values[k]);
|
|
186
|
+
}
|
|
187
|
+
return values;
|
|
188
|
+
};
|
|
178
189
|
|
|
179
190
|
const quartet = impureCapability({
|
|
180
191
|
aggregateId,
|
|
@@ -198,8 +209,11 @@ export function capability<
|
|
|
198
209
|
// discipline), present keys must validate.
|
|
199
210
|
...(receiptEntries.length > 0
|
|
200
211
|
? {
|
|
212
|
+
completeResultSchema: z.string().refine((value) => {
|
|
213
|
+
try { decodeReceipt(value); return true; } catch { return false; }
|
|
214
|
+
}, "must encode a valid typed capability receipt JSON object"),
|
|
201
215
|
completeResult: (taskId: string, resultRef: string): PlannedOp[] => {
|
|
202
|
-
const decoded =
|
|
216
|
+
const decoded = decodeReceipt(resultRef);
|
|
203
217
|
const ops: PlannedOp[] = [];
|
|
204
218
|
for (const [k, f] of receiptEntries) {
|
|
205
219
|
if (decoded[k] === undefined) continue;
|
|
@@ -23,6 +23,14 @@ import { Lww, MapOf } from "../drivers.js";
|
|
|
23
23
|
import { attributionField, t } from "../fields.js";
|
|
24
24
|
import { directive } from "../directive.js";
|
|
25
25
|
import { set, withMarker, addToSet, type PlannedOp } from "../ops.js";
|
|
26
|
+
import { key, relationType, self, user } from "../relation_schema.js";
|
|
27
|
+
|
|
28
|
+
/** Framework-global authority to install or roll out a model in this workspace.
|
|
29
|
+
* The verified founder receives the first leaf in `seedGenesis`; every later
|
|
30
|
+
* delegation remains an ordinary, replayable Zanzibar edge. */
|
|
31
|
+
export const modelLifecycleRelations = relationType("workspace")
|
|
32
|
+
.asGlobal()
|
|
33
|
+
.relation("installDomain", { rewrite: self(), allow: [user(), key()] });
|
|
26
34
|
|
|
27
35
|
export const DOMAIN_INSTALLATION_PHASES = [
|
|
28
36
|
"Pending",
|
|
@@ -36,13 +36,13 @@ import { nomosSourceFrontierEnsure } from "../attested.js";
|
|
|
36
36
|
import * as birthMeter from "../birth_meter.js";
|
|
37
37
|
import * as birthMeterEnsure from "../birth_meter_ensure.js";
|
|
38
38
|
import * as birthMeterFramework from "./birth_meter.js";
|
|
39
|
+
import * as authProviderLaw from "./auth_provider_law.js";
|
|
39
40
|
import * as disclosure from "./disclosure.js";
|
|
40
41
|
import * as domainLifecycle from "./domain_lifecycle.js";
|
|
41
42
|
import * as encryptionIdentity from "./encryption_identity.js";
|
|
42
43
|
import * as encryptionScope from "./encryption_scope.js";
|
|
43
44
|
import * as genesis from "./genesis.js";
|
|
44
45
|
import * as home from "./home.js";
|
|
45
|
-
import * as identity from "./identity.js";
|
|
46
46
|
import * as ownedCustody from "./owned_custody.js";
|
|
47
47
|
import * as relations from "./relations.js";
|
|
48
48
|
import * as repair from "./repair.js";
|
|
@@ -59,13 +59,13 @@ const FRAMEWORK_NAMESPACES: ReadonlyArray<Record<string, unknown>> = [
|
|
|
59
59
|
birthMeter,
|
|
60
60
|
birthMeterEnsure,
|
|
61
61
|
birthMeterFramework,
|
|
62
|
+
authProviderLaw,
|
|
62
63
|
disclosure,
|
|
63
64
|
domainLifecycle,
|
|
64
65
|
encryptionIdentity,
|
|
65
66
|
encryptionScope,
|
|
66
67
|
genesis,
|
|
67
68
|
home,
|
|
68
|
-
identity,
|
|
69
69
|
ownedCustody,
|
|
70
70
|
relations,
|
|
71
71
|
repair,
|
package/src/framework/genesis.ts
CHANGED
|
@@ -32,6 +32,7 @@ import { t } from "../fields.js";
|
|
|
32
32
|
import { Lww } from "../drivers.js";
|
|
33
33
|
import { directive } from "../directive.js";
|
|
34
34
|
import { set, type PlannedOp } from "../ops.js";
|
|
35
|
+
import { writeTuple } from "./relations.js";
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
38
|
* Deterministic singleton id of the founding warrant — MUST equal
|
|
@@ -94,4 +95,13 @@ export const seedGenesis = directive("seedGenesis")
|
|
|
94
95
|
bornAt: z.string().min(1),
|
|
95
96
|
}),
|
|
96
97
|
)
|
|
97
|
-
.plan((p) =>
|
|
98
|
+
.plan((p) => [
|
|
99
|
+
...writeGenesisWarrant(p),
|
|
100
|
+
...writeTuple({
|
|
101
|
+
object: "workspace:self",
|
|
102
|
+
relation: "installDomain",
|
|
103
|
+
subject: /^(user|key):/.test(p.principal) ? p.principal : `user:${p.principal}`,
|
|
104
|
+
grantedBy: p.principal,
|
|
105
|
+
grantedAt: p.bornAt,
|
|
106
|
+
}),
|
|
107
|
+
]);
|
package/src/framework/home.ts
CHANGED
|
@@ -25,13 +25,13 @@
|
|
|
25
25
|
* the result, and the owner-authored lane to maintain it.)
|
|
26
26
|
*/
|
|
27
27
|
import { z } from "zod";
|
|
28
|
-
import { aggregate, instance } from "../aggregate.js";
|
|
28
|
+
import { aggregate, instance, OWNED_READ_RELATION } from "../aggregate.js";
|
|
29
29
|
// The device-key-hash wire shape (same validator signing_identity/workspaces use module-locally).
|
|
30
30
|
const sha256Hex = z.string().regex(/^[0-9a-f]{64}$/, "expected lowercase sha256 hex (the owner's device pubkey hash)");
|
|
31
31
|
import { t } from "../fields.js";
|
|
32
32
|
import { Lww } from "../drivers.js";
|
|
33
33
|
import { directive } from "../directive.js";
|
|
34
|
-
import {
|
|
34
|
+
import { ensure } from "../authoring.js";
|
|
35
35
|
import { set, type PlannedOp } from "../ops.js";
|
|
36
36
|
import { query } from "../query.js";
|
|
37
37
|
import { birth } from "../births.js";
|
|
@@ -43,6 +43,7 @@ import { writeTuple } from "./relations.js";
|
|
|
43
43
|
import { relationType, self, user, selfObject } from "../relation_schema.js";
|
|
44
44
|
import { requireParentAttestedBirth, type ParentAttestedBirthMarker } from "../births.js";
|
|
45
45
|
import { GenesisWarrant, seedGenesis } from "./genesis.js";
|
|
46
|
+
import { idOf } from "../value_object.js";
|
|
46
47
|
|
|
47
48
|
// `nomos.sha256` — the TRUSTLESS, pure hash computed IN the githolon (Rust sha2 bound in the sealed engine;
|
|
48
49
|
// see deterministic-rquickjs). Available as a global to plans + invariant predicates (like `nomos.mint`); it
|
|
@@ -60,56 +61,96 @@ export { identityRelations };
|
|
|
60
61
|
export { GenesisWarrant, seedGenesis };
|
|
61
62
|
|
|
62
63
|
/**
|
|
63
|
-
* THE
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
64
|
+
* THE RECEIVING GROUP — one sovereign framework identity whose enrolled devices clone this same holon.
|
|
65
|
+
* It is not a tenant Person, an IdP account or a host-owned account. The native global
|
|
66
|
+
* `identity:<principal>#signer@key:<hash>` relation is its device roster; this singleton binds the group's
|
|
67
|
+
* stable principal to the workspace that carries that roster and its access manifest.
|
|
68
|
+
*
|
|
69
|
+
* The wire identifiers are deliberately obscure framework names. Tenant business packages may model their
|
|
70
|
+
* own `ReceivingGroup`, `home` or `group` concepts without silently colliding with this implementation.
|
|
69
71
|
*/
|
|
70
|
-
export const
|
|
71
|
-
/** A
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
export const
|
|
75
|
-
export const
|
|
76
|
-
|
|
77
|
-
/** The identity this home belongs to — the canonical `user:<uid>` subject the §3 gate binds quotes to. */
|
|
72
|
+
export const RECEIVING_GROUP_ID = "nomos-internal-receiving-group-7c1f5e:self";
|
|
73
|
+
/** A receiving group's self-committed workspace-name prefix. */
|
|
74
|
+
export const RECEIVING_GROUP_NAME_PREFIX = "nomos-rg-";
|
|
75
|
+
export const RECEIVING_GROUP_NAME_HASH_LEN = 40;
|
|
76
|
+
export const RECEIVING_GROUP_POLICY_ID = "nomos-internal-receiving-group-policy-7c1f5e:self";
|
|
77
|
+
export const ReceivingGroup = aggregate("NomosInternalReceivingGroup7c1f5e", {
|
|
78
|
+
/** The stable Zanzibar principal represented by this group. Credentials may locate it but do not own it. */
|
|
78
79
|
principal: t.string().merge(Lww),
|
|
79
|
-
/**
|
|
80
|
-
|
|
81
|
-
homeName: t.string().merge(Lww),
|
|
80
|
+
/** The group's self-committed workspace name, deterministically bound to its stable principal. */
|
|
81
|
+
workspaceName: t.string().merge(Lww),
|
|
82
82
|
/** ISO-8601, caller-stamped (determinism — no clock in the plan). */
|
|
83
83
|
bornAt: t.string().merge(Lww),
|
|
84
|
-
}).public();
|
|
84
|
+
}, { cap: 1 }).public();
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* bootstrap
|
|
91
|
-
*
|
|
92
|
-
* self`, subject the owner) — the {@link homeNameBindsOwner} squat invariant already proves the owner is the
|
|
93
|
-
* rightful one, so no separate grant is needed.
|
|
87
|
+
* The framework-owned device lifecycle policy for one receiving group. This is deliberately data, not a
|
|
88
|
+
* client-side branch: every peer replays the same rule. The first supported posture is intentionally small
|
|
89
|
+
* and secure: a live, verified device may add or revoke another device; keyless IdP enrolment is only the
|
|
90
|
+
* pre-group bootstrap lane; all-device-loss recovery requires guardians and remains fail-closed until that
|
|
91
|
+
* proof ceremony is installed.
|
|
94
92
|
*/
|
|
95
|
-
export const
|
|
93
|
+
export const ReceivingGroupPolicy = aggregate("NomosInternalReceivingGroupPolicy7c1f5e", {
|
|
94
|
+
additionalDevice: t.string().merge(Lww),
|
|
95
|
+
revocation: t.string().merge(Lww),
|
|
96
|
+
recovery: t.string().merge(Lww),
|
|
97
|
+
guardianQuorum: t.int().merge(Lww),
|
|
98
|
+
setAt: t.string().merge(Lww),
|
|
99
|
+
}, { cap: 1 }).public();
|
|
100
|
+
|
|
101
|
+
export const DEFAULT_RECEIVING_GROUP_POLICY = {
|
|
102
|
+
additionalDevice: "current-device",
|
|
103
|
+
revocation: "current-device",
|
|
104
|
+
recovery: "guardian-quorum",
|
|
105
|
+
guardianQuorum: 1,
|
|
106
|
+
} as const;
|
|
107
|
+
|
|
108
|
+
function writeReceivingGroupPolicy(p: { setAt: string }): PlannedOp[] {
|
|
109
|
+
const policy = instance(ReceivingGroupPolicy, RECEIVING_GROUP_POLICY_ID);
|
|
110
|
+
return [
|
|
111
|
+
set(policy, "additionalDevice", DEFAULT_RECEIVING_GROUP_POLICY.additionalDevice),
|
|
112
|
+
set(policy, "revocation", DEFAULT_RECEIVING_GROUP_POLICY.revocation),
|
|
113
|
+
set(policy, "recovery", DEFAULT_RECEIVING_GROUP_POLICY.recovery),
|
|
114
|
+
set(policy, "guardianQuorum", DEFAULT_RECEIVING_GROUP_POLICY.guardianQuorum),
|
|
115
|
+
set(policy, "setAt", p.setAt),
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Genesis-only default policy. Policy changes become a separate, guardian-aware ceremony. */
|
|
120
|
+
export const seedReceivingGroupPolicy = directive("nomosInternalSeedReceivingGroupPolicy7c1f5e")
|
|
121
|
+
.ensures(ReceivingGroupPolicy)
|
|
122
|
+
.payload(z.object({ setAt: z.string().min(1) }))
|
|
123
|
+
.plan((p) => writeReceivingGroupPolicy(p));
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The group's controller relation binds its stable principal inside the same genesis. Business representation
|
|
127
|
+
* remains separate Zanzibar edges; this relation only establishes who controls this receiving-group holon.
|
|
128
|
+
*/
|
|
129
|
+
export const receivingGroupRelations = relationType("nomos.internal.receiving-group.7c1f5e").relation("controller", {
|
|
96
130
|
rewrite: self(),
|
|
97
131
|
allow: [user()],
|
|
98
132
|
});
|
|
99
133
|
|
|
100
|
-
/**
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const h = instance(HomeOwner, HOME_OWNER_ID);
|
|
134
|
+
/** Stamp the receiving group and atomically seat its controller relation. */
|
|
135
|
+
export function writeReceivingGroup(p: { principal: string; workspaceName: string; bornAt: string }): PlannedOp[] {
|
|
136
|
+
const h = instance(ReceivingGroup, RECEIVING_GROUP_ID);
|
|
104
137
|
return [
|
|
105
138
|
set(h, "principal", p.principal),
|
|
106
|
-
set(h, "
|
|
139
|
+
set(h, "workspaceName", p.workspaceName),
|
|
107
140
|
set(h, "bornAt", p.bornAt),
|
|
108
|
-
// ATOMIC ReBAC seat — the owner subject is the canonical `user:<uid>` on HomeOwner.principal; grantedBy is
|
|
109
|
-
// the owner itself (self-bound, no external granter) and grantedAt rides the caller-stamped bornAt.
|
|
110
141
|
...writeTuple({
|
|
111
|
-
object: selfObject(
|
|
112
|
-
relation: "
|
|
142
|
+
object: selfObject(receivingGroupRelations),
|
|
143
|
+
relation: "controller",
|
|
144
|
+
subject: p.principal,
|
|
145
|
+
grantedBy: p.principal,
|
|
146
|
+
grantedAt: p.bornAt,
|
|
147
|
+
}),
|
|
148
|
+
// A device clones the encrypted workspace before record-level ownership can be evaluated locally.
|
|
149
|
+
// Seat the group's controller for that ciphertext transport in the same genesis; individual owned
|
|
150
|
+
// records still require their own per-record reader edge before they decrypt or appear in reads.
|
|
151
|
+
...writeTuple({
|
|
152
|
+
object: "workspace:self",
|
|
153
|
+
relation: OWNED_READ_RELATION,
|
|
113
154
|
subject: p.principal,
|
|
114
155
|
grantedBy: p.principal,
|
|
115
156
|
grantedAt: p.bornAt,
|
|
@@ -118,16 +159,12 @@ export function writeHomeOwner(p: { principal: string; homeName: string; bornAt:
|
|
|
118
159
|
}
|
|
119
160
|
|
|
120
161
|
/**
|
|
121
|
-
*
|
|
122
|
-
* the fixed singleton id. It is authored by the birth/genesis chain and structurally refused on the open
|
|
123
|
-
* session lane. `.ensures` (a deterministic-keyed singleton). The `homeName`
|
|
124
|
-
* is caller-supplied but the {@link homeNameBindsOwner} invariant proves it `== home-<sha256(principal)>`,
|
|
125
|
-
* so a squatter cannot claim a name slot that doesn't hash to ITS own owner.
|
|
162
|
+
* Genesis-only receiving-group stamp. The name is caller-supplied but the invariant below re-derives it.
|
|
126
163
|
*/
|
|
127
|
-
export const
|
|
128
|
-
.ensures(
|
|
129
|
-
.payload(z.object({ principal: z.string().min(1),
|
|
130
|
-
.plan((p) =>
|
|
164
|
+
export const seedReceivingGroup = directive("nomosInternalSeedReceivingGroup7c1f5e")
|
|
165
|
+
.ensures(ReceivingGroup)
|
|
166
|
+
.payload(z.object({ principal: z.string().min(1), workspaceName: z.string().min(1), bornAt: z.string().min(1) }))
|
|
167
|
+
.plan((p) => writeReceivingGroup(p));
|
|
131
168
|
|
|
132
169
|
/**
|
|
133
170
|
* THE SQUAT INVARIANT (home_holons §5) — a home holon's self-committed `homeName` MUST be the hash of its
|
|
@@ -137,22 +174,62 @@ export const seedHomeOwner = directive("seedHomeOwner")
|
|
|
137
174
|
* never by a host check. The host only routes the chain to the name it commits to (custody). `nomos.sha256`
|
|
138
175
|
* re-derives the hash in-law on every peer, so the verdict is identical everywhere.
|
|
139
176
|
*/
|
|
140
|
-
export const
|
|
141
|
-
.on("
|
|
142
|
-
.reads(() => [refAs("
|
|
143
|
-
.assert(({
|
|
144
|
-
const
|
|
145
|
-
const expected =
|
|
146
|
-
|
|
177
|
+
export const receivingGroupNameBindsPrincipal = workspaceInvariant("ReceivingGroupNameBindsPrincipal")
|
|
178
|
+
.on("nomosInternalSeedReceivingGroup7c1f5e")
|
|
179
|
+
.reads(() => [refAs("group", "NomosInternalReceivingGroup7c1f5e", RECEIVING_GROUP_ID)])
|
|
180
|
+
.assert(({ group }) => {
|
|
181
|
+
const g = (group ?? {}) as { principal?: string; workspaceName?: string };
|
|
182
|
+
const expected = RECEIVING_GROUP_NAME_PREFIX
|
|
183
|
+
+ nomos.sha256(String(g.principal ?? "")).slice(0, RECEIVING_GROUP_NAME_HASH_LEN);
|
|
184
|
+
return g.workspaceName === expected
|
|
147
185
|
? { accept: true }
|
|
148
|
-
: { reject: "a
|
|
186
|
+
: { reject: "a receiving group's workspace name must hash its stable principal" };
|
|
149
187
|
});
|
|
150
188
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
189
|
+
const receivingGroupPolicyRefs = () => [
|
|
190
|
+
refAs("group", "NomosInternalReceivingGroup7c1f5e", RECEIVING_GROUP_ID),
|
|
191
|
+
refAs("policy", "NomosInternalReceivingGroupPolicy7c1f5e", RECEIVING_GROUP_POLICY_ID),
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
/** An IdP token may bootstrap the parent before the group exists; it may never bypass an existing group's
|
|
195
|
+
* device policy. A new group device must be approved by a current device or, later, guardian evidence. */
|
|
196
|
+
export const receivingGroupRejectsKeylessAdditionalDevice = workspaceInvariant("ReceivingGroupRejectsKeylessAdditionalDevice")
|
|
197
|
+
.on("enrollFirstDevice")
|
|
198
|
+
.reads(receivingGroupPolicyRefs)
|
|
199
|
+
.assert(({ group }) => typeof group?.principal !== "string" || group.principal.length === 0
|
|
200
|
+
? { accept: true }
|
|
201
|
+
: { reject: "receiving-group-additional-device-requires-current-device-or-guardian" });
|
|
202
|
+
|
|
203
|
+
/** The shipped additional-device policy: an already enrolled, verified device signs the self-bound enrol. */
|
|
204
|
+
export const receivingGroupAdditionalDeviceFollowsPolicy = workspaceInvariant("ReceivingGroupAdditionalDeviceFollowsPolicy")
|
|
205
|
+
.on("enrollSignerSelf")
|
|
206
|
+
.reads(receivingGroupPolicyRefs)
|
|
207
|
+
.declassify("device lifecycle policy intentionally branches on principal_verified")
|
|
208
|
+
.assert(({ group, policy }, ctx) => {
|
|
209
|
+
if (typeof group?.principal !== "string" || group.principal.length === 0) return { accept: true };
|
|
210
|
+
const p = policy as { additionalDevice?: string } | undefined;
|
|
211
|
+
return p?.additionalDevice === "current-device" && ctx?.principalVerified === true
|
|
212
|
+
? { accept: true }
|
|
213
|
+
: { reject: "receiving-group-additional-device-policy-refused" };
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
/** The shipped revocation policy: a live verified group device authors the remove-wins signer change. */
|
|
217
|
+
export const receivingGroupRevocationFollowsPolicy = workspaceInvariant("ReceivingGroupRevocationFollowsPolicy")
|
|
218
|
+
.on("revokeSigner")
|
|
219
|
+
.reads(receivingGroupPolicyRefs)
|
|
220
|
+
.declassify("device lifecycle policy intentionally branches on principal_verified")
|
|
221
|
+
.assert(({ group, policy }, ctx) => {
|
|
222
|
+
if (typeof group?.principal !== "string" || group.principal.length === 0) return { accept: true };
|
|
223
|
+
const p = policy as { revocation?: string } | undefined;
|
|
224
|
+
return p?.revocation === "current-device" && ctx?.principalVerified === true
|
|
225
|
+
? { accept: true }
|
|
226
|
+
: { reject: "receiving-group-revocation-policy-refused" };
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
/** Re-derive a receiving group's workspace name in-law. */
|
|
230
|
+
export function receivingGroupNameOf(principal: string): string {
|
|
231
|
+
return RECEIVING_GROUP_NAME_PREFIX
|
|
232
|
+
+ nomos.sha256(String(principal)).slice(0, RECEIVING_GROUP_NAME_HASH_LEN);
|
|
156
233
|
}
|
|
157
234
|
|
|
158
235
|
/**
|
|
@@ -184,7 +261,9 @@ export function homeNameOf(subject: string): string {
|
|
|
184
261
|
* so HomeOwner is seeded and `homeNameBindsOwner` passes (a birth without it would be an invalid, un-owned
|
|
185
262
|
* home).
|
|
186
263
|
*/
|
|
187
|
-
|
|
264
|
+
// The kernel's era-0 birth-seed resolver retains `home` as its fallback dispatch domain for standalone
|
|
265
|
+
// framework laws. Compiler-composed business applications use the obscure APPLICATION_HOME_DOMAIN instead.
|
|
266
|
+
export const RECEIVING_GROUP_LAW_DOMAIN = "home";
|
|
188
267
|
/**
|
|
189
268
|
* Build the private home-birth primitive for the domain key that carries the home law.
|
|
190
269
|
*
|
|
@@ -193,10 +272,12 @@ export const HOME_LAW_DOMAIN = "home";
|
|
|
193
272
|
* tenant remains free to model a business concept called "home". The key is closed over
|
|
194
273
|
* here and written into every child-genesis step; it is never a tenant payload field.
|
|
195
274
|
*/
|
|
196
|
-
export function
|
|
197
|
-
if (
|
|
198
|
-
|
|
199
|
-
|
|
275
|
+
export function birthReceivingGroupForDomain(groupLawDomain: string) {
|
|
276
|
+
if (groupLawDomain.length === 0) {
|
|
277
|
+
throw new Error("birthReceivingGroupForDomain requires a non-empty receiving-group law domain");
|
|
278
|
+
}
|
|
279
|
+
return directive("nomosInternalBirthReceivingGroup7c1f5e")
|
|
280
|
+
.creates(ReceivingGroup)
|
|
200
281
|
.payload(
|
|
201
282
|
z.object({
|
|
202
283
|
/** The identity this home belongs to (a `user:<uid>` subject). The workspace name + seeded homeName are
|
|
@@ -240,7 +321,7 @@ export function birthHomeForDomain(homeLawDomain: string) {
|
|
|
240
321
|
)
|
|
241
322
|
.plan((p) => {
|
|
242
323
|
const founder = p.owner; // the actor for the child's own genesis steps (its self-bound owner subject)
|
|
243
|
-
const
|
|
324
|
+
const groupName = receivingGroupNameOf(p.owner);
|
|
244
325
|
// THE PARENT'S DECLARED VERIFIER (one captured read serves BOTH consumers below): the inherited
|
|
245
326
|
// `setAuthProvider` genesis step AND the enroll step's `verifiedVia` provenance stamp. Read it up
|
|
246
327
|
// front so the enroll step (which precedes the inheritance step in the chain) can see the issuer.
|
|
@@ -270,9 +351,20 @@ export function birthHomeForDomain(homeLawDomain: string) {
|
|
|
270
351
|
// from the parent's custody when it folds the birth; never the host). Matches buildGenesisChain's shape.
|
|
271
352
|
{ domain: "bootstrap", directiveId: "installDomain", actor: founder, payload: { domainHash: p.frameworkHash }, domainHash: p.frameworkHash, domainPackageB64: "" },
|
|
272
353
|
{ domain: "nomos", directiveId: "installDomain", actor: founder, payload: { domainHash: p.lawHash }, domainHash: p.lawHash, domainPackageB64: "" },
|
|
354
|
+
{
|
|
355
|
+
domain: groupLawDomain,
|
|
356
|
+
directiveId: "nomosInternalSeedReceivingGroupPolicy7c1f5e",
|
|
357
|
+
actor: founder,
|
|
358
|
+
payload: { setAt: p.bornAt },
|
|
359
|
+
},
|
|
273
360
|
// SEED THE OWNER — the home is born OWNED; the homeNameBindsOwner invariant in the child's law proves
|
|
274
361
|
// homeName == home-<sha256(owner)> at the child's own gate, so the slot can ONLY be its rightful owner's.
|
|
275
|
-
{
|
|
362
|
+
{
|
|
363
|
+
domain: groupLawDomain,
|
|
364
|
+
directiveId: "nomosInternalSeedReceivingGroup7c1f5e",
|
|
365
|
+
actor: founder,
|
|
366
|
+
payload: { principal: p.owner, workspaceName: groupName, bornAt: p.bornAt },
|
|
367
|
+
},
|
|
276
368
|
// ENROL THE OWNER'S DEVICE on the warrant-free prefix (when supplied) — enrollSigner self-binds to
|
|
277
369
|
// ctx.principal() (the owner), so this binds `ownerKeyHash` as the owner's signer BEFORE the warrant
|
|
278
370
|
// closes bootstrap. This is what makes the born home SIGNER-READY (able to sign + self-serve-birth its own
|
|
@@ -282,7 +374,7 @@ export function birthHomeForDomain(homeLawDomain: string) {
|
|
|
282
374
|
// ladder: every seed runs UNSIGNED on the still-warrant-free prefix; the warrant closes it). ERA-SAFE: a
|
|
283
375
|
// home born earlier (pre-feature law, no such step in its recipe) replays byte-identical — the era rule
|
|
284
376
|
// rides the LAW hash, not this branch.
|
|
285
|
-
{ domain:
|
|
377
|
+
{ domain: groupLawDomain, directiveId: "enrollSigner", actor: founder, payload: { keyHash: p.ownerKeyHash, grantedBy: p.owner, grantedAt: 1, ...(verifiedVia !== undefined ? { verifiedVia } : {}) } },
|
|
286
378
|
];
|
|
287
379
|
// AUTHPROVIDER INHERITANCE (default ON — see the payload doc): copy the parent's declared provider
|
|
288
380
|
// (the up-front captured read above) AS DATA into the child genesis. The child owns the copy as its
|
|
@@ -292,7 +384,7 @@ export function birthHomeForDomain(homeLawDomain: string) {
|
|
|
292
384
|
const issuer = str("issuer");
|
|
293
385
|
if (issuer !== undefined) {
|
|
294
386
|
genesisChain.push({
|
|
295
|
-
domain:
|
|
387
|
+
domain: groupLawDomain,
|
|
296
388
|
directiveId: "seedAuthProvider",
|
|
297
389
|
actor: founder,
|
|
298
390
|
payload: {
|
|
@@ -314,14 +406,14 @@ export function birthHomeForDomain(homeLawDomain: string) {
|
|
|
314
406
|
// THE DECLARED BIRTH EFFECT (the unix model): the recipe rides the committed intent as a declared output
|
|
315
407
|
// (captured_ports.births). The kernel — reading the `.births()` declaration off the law manifest — takes
|
|
316
408
|
// the recipe and spawns + self-validates the child through the child's OWN gate (each step self-verifies).
|
|
317
|
-
birth({ workspace:
|
|
409
|
+
birth({ workspace: groupName, genesisChain });
|
|
318
410
|
return [];
|
|
319
411
|
})
|
|
320
412
|
.reads(authProviderQuery) // the parent-side AuthProvider capture the inheritance copies (the #58 lane)
|
|
321
413
|
.births(); // BIRTH IS A DECLARED KERNEL PRIMITIVE — emits the `nomosBirthGate` era key; the kernel folds the recipe
|
|
322
414
|
}
|
|
323
415
|
|
|
324
|
-
export const
|
|
416
|
+
export const birthReceivingGroup = birthReceivingGroupForDomain(RECEIVING_GROUP_LAW_DOMAIN);
|
|
325
417
|
|
|
326
418
|
/**
|
|
327
419
|
* THE SHARED-WITH-ME INDEX (§5) — one record per cross-custody attestation delivered to this home holon:
|
|
@@ -345,6 +437,15 @@ export const SharedWithMe = aggregate("SharedWithMe", {
|
|
|
345
437
|
prunedAt: t.string().merge(Lww),
|
|
346
438
|
}).public();
|
|
347
439
|
|
|
440
|
+
/** One idempotent access-manifest row per source edge in this home. Delivery is at-least-once: a retry,
|
|
441
|
+
* reconnect or source outbox replay must update the same bookmark rather than manufacture another thing the
|
|
442
|
+
* recipient appears to have access to. The home itself scopes the recipient, so the edge coordinate is the
|
|
443
|
+
* source workspace + protected object + granted relation. */
|
|
444
|
+
export function sharedWithMeId(asserterWorkspace: string, object: string, relation: string): string {
|
|
445
|
+
const coordinate = JSON.stringify([asserterWorkspace, object, relation]);
|
|
446
|
+
return `SharedWithMe_${nomos.sha256(coordinate)}`;
|
|
447
|
+
}
|
|
448
|
+
|
|
348
449
|
/**
|
|
349
450
|
* `recordShare` — index a cross-custody share into the home holon's access list. AUTHORED on the home
|
|
350
451
|
* holon (the identity's own subject-side root). The payload carries the §3 CROSS-CUSTODY ATTESTATION — a
|
|
@@ -357,7 +458,7 @@ export const SharedWithMe = aggregate("SharedWithMe", {
|
|
|
357
458
|
* asserter when the identity opens it (§5 fail-safe); a stale/forged quote can't enter the list at all.
|
|
358
459
|
*/
|
|
359
460
|
export const recordShare = directive("recordShare")
|
|
360
|
-
.
|
|
461
|
+
.ensures(SharedWithMe)
|
|
361
462
|
.payload(
|
|
362
463
|
z.object({
|
|
363
464
|
asserterWorkspace: z.string().min(1),
|
|
@@ -372,7 +473,9 @@ export const recordShare = directive("recordShare")
|
|
|
372
473
|
}),
|
|
373
474
|
)
|
|
374
475
|
.plan((p) => {
|
|
375
|
-
|
|
476
|
+
// ENSURE, never CREATE: signed delivery is retryable and therefore at-least-once. Re-delivery (including
|
|
477
|
+
// a later re-grant after prune) refreshes this exact access edge and makes it live again.
|
|
478
|
+
ensure(SharedWithMe, sharedWithMeId(p.asserterWorkspace, p.object, p.relation))
|
|
376
479
|
.set("asserterWorkspace", p.asserterWorkspace)
|
|
377
480
|
.set("object", p.object)
|
|
378
481
|
.set("relation", p.relation)
|
|
@@ -385,7 +488,7 @@ export const recordShare = directive("recordShare")
|
|
|
385
488
|
// into the `nomosAttestationGate` era key) — the one gate verifies it against the home's owner (the
|
|
386
489
|
// LAW-DECLARED owner-source below, NOT a kernel-hardcoded id) + pins the principal to the attestation key,
|
|
387
490
|
// so the bailiff delivers it host-free to an OFFLINE subject (no device key, no author block).
|
|
388
|
-
.attestationAuthorized({ ownerObject:
|
|
491
|
+
.attestationAuthorized({ ownerObject: RECEIVING_GROUP_ID, ownerField: "principal" });
|
|
389
492
|
|
|
390
493
|
/**
|
|
391
494
|
* `pruneShare` — fail-safe drop of a hint whose forward `Check` denied (the asserter revoked it). The row
|
|
@@ -394,7 +497,7 @@ export const recordShare = directive("recordShare")
|
|
|
394
497
|
*/
|
|
395
498
|
export const pruneShare = directive("pruneShare")
|
|
396
499
|
.mutates(SharedWithMe)
|
|
397
|
-
.payload(z.object({ shareId:
|
|
500
|
+
.payload(z.object({ shareId: idOf(SharedWithMe), prunedAt: z.string().min(1) }))
|
|
398
501
|
.plan((p) => [set(instance(SharedWithMe, p.shareId), "prunedAt", p.prunedAt)]);
|
|
399
502
|
|
|
400
503
|
/** "Show me everything <asserter> shared with me" — an indexed probe over the local access list. */
|
|
@@ -485,7 +588,7 @@ export const parentAttestedBirth: ParentAttestedBirthMarker = {
|
|
|
485
588
|
// DECLARE the owner + cert locators so the KERNEL reads them (it names neither): the home owner lives at
|
|
486
589
|
// the `HomeOwner` singleton's `principal`, the travelling cert at the `BirthAttestation` singleton's
|
|
487
590
|
// `certChain`. Mirrors how `.attestationAuthorized()` declares its owner-source — the kernel hardcodes none.
|
|
488
|
-
ownerObject:
|
|
591
|
+
ownerObject: RECEIVING_GROUP_ID,
|
|
489
592
|
ownerField: "principal",
|
|
490
593
|
certObject: BIRTH_ATTESTATION_ID,
|
|
491
594
|
certField: "certChain",
|