@githolon/dsl 0.100.6 → 0.101.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/dart/CHANGELOG.md +6 -0
- package/dart/lib/src/dev_cloud.dart +46 -15
- package/dart/lib/src/test_harness.dart +78 -16
- package/dart/lib/src/webview_bridge.dart +640 -126
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart/test/bundle_version_pairing_test.dart +11 -22
- package/dart/test/co2_product_gauntlet_test.dart +228 -0
- package/dart/test/errors_test.dart +63 -12
- package/dart/test/first_app_gauntlet_test.dart +29 -3
- package/dart/test/webview_bridge_test.dart +274 -0
- package/dart_types/CHANGELOG.md +6 -0
- package/dart_types/lib/src/errors.dart +136 -30
- package/dart_types/lib/src/surfaced.dart +48 -2
- package/dart_types/lib/src/wire.dart +3 -1
- package/dart_types/pubspec.yaml +1 -1
- package/dart_types/test/surfaced_test.dart +19 -0
- package/package.json +1 -1
- package/src/activity.ts +43 -4
- package/src/aggregate.ts +132 -22
- package/src/attested_core.ts +1 -1
- package/src/authoring.ts +119 -6
- package/src/birth_meter.ts +4 -4
- package/src/birth_meter_ensure.ts +9 -1
- package/src/births.ts +53 -22
- package/src/build_package.ts +55 -14
- package/src/business_birth.ts +48 -7
- package/src/codegen_dart.ts +267 -37
- package/src/codegen_dart_client.ts +921 -82
- package/src/codegen_openapi.ts +28 -5
- package/src/codegen_proof.ts +107 -11
- package/src/codegen_ts.ts +961 -135
- package/src/compile_package_main.ts +155 -45
- package/src/conflicts.ts +252 -0
- package/src/describe_model.ts +448 -0
- package/src/directive.ts +5 -38
- package/src/drivers.ts +12 -4
- package/src/engine_entry.ts +4 -0
- package/src/evolve_preview.ts +72 -11
- package/src/fields.ts +201 -55
- package/src/framework/application_home.ts +103 -0
- package/src/framework/application_home_domain.ts +2 -0
- package/src/framework/attested_receipts.ts +2 -2
- package/src/framework/auth_provider_law.ts +51 -24
- package/src/framework/business_corrections.ts +170 -0
- package/src/framework/disclosure.ts +1 -1
- package/src/framework/domain_lifecycle.ts +250 -16
- package/src/framework/encryption_identity.ts +1 -1
- package/src/framework/encryption_scope.ts +21 -13
- package/src/framework/era_certification.ts +1 -1
- package/src/framework/framework_aggregates.ts +2 -0
- package/src/framework/genesis.ts +1 -1
- package/src/framework/home.ts +22 -9
- package/src/framework/identity.ts +5 -5
- package/src/framework/impure_capability.ts +2 -2
- package/src/framework/owned_custody.ts +561 -0
- package/src/framework/platform_admin.ts +1 -1
- package/src/framework/relations.ts +9 -1
- package/src/framework/repair.ts +1 -1
- package/src/framework/signing_identity.ts +2 -2
- package/src/framework/source_roster.ts +1 -1
- package/src/framework/workspaces.ts +17 -10
- package/src/generated_client_names.ts +39 -0
- package/src/index.ts +20 -15
- package/src/kernel_capabilities.ts +25 -1
- package/src/law_check.ts +4 -1
- package/src/manifest.ts +356 -63
- package/src/read_projection_manifest.ts +8 -0
- package/src/read_projection_namespace.ts +4 -0
- package/src/relation.ts +1 -1
- package/src/shape_compat.ts +47 -2
- package/src/stable_ids.ts +38 -6
- package/src/stable_ids_types.ts +30 -0
- package/src/surfaced_lint.ts +0 -0
- package/src/usd.ts +62 -11
- package/src/usd_layers.ts +54 -3
- package/src/value_object.ts +23 -19
- package/src/wire.ts +1 -0
- package/src/wire_encode.ts +48 -4
- package/src/workspace_routing.ts +57 -0
- package/src/workspace_sharding.ts +9 -9
- package/src/certified_read.ts +0 -313
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
// NOMOS — framework-owned multi-device custody for `.owned()` business state.
|
|
2
|
+
// Tenant code sees only generated `devices.request()` / `devices.approve(request)`.
|
|
3
|
+
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { OWNED_KEY_RELATION, OWNED_READ_RELATION, aggregate, instance, ownedKeyObject, type AggregateHandle } from "../aggregate.js";
|
|
6
|
+
import { Lww } from "../drivers.js";
|
|
7
|
+
import { directive } from "../directive.js";
|
|
8
|
+
import { endorsementOf } from "../principal_evidence.js";
|
|
9
|
+
import { isRelationTypeDecl } from "../relation_schema.js";
|
|
10
|
+
import { t } from "../fields.js";
|
|
11
|
+
import { set, withMarker } from "../ops.js";
|
|
12
|
+
import { query } from "../query.js";
|
|
13
|
+
import {
|
|
14
|
+
KeyScope,
|
|
15
|
+
WrappedKey,
|
|
16
|
+
keyScopeByScope,
|
|
17
|
+
wrappedKeysByRecipient,
|
|
18
|
+
writeKeyEpoch,
|
|
19
|
+
writeWrap,
|
|
20
|
+
} from "./encryption_scope.js";
|
|
21
|
+
import { GenesisWarrant, seedGenesis } from "./genesis.js";
|
|
22
|
+
import { RelationTuple, __qualifyAggregateType, removeTuple, writeTuple } from "./relations.js";
|
|
23
|
+
import { SIGNER_RELATION, enrollSigner, signerObject, identityRelations } from "./signing_identity.js";
|
|
24
|
+
import { impureCapability, impureCapabilityAggregate } from "./impure_capability.js";
|
|
25
|
+
|
|
26
|
+
/** Obscure stable law id: deliberately unavailable as a generated tenant directive method. */
|
|
27
|
+
export const OWNED_DEVICE_APPROVAL_DIRECTIVE = "nomos_internal_approve_owned_device_7c1f5e";
|
|
28
|
+
export const OWNED_INITIALIZE_DIRECTIVE = "nomos_internal_initialize_owned_identity_7c1f5e";
|
|
29
|
+
export const OWNED_RECOVERY_RELATION = "nomos_internal_owned_recovery_principal_7c1f5e";
|
|
30
|
+
export const OWNED_RECOVERY_PROTECT_DIRECTIVE = "nomos_internal_protect_owned_identity_7c1f5e";
|
|
31
|
+
export const OWNED_RECOVERY_REQUEST_DIRECTIVE = "nomos_internal_request_owned_recovery_7c1f5e";
|
|
32
|
+
export const OWNED_RECOVERY_COMPLETE_DIRECTIVE = "nomos_internal_complete_owned_recovery_7c1f5e";
|
|
33
|
+
export const OWNED_RECOVERY_ACCEPT_DIRECTIVE = "nomos_internal_accept_owned_recovery_7c1f5e";
|
|
34
|
+
const OWNED_RECOVERY_FAIL_DIRECTIVE = "nomos_internal_fail_owned_recovery_7c1f5e";
|
|
35
|
+
const OWNED_RECOVERY_BLOCK_DIRECTIVE = "nomos_internal_block_owned_recovery_7c1f5e";
|
|
36
|
+
const OWNED_RECOVERY_DEAD_LETTER_DIRECTIVE = "nomos_internal_dead_letter_owned_recovery_7c1f5e";
|
|
37
|
+
const keySubject = z.string().regex(/^key:[0-9a-f]{64}$/);
|
|
38
|
+
const principal = z.string().regex(/^user:[A-Za-z0-9][A-Za-z0-9_.-]*$/);
|
|
39
|
+
const b64 = z.string().regex(/^[A-Za-z0-9+/]+={0,2}$/);
|
|
40
|
+
const hidden = <T extends object>(value: T): T & { readonly businessBirthInternal: true } =>
|
|
41
|
+
({ ...value, businessBirthInternal: true });
|
|
42
|
+
|
|
43
|
+
/** The recovery principal can recover this workspace's owned root, never the operator by default. */
|
|
44
|
+
export function ownedRecoveryScope(owner: string): string {
|
|
45
|
+
return `${ownedKeyObject(owner)}#${OWNED_RECOVERY_RELATION}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function ownedRecoveryPolicyId(owner: string): string {
|
|
49
|
+
return `nomos-owned-recovery-policy:${ownedKeyObject(owner).slice("nomos-owned-principal:".length)}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Auditable policy fact. The secret is absent: only the owner-root commitment and designated peer live here. */
|
|
53
|
+
export const OwnedRecoveryPolicy = aggregate("NomosOwnedRecoveryPolicy", {
|
|
54
|
+
ownerPrincipal: t.string().merge(Lww),
|
|
55
|
+
recoveryPrincipal: t.string().merge(Lww),
|
|
56
|
+
scope: t.string().merge(Lww),
|
|
57
|
+
keyCommitment: t.string().merge(Lww),
|
|
58
|
+
protectedAt: t.string().merge(Lww),
|
|
59
|
+
status: t.string().merge(Lww),
|
|
60
|
+
}).public();
|
|
61
|
+
|
|
62
|
+
const recoveryOrderParams = {
|
|
63
|
+
ownerPrincipal: t.string().merge(Lww),
|
|
64
|
+
recoveryPrincipal: t.string().merge(Lww),
|
|
65
|
+
deviceRequest: t.string().merge(Lww),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** Private framework Order -> Receipt journal. Generated business clients never expose this aggregate. */
|
|
69
|
+
export const OwnedRecoveryOrder = impureCapabilityAggregate("NomosOwnedRecoveryOrder", recoveryOrderParams);
|
|
70
|
+
const ownedRecoveryLifecycle = impureCapability({
|
|
71
|
+
aggregateId: OwnedRecoveryOrder.id,
|
|
72
|
+
obligationDirectiveId: OWNED_RECOVERY_REQUEST_DIRECTIVE,
|
|
73
|
+
completeDirectiveId: OWNED_RECOVERY_COMPLETE_DIRECTIVE,
|
|
74
|
+
failDirectiveId: OWNED_RECOVERY_FAIL_DIRECTIVE,
|
|
75
|
+
blockDirectiveId: OWNED_RECOVERY_BLOCK_DIRECTIVE,
|
|
76
|
+
deadLetterDirectiveId: OWNED_RECOVERY_DEAD_LETTER_DIRECTIVE,
|
|
77
|
+
params: recoveryOrderParams,
|
|
78
|
+
paramsSchema: {
|
|
79
|
+
ownerPrincipal: principal,
|
|
80
|
+
recoveryPrincipal: principal,
|
|
81
|
+
deviceRequest: z.string().min(1),
|
|
82
|
+
},
|
|
83
|
+
writeParams: (id, p) => [
|
|
84
|
+
set(instance(OwnedRecoveryOrder, id), "ownerPrincipal", p.ownerPrincipal),
|
|
85
|
+
set(instance(OwnedRecoveryOrder, id), "recoveryPrincipal", p.recoveryPrincipal),
|
|
86
|
+
set(instance(OwnedRecoveryOrder, id), "deviceRequest", p.deviceRequest),
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
export const ownedRecoveryOrdersByRecoveryPrincipal = query("nomosOwnedRecoveryOrdersByRecoveryPrincipal")
|
|
91
|
+
.key("recoveryPrincipal")
|
|
92
|
+
.returns(OwnedRecoveryOrder);
|
|
93
|
+
|
|
94
|
+
/** Opt in once. Retrying the same policy is idempotent; replacing the principal is refused by the kernel. */
|
|
95
|
+
const protectOwnedIdentity = Object.assign(
|
|
96
|
+
directive(OWNED_RECOVERY_PROTECT_DIRECTIVE)
|
|
97
|
+
.ensures(OwnedRecoveryPolicy)
|
|
98
|
+
.payload(z.object({
|
|
99
|
+
recoveryPrincipal: principal,
|
|
100
|
+
recipientPubKey: b64,
|
|
101
|
+
hpkeEpk: b64,
|
|
102
|
+
ct: b64,
|
|
103
|
+
keyCommitment: z.string().regex(/^[0-9a-f]{64}$/),
|
|
104
|
+
protectedAt: z.string().min(1),
|
|
105
|
+
}))
|
|
106
|
+
.plan((p, ctx) => {
|
|
107
|
+
const me = ctx.principal().asAuthoritySubject<string>("designate my owned-data recovery principal");
|
|
108
|
+
const owner = endorsementOf(me)?.principal ?? "";
|
|
109
|
+
const scope = ownedRecoveryScope(owner);
|
|
110
|
+
const policy = instance(OwnedRecoveryPolicy, ownedRecoveryPolicyId(owner));
|
|
111
|
+
return [
|
|
112
|
+
withMarker(set(policy, "ownerPrincipal", owner), "ensures"),
|
|
113
|
+
set(policy, "recoveryPrincipal", p.recoveryPrincipal),
|
|
114
|
+
set(policy, "scope", scope),
|
|
115
|
+
set(policy, "keyCommitment", p.keyCommitment),
|
|
116
|
+
set(policy, "protectedAt", p.protectedAt),
|
|
117
|
+
set(policy, "status", "protected"),
|
|
118
|
+
...writeTuple({
|
|
119
|
+
object: ownedKeyObject(owner),
|
|
120
|
+
relation: OWNED_RECOVERY_RELATION,
|
|
121
|
+
subject: p.recoveryPrincipal,
|
|
122
|
+
grantedBy: ctx.principal().asAttribution(),
|
|
123
|
+
grantedAt: p.protectedAt,
|
|
124
|
+
}),
|
|
125
|
+
...writeKeyEpoch(scope, 0, p.protectedAt, [{
|
|
126
|
+
recipientPrincipal: p.recoveryPrincipal,
|
|
127
|
+
recipientPubKey: p.recipientPubKey,
|
|
128
|
+
hpkeEpk: p.hpkeEpk,
|
|
129
|
+
ct: p.ct,
|
|
130
|
+
}]),
|
|
131
|
+
];
|
|
132
|
+
})
|
|
133
|
+
.alsoEnsures(RelationTuple)
|
|
134
|
+
.alsoEnsures(KeyScope)
|
|
135
|
+
.alsoEnsures(WrappedKey),
|
|
136
|
+
{ businessBirthInternal: true as const },
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const acceptOwnedRecovery = Object.assign(
|
|
140
|
+
directive(OWNED_RECOVERY_ACCEPT_DIRECTIVE)
|
|
141
|
+
.ensures(RelationTuple)
|
|
142
|
+
.payload(z.object({
|
|
143
|
+
requestId: z.string().min(1),
|
|
144
|
+
newKeySubject: keySubject,
|
|
145
|
+
ownedCustody: z.string().min(1),
|
|
146
|
+
recoveredAt: z.string().min(1),
|
|
147
|
+
}))
|
|
148
|
+
.plan((p, ctx) => {
|
|
149
|
+
const me = ctx.principal().asAuthoritySubject<string>("accept my designated recovery handoff");
|
|
150
|
+
const owner = endorsementOf(me)?.principal ?? "";
|
|
151
|
+
return writeTuple({
|
|
152
|
+
object: ownedKeyObject(owner),
|
|
153
|
+
relation: OWNED_KEY_RELATION,
|
|
154
|
+
subject: p.newKeySubject,
|
|
155
|
+
grantedBy: ctx.principal().asAttribution(),
|
|
156
|
+
grantedAt: p.recoveredAt,
|
|
157
|
+
ownedCustody: p.ownedCustody,
|
|
158
|
+
});
|
|
159
|
+
}),
|
|
160
|
+
{ businessBirthInternal: true as const },
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const hiddenRecoveryLifecycle = {
|
|
164
|
+
aggregate: hidden(OwnedRecoveryOrder),
|
|
165
|
+
obligation: hidden(ownedRecoveryLifecycle.obligation),
|
|
166
|
+
complete: hidden(ownedRecoveryLifecycle.complete),
|
|
167
|
+
fail: hidden(ownedRecoveryLifecycle.fail),
|
|
168
|
+
block: hidden(ownedRecoveryLifecycle.block),
|
|
169
|
+
deadLetter: hidden(ownedRecoveryLifecycle.deadLetter),
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export type OwnedRecordAction = "share" | "unshare" | "transfer";
|
|
173
|
+
|
|
174
|
+
/** One collision-resistant framework id per aggregate/action. Tenant code never sees these ids. */
|
|
175
|
+
export function ownedRecordActionDirectiveId(action: OwnedRecordAction, aggregateId: string): string {
|
|
176
|
+
let encoded = "";
|
|
177
|
+
for (let i = 0; i < aggregateId.length; i += 1) encoded += aggregateId.charCodeAt(i).toString(16).padStart(4, "0");
|
|
178
|
+
return `nomos_internal_${action}_owned_record_${encoded}_7c1f5e`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const OWNED_ACTION_PREFIX = "nomos_internal_";
|
|
182
|
+
const OWNED_ACTION_MARKER = "_owned_record_";
|
|
183
|
+
|
|
184
|
+
const wrap = z.object({
|
|
185
|
+
recipientPrincipal: principal,
|
|
186
|
+
recipientPubKey: b64,
|
|
187
|
+
hpkeEpk: b64,
|
|
188
|
+
ct: b64,
|
|
189
|
+
});
|
|
190
|
+
const historicWrap = wrap.extend({ epoch: z.number().int().nonnegative() });
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* One atomic framework act: enroll the requesting signing key and add its encrypted handoff to the
|
|
194
|
+
* owned-device roster. The executor independently proves the opaque request, the same-device
|
|
195
|
+
* Ed25519->X25519 binding, approver provenance, current key commitment and exact two tuple writes.
|
|
196
|
+
*/
|
|
197
|
+
const approval = directive(OWNED_DEVICE_APPROVAL_DIRECTIVE)
|
|
198
|
+
.ensures(RelationTuple)
|
|
199
|
+
.payload(z.object({
|
|
200
|
+
newKeySubject: keySubject,
|
|
201
|
+
ownedCustody: z.string().min(1),
|
|
202
|
+
verifiedVia: z.string().min(1),
|
|
203
|
+
validFromEpoch: z.number().int().nonnegative(),
|
|
204
|
+
approvedAt: z.string().min(1),
|
|
205
|
+
}))
|
|
206
|
+
.plan((p, ctx) => {
|
|
207
|
+
const me = ctx.principal().asAuthoritySubject<string>(
|
|
208
|
+
"approve another device for my own Nomos business state",
|
|
209
|
+
);
|
|
210
|
+
const principal = endorsementOf(me)?.principal ?? "";
|
|
211
|
+
return [
|
|
212
|
+
...writeTuple({
|
|
213
|
+
object: signerObject(principal),
|
|
214
|
+
relation: SIGNER_RELATION,
|
|
215
|
+
subject: p.newKeySubject,
|
|
216
|
+
grantedBy: ctx.principal().asAttribution(),
|
|
217
|
+
grantedAt: p.approvedAt,
|
|
218
|
+
verifiedVia: p.verifiedVia,
|
|
219
|
+
validFromEpoch: p.validFromEpoch,
|
|
220
|
+
}),
|
|
221
|
+
...writeTuple({
|
|
222
|
+
object: ownedKeyObject(principal),
|
|
223
|
+
relation: OWNED_KEY_RELATION,
|
|
224
|
+
subject: p.newKeySubject,
|
|
225
|
+
grantedBy: ctx.principal().asAttribution(),
|
|
226
|
+
grantedAt: p.approvedAt,
|
|
227
|
+
ownedCustody: p.ownedCustody,
|
|
228
|
+
}),
|
|
229
|
+
];
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
/** Branded internal so ordinary directive codegen never exposes its cryptographic payload. */
|
|
233
|
+
export const approveOwnedDevice = Object.assign(approval, {
|
|
234
|
+
businessBirthInternal: true as const,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
/** Establish this principal's shared owned-data root before another principal shares to them. */
|
|
238
|
+
const initializeOwnedIdentity = Object.assign(
|
|
239
|
+
directive(OWNED_INITIALIZE_DIRECTIVE)
|
|
240
|
+
.ensures(RelationTuple)
|
|
241
|
+
.payload(z.object({
|
|
242
|
+
newKeySubject: keySubject,
|
|
243
|
+
ownedCustody: z.string().min(1),
|
|
244
|
+
initializedAt: z.string().min(1),
|
|
245
|
+
}))
|
|
246
|
+
.plan((p, ctx) => {
|
|
247
|
+
const me = ctx.principal().asAuthoritySubject<string>(
|
|
248
|
+
"initialize my framework-owned private-data identity",
|
|
249
|
+
);
|
|
250
|
+
const raw = endorsementOf(me)?.principal ?? "";
|
|
251
|
+
return writeTuple({
|
|
252
|
+
object: ownedKeyObject(raw),
|
|
253
|
+
relation: OWNED_KEY_RELATION,
|
|
254
|
+
subject: p.newKeySubject,
|
|
255
|
+
grantedBy: ctx.principal().asAttribution(),
|
|
256
|
+
grantedAt: p.initializedAt,
|
|
257
|
+
ownedCustody: p.ownedCustody,
|
|
258
|
+
});
|
|
259
|
+
}),
|
|
260
|
+
{ businessBirthInternal: true as const },
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
function recordScope(aggregate: AggregateHandle, recordId: string): string {
|
|
264
|
+
return `${__qualifyAggregateType(aggregate.id)}:${recordId}#${OWNED_READ_RELATION}`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function recordObject(aggregate: AggregateHandle, recordId: string): string {
|
|
268
|
+
return `${__qualifyAggregateType(aggregate.id)}:${recordId}`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function ownedRecordActionDirectives(aggregate: AggregateHandle) {
|
|
272
|
+
const share = Object.assign(
|
|
273
|
+
directive(ownedRecordActionDirectiveId("share", aggregate.id))
|
|
274
|
+
.ensures(WrappedKey)
|
|
275
|
+
.payload(z.object({
|
|
276
|
+
recordId: z.string().min(1),
|
|
277
|
+
peer: principal,
|
|
278
|
+
epoch: z.number().int().nonnegative(),
|
|
279
|
+
sharedAt: z.string().min(1),
|
|
280
|
+
wraps: z.array(historicWrap),
|
|
281
|
+
}))
|
|
282
|
+
.plan((p, ctx) => [
|
|
283
|
+
...writeTuple({
|
|
284
|
+
object: recordObject(aggregate, p.recordId),
|
|
285
|
+
relation: OWNED_READ_RELATION,
|
|
286
|
+
subject: p.peer,
|
|
287
|
+
grantedBy: ctx.principal().asAttribution(),
|
|
288
|
+
grantedAt: p.sharedAt,
|
|
289
|
+
}),
|
|
290
|
+
...p.wraps.flatMap((entry) => writeWrap(recordScope(aggregate, p.recordId), entry.epoch, entry)),
|
|
291
|
+
]),
|
|
292
|
+
{ businessBirthInternal: true as const },
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
const unshare = Object.assign(
|
|
296
|
+
directive(ownedRecordActionDirectiveId("unshare", aggregate.id))
|
|
297
|
+
.ensures(KeyScope)
|
|
298
|
+
.payload(z.object({
|
|
299
|
+
recordId: z.string().min(1),
|
|
300
|
+
peer: principal,
|
|
301
|
+
epoch: z.number().int().positive(),
|
|
302
|
+
changedAt: z.string().min(1),
|
|
303
|
+
wraps: z.array(wrap),
|
|
304
|
+
}))
|
|
305
|
+
.plan((p, ctx) => [
|
|
306
|
+
...removeTuple({
|
|
307
|
+
object: recordObject(aggregate, p.recordId),
|
|
308
|
+
relation: OWNED_READ_RELATION,
|
|
309
|
+
subject: p.peer,
|
|
310
|
+
grantedBy: ctx.principal().asAttribution(),
|
|
311
|
+
grantedAt: p.changedAt,
|
|
312
|
+
revokedBy: endorsementOf(ctx.principal().asAttribution())?.principal ?? "",
|
|
313
|
+
revokedAt: p.changedAt,
|
|
314
|
+
}),
|
|
315
|
+
...writeKeyEpoch(recordScope(aggregate, p.recordId), p.epoch, p.changedAt, p.wraps),
|
|
316
|
+
]),
|
|
317
|
+
{ businessBirthInternal: true as const },
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
const ownerField = aggregate.audience?.kind === "owned" ? aggregate.audience.ownerField : "";
|
|
321
|
+
const transfer = Object.assign(
|
|
322
|
+
directive(ownedRecordActionDirectiveId("transfer", aggregate.id))
|
|
323
|
+
.ensures(KeyScope)
|
|
324
|
+
.payload(z.object({
|
|
325
|
+
recordId: z.string().min(1),
|
|
326
|
+
newOwner: principal,
|
|
327
|
+
previousReaders: z.array(principal),
|
|
328
|
+
epoch: z.number().int().positive(),
|
|
329
|
+
changedAt: z.string().min(1),
|
|
330
|
+
historicWraps: z.array(historicWrap),
|
|
331
|
+
}))
|
|
332
|
+
.plan((p, ctx) => {
|
|
333
|
+
const object = recordObject(aggregate, p.recordId);
|
|
334
|
+
const scope = recordScope(aggregate, p.recordId);
|
|
335
|
+
const ops = p.previousReaders
|
|
336
|
+
.filter((reader) => reader !== p.newOwner)
|
|
337
|
+
.flatMap((reader) => removeTuple({
|
|
338
|
+
object,
|
|
339
|
+
relation: OWNED_READ_RELATION,
|
|
340
|
+
subject: reader,
|
|
341
|
+
grantedBy: ctx.principal().asAttribution(),
|
|
342
|
+
grantedAt: p.changedAt,
|
|
343
|
+
revokedBy: endorsementOf(ctx.principal().asAttribution())?.principal ?? "",
|
|
344
|
+
revokedAt: p.changedAt,
|
|
345
|
+
}));
|
|
346
|
+
ops.push(...writeTuple({
|
|
347
|
+
object,
|
|
348
|
+
relation: OWNED_READ_RELATION,
|
|
349
|
+
subject: p.newOwner,
|
|
350
|
+
grantedBy: ctx.principal().asAttribution(),
|
|
351
|
+
grantedAt: p.changedAt,
|
|
352
|
+
}));
|
|
353
|
+
ops.push(set(instance(aggregate, p.recordId), ownerField as never, p.newOwner as never));
|
|
354
|
+
ops.push(...writeKeyEpoch(scope, p.epoch, p.changedAt, []));
|
|
355
|
+
for (const entry of p.historicWraps) ops.push(...writeWrap(scope, entry.epoch, entry));
|
|
356
|
+
return ops;
|
|
357
|
+
}),
|
|
358
|
+
{ businessBirthInternal: true as const },
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
return { share, unshare, transfer };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const APPROVAL_EXPORT = "__nomos_owned_device_approval_7c1f5e";
|
|
365
|
+
const IDENTITY_EXPORT = "__nomos_owned_identity_relations_7c1f5e";
|
|
366
|
+
const WARRANT_EXPORT = "__nomos_owned_genesis_warrant_7c1f5e";
|
|
367
|
+
const SEED_EXPORT = "__nomos_owned_seed_genesis_7c1f5e";
|
|
368
|
+
const ENROLL_EXPORT = "__nomos_owned_enroll_signer_7c1f5e";
|
|
369
|
+
const INITIALIZE_EXPORT = "__nomos_owned_initialize_identity_7c1f5e";
|
|
370
|
+
const KEY_SCOPE_EXPORT = "__nomos_owned_key_scope_7c1f5e";
|
|
371
|
+
const WRAPPED_KEY_EXPORT = "__nomos_owned_wrapped_key_7c1f5e";
|
|
372
|
+
const KEY_SCOPE_QUERY_EXPORT = "__nomos_owned_key_scope_query_7c1f5e";
|
|
373
|
+
const WRAPS_QUERY_EXPORT = "__nomos_owned_wraps_query_7c1f5e";
|
|
374
|
+
const RECOVERY_POLICY_EXPORT = "__nomos_owned_recovery_policy_7c1f5e";
|
|
375
|
+
const RECOVERY_PROTECT_EXPORT = "__nomos_owned_recovery_protect_7c1f5e";
|
|
376
|
+
const RECOVERY_ACCEPT_EXPORT = "__nomos_owned_recovery_accept_7c1f5e";
|
|
377
|
+
const RECOVERY_ORDER_EXPORT = "__nomos_owned_recovery_order_7c1f5e";
|
|
378
|
+
const RECOVERY_REQUEST_EXPORT = "__nomos_owned_recovery_request_7c1f5e";
|
|
379
|
+
const RECOVERY_COMPLETE_EXPORT = "__nomos_owned_recovery_complete_7c1f5e";
|
|
380
|
+
const RECOVERY_FAIL_EXPORT = "__nomos_owned_recovery_fail_7c1f5e";
|
|
381
|
+
const RECOVERY_BLOCK_EXPORT = "__nomos_owned_recovery_block_7c1f5e";
|
|
382
|
+
const RECOVERY_DEAD_LETTER_EXPORT = "__nomos_owned_recovery_dead_letter_7c1f5e";
|
|
383
|
+
const RECOVERY_QUERY_EXPORT = "__nomos_owned_recovery_query_7c1f5e";
|
|
384
|
+
|
|
385
|
+
const ownedGenesisWarrant = hidden(GenesisWarrant);
|
|
386
|
+
const ownedSeedGenesis = hidden(seedGenesis);
|
|
387
|
+
const ownedEnrollSigner = hidden(enrollSigner);
|
|
388
|
+
const ownedIdentityRelations = hidden(identityRelations);
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* The published CLI bundles its compiler while a tenant module imports declarations from the separately
|
|
392
|
+
* installed @githolon/dsl tarball. Object identity therefore cannot identify framework declarations at this
|
|
393
|
+
* seam. The compiler's existing private brand + stable law identity can: ordinary same-named tenant values
|
|
394
|
+
* do not carry the brand and still fail loudly.
|
|
395
|
+
*/
|
|
396
|
+
function sameFrameworkDeclaration(existing: unknown, generated: unknown): boolean {
|
|
397
|
+
if (existing === generated) return true;
|
|
398
|
+
if (
|
|
399
|
+
existing === null || typeof existing !== "object"
|
|
400
|
+
|| generated === null || typeof generated !== "object"
|
|
401
|
+
|| (existing as { businessBirthInternal?: unknown }).businessBirthInternal !== true
|
|
402
|
+
|| (generated as { businessBirthInternal?: unknown }).businessBirthInternal !== true
|
|
403
|
+
) return false;
|
|
404
|
+
const existingId = (existing as { id?: unknown }).id;
|
|
405
|
+
const generatedId = (generated as { id?: unknown }).id;
|
|
406
|
+
if (typeof existingId === "string" || typeof generatedId === "string") return existingId === generatedId;
|
|
407
|
+
if (isRelationTypeDecl(existing) && isRelationTypeDecl(generated)) {
|
|
408
|
+
return existing.type === generated.type && existing.global === generated.global;
|
|
409
|
+
}
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function declarationsOf(mod: Record<string, unknown>): unknown[] {
|
|
414
|
+
return Object.values(mod).flatMap((value) => Array.isArray(value) ? value : [value]);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function reservedCollision(concept: string): never {
|
|
418
|
+
throw new Error(
|
|
419
|
+
`Nomos internal concept '${concept}' is already taken by the framework. Sorry mate, Nomos will not ` +
|
|
420
|
+
"silently overwrite or reroute the tenant declaration; give the business concept a different name.",
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Add the approval mechanism only to domains that actually declare owned business state. This runs
|
|
426
|
+
* at the same compile/runtime merge seams as the other framework conveniences, preserving one law.
|
|
427
|
+
*/
|
|
428
|
+
export function withOwnedCustodyExports<M extends Record<string, unknown>>(mod: M): Record<string, unknown> {
|
|
429
|
+
const ownedAggregates = Object.values(mod).filter((value): value is AggregateHandle =>
|
|
430
|
+
value !== null
|
|
431
|
+
&& typeof value === "object"
|
|
432
|
+
&& (value as { __isAggregateHandle?: unknown }).__isAggregateHandle === true
|
|
433
|
+
&& (value as { audience?: { kind?: unknown } }).audience?.kind === "owned"
|
|
434
|
+
&& ((value as { encryptedFields?: readonly unknown[] }).encryptedFields?.length ?? 0) > 0
|
|
435
|
+
);
|
|
436
|
+
if (ownedAggregates.length === 0) return mod;
|
|
437
|
+
const actions = ownedAggregates.map((aggregate) => ({ aggregate, ...ownedRecordActionDirectives(aggregate) }));
|
|
438
|
+
const recoveryInjections = [
|
|
439
|
+
[RECOVERY_POLICY_EXPORT, hidden(OwnedRecoveryPolicy)],
|
|
440
|
+
[RECOVERY_PROTECT_EXPORT, protectOwnedIdentity],
|
|
441
|
+
[RECOVERY_ACCEPT_EXPORT, acceptOwnedRecovery],
|
|
442
|
+
[RECOVERY_ORDER_EXPORT, hiddenRecoveryLifecycle.aggregate],
|
|
443
|
+
[RECOVERY_REQUEST_EXPORT, hiddenRecoveryLifecycle.obligation],
|
|
444
|
+
[RECOVERY_COMPLETE_EXPORT, hiddenRecoveryLifecycle.complete],
|
|
445
|
+
[RECOVERY_FAIL_EXPORT, hiddenRecoveryLifecycle.fail],
|
|
446
|
+
[RECOVERY_BLOCK_EXPORT, hiddenRecoveryLifecycle.block],
|
|
447
|
+
[RECOVERY_DEAD_LETTER_EXPORT, hiddenRecoveryLifecycle.deadLetter],
|
|
448
|
+
[RECOVERY_QUERY_EXPORT, hidden(ownedRecoveryOrdersByRecoveryPrincipal)],
|
|
449
|
+
] as const;
|
|
450
|
+
|
|
451
|
+
// These names are deliberately obscure, not magical. A tenant can still reach one accidentally or
|
|
452
|
+
// deliberately, and stable ids can collide under a completely different JS export. Both cases fail
|
|
453
|
+
// loudly: framework injection must never turn the tenant's declaration into some other concept.
|
|
454
|
+
if (Object.prototype.hasOwnProperty.call(mod, APPROVAL_EXPORT)
|
|
455
|
+
&& !sameFrameworkDeclaration(mod[APPROVAL_EXPORT], approveOwnedDevice)) {
|
|
456
|
+
reservedCollision(APPROVAL_EXPORT);
|
|
457
|
+
}
|
|
458
|
+
if (Object.prototype.hasOwnProperty.call(mod, IDENTITY_EXPORT)
|
|
459
|
+
&& !sameFrameworkDeclaration(mod[IDENTITY_EXPORT], ownedIdentityRelations)) {
|
|
460
|
+
reservedCollision(IDENTITY_EXPORT);
|
|
461
|
+
}
|
|
462
|
+
for (const [key, generated] of recoveryInjections) {
|
|
463
|
+
if (Object.prototype.hasOwnProperty.call(mod, key) && !sameFrameworkDeclaration(mod[key], generated)) {
|
|
464
|
+
reservedCollision(key);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
for (const [key, generated] of [
|
|
468
|
+
[WARRANT_EXPORT, ownedGenesisWarrant],
|
|
469
|
+
[SEED_EXPORT, ownedSeedGenesis],
|
|
470
|
+
[ENROLL_EXPORT, ownedEnrollSigner],
|
|
471
|
+
] as const) {
|
|
472
|
+
if (Object.prototype.hasOwnProperty.call(mod, key) && !sameFrameworkDeclaration(mod[key], generated)) {
|
|
473
|
+
reservedCollision(key);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
for (const value of declarationsOf(mod)) {
|
|
477
|
+
if (
|
|
478
|
+
!sameFrameworkDeclaration(value, approveOwnedDevice)
|
|
479
|
+
&& value !== null
|
|
480
|
+
&& typeof value === "object"
|
|
481
|
+
&& (value as { id?: unknown }).id === OWNED_DEVICE_APPROVAL_DIRECTIVE
|
|
482
|
+
) {
|
|
483
|
+
reservedCollision(OWNED_DEVICE_APPROVAL_DIRECTIVE);
|
|
484
|
+
}
|
|
485
|
+
for (const [, generated] of recoveryInjections) {
|
|
486
|
+
if (
|
|
487
|
+
!sameFrameworkDeclaration(value, generated)
|
|
488
|
+
&& value !== null
|
|
489
|
+
&& typeof value === "object"
|
|
490
|
+
&& typeof (generated as { id?: unknown }).id === "string"
|
|
491
|
+
&& (value as { id?: unknown }).id === (generated as { id: string }).id
|
|
492
|
+
) {
|
|
493
|
+
reservedCollision((generated as { id: string }).id);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
for (const [framework, generated] of [
|
|
497
|
+
[GenesisWarrant, ownedGenesisWarrant],
|
|
498
|
+
[seedGenesis, ownedSeedGenesis],
|
|
499
|
+
[enrollSigner, ownedEnrollSigner],
|
|
500
|
+
] as const) {
|
|
501
|
+
if (
|
|
502
|
+
!sameFrameworkDeclaration(value, framework)
|
|
503
|
+
&& !sameFrameworkDeclaration(value, generated)
|
|
504
|
+
&& value !== null
|
|
505
|
+
&& typeof value === "object"
|
|
506
|
+
&& (value as { id?: unknown }).id === framework.id
|
|
507
|
+
) {
|
|
508
|
+
reservedCollision(framework.id);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (
|
|
512
|
+
!sameFrameworkDeclaration(value, ownedIdentityRelations)
|
|
513
|
+
&& isRelationTypeDecl(value)
|
|
514
|
+
&& value.global === true
|
|
515
|
+
&& value.type === identityRelations.type
|
|
516
|
+
) {
|
|
517
|
+
reservedCollision(`global relation type '${identityRelations.type}'`);
|
|
518
|
+
}
|
|
519
|
+
if (value !== null && typeof value === "object" && typeof (value as { id?: unknown }).id === "string") {
|
|
520
|
+
const id = (value as { id: string }).id;
|
|
521
|
+
const generated = actions.flatMap((entry) => [entry.share, entry.unshare, entry.transfer]);
|
|
522
|
+
if (id.startsWith(OWNED_ACTION_PREFIX) && id.includes(OWNED_ACTION_MARKER)
|
|
523
|
+
&& !generated.some((candidate) => sameFrameworkDeclaration(value, candidate))) {
|
|
524
|
+
reservedCollision(id);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
const result: Record<string, unknown> = {
|
|
529
|
+
...mod,
|
|
530
|
+
[APPROVAL_EXPORT]: approveOwnedDevice,
|
|
531
|
+
[INITIALIZE_EXPORT]: initializeOwnedIdentity,
|
|
532
|
+
[IDENTITY_EXPORT]: ownedIdentityRelations,
|
|
533
|
+
[KEY_SCOPE_EXPORT]: hidden(KeyScope),
|
|
534
|
+
[WRAPPED_KEY_EXPORT]: hidden(WrappedKey),
|
|
535
|
+
[KEY_SCOPE_QUERY_EXPORT]: hidden(keyScopeByScope),
|
|
536
|
+
[WRAPS_QUERY_EXPORT]: hidden(wrappedKeysByRecipient),
|
|
537
|
+
[RECOVERY_POLICY_EXPORT]: hidden(OwnedRecoveryPolicy),
|
|
538
|
+
[RECOVERY_PROTECT_EXPORT]: protectOwnedIdentity,
|
|
539
|
+
[RECOVERY_ACCEPT_EXPORT]: acceptOwnedRecovery,
|
|
540
|
+
[RECOVERY_ORDER_EXPORT]: hiddenRecoveryLifecycle.aggregate,
|
|
541
|
+
[RECOVERY_REQUEST_EXPORT]: hiddenRecoveryLifecycle.obligation,
|
|
542
|
+
[RECOVERY_COMPLETE_EXPORT]: hiddenRecoveryLifecycle.complete,
|
|
543
|
+
[RECOVERY_FAIL_EXPORT]: hiddenRecoveryLifecycle.fail,
|
|
544
|
+
[RECOVERY_BLOCK_EXPORT]: hiddenRecoveryLifecycle.block,
|
|
545
|
+
[RECOVERY_DEAD_LETTER_EXPORT]: hiddenRecoveryLifecycle.deadLetter,
|
|
546
|
+
[RECOVERY_QUERY_EXPORT]: hidden(ownedRecoveryOrdersByRecoveryPrincipal),
|
|
547
|
+
// A local law test starts from an empty root, while production business state is normally born into a
|
|
548
|
+
// warranted child. Keep the same lawful bootstrap machinery available in both cases, but framework-only:
|
|
549
|
+
// the harness can seat its test identity and warrant the root without adding a tenant-facing lane.
|
|
550
|
+
[WARRANT_EXPORT]: ownedGenesisWarrant,
|
|
551
|
+
[SEED_EXPORT]: ownedSeedGenesis,
|
|
552
|
+
[ENROLL_EXPORT]: ownedEnrollSigner,
|
|
553
|
+
};
|
|
554
|
+
for (const { aggregate, share, unshare, transfer } of actions) {
|
|
555
|
+
const suffix = ownedRecordActionDirectiveId("share", aggregate.id).split("_owned_record_")[1]!;
|
|
556
|
+
result[`__nomos_owned_share_${suffix}`] = share;
|
|
557
|
+
result[`__nomos_owned_unshare_${suffix}`] = unshare;
|
|
558
|
+
result[`__nomos_owned_transfer_${suffix}`] = transfer;
|
|
559
|
+
}
|
|
560
|
+
return result;
|
|
561
|
+
}
|
|
@@ -191,7 +191,7 @@ export function seedPlatformAdmin(opts: SeedPlatformAdminOptions): PlatformAdmin
|
|
|
191
191
|
// GLOBAL (read-projection namespacing opt-out): a dynamically-named framework aggregate —
|
|
192
192
|
// platform governance law deploys under differing domain keys (root vs each platform), and the
|
|
193
193
|
// founder singleton's bare identity must stay stable across them.
|
|
194
|
-
}, { global: true });
|
|
194
|
+
}, { global: true }).public();
|
|
195
195
|
|
|
196
196
|
/**
|
|
197
197
|
* GENESIS SEED 1 — stamp the founder singleton at the fixed id. Authored by the platform's birth/genesis
|
|
@@ -191,7 +191,7 @@ export const RelationTuple = aggregate("RelationTuple", {
|
|
|
191
191
|
object: t.string().merge(Lww),
|
|
192
192
|
relation: t.string().merge(Lww),
|
|
193
193
|
subjects: t.map(t.json()).merge(MapOf(Lww)),
|
|
194
|
-
});
|
|
194
|
+
}).public();
|
|
195
195
|
export type RelationTupleHandle = typeof RelationTuple;
|
|
196
196
|
|
|
197
197
|
/** The per-subject leaf the `subjects` map stores (JSON-encoded). Frozen wire shape. */
|
|
@@ -215,6 +215,11 @@ export interface RelationSubjectLeaf {
|
|
|
215
215
|
* every lane. OMITTED when absent (legacy / unverified enrollment) so existing chains stay byte-identical.
|
|
216
216
|
*/
|
|
217
217
|
verifiedVia?: string;
|
|
218
|
+
/**
|
|
219
|
+
* Secure-by-default owned-data device handoff. Framework-reserved and omitted from every ordinary
|
|
220
|
+
* relation leaf; the opaque JSON is independently verified by the kernel before it may land.
|
|
221
|
+
*/
|
|
222
|
+
ownedCustody?: string;
|
|
218
223
|
}
|
|
219
224
|
|
|
220
225
|
/** Strip undefined keys so the JSON leaf is byte-stable (no dropped-slot `"k":undefined`). */
|
|
@@ -247,6 +252,8 @@ interface WriteTupleInput {
|
|
|
247
252
|
validFromEpoch?: number;
|
|
248
253
|
/** VA-core: the IdP/issuer that vouched for the enrolling principal (signer relations only; omitted otherwise). */
|
|
249
254
|
verifiedVia?: string;
|
|
255
|
+
/** Framework-internal encrypted owned-data handoff (owned device relation only). */
|
|
256
|
+
ownedCustody?: string;
|
|
250
257
|
}
|
|
251
258
|
|
|
252
259
|
/** Lower a tuple write to plan ops. The first op MATERIALIZES the (object, relation)
|
|
@@ -283,6 +290,7 @@ function lowerTuple(
|
|
|
283
290
|
// (these never set) stays byte-identical to a pre-VA leaf (the era rule on the wire shape).
|
|
284
291
|
validFromEpoch: p.validFromEpoch,
|
|
285
292
|
verifiedVia: p.verifiedVia,
|
|
293
|
+
ownedCustody: p.ownedCustody,
|
|
286
294
|
});
|
|
287
295
|
const subjectEntry = setEntry(rt, "subjects", subjectRaw, JSON.stringify(leaf));
|
|
288
296
|
return [
|
package/src/framework/repair.ts
CHANGED
|
@@ -91,7 +91,7 @@ export const RepairRecord = aggregate("RepairRecord", {
|
|
|
91
91
|
revertedBy: t.string().merge(Lww),
|
|
92
92
|
/** Epoch-ms the repair was authored. */
|
|
93
93
|
revertedAt: t.int().merge(Lww),
|
|
94
|
-
});
|
|
94
|
+
}).public();
|
|
95
95
|
|
|
96
96
|
// ── `revert` directive ──────────────────────────────────────────────────────
|
|
97
97
|
|
|
@@ -100,7 +100,7 @@ export const SIGNER_EPOCH_ID = "signer-epoch:self";
|
|
|
100
100
|
export const SignerEpoch = aggregate("SignerEpoch", {
|
|
101
101
|
scope: t.string().merge(Lww), // singleton key — always "self" (mirrors KeyScope.scope; the query key)
|
|
102
102
|
currentEpoch: t.int().merge(Lww),
|
|
103
|
-
});
|
|
103
|
+
}).public();
|
|
104
104
|
export type SignerEpochHandle = typeof SignerEpoch;
|
|
105
105
|
|
|
106
106
|
/** The current signer epoch (advisory read; the strict-increment law is the workspaceInvariant below). */
|
|
@@ -446,7 +446,7 @@ export const SignerDelegation = aggregate("SignerDelegation", {
|
|
|
446
446
|
certJson: t.string().merge(Lww),
|
|
447
447
|
/** ISO-8601, caller-stamped (determinism — no clock in the plan). */
|
|
448
448
|
enrolledAt: t.string().merge(Lww),
|
|
449
|
-
});
|
|
449
|
+
}).public();
|
|
450
450
|
export type SignerDelegationHandle = typeof SignerDelegation;
|
|
451
451
|
|
|
452
452
|
/** The deterministic singleton id of a user's signer delegation (the kernel reads THIS id by-user). */
|
|
@@ -55,7 +55,7 @@ export const NomosSourceRoster = aggregate("NomosSourceRoster", {
|
|
|
55
55
|
rosterJson: t.json().merge(Lww),
|
|
56
56
|
/** The source's `SignerEpoch` at the attested sync. */
|
|
57
57
|
currentEpoch: t.int().merge(Lww),
|
|
58
|
-
});
|
|
58
|
+
}).public();
|
|
59
59
|
|
|
60
60
|
/** One roster row as the source's `attestorRoster` query answers it. */
|
|
61
61
|
interface AttestorRosterRow {
|