@serve.zone/interfaces 32.9.0 → 32.10.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/changelog.md +13 -0
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/data/index.d.ts +1 -0
- package/dist_ts/data/index.js +2 -1
- package/dist_ts/data/runtimesession.d.ts +2 -0
- package/dist_ts/data/runtimesession.js +5 -3
- package/dist_ts/data/runtimestorageclaim.d.ts +44 -0
- package/dist_ts/data/runtimestorageclaim.js +129 -0
- package/dist_ts/data/secret.d.ts +10 -3
- package/dist_ts/data/secret.js +3 -1
- package/dist_ts/runtime.assignmentsecret.d.ts +4 -3
- package/dist_ts/runtime.assignmentsecret.js +9 -7
- package/package.json +1 -1
- package/readme.md +46 -7
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/index.ts +1 -0
- package/ts/data/runtimesession.ts +6 -2
- package/ts/data/runtimestorageclaim.ts +162 -0
- package/ts/data/secret.ts +16 -2
- package/ts/runtime.assignmentsecret.ts +8 -6
|
@@ -23,6 +23,8 @@ import { isClusterNodeId } from '../private/nodeidentifier.js';
|
|
|
23
23
|
export interface IRuntimeSessionBinding {
|
|
24
24
|
reporterSessionId: string;
|
|
25
25
|
nodeId: string;
|
|
26
|
+
/** Cluster read from the controller's owned node record when this session is admitted. */
|
|
27
|
+
clusterId: string;
|
|
26
28
|
credentialGeneration: number;
|
|
27
29
|
sessionEpoch: number;
|
|
28
30
|
controllerInstanceId: string;
|
|
@@ -254,12 +256,13 @@ const reference = (valueArg: unknown): IRuntimeAssignmentReference => {
|
|
|
254
256
|
return { assignmentId: identifier(value.assignmentId), generation: positive(value.generation), digest: value.digest };
|
|
255
257
|
};
|
|
256
258
|
const session = (valueArg: unknown): IRuntimeSessionBinding => {
|
|
257
|
-
const value = record(valueArg, ['reporterSessionId', 'nodeId', 'credentialGeneration',
|
|
259
|
+
const value = record(valueArg, ['reporterSessionId', 'nodeId', 'clusterId', 'credentialGeneration',
|
|
258
260
|
'sessionEpoch', 'controllerInstanceId', 'controller', 'runtimeNamespace']);
|
|
259
261
|
const controller = record(value.controller, ['kind', 'id', 'epoch']);
|
|
260
262
|
requireValue(controller.kind === 'cloudly' || controller.kind === 'onebox');
|
|
261
263
|
return {
|
|
262
264
|
reporterSessionId: identifier(value.reporterSessionId), nodeId: nodeId(value.nodeId),
|
|
265
|
+
clusterId: identifier(value.clusterId),
|
|
263
266
|
credentialGeneration: positive(value.credentialGeneration), sessionEpoch: positive(value.sessionEpoch),
|
|
264
267
|
controllerInstanceId: identifier(value.controllerInstanceId),
|
|
265
268
|
controller: { kind: controller.kind, id: identifier(controller.id), epoch: identifier(controller.epoch) },
|
|
@@ -269,7 +272,8 @@ const session = (valueArg: unknown): IRuntimeSessionBinding => {
|
|
|
269
272
|
const sameSession = (leftArg: IRuntimeSessionBinding, rightArg: IRuntimeSessionBinding): boolean =>
|
|
270
273
|
canonical(leftArg) === canonical(rightArg);
|
|
271
274
|
const sameOwner = (leftArg: IRuntimeSessionBinding, rightArg: IRuntimeSessionBinding): boolean =>
|
|
272
|
-
leftArg.nodeId === rightArg.nodeId && leftArg.
|
|
275
|
+
leftArg.nodeId === rightArg.nodeId && leftArg.clusterId === rightArg.clusterId
|
|
276
|
+
&& leftArg.runtimeNamespace === rightArg.runtimeNamespace
|
|
273
277
|
&& canonical(leftArg.controller) === canonical(rightArg.controller);
|
|
274
278
|
const matchesAssignment = (sessionArg: IRuntimeSessionBinding, assignmentArg: IRuntimeAssignment): boolean =>
|
|
275
279
|
sessionArg.nodeId === assignmentArg.nodeId && sessionArg.runtimeNamespace === assignmentArg.runtimeNamespace
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { TSha256Digest } from './immutableimage.js';
|
|
2
|
+
import type { IRuntimeAssignment, IRuntimeAuthorityReference, IRuntimeController } from './runtimeassignment.js';
|
|
3
|
+
import { snapshotRuntimeAssignment, validateRuntimeAssignment } from './runtimeassignment.js';
|
|
4
|
+
import { normalizeServiceAbsolutePath } from './service.js';
|
|
5
|
+
import type { IResolvedFilesystemStorageBinding } from '../platform/storage.js';
|
|
6
|
+
import { canonicalizeStrictJson, createCanonicalJsonSha256Hex } from '../private/canonicaljson.js';
|
|
7
|
+
import { isClusterNodeId } from '../private/nodeidentifier.js';
|
|
8
|
+
|
|
9
|
+
/** Only immutable, portable fields of a ready Corestore filesystem binding. */
|
|
10
|
+
export interface IRuntimeStorageClaimBinding extends Pick<IResolvedFilesystemStorageBinding,
|
|
11
|
+
'id' | 'serviceId' | 'requestId' | 'requestDigest' | 'generation' | 'observedGeneration'
|
|
12
|
+
| 'policy' | 'resourceRef' | 'mountPath'> {
|
|
13
|
+
kind: 'filesystem';
|
|
14
|
+
status: 'ready';
|
|
15
|
+
accessMode: 'ReadWriteOnce';
|
|
16
|
+
capabilities: { durability: 'persistent'; topology: 'singleNode' };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Cloudly authority for one retained, node-local volume; no host path or attachment attempt. */
|
|
20
|
+
export interface IRuntimeStorageClaim {
|
|
21
|
+
id: string;
|
|
22
|
+
generation: number;
|
|
23
|
+
previous: IRuntimeAuthorityReference | null;
|
|
24
|
+
controller: IRuntimeController & { kind: 'cloudly' };
|
|
25
|
+
organizationId: string;
|
|
26
|
+
clusterId: string;
|
|
27
|
+
nodeId: string;
|
|
28
|
+
runtimeNamespace: string;
|
|
29
|
+
serviceId: string;
|
|
30
|
+
binding: IRuntimeStorageClaimBinding;
|
|
31
|
+
reclaimPolicy: 'retain';
|
|
32
|
+
digest: TSha256Digest;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const runtimeStorageClaimContract = Object.freeze({
|
|
36
|
+
digestDomain: 'serve.zone/runtime-storage-claim',
|
|
37
|
+
accessMode: 'ReadWriteOnce',
|
|
38
|
+
reclaimPolicy: 'retain',
|
|
39
|
+
} as const);
|
|
40
|
+
|
|
41
|
+
const fail = (): never => { throw new Error('Runtime storage claim contract rejected.'); };
|
|
42
|
+
function requireValue(conditionArg: unknown): asserts conditionArg { if (!conditionArg) fail(); }
|
|
43
|
+
const record = (valueArg: unknown, keysArg: readonly string[]): Record<string, unknown> => {
|
|
44
|
+
requireValue(valueArg !== null && typeof valueArg === 'object' && !Array.isArray(valueArg));
|
|
45
|
+
const prototype = Object.getPrototypeOf(valueArg);
|
|
46
|
+
requireValue(prototype === Object.prototype || prototype === null);
|
|
47
|
+
const keys = Reflect.ownKeys(valueArg);
|
|
48
|
+
requireValue(keys.length === keysArg.length && keysArg.every((keyArg) => keys.includes(keyArg)));
|
|
49
|
+
const result: Record<string, unknown> = Object.create(null);
|
|
50
|
+
for (const key of keysArg) {
|
|
51
|
+
const descriptor = Object.getOwnPropertyDescriptor(valueArg, key);
|
|
52
|
+
requireValue(descriptor?.enumerable && Object.hasOwn(descriptor, 'value'));
|
|
53
|
+
result[key] = descriptor.value;
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
};
|
|
57
|
+
const id = (valueArg: unknown): string => {
|
|
58
|
+
requireValue(typeof valueArg === 'string' && /^[A-Za-z0-9][A-Za-z0-9:._@-]{0,191}$/.test(valueArg));
|
|
59
|
+
return valueArg as string;
|
|
60
|
+
};
|
|
61
|
+
const assignmentId = (valueArg: unknown): string => {
|
|
62
|
+
requireValue(typeof valueArg === 'string' && /^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/.test(valueArg));
|
|
63
|
+
return valueArg as string;
|
|
64
|
+
};
|
|
65
|
+
const generation = (valueArg: unknown): number => {
|
|
66
|
+
requireValue(typeof valueArg === 'number' && Number.isSafeInteger(valueArg)
|
|
67
|
+
&& valueArg > 0 && valueArg < Number.MAX_SAFE_INTEGER);
|
|
68
|
+
return valueArg as number;
|
|
69
|
+
};
|
|
70
|
+
const digest = (valueArg: unknown): TSha256Digest => {
|
|
71
|
+
requireValue(typeof valueArg === 'string' && /^sha256:[a-f0-9]{64}$/.test(valueArg));
|
|
72
|
+
return valueArg as TSha256Digest;
|
|
73
|
+
};
|
|
74
|
+
const reference = (valueArg: unknown): IRuntimeAuthorityReference => {
|
|
75
|
+
const value = record(valueArg, ['id', 'generation', 'digest']);
|
|
76
|
+
return { id: assignmentId(value.id), generation: generation(value.generation), digest: digest(value.digest) };
|
|
77
|
+
};
|
|
78
|
+
const controller = (valueArg: unknown): IRuntimeStorageClaim['controller'] => {
|
|
79
|
+
const value = record(valueArg, ['kind', 'id', 'epoch']);
|
|
80
|
+
requireValue(value.kind === 'cloudly');
|
|
81
|
+
return { kind: 'cloudly', id: assignmentId(value.id), epoch: assignmentId(value.epoch) };
|
|
82
|
+
};
|
|
83
|
+
const mountPath = (valueArg: unknown): string => {
|
|
84
|
+
requireValue(typeof valueArg === 'string' && normalizeServiceAbsolutePath(valueArg) === valueArg
|
|
85
|
+
&& valueArg !== '/' && valueArg !== '/run' && valueArg !== '/run/secrets'
|
|
86
|
+
&& !valueArg.startsWith('/run/secrets/') && !'/run/secrets'.startsWith(`${valueArg}/`));
|
|
87
|
+
return valueArg as string;
|
|
88
|
+
};
|
|
89
|
+
const binding = (valueArg: unknown): IRuntimeStorageClaimBinding => {
|
|
90
|
+
const value = record(valueArg, ['id', 'serviceId', 'requestId', 'requestDigest', 'kind',
|
|
91
|
+
'generation', 'observedGeneration', 'status', 'policy', 'resourceRef', 'mountPath',
|
|
92
|
+
'accessMode', 'capabilities']);
|
|
93
|
+
requireValue(value.kind === 'filesystem' && value.status === 'ready'
|
|
94
|
+
&& value.accessMode === 'ReadWriteOnce'
|
|
95
|
+
&& typeof value.requestDigest === 'string' && /^[a-f0-9]{64}$/.test(value.requestDigest));
|
|
96
|
+
const policy = record(value.policy, ['classId', 'revision']);
|
|
97
|
+
const capabilities = record(value.capabilities, ['durability', 'topology']);
|
|
98
|
+
requireValue(capabilities.durability === 'persistent' && capabilities.topology === 'singleNode');
|
|
99
|
+
const bindingGeneration = generation(value.generation);
|
|
100
|
+
requireValue(generation(value.observedGeneration) === bindingGeneration);
|
|
101
|
+
return { id: id(value.id), serviceId: assignmentId(value.serviceId), requestId: id(value.requestId),
|
|
102
|
+
requestDigest: value.requestDigest as string, kind: 'filesystem', generation: bindingGeneration,
|
|
103
|
+
observedGeneration: bindingGeneration, status: 'ready',
|
|
104
|
+
policy: { classId: id(policy.classId), revision: id(policy.revision) },
|
|
105
|
+
resourceRef: id(value.resourceRef), mountPath: mountPath(value.mountPath),
|
|
106
|
+
accessMode: 'ReadWriteOnce', capabilities: { durability: 'persistent', topology: 'singleNode' } };
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** Detached exact-schema snapshot. Digest and authoritative provider readiness are separate checks. */
|
|
110
|
+
export const snapshotRuntimeStorageClaim = (valueArg: unknown): IRuntimeStorageClaim => {
|
|
111
|
+
const value = record(valueArg, ['id', 'generation', 'previous', 'controller', 'organizationId',
|
|
112
|
+
'clusterId', 'nodeId', 'runtimeNamespace', 'serviceId', 'binding', 'reclaimPolicy', 'digest']);
|
|
113
|
+
const claimId = assignmentId(value.id), claimGeneration = generation(value.generation);
|
|
114
|
+
const previous = value.previous === null ? null : reference(value.previous);
|
|
115
|
+
requireValue(claimGeneration === 1 ? previous === null
|
|
116
|
+
: previous?.id === claimId && previous.generation === claimGeneration - 1);
|
|
117
|
+
requireValue(isClusterNodeId(value.nodeId) && value.reclaimPolicy === 'retain');
|
|
118
|
+
const claimBinding = binding(value.binding), serviceId = assignmentId(value.serviceId);
|
|
119
|
+
requireValue(claimBinding.serviceId === serviceId);
|
|
120
|
+
return { id: claimId, generation: claimGeneration, previous, controller: controller(value.controller),
|
|
121
|
+
organizationId: assignmentId(value.organizationId), clusterId: id(value.clusterId),
|
|
122
|
+
nodeId: value.nodeId as string, runtimeNamespace: assignmentId(value.runtimeNamespace), serviceId,
|
|
123
|
+
binding: claimBinding, reclaimPolicy: 'retain', digest: digest(value.digest) };
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const computeRuntimeStorageClaimDigest = async (valueArg: unknown): Promise<TSha256Digest> => {
|
|
127
|
+
const { digest: _digest, ...payload } = snapshotRuntimeStorageClaim(valueArg);
|
|
128
|
+
return `sha256:${await createCanonicalJsonSha256Hex(canonicalizeStrictJson({
|
|
129
|
+
domain: runtimeStorageClaimContract.digestDomain, payload,
|
|
130
|
+
}, fail), fail)}`;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export const validateRuntimeStorageClaim = async (valueArg: unknown): Promise<string[]> => {
|
|
134
|
+
try {
|
|
135
|
+
const claim = snapshotRuntimeStorageClaim(valueArg);
|
|
136
|
+
return claim.digest === await computeRuntimeStorageClaimDigest(claim)
|
|
137
|
+
? [] : ['runtime storage claim digest does not match'];
|
|
138
|
+
} catch {
|
|
139
|
+
return ['runtime storage claim must use its exact canonical schema'];
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/** Content binding only; controller authentication, provider checks and attachment fencing remain owner-owned. */
|
|
144
|
+
export const bindRuntimeStorageClaimToAssignment = async (
|
|
145
|
+
claimArg: unknown, assignmentArg: unknown, expectedClusterIdArg: unknown,
|
|
146
|
+
): Promise<{ claim: IRuntimeStorageClaim; assignment: IRuntimeAssignment }> => {
|
|
147
|
+
const claim = snapshotRuntimeStorageClaim(claimArg);
|
|
148
|
+
const assignment = snapshotRuntimeAssignment(assignmentArg);
|
|
149
|
+
if ((await validateRuntimeStorageClaim(claim)).length || (await validateRuntimeAssignment(assignment)).length) fail();
|
|
150
|
+
const expectedClusterId = id(expectedClusterIdArg);
|
|
151
|
+
const reference = assignment.workload.storage.find((itemArg) => itemArg.id === claim.id);
|
|
152
|
+
requireValue(reference?.generation === claim.generation && reference.digest === claim.digest
|
|
153
|
+
&& claim.clusterId === expectedClusterId
|
|
154
|
+
&& assignment.controller.kind === 'cloudly'
|
|
155
|
+
&& assignment.controller.id === claim.controller.id
|
|
156
|
+
&& assignment.controller.epoch === claim.controller.epoch
|
|
157
|
+
&& assignment.organizationId === claim.organizationId
|
|
158
|
+
&& assignment.nodeId === claim.nodeId
|
|
159
|
+
&& assignment.runtimeNamespace === claim.runtimeNamespace
|
|
160
|
+
&& assignment.serviceId === claim.serviceId);
|
|
161
|
+
return { claim, assignment };
|
|
162
|
+
};
|
package/ts/data/secret.ts
CHANGED
|
@@ -84,15 +84,27 @@ export type TSecretVersionRetentionReferenceKind =
|
|
|
84
84
|
| 'applied-manifest'
|
|
85
85
|
| 'previous-accepted-manifest'
|
|
86
86
|
| 'rollback-manifest'
|
|
87
|
+
| 'runtime-assignment'
|
|
87
88
|
| 'backup'
|
|
88
89
|
| 'retention-window';
|
|
89
90
|
|
|
90
|
-
export interface
|
|
91
|
-
kind:
|
|
91
|
+
export interface IRuntimeAssignmentSecretVersionRetentionReference {
|
|
92
|
+
kind: 'runtime-assignment';
|
|
93
|
+
resourceId: string;
|
|
94
|
+
expiresAt?: never;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface IExpiringSecretVersionRetentionReference {
|
|
98
|
+
kind: Exclude<TSecretVersionRetentionReferenceKind, 'runtime-assignment'>;
|
|
92
99
|
resourceId: string;
|
|
93
100
|
expiresAt?: number;
|
|
94
101
|
}
|
|
95
102
|
|
|
103
|
+
/** Retains the existing public name while discriminating indefinite assignment holds. */
|
|
104
|
+
export type ISecretVersionRetentionReference =
|
|
105
|
+
| IRuntimeAssignmentSecretVersionRetentionReference
|
|
106
|
+
| IExpiringSecretVersionRetentionReference;
|
|
107
|
+
|
|
96
108
|
export const secretVersionRetentionReferenceLimits = Object.freeze({
|
|
97
109
|
pageSizeMaximum: 256,
|
|
98
110
|
});
|
|
@@ -435,6 +447,7 @@ export const validateSecretVersionRetentionReferences = (
|
|
|
435
447
|
'applied-manifest',
|
|
436
448
|
'previous-accepted-manifest',
|
|
437
449
|
'rollback-manifest',
|
|
450
|
+
'runtime-assignment',
|
|
438
451
|
'backup',
|
|
439
452
|
'retention-window',
|
|
440
453
|
]);
|
|
@@ -444,6 +457,7 @@ export const validateSecretVersionRetentionReferences = (
|
|
|
444
457
|
|| !hasOnlyKeys(referenceArg, ['kind', 'resourceId', 'expiresAt'])
|
|
445
458
|
|| !kinds.has(referenceArg.kind as TSecretVersionRetentionReferenceKind)
|
|
446
459
|
|| !isSecretIdentifier(referenceArg.resourceId)
|
|
460
|
+
|| (referenceArg.kind === 'runtime-assignment' && Object.hasOwn(referenceArg, 'expiresAt'))
|
|
447
461
|
|| (referenceArg.expiresAt !== undefined
|
|
448
462
|
&& !isNonNegativeSafeInteger(referenceArg.expiresAt))) {
|
|
449
463
|
errors.push(`secret version retention references[${index}] must be canonical`);
|
|
@@ -501,28 +501,30 @@ export const bindGetRuntimeAssignmentSecretMaterialRequest = async (
|
|
|
501
501
|
|
|
502
502
|
/**
|
|
503
503
|
* Verify one complete response without opening a private key. The caller must
|
|
504
|
-
* supply its authenticated current session, current assignment, pinned config
|
|
505
|
-
*
|
|
504
|
+
* supply its authenticated current session, current assignment, pinned config
|
|
505
|
+
* and active recipient from their respective trusted owners. The controller-issued
|
|
506
|
+
* session binding supplies the enrolled cluster.
|
|
506
507
|
*/
|
|
507
508
|
export const bindGetRuntimeAssignmentSecretMaterialResponse = async (
|
|
508
509
|
valueArg: unknown, requestArg: unknown, assignmentArg: unknown, configArg: unknown,
|
|
509
|
-
trustedSessionArg: unknown,
|
|
510
|
+
trustedSessionArg: unknown, activeRecipientArg: unknown,
|
|
510
511
|
): Promise<IGetRuntimeAssignmentSecretMaterialResponse> => {
|
|
511
512
|
try {
|
|
512
513
|
const value = exact(capture(valueArg, runtimeAssignmentSecretContract.maximumMaterialBytes), ['material']);
|
|
514
|
+
const trustedSession = session(capture(trustedSessionArg));
|
|
513
515
|
const material = exact(value.material, [
|
|
514
516
|
'assignment', 'nodeId', 'secrets', 'recipientKeyId', 'recipientGeneration',
|
|
515
517
|
'authority', 'manifest', 'workloadInit', 'entries',
|
|
516
518
|
]);
|
|
517
519
|
const request = await bindGetRuntimeAssignmentSecretMaterialRequest(
|
|
518
|
-
requestArg, assignmentArg,
|
|
520
|
+
requestArg, assignmentArg, trustedSession, activeRecipientArg,
|
|
519
521
|
);
|
|
520
522
|
const assignment = snapshotRuntimeAssignment(assignmentArg);
|
|
521
523
|
const authority = await bindRuntimeAssignmentSecretAuthority(
|
|
522
|
-
material.authority, material.manifest, assignment, configArg,
|
|
524
|
+
material.authority, material.manifest, assignment, configArg, trustedSession.clusterId,
|
|
523
525
|
);
|
|
524
526
|
const manifest = material.manifest as IResolvedSecretManifest;
|
|
525
|
-
requireValue(
|
|
527
|
+
requireValue(trustedSession.clusterId === manifest.clusterId
|
|
526
528
|
&& material.nodeId === assignment.nodeId
|
|
527
529
|
&& same(assignmentReference(material.assignment), request.assignment)
|
|
528
530
|
&& same(authorityReference(material.secrets), request.secrets)
|