@serve.zone/interfaces 32.8.0 → 32.9.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.
@@ -0,0 +1,581 @@
1
+ import * as plugins from './plugins.js';
2
+ import type {
3
+ IActiveSecretRecipientMetadata, IResolvedSecretManifest, IResolvedSecretManifestReference,
4
+ IRuntimeAssignment, IRuntimeAssignmentReference, IRuntimeAuthorityReference, IRuntimeConfig,
5
+ IRuntimeSessionBinding, TImmutableContainerPlatform, TSecretRecipientMetadata, TSha256Digest,
6
+ } from './data/index.js';
7
+ import {
8
+ bindRuntimeConfigToAssignment, getRuntimeAssignmentReference, isSha256Digest,
9
+ snapshotRuntimeAssignment, snapshotRuntimeSessionBinding, validateRuntimeAssignment,
10
+ validateResolvedSecretManifest, validateResolvedSecretManifestReference,
11
+ validateSecretRecipientMetadata, validateSecretRecipientSet, verifyResolvedSecretManifestDigest,
12
+ verifySecretEnvelopeContext,
13
+ } from './data/index.js';
14
+ import { secretValueLimits } from './data/secret.js';
15
+ import {
16
+ validateWorkloadInitActiveApprovalAuthority, validateWorkloadInitApprovalAuthorityReference,
17
+ type IWorkloadInitActiveApprovalAuthority, type IWorkloadInitApprovalAuthorityReference,
18
+ type IWorkloadInitApprovedArtifact,
19
+ } from './runtime.workloadinit.js';
20
+ import { canonicalizeStrictJson, createCanonicalJsonSha256Hex } from './private/canonicaljson.js';
21
+ import { isClusterNodeId } from './private/nodeidentifier.js';
22
+
23
+ /**
24
+ * Content contracts only. Authentication of the physical peer, current session,
25
+ * node credential and slot, recipient-generation CAS, accepted manifest/version
26
+ * retention, challenge consumption and material issuance are Cloudly-owned.
27
+ */
28
+ export const runtimeAssignmentSecretContract = Object.freeze({
29
+ descriptorDomain: 'serve.zone/runtime-assignment-secret-authority',
30
+ maximumRequestBytes: 327_680,
31
+ maximumMaterialBytes: 192 * 1024 * 1024,
32
+ error: 'Runtime assignment secret contract rejected.',
33
+ } as const);
34
+
35
+ const fail = (): never => { throw new Error(runtimeAssignmentSecretContract.error); };
36
+ function requireValue(conditionArg: unknown): asserts conditionArg {
37
+ if (!conditionArg) fail();
38
+ }
39
+ const exact = (valueArg: unknown, keysArg: readonly string[]): Record<string, unknown> => {
40
+ requireValue(valueArg !== null && typeof valueArg === 'object' && !Array.isArray(valueArg));
41
+ const keys = Object.keys(valueArg);
42
+ requireValue(keys.length === keysArg.length && keysArg.every((keyArg) => keys.includes(keyArg)));
43
+ return valueArg as Record<string, unknown>;
44
+ };
45
+ const positive = (valueArg: unknown): number => {
46
+ requireValue(Number.isSafeInteger(valueArg) && (valueArg as number) > 0);
47
+ return valueArg as number;
48
+ };
49
+ const nonnegative = (valueArg: unknown): number => {
50
+ requireValue(Number.isSafeInteger(valueArg) && (valueArg as number) >= 0);
51
+ return valueArg as number;
52
+ };
53
+ const identifier = (valueArg: unknown): string => {
54
+ requireValue(typeof valueArg === 'string' && /^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/.test(valueArg));
55
+ return valueArg;
56
+ };
57
+ const digest = (valueArg: unknown): TSha256Digest => {
58
+ requireValue(typeof valueArg === 'string' && isSha256Digest(valueArg));
59
+ return valueArg;
60
+ };
61
+ const canonical = (valueArg: unknown): string => canonicalizeStrictJson(valueArg, fail);
62
+ const capture = (valueArg: unknown, maximumArg: number = runtimeAssignmentSecretContract.maximumRequestBytes): unknown => {
63
+ const serialized = canonical(valueArg);
64
+ requireValue(new TextEncoder().encode(serialized).byteLength <= maximumArg);
65
+ return JSON.parse(serialized) as unknown;
66
+ };
67
+ const same = (leftArg: unknown, rightArg: unknown): boolean => canonical(leftArg) === canonical(rightArg);
68
+ const snapshot = <T>(callbackArg: () => T): T => {
69
+ try { return callbackArg(); } catch { return fail(); }
70
+ };
71
+ const session = (valueArg: unknown): IRuntimeSessionBinding =>
72
+ snapshotRuntimeSessionBinding(valueArg);
73
+ const assignmentReference = (valueArg: unknown): IRuntimeAssignmentReference => {
74
+ const value = exact(valueArg, ['assignmentId', 'generation', 'digest']);
75
+ return { assignmentId: identifier(value.assignmentId), generation: positive(value.generation), digest: digest(value.digest) };
76
+ };
77
+ const authorityReference = (valueArg: unknown): IRuntimeAuthorityReference => {
78
+ const value = exact(valueArg, ['id', 'generation', 'digest']);
79
+ return { id: identifier(value.id), generation: positive(value.generation), digest: digest(value.digest) };
80
+ };
81
+ const manifestReference = (manifestArg: IResolvedSecretManifest): IResolvedSecretManifestReference => ({
82
+ manifestId: manifestArg.id, manifestDigest: manifestArg.digest, clusterId: manifestArg.clusterId,
83
+ imageRolloutId: manifestArg.imageRolloutId, imageRolloutGeneration: manifestArg.imageRolloutGeneration,
84
+ requestedImageDigest: manifestArg.requestedImageDigest, invocationDigest: manifestArg.invocationDigest,
85
+ secretRolloutId: manifestArg.secretRolloutId, secretRolloutGeneration: manifestArg.secretRolloutGeneration,
86
+ });
87
+
88
+ export interface IRuntimeAssignmentSecretLauncher {
89
+ approval: IWorkloadInitApprovalAuthorityReference;
90
+ artifact: IWorkloadInitApprovedArtifact;
91
+ }
92
+
93
+ /**
94
+ * Value-free descriptor hashed BEFORE the assignment is sealed. Its manifest is
95
+ * already independently hashed; neither digest contains the assignment digest.
96
+ */
97
+ export interface IRuntimeAssignmentSecretAuthority extends IRuntimeAuthorityReference {
98
+ organizationId: string;
99
+ serviceId: string;
100
+ platform: TImmutableContainerPlatform;
101
+ manifest: IResolvedSecretManifestReference;
102
+ workloadInit: IRuntimeAssignmentSecretLauncher | null;
103
+ }
104
+
105
+ const launcher = (valueArg: unknown, platformArg: TImmutableContainerPlatform): IRuntimeAssignmentSecretLauncher => {
106
+ const value = exact(valueArg, ['approval', 'artifact']);
107
+ requireValue(validateWorkloadInitApprovalAuthorityReference(value.approval).length === 0);
108
+ const artifact = exact(value.artifact, [
109
+ 'platform', 'workloadInitPlatformManifestDigest', 'workloadInitExecutableDigest',
110
+ ]);
111
+ requireValue(artifact.platform === platformArg);
112
+ digest(artifact.workloadInitPlatformManifestDigest);
113
+ digest(artifact.workloadInitExecutableDigest);
114
+ return valueArg as IRuntimeAssignmentSecretLauncher;
115
+ };
116
+
117
+ export const snapshotRuntimeAssignmentSecretAuthority = (valueArg: unknown): IRuntimeAssignmentSecretAuthority =>
118
+ snapshot(() => {
119
+ const value = exact(capture(valueArg), [
120
+ 'id', 'generation', 'digest', 'organizationId', 'serviceId', 'platform', 'manifest', 'workloadInit',
121
+ ]);
122
+ const reference = authorityReference({
123
+ id: value.id, generation: value.generation, digest: value.digest,
124
+ });
125
+ requireValue(value.platform === 'linux/amd64' || value.platform === 'linux/arm64');
126
+ const manifest = value.manifest;
127
+ requireValue(validateResolvedSecretManifestReference(manifest, (manifest as IResolvedSecretManifestReference)?.clusterId).length === 0);
128
+ requireValue(reference.id === (manifest as IResolvedSecretManifestReference).manifestId
129
+ && reference.generation === (manifest as IResolvedSecretManifestReference).secretRolloutGeneration);
130
+ if (value.workloadInit !== null) launcher(value.workloadInit, value.platform);
131
+ return value as unknown as IRuntimeAssignmentSecretAuthority;
132
+ });
133
+
134
+ export const computeRuntimeAssignmentSecretAuthorityDigest = async (
135
+ authorityArg: IRuntimeAssignmentSecretAuthority,
136
+ ): Promise<TSha256Digest> => {
137
+ const authority = snapshotRuntimeAssignmentSecretAuthority(authorityArg);
138
+ const { digest: _digest, ...payload } = authority;
139
+ return `sha256:${await createCanonicalJsonSha256Hex(canonical({
140
+ domain: runtimeAssignmentSecretContract.descriptorDomain, payload,
141
+ }), fail)}`;
142
+ };
143
+
144
+ export const validateRuntimeAssignmentSecretAuthority = async (valueArg: unknown): Promise<string[]> => {
145
+ try {
146
+ const value = snapshotRuntimeAssignmentSecretAuthority(valueArg);
147
+ return value.digest === await computeRuntimeAssignmentSecretAuthorityDigest(value)
148
+ ? [] : [runtimeAssignmentSecretContract.error];
149
+ } catch { return [runtimeAssignmentSecretContract.error]; }
150
+ };
151
+
152
+ /** Producer creates this before assignment digest computation. */
153
+ export const createRuntimeAssignmentSecretAuthority = async (
154
+ manifestArg: unknown, platformArg: TImmutableContainerPlatform,
155
+ workloadInitArg: IRuntimeAssignmentSecretLauncher | null,
156
+ ): Promise<IRuntimeAssignmentSecretAuthority> => {
157
+ try {
158
+ const manifest = capture(manifestArg) as IResolvedSecretManifest;
159
+ requireValue(validateResolvedSecretManifest(manifest).length === 0
160
+ && await verifyResolvedSecretManifestDigest(manifest)
161
+ && (platformArg === 'linux/amd64' || platformArg === 'linux/arm64')
162
+ && (manifest.entries.some((entryArg) => entryArg.delivery.type === 'launcher-environment')
163
+ ? workloadInitArg !== null : workloadInitArg === null));
164
+ const workloadInit = workloadInitArg === null
165
+ ? null : launcher(capture(workloadInitArg), platformArg);
166
+ const authority: IRuntimeAssignmentSecretAuthority = {
167
+ id: manifest.id, generation: manifest.secretRolloutGeneration,
168
+ digest: 'sha256:0000000000000000000000000000000000000000000000000000000000000000',
169
+ organizationId: manifest.organizationId, serviceId: manifest.serviceId,
170
+ platform: platformArg, manifest: manifestReference(manifest), workloadInit,
171
+ };
172
+ authority.digest = await computeRuntimeAssignmentSecretAuthorityDigest(authority);
173
+ return authority;
174
+ } catch { return fail(); }
175
+ };
176
+
177
+ /** Trusted manifest/config are required: the reference alone is no authorization. */
178
+ export const bindRuntimeAssignmentSecretAuthority = async (
179
+ authorityArg: unknown, manifestArg: unknown, assignmentArg: unknown, configArg: unknown,
180
+ trustedClusterIdArg: string,
181
+ ): Promise<IRuntimeAssignmentSecretAuthority> => {
182
+ try {
183
+ const authority = snapshotRuntimeAssignmentSecretAuthority(authorityArg);
184
+ const assignment = snapshotRuntimeAssignment(assignmentArg);
185
+ const manifest = capture(manifestArg) as IResolvedSecretManifest;
186
+ requireValue((await validateRuntimeAssignmentSecretAuthority(authority)).length === 0
187
+ && (await validateRuntimeAssignment(assignment)).length === 0
188
+ && validateResolvedSecretManifest(manifest).length === 0
189
+ && await verifyResolvedSecretManifestDigest(manifest));
190
+ const { config } = await bindRuntimeConfigToAssignment(configArg, assignment);
191
+ requireValue(assignment.disposition === 'run' && assignment.generation === 1
192
+ && assignment.previous === null && assignment.workload.secrets !== null
193
+ && same(assignment.workload.secrets, {
194
+ id: authority.id, generation: authority.generation, digest: authority.digest,
195
+ })
196
+ && same(authority.manifest, manifestReference(manifest))
197
+ && authority.organizationId === assignment.organizationId
198
+ && authority.serviceId === assignment.serviceId
199
+ && authority.platform === assignment.workload.platform
200
+ && identifier(trustedClusterIdArg) === manifest.clusterId
201
+ && manifest.organizationId === assignment.organizationId
202
+ && manifest.serviceId === assignment.serviceId
203
+ && manifest.requestedImageDigest === assignment.workload.imageDigest
204
+ && manifest.invocationDigest === config.image.invocation.digest
205
+ && (manifest.entries.some((entryArg) => entryArg.delivery.type === 'launcher-environment')
206
+ ? authority.workloadInit !== null : authority.workloadInit === null));
207
+ return authority;
208
+ } catch { return fail(); }
209
+ };
210
+
211
+ export interface IRuntimeSecretRecipientState {
212
+ nodeId: string;
213
+ generation: number;
214
+ recipients: TSecretRecipientMetadata[];
215
+ }
216
+
217
+ export interface IGetRuntimeSecretRecipientStateRequest { session: IRuntimeSessionBinding }
218
+ export interface IGetRuntimeSecretRecipientStateResponse { state: IRuntimeSecretRecipientState }
219
+ export interface IBeginRuntimeSecretRecipientEnrollmentRequest {
220
+ session: IRuntimeSessionBinding;
221
+ proposedRecipientKeyId: string;
222
+ proposedPublicKey: string;
223
+ expectedGeneration: number;
224
+ }
225
+ export interface IRuntimeSecretRecipientEnrollmentChallenge {
226
+ session: IRuntimeSessionBinding;
227
+ enrollmentId: string;
228
+ recipientKeyId: string;
229
+ publicKey: string;
230
+ generation: number;
231
+ expiresAt: number;
232
+ envelope: plugins.smartcrypto.IX25519EnvelopeV1;
233
+ }
234
+ export interface IBeginRuntimeSecretRecipientEnrollmentResponse {
235
+ challenge: IRuntimeSecretRecipientEnrollmentChallenge;
236
+ }
237
+ export interface ICompleteRuntimeSecretRecipientEnrollmentRequest {
238
+ session: IRuntimeSessionBinding;
239
+ enrollmentId: string;
240
+ expectedGeneration: number;
241
+ /** Canonical unpadded base64url encoding of the opened 32-byte challenge. */
242
+ response: string;
243
+ }
244
+ export interface ICompleteRuntimeSecretRecipientEnrollmentResponse {
245
+ recipient: IActiveSecretRecipientMetadata;
246
+ }
247
+
248
+ const raw32ByteBase64Url = /^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/;
249
+ export const snapshotRuntimeSecretRecipientState = (valueArg: unknown): IRuntimeSecretRecipientState =>
250
+ snapshot(() => {
251
+ const state = exact(capture(valueArg), ['nodeId', 'generation', 'recipients']);
252
+ requireValue(isClusterNodeId(state.nodeId));
253
+ const generation = nonnegative(state.generation);
254
+ requireValue(Array.isArray(state.recipients));
255
+ requireValue(generation === 0
256
+ ? state.recipients.length === 0
257
+ : validateSecretRecipientSet(state.recipients).length === 0
258
+ && state.recipients.some((recipientArg: TSecretRecipientMetadata) =>
259
+ recipientArg.lifecycleState === 'active' && recipientArg.generation === generation));
260
+ return state as unknown as IRuntimeSecretRecipientState;
261
+ });
262
+
263
+ export const bindGetRuntimeSecretRecipientStateRequest = (
264
+ valueArg: unknown, trustedSessionArg: unknown,
265
+ ): IGetRuntimeSecretRecipientStateRequest => snapshot(() => {
266
+ const value = exact(capture(valueArg), ['session']);
267
+ const trusted = session(trustedSessionArg);
268
+ requireValue(same(session(value.session), trusted));
269
+ return { session: trusted };
270
+ });
271
+
272
+ export const bindGetRuntimeSecretRecipientStateResponse = (
273
+ valueArg: unknown, requestArg: unknown,
274
+ ): IGetRuntimeSecretRecipientStateResponse => snapshot(() => {
275
+ const request = exact(capture(requestArg), ['session']);
276
+ const value = exact(capture(valueArg), ['state']);
277
+ const state = snapshotRuntimeSecretRecipientState(value.state);
278
+ requireValue(state.nodeId === session(request.session).nodeId);
279
+ return { state };
280
+ });
281
+
282
+ export const bindBeginRuntimeSecretRecipientEnrollmentRequest = (
283
+ valueArg: unknown, trustedSessionArg: unknown, currentStateArg: unknown,
284
+ ): IBeginRuntimeSecretRecipientEnrollmentRequest => snapshot(() => {
285
+ const value = exact(capture(valueArg), [
286
+ 'session', 'proposedRecipientKeyId', 'proposedPublicKey', 'expectedGeneration',
287
+ ]);
288
+ const trusted = session(trustedSessionArg);
289
+ const state = snapshotRuntimeSecretRecipientState(currentStateArg);
290
+ requireValue(same(session(value.session), trusted) && state.nodeId === trusted.nodeId
291
+ && nonnegative(value.expectedGeneration) === state.generation
292
+ && raw32ByteBase64Url.test(value.proposedPublicKey as string));
293
+ return {
294
+ session: trusted, proposedRecipientKeyId: identifier(value.proposedRecipientKeyId),
295
+ proposedPublicKey: value.proposedPublicKey as string, expectedGeneration: state.generation,
296
+ };
297
+ });
298
+
299
+ /** Use the existing SmartCrypto X25519 envelope and one-time challenge proof. */
300
+ export const createRuntimeSecretRecipientEnrollmentChallengeContext = (
301
+ challengeArg: Omit<IRuntimeSecretRecipientEnrollmentChallenge, 'envelope'>,
302
+ ): Uint8Array => new TextEncoder().encode(canonical({
303
+ purpose: 'serve.zone/runtime-secret-recipient-enrollment-challenge',
304
+ session: challengeArg.session,
305
+ enrollmentId: challengeArg.enrollmentId,
306
+ recipientKeyId: challengeArg.recipientKeyId,
307
+ publicKey: challengeArg.publicKey,
308
+ generation: challengeArg.generation,
309
+ expiresAt: challengeArg.expiresAt,
310
+ }));
311
+
312
+ export const bindBeginRuntimeSecretRecipientEnrollmentResponse = async (
313
+ valueArg: unknown, requestArg: unknown, trustedSessionArg: unknown, nowArg: number,
314
+ ): Promise<IBeginRuntimeSecretRecipientEnrollmentResponse> => {
315
+ try {
316
+ const value = exact(capture(valueArg), ['challenge']);
317
+ const challenge = exact(value.challenge, [
318
+ 'session', 'enrollmentId', 'recipientKeyId', 'publicKey', 'generation', 'expiresAt', 'envelope',
319
+ ]);
320
+ const request = exact(capture(requestArg), [
321
+ 'session', 'proposedRecipientKeyId', 'proposedPublicKey', 'expectedGeneration',
322
+ ]);
323
+ const trusted = session(trustedSessionArg);
324
+ requireValue(same(session(request.session), trusted) && same(session(challenge.session), trusted)
325
+ && identifier(challenge.enrollmentId).length > 0
326
+ && challenge.recipientKeyId === identifier(request.proposedRecipientKeyId)
327
+ && challenge.publicKey === request.proposedPublicKey
328
+ && typeof challenge.publicKey === 'string' && raw32ByteBase64Url.test(challenge.publicKey)
329
+ && positive(challenge.generation) === nonnegative(request.expectedGeneration) + 1
330
+ && positive(challenge.expiresAt) > positive(nowArg));
331
+ const envelope = plugins.smartcrypto.parseX25519Envelope(challenge.envelope);
332
+ requireValue(envelope.recipientKeyId === challenge.recipientKeyId
333
+ && raw32ByteBase64Url.test(envelope.ciphertext));
334
+ const captured = { ...challenge, session: trusted, envelope } as IRuntimeSecretRecipientEnrollmentChallenge;
335
+ requireValue(await verifySecretEnvelopeContext(
336
+ envelope, createRuntimeSecretRecipientEnrollmentChallengeContext(captured),
337
+ ));
338
+ return { challenge: captured };
339
+ } catch { return fail(); }
340
+ };
341
+
342
+ export const bindCompleteRuntimeSecretRecipientEnrollmentRequest = async (
343
+ valueArg: unknown, challengeArg: unknown, trustedSessionArg: unknown, nowArg: number,
344
+ ): Promise<ICompleteRuntimeSecretRecipientEnrollmentRequest> => {
345
+ try {
346
+ const value = exact(capture(valueArg), ['session', 'enrollmentId', 'expectedGeneration', 'response']);
347
+ const challenge = exact(capture(challengeArg), [
348
+ 'session', 'enrollmentId', 'recipientKeyId', 'publicKey', 'generation', 'expiresAt', 'envelope',
349
+ ]);
350
+ const trusted = session(trustedSessionArg);
351
+ requireValue(same(session(value.session), trusted) && same(session(challenge.session), trusted)
352
+ && identifier(value.enrollmentId) === identifier(challenge.enrollmentId)
353
+ && identifier(challenge.recipientKeyId).length > 0
354
+ && nonnegative(value.expectedGeneration) + 1 === positive(challenge.generation)
355
+ && positive(challenge.expiresAt) > positive(nowArg)
356
+ && typeof value.response === 'string' && raw32ByteBase64Url.test(value.response));
357
+ const envelope = plugins.smartcrypto.parseX25519Envelope(challenge.envelope);
358
+ requireValue(envelope.recipientKeyId === challenge.recipientKeyId
359
+ && typeof challenge.publicKey === 'string' && raw32ByteBase64Url.test(challenge.publicKey)
360
+ && raw32ByteBase64Url.test(envelope.ciphertext)
361
+ && await verifySecretEnvelopeContext(
362
+ envelope,
363
+ createRuntimeSecretRecipientEnrollmentChallengeContext(
364
+ challenge as unknown as IRuntimeSecretRecipientEnrollmentChallenge),
365
+ ));
366
+ return value as unknown as ICompleteRuntimeSecretRecipientEnrollmentRequest;
367
+ } catch { return fail(); }
368
+ };
369
+
370
+ export const bindCompleteRuntimeSecretRecipientEnrollmentResponse = (
371
+ valueArg: unknown, challengeArg: unknown,
372
+ ): ICompleteRuntimeSecretRecipientEnrollmentResponse => snapshot(() => {
373
+ const value = exact(capture(valueArg), ['recipient']);
374
+ const challenge = exact(capture(challengeArg), [
375
+ 'session', 'enrollmentId', 'recipientKeyId', 'publicKey', 'generation', 'expiresAt', 'envelope',
376
+ ]);
377
+ requireValue(validateSecretRecipientMetadata(value.recipient).length === 0);
378
+ const recipient = value.recipient as IActiveSecretRecipientMetadata;
379
+ requireValue(recipient.lifecycleState === 'active' && recipient.recipientKeyId === challenge.recipientKeyId
380
+ && recipient.publicKey === challenge.publicKey && recipient.generation === challenge.generation);
381
+ return { recipient };
382
+ });
383
+
384
+ export interface IGetRuntimeAssignmentSecretMaterialRequest {
385
+ session: IRuntimeSessionBinding;
386
+ /** Exact current generation-one run assignment, never a historical receipt. */
387
+ assignment: IRuntimeAssignmentReference;
388
+ secrets: IRuntimeAuthorityReference;
389
+ expectedRecipientKeyId: string;
390
+ expectedRecipientGeneration: number;
391
+ }
392
+
393
+ export interface ISealedRuntimeAssignmentSecretMaterialEntry {
394
+ secretVersionId: string;
395
+ envelope: plugins.smartcrypto.IX25519EnvelopeV1;
396
+ }
397
+
398
+ /** Sensitive ephemeral sealed material; exclude the complete body from hooks and logs. */
399
+ export interface ISealedRuntimeAssignmentSecretMaterial {
400
+ assignment: IRuntimeAssignmentReference;
401
+ nodeId: string;
402
+ secrets: IRuntimeAuthorityReference;
403
+ recipientKeyId: string;
404
+ recipientGeneration: number;
405
+ authority: IRuntimeAssignmentSecretAuthority;
406
+ manifest: IResolvedSecretManifest;
407
+ /** Full approved immutable launcher authority, exactly when an entry needs it. */
408
+ workloadInit: IWorkloadInitActiveApprovalAuthority | null;
409
+ /** Sorted exact one-to-one coverage of the pinned manifest's versions. */
410
+ entries: ISealedRuntimeAssignmentSecretMaterialEntry[];
411
+ }
412
+
413
+ export interface IGetRuntimeAssignmentSecretMaterialResponse {
414
+ material: ISealedRuntimeAssignmentSecretMaterial;
415
+ }
416
+
417
+ /**
418
+ * The descriptor was hashed before assignment seal. This context binds each
419
+ * envelope to the final run digest, node and recipient generation afterwards.
420
+ * Use SmartCrypto sealX25519Envelope with these exact bytes.
421
+ */
422
+ export const createRuntimeAssignmentSecretMaterialEnvelopeContext = (contextArg: {
423
+ recipientKeyId: string;
424
+ recipientGeneration: number;
425
+ organizationId: string;
426
+ clusterId: string;
427
+ serviceId: string;
428
+ nodeId: string;
429
+ assignment: IRuntimeAssignmentReference;
430
+ authorityDigest: TSha256Digest;
431
+ manifestId: string;
432
+ manifestDigest: TSha256Digest;
433
+ imageRolloutId: string;
434
+ imageRolloutGeneration: number;
435
+ requestedImageDigest: TSha256Digest;
436
+ invocationDigest: TSha256Digest;
437
+ secretRolloutId: string;
438
+ secretRolloutGeneration: number;
439
+ secretId: string;
440
+ secretVersionId: string;
441
+ }): Uint8Array => new TextEncoder().encode(canonical({
442
+ purpose: 'serve.zone/runtime-assignment-secret-material',
443
+ recipientKeyId: contextArg.recipientKeyId,
444
+ recipientGeneration: contextArg.recipientGeneration,
445
+ organizationId: contextArg.organizationId,
446
+ clusterId: contextArg.clusterId,
447
+ serviceId: contextArg.serviceId,
448
+ nodeId: contextArg.nodeId,
449
+ assignment: contextArg.assignment,
450
+ authorityDigest: contextArg.authorityDigest,
451
+ manifestId: contextArg.manifestId,
452
+ manifestDigest: contextArg.manifestDigest,
453
+ imageRolloutId: contextArg.imageRolloutId,
454
+ imageRolloutGeneration: contextArg.imageRolloutGeneration,
455
+ requestedImageDigest: contextArg.requestedImageDigest,
456
+ invocationDigest: contextArg.invocationDigest,
457
+ secretRolloutId: contextArg.secretRolloutId,
458
+ secretRolloutGeneration: contextArg.secretRolloutGeneration,
459
+ secretId: contextArg.secretId,
460
+ secretVersionId: contextArg.secretVersionId,
461
+ }));
462
+
463
+ /**
464
+ * trustedSessionArg MUST be the current authenticated connection's server-owned
465
+ * binding; assignmentArg MUST be its current durable run. Content checks here
466
+ * do not prove either source, recipient possession, version issuance policy or
467
+ * the native workload's mount ownership.
468
+ */
469
+ export const bindGetRuntimeAssignmentSecretMaterialRequest = async (
470
+ valueArg: unknown, assignmentArg: unknown, trustedSessionArg: unknown,
471
+ activeRecipientArg: unknown,
472
+ ): Promise<IGetRuntimeAssignmentSecretMaterialRequest> => {
473
+ try {
474
+ const value = exact(capture(valueArg), [
475
+ 'session', 'assignment', 'secrets', 'expectedRecipientKeyId', 'expectedRecipientGeneration',
476
+ ]);
477
+ const assignment = snapshotRuntimeAssignment(assignmentArg);
478
+ const trusted = session(trustedSessionArg);
479
+ const recipient = activeRecipientArg as IActiveSecretRecipientMetadata;
480
+ requireValue((await validateRuntimeAssignment(assignment)).length === 0
481
+ && validateSecretRecipientMetadata(recipient).length === 0
482
+ && recipient.lifecycleState === 'active'
483
+ && same(session(value.session), trusted)
484
+ && trusted.nodeId === assignment.nodeId
485
+ && trusted.runtimeNamespace === assignment.runtimeNamespace
486
+ && same(trusted.controller, assignment.controller)
487
+ && assignment.generation === 1 && assignment.previous === null
488
+ && assignment.disposition === 'run' && assignment.workload.secrets !== null
489
+ && same(assignmentReference(value.assignment), getRuntimeAssignmentReference(assignment))
490
+ && same(authorityReference(value.secrets), assignment.workload.secrets)
491
+ && identifier(value.expectedRecipientKeyId) === recipient.recipientKeyId
492
+ && positive(value.expectedRecipientGeneration) === recipient.generation);
493
+ return {
494
+ session: trusted, assignment: getRuntimeAssignmentReference(assignment),
495
+ secrets: assignment.workload.secrets,
496
+ expectedRecipientKeyId: recipient.recipientKeyId,
497
+ expectedRecipientGeneration: recipient.generation,
498
+ };
499
+ } catch { return fail(); }
500
+ };
501
+
502
+ /**
503
+ * Verify one complete response without opening a private key. The caller must
504
+ * supply its authenticated current session, current assignment, pinned config,
505
+ * enrolled cluster and active recipient from their respective trusted owners.
506
+ */
507
+ export const bindGetRuntimeAssignmentSecretMaterialResponse = async (
508
+ valueArg: unknown, requestArg: unknown, assignmentArg: unknown, configArg: unknown,
509
+ trustedSessionArg: unknown, trustedClusterIdArg: string, activeRecipientArg: unknown,
510
+ ): Promise<IGetRuntimeAssignmentSecretMaterialResponse> => {
511
+ try {
512
+ const value = exact(capture(valueArg, runtimeAssignmentSecretContract.maximumMaterialBytes), ['material']);
513
+ const material = exact(value.material, [
514
+ 'assignment', 'nodeId', 'secrets', 'recipientKeyId', 'recipientGeneration',
515
+ 'authority', 'manifest', 'workloadInit', 'entries',
516
+ ]);
517
+ const request = await bindGetRuntimeAssignmentSecretMaterialRequest(
518
+ requestArg, assignmentArg, trustedSessionArg, activeRecipientArg,
519
+ );
520
+ const assignment = snapshotRuntimeAssignment(assignmentArg);
521
+ const authority = await bindRuntimeAssignmentSecretAuthority(
522
+ material.authority, material.manifest, assignment, configArg, trustedClusterIdArg,
523
+ );
524
+ const manifest = material.manifest as IResolvedSecretManifest;
525
+ requireValue(identifier(trustedClusterIdArg) === manifest.clusterId
526
+ && material.nodeId === assignment.nodeId
527
+ && same(assignmentReference(material.assignment), request.assignment)
528
+ && same(authorityReference(material.secrets), request.secrets)
529
+ && material.recipientKeyId === request.expectedRecipientKeyId
530
+ && material.recipientGeneration === request.expectedRecipientGeneration
531
+ && Array.isArray(material.entries)
532
+ && material.entries.length === manifest.entries.length);
533
+ if (authority.workloadInit === null) {
534
+ requireValue(material.workloadInit === null);
535
+ } else {
536
+ requireValue((await validateWorkloadInitActiveApprovalAuthority(material.workloadInit)).length === 0);
537
+ const approval = material.workloadInit as IWorkloadInitActiveApprovalAuthority;
538
+ requireValue(same({
539
+ authorityGeneration: approval.authorityGeneration, approvalDigest: approval.approvalDigest,
540
+ }, authority.workloadInit.approval)
541
+ && approval.approval.artifacts.some((artifactArg) =>
542
+ same(artifactArg, authority.workloadInit!.artifact)));
543
+ }
544
+ const expectedIds = manifest.entries.map((entryArg) => entryArg.secretVersionId).sort();
545
+ const actualIds = material.entries.map((entryArg: unknown) =>
546
+ exact(entryArg, ['secretVersionId', 'envelope']).secretVersionId);
547
+ requireValue(actualIds.every((idArg: unknown) => typeof idArg === 'string')
548
+ && same(actualIds, expectedIds));
549
+ const byVersion = new Map(manifest.entries.map((entryArg) => [entryArg.secretVersionId, entryArg]));
550
+ for (const entryValue of material.entries) {
551
+ const entry = exact(entryValue, ['secretVersionId', 'envelope']);
552
+ const manifestEntry = byVersion.get(entry.secretVersionId as string);
553
+ requireValue(manifestEntry);
554
+ const envelope = plugins.smartcrypto.parseX25519Envelope(entry.envelope);
555
+ requireValue(envelope.recipientKeyId === request.expectedRecipientKeyId
556
+ && envelope.ciphertext.length <= Math.ceil(secretValueLimits.maximumBytes * 4 / 3));
557
+ const context = createRuntimeAssignmentSecretMaterialEnvelopeContext({
558
+ recipientKeyId: request.expectedRecipientKeyId,
559
+ recipientGeneration: request.expectedRecipientGeneration,
560
+ organizationId: manifest.organizationId,
561
+ clusterId: manifest.clusterId,
562
+ serviceId: manifest.serviceId,
563
+ nodeId: assignment.nodeId,
564
+ assignment: request.assignment,
565
+ authorityDigest: authority.digest,
566
+ manifestId: manifest.id,
567
+ manifestDigest: manifest.digest,
568
+ imageRolloutId: manifest.imageRolloutId,
569
+ imageRolloutGeneration: manifest.imageRolloutGeneration,
570
+ requestedImageDigest: manifest.requestedImageDigest,
571
+ invocationDigest: manifest.invocationDigest,
572
+ secretRolloutId: manifest.secretRolloutId,
573
+ secretRolloutGeneration: manifest.secretRolloutGeneration,
574
+ secretId: manifestEntry.secretId,
575
+ secretVersionId: manifestEntry.secretVersionId,
576
+ });
577
+ requireValue(await verifySecretEnvelopeContext(envelope, context));
578
+ }
579
+ return { material: material as unknown as ISealedRuntimeAssignmentSecretMaterial };
580
+ } catch { return fail(); }
581
+ };
package/ts/runtime.ts CHANGED
@@ -45,6 +45,7 @@ export * from './runtime.networkprojection.js';
45
45
  export * from './runtime.corestore.js';
46
46
  export * from './runtime.corestore.databaseexportpackage.js';
47
47
  export * from './runtime.corestore.relay.js';
48
+ export * from './runtime.assignmentsecret.js';
48
49
 
49
50
  export interface ISealedResolvedSecretMaterialEntry {
50
51
  secretVersionId: string;