@serve.zone/interfaces 27.4.0 → 28.0.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/.smartconfig.json +11 -4
- package/changelog.md +27 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/index.d.ts +2 -0
- package/dist_ts/data/index.js +3 -1
- package/dist_ts/data/nodecredential.d.ts +38 -0
- package/dist_ts/data/nodecredential.js +2 -0
- package/dist_ts/data/runtimeassignment.d.ts +120 -0
- package/dist_ts/data/runtimeassignment.js +267 -0
- package/dist_ts/requests/node.d.ts +40 -1
- package/package.json +4 -4
- package/readme.md +62 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/index.ts +2 -0
- package/ts/data/nodecredential.ts +41 -0
- package/ts/data/runtimeassignment.ts +402 -0
- package/ts/requests/node.ts +60 -1
- /package/{license → license.md} +0 -0
package/.smartconfig.json
CHANGED
|
@@ -20,10 +20,16 @@
|
|
|
20
20
|
]
|
|
21
21
|
},
|
|
22
22
|
"release": {
|
|
23
|
+
"preflight": {
|
|
24
|
+
"test": true,
|
|
25
|
+
"build": true
|
|
26
|
+
},
|
|
23
27
|
"targets": {
|
|
24
28
|
"git": {
|
|
25
29
|
"enabled": true,
|
|
26
|
-
"remote": "origin"
|
|
30
|
+
"remote": "origin",
|
|
31
|
+
"pushBranch": true,
|
|
32
|
+
"pushTags": true
|
|
27
33
|
},
|
|
28
34
|
"npm": {
|
|
29
35
|
"enabled": true,
|
|
@@ -31,7 +37,8 @@
|
|
|
31
37
|
"https://verdaccio.lossless.digital",
|
|
32
38
|
"https://registry.npmjs.org"
|
|
33
39
|
],
|
|
34
|
-
"accessLevel": "public"
|
|
40
|
+
"accessLevel": "public",
|
|
41
|
+
"alreadyPublished": "success"
|
|
35
42
|
},
|
|
36
43
|
"docker": {
|
|
37
44
|
"enabled": false,
|
|
@@ -41,7 +48,7 @@
|
|
|
41
48
|
}
|
|
42
49
|
},
|
|
43
50
|
"@git.zone/tsdoc": {
|
|
44
|
-
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
|
51
|
+
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license.md](license.md) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
|
45
52
|
},
|
|
46
53
|
"@ship.zone/szci": {}
|
|
47
|
-
}
|
|
54
|
+
}
|
package/changelog.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2026-09-05 - 28.0.0
|
|
4
|
+
|
|
5
|
+
### Breaking Changes
|
|
6
|
+
|
|
7
|
+
- require identity for node config and add credential administration contracts (node-credentials)
|
|
8
|
+
- Add node credential metadata, mutation precondition, and mutation result interfaces for spark and pallet purposes.
|
|
9
|
+
- Add getNodeCredential, rotateNodeCredential, and revokeNodeCredential typed request contracts with infrastructure-admin identity requirements.
|
|
10
|
+
- Require identity on getNodeConfig requests, changing the existing request shape.
|
|
11
|
+
- Document node credential administration semantics and add type-level contract tests.
|
|
12
|
+
|
|
13
|
+
## 2026-09-05 - 27.5.0
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
- add node-bound runtime assignment contracts (data)
|
|
18
|
+
- Add runtime assignment interfaces, canonical digest helpers, and data namespace exports.
|
|
19
|
+
- Add assignment and observation validators/evaluators for scope binding, CAS progression, replay handling, and sequence checks.
|
|
20
|
+
- Document node-bound runtime assignment semantics and add node test coverage.
|
|
21
|
+
|
|
22
|
+
### Fixes
|
|
23
|
+
|
|
24
|
+
- update release workflow and package metadata (release)
|
|
25
|
+
- Enable test and build preflight checks before releases
|
|
26
|
+
- Configure git releases to push branches and tags
|
|
27
|
+
- Update packaged license file references to license.md
|
|
28
|
+
- Bump dev tooling and package manager versions
|
|
29
|
+
|
|
3
30
|
## 2026-08-29 - 27.4.0
|
|
4
31
|
|
|
5
32
|
### Features
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@serve.zone/interfaces',
|
|
6
|
-
version: '
|
|
6
|
+
version: '28.0.0',
|
|
7
7
|
description: 'Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.'
|
|
8
8
|
};
|
|
9
9
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSx3QkFBd0I7SUFDOUIsT0FBTyxFQUFFLFFBQVE7SUFDakIsV0FBVyxFQUFFLHVGQUF1RjtDQUNyRyxDQUFBIn0=
|
package/dist_ts/data/index.d.ts
CHANGED
|
@@ -20,7 +20,9 @@ export * from './immutableimage.js';
|
|
|
20
20
|
export * from './isolatedrestore.js';
|
|
21
21
|
export * from './isolatedrestore.golden.js';
|
|
22
22
|
export * from './mail.js';
|
|
23
|
+
export * from './nodecredential.js';
|
|
23
24
|
export * from './registry.js';
|
|
25
|
+
export * from './runtimeassignment.js';
|
|
24
26
|
export * from './secret.js';
|
|
25
27
|
export * from './baremetal.js';
|
|
26
28
|
export * from './baseos.js';
|
package/dist_ts/data/index.js
CHANGED
|
@@ -20,7 +20,9 @@ export * from './immutableimage.js';
|
|
|
20
20
|
export * from './isolatedrestore.js';
|
|
21
21
|
export * from './isolatedrestore.golden.js';
|
|
22
22
|
export * from './mail.js';
|
|
23
|
+
export * from './nodecredential.js';
|
|
23
24
|
export * from './registry.js';
|
|
25
|
+
export * from './runtimeassignment.js';
|
|
24
26
|
export * from './secret.js';
|
|
25
27
|
export * from './baremetal.js';
|
|
26
28
|
export * from './baseos.js';
|
|
@@ -35,4 +37,4 @@ export * from './traffic.js';
|
|
|
35
37
|
export * from './user.js';
|
|
36
38
|
export * from './version.js';
|
|
37
39
|
export * from './webpush.js';
|
|
38
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
40
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9kYXRhL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLGNBQWMsb0JBQW9CLENBQUM7QUFDbkMsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxlQUFlLENBQUM7QUFDOUIsY0FBYyx1QkFBdUIsQ0FBQztBQUN0QyxjQUFjLGlCQUFpQixDQUFDO0FBQ2hDLGNBQWMseUJBQXlCLENBQUM7QUFDeEMsY0FBYywwQkFBMEIsQ0FBQztBQUN6QyxjQUFjLDBCQUEwQixDQUFDO0FBQ3pDLGNBQWMsZ0JBQWdCLENBQUM7QUFDL0IsY0FBYyxVQUFVLENBQUM7QUFDekIsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxZQUFZLENBQUM7QUFDM0IsY0FBYyx1QkFBdUIsQ0FBQztBQUN0QyxjQUFjLGNBQWMsQ0FBQztBQUM3QixjQUFjLGdCQUFnQixDQUFDO0FBQy9CLGNBQWMsWUFBWSxDQUFDO0FBQzNCLGNBQWMscUJBQXFCLENBQUM7QUFDcEMsY0FBYyxzQkFBc0IsQ0FBQztBQUNyQyxjQUFjLDZCQUE2QixDQUFDO0FBQzVDLGNBQWMsV0FBVyxDQUFDO0FBQzFCLGNBQWMscUJBQXFCLENBQUM7QUFDcEMsY0FBYyxlQUFlLENBQUM7QUFDOUIsY0FBYyx3QkFBd0IsQ0FBQztBQUN2QyxjQUFjLGFBQWEsQ0FBQztBQUM1QixjQUFjLGdCQUFnQixDQUFDO0FBQy9CLGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsa0JBQWtCLENBQUM7QUFDakMsY0FBYyxlQUFlLENBQUM7QUFDOUIsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyxtQkFBbUIsQ0FBQztBQUNsQyxjQUFjLGFBQWEsQ0FBQztBQUM1QixjQUFjLG9CQUFvQixDQUFDO0FBQ25DLGNBQWMsY0FBYyxDQUFDO0FBQzdCLGNBQWMsV0FBVyxDQUFDO0FBQzFCLGNBQWMsY0FBYyxDQUFDO0FBQzdCLGNBQWMsY0FBYyxDQUFDIn0=
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Independent infrastructure credentials; neither purpose grants workload RBAC. */
|
|
2
|
+
export type TNodeCredentialPurpose = 'spark' | 'pallet';
|
|
3
|
+
/**
|
|
4
|
+
* Public inspection and mutation-result metadata. Never includes bearer material,
|
|
5
|
+
* hashes, socket peer IDs or controller process IDs. On replay this is historical
|
|
6
|
+
* evidence, not an assertion that a credential or reporter session is still live.
|
|
7
|
+
*/
|
|
8
|
+
export interface INodeCredentialMetadataV1 {
|
|
9
|
+
schemaVersion: 1;
|
|
10
|
+
nodeId: string;
|
|
11
|
+
purpose: TNodeCredentialPurpose;
|
|
12
|
+
state: 'active' | 'revoked';
|
|
13
|
+
generation: number;
|
|
14
|
+
/** Migrated Spark credentials remain usable only until their staged rotation. */
|
|
15
|
+
requiresRotation: boolean;
|
|
16
|
+
sessionEpoch: number;
|
|
17
|
+
sessionId: string | null;
|
|
18
|
+
createdAt: number;
|
|
19
|
+
updatedAt: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Infrastructure-admin mutation preconditions. Both counters are exact CAS
|
|
23
|
+
* preconditions, not minimums. Retrying must retain this complete input and the
|
|
24
|
+
* original mutationId; a changed request with the same ID must fail closed.
|
|
25
|
+
*/
|
|
26
|
+
export interface INodeCredentialMutationPreconditionsV1 {
|
|
27
|
+
schemaVersion: 1;
|
|
28
|
+
nodeId: string;
|
|
29
|
+
purpose: TNodeCredentialPurpose;
|
|
30
|
+
mutationId: string;
|
|
31
|
+
expectedGeneration: number;
|
|
32
|
+
expectedSessionEpoch: number;
|
|
33
|
+
}
|
|
34
|
+
export interface INodeCredentialMutationResultV1 {
|
|
35
|
+
metadata: INodeCredentialMetadataV1;
|
|
36
|
+
/** A replay returns the original result without restoring its authority. */
|
|
37
|
+
replayed: boolean;
|
|
38
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { TSha256Digest, TImmutableContainerPlatform } from './immutableimage.js';
|
|
2
|
+
export interface IRuntimeControllerV1 {
|
|
3
|
+
kind: 'cloudly' | 'onebox';
|
|
4
|
+
id: string;
|
|
5
|
+
/** Persisted authority incarnation; changing it requires explicit re-admission. */
|
|
6
|
+
epoch: string;
|
|
7
|
+
}
|
|
8
|
+
/** Content-bound reference, not a credential, mount path or permission grant. */
|
|
9
|
+
export interface IRuntimeAuthorityReferenceV1 {
|
|
10
|
+
id: string;
|
|
11
|
+
generation: number;
|
|
12
|
+
digest: TSha256Digest;
|
|
13
|
+
}
|
|
14
|
+
export interface IRuntimeAssignmentReferenceV1 {
|
|
15
|
+
assignmentId: string;
|
|
16
|
+
generation: number;
|
|
17
|
+
digest: TSha256Digest;
|
|
18
|
+
}
|
|
19
|
+
/** Derived from authenticated transport and locally configured ownership. */
|
|
20
|
+
export interface IRuntimeAssignmentScopeV1 {
|
|
21
|
+
controller: IRuntimeControllerV1;
|
|
22
|
+
organizationId: string;
|
|
23
|
+
nodeId: string;
|
|
24
|
+
runtimeNamespace: string;
|
|
25
|
+
}
|
|
26
|
+
export type TRuntimeAssignmentDispositionV1 = 'run' | 'stop-preserve' | 'remove-runtime-preserve-storage';
|
|
27
|
+
export interface IRuntimeAssignmentV1 extends IRuntimeAssignmentScopeV1 {
|
|
28
|
+
schemaVersion: 1;
|
|
29
|
+
assignmentId: string;
|
|
30
|
+
serviceId: string;
|
|
31
|
+
/** Stable logical replica; independent of container IDs and execution attempts. */
|
|
32
|
+
replicaId: string;
|
|
33
|
+
/** One execution attempt, immutable for this assignment. */
|
|
34
|
+
attemptId: string;
|
|
35
|
+
/** Logical topology provenance; placement authority remains nodeId. */
|
|
36
|
+
workerGroupId: string | null;
|
|
37
|
+
siteId: string | null;
|
|
38
|
+
generation: number;
|
|
39
|
+
/** Exact CAS predecessor; null only at generation 1. */
|
|
40
|
+
previous: IRuntimeAssignmentReferenceV1 | null;
|
|
41
|
+
disposition: TRuntimeAssignmentDispositionV1;
|
|
42
|
+
/** No automatic relocation or expiry-based claim of a stopped writer. */
|
|
43
|
+
offlinePolicy: 'continue-node-bound';
|
|
44
|
+
workload: {
|
|
45
|
+
registryHost: string;
|
|
46
|
+
repository: string;
|
|
47
|
+
imageDigest: TSha256Digest;
|
|
48
|
+
platform: TImmutableContainerPlatform;
|
|
49
|
+
/** Immutable complete invocation/configuration owned by the controller. */
|
|
50
|
+
config: IRuntimeAuthorityReferenceV1;
|
|
51
|
+
network: IRuntimeAuthorityReferenceV1 | null;
|
|
52
|
+
secrets: IRuntimeAuthorityReferenceV1 | null;
|
|
53
|
+
/** Sorted unique reference IDs. Empty means no persistent storage claims. */
|
|
54
|
+
storage: IRuntimeAuthorityReferenceV1[];
|
|
55
|
+
};
|
|
56
|
+
digest: TSha256Digest;
|
|
57
|
+
}
|
|
58
|
+
export type TRuntimeAssignmentDecisionV1 = {
|
|
59
|
+
status: 'accept' | 'replay';
|
|
60
|
+
assignment: IRuntimeAssignmentV1;
|
|
61
|
+
} | {
|
|
62
|
+
status: 'reject';
|
|
63
|
+
reason: 'invalid' | 'scope-mismatch' | 'conflict' | 'stale' | 'predecessor-mismatch' | 'attempt-mismatch' | 'disposition-regression';
|
|
64
|
+
};
|
|
65
|
+
/** Value-free execution evidence. Receipt/acceptance alone never means ready. */
|
|
66
|
+
export interface IRuntimeAssignmentObservationV1 {
|
|
67
|
+
schemaVersion: 1;
|
|
68
|
+
assignment: IRuntimeAssignmentReferenceV1;
|
|
69
|
+
/** Authenticated reporter session; its authority is verified by the receiver. */
|
|
70
|
+
reporterSessionId: string;
|
|
71
|
+
/** Durable per-assignment counter; never resets when a reporter reconnects. */
|
|
72
|
+
sequence: number;
|
|
73
|
+
observedAt: number;
|
|
74
|
+
phase: 'pending' | 'running' | 'stopped' | 'removed' | 'failed' | 'unknown';
|
|
75
|
+
ready: boolean;
|
|
76
|
+
runtime: {
|
|
77
|
+
sandboxId: string;
|
|
78
|
+
containerId: string;
|
|
79
|
+
imageDigest: TSha256Digest;
|
|
80
|
+
} | null;
|
|
81
|
+
digest: TSha256Digest;
|
|
82
|
+
}
|
|
83
|
+
export declare const runtimeAssignmentV1Contract: Readonly<{
|
|
84
|
+
readonly assignmentDigestDomain: "serve.zone/runtime-assignment-v1";
|
|
85
|
+
readonly observationDigestDomain: "serve.zone/runtime-assignment-observation-v1";
|
|
86
|
+
readonly maximumStorageReferences: 64;
|
|
87
|
+
readonly omission: "no-effect";
|
|
88
|
+
readonly removal: "retain-assignment-tombstone-and-storage";
|
|
89
|
+
}>;
|
|
90
|
+
/** Digest helpers validate structure and exclude only the object's own digest. */
|
|
91
|
+
export declare const computeRuntimeAssignmentV1Digest: (assignmentArg: IRuntimeAssignmentV1) => Promise<TSha256Digest>;
|
|
92
|
+
export declare const validateRuntimeAssignmentV1: (valueArg: unknown) => Promise<string[]>;
|
|
93
|
+
export declare const getRuntimeAssignmentV1Reference: (assignmentArg: IRuntimeAssignmentV1) => IRuntimeAssignmentReferenceV1;
|
|
94
|
+
/**
|
|
95
|
+
* Pure CAS decision, not authentication or execution. The caller must persist
|
|
96
|
+
* acceptance atomically before effects, serialize replica ownership, and retain
|
|
97
|
+
* removed assignments as tombstones. New attempts require separate stop/fencing
|
|
98
|
+
* admission for every older writer. An omitted assignment causes no transition.
|
|
99
|
+
* Persist only the returned detached snapshot, never the mutable input object.
|
|
100
|
+
*/
|
|
101
|
+
export declare const evaluateRuntimeAssignmentV1: (incomingArg: IRuntimeAssignmentV1, acceptedArg: IRuntimeAssignmentV1 | null, trustedScopeArg: IRuntimeAssignmentScopeV1) => Promise<TRuntimeAssignmentDecisionV1>;
|
|
102
|
+
export declare const computeRuntimeAssignmentObservationV1Digest: (observationArg: IRuntimeAssignmentObservationV1) => Promise<TSha256Digest>;
|
|
103
|
+
/** Checks exact assignment evidence; transport must separately bind reporter/session. */
|
|
104
|
+
export declare const validateRuntimeAssignmentObservationV1: (observationArg: unknown, assignmentArg: IRuntimeAssignmentV1) => Promise<string[]>;
|
|
105
|
+
export type TRuntimeObservationDecisionV1 = {
|
|
106
|
+
status: 'accept' | 'replay';
|
|
107
|
+
observation: IRuntimeAssignmentObservationV1;
|
|
108
|
+
} | {
|
|
109
|
+
status: 'reject';
|
|
110
|
+
reason: 'invalid' | 'session-mismatch' | 'stale' | 'conflict' | 'sequence-gap';
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Receiver must authorize the assignment's node before calling this helper.
|
|
114
|
+
* Compare/persist the returned snapshot atomically with the old receipt. A new
|
|
115
|
+
* session continues the durable counter; it does not reset observation history.
|
|
116
|
+
* Across dispositions, the old receipt must reference the exact predecessor;
|
|
117
|
+
* receivers must process each revision's observations in order.
|
|
118
|
+
* These observations are never sufficient authority to relocate a storage writer.
|
|
119
|
+
*/
|
|
120
|
+
export declare const evaluateRuntimeAssignmentObservationV1: (incomingArg: IRuntimeAssignmentObservationV1, acceptedArg: IRuntimeAssignmentObservationV1 | null, assignmentArg: IRuntimeAssignmentV1, trustedReporterSessionIdArg: string) => Promise<TRuntimeObservationDecisionV1>;
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { buildDigestPinnedImageReference, isSha256Digest } from './immutableimage.js';
|
|
2
|
+
import { canonicalizeStrictJson, createCanonicalJsonSha256Hex } from '../private/canonicaljson.js';
|
|
3
|
+
export const runtimeAssignmentV1Contract = Object.freeze({
|
|
4
|
+
assignmentDigestDomain: 'serve.zone/runtime-assignment-v1',
|
|
5
|
+
observationDigestDomain: 'serve.zone/runtime-assignment-observation-v1',
|
|
6
|
+
maximumStorageReferences: 64,
|
|
7
|
+
omission: 'no-effect',
|
|
8
|
+
removal: 'retain-assignment-tombstone-and-storage',
|
|
9
|
+
});
|
|
10
|
+
const fail = (messageArg) => { throw new Error(messageArg); };
|
|
11
|
+
const isIdentifier = (valueArg) => typeof valueArg === 'string' && /^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/.test(valueArg);
|
|
12
|
+
const isPositiveInteger = (valueArg) => typeof valueArg === 'number' && Number.isSafeInteger(valueArg) && valueArg > 0;
|
|
13
|
+
const isDigest = (valueArg) => typeof valueArg === 'string' && isSha256Digest(valueArg);
|
|
14
|
+
/** Reject accessors before reading any fields; do not run input-owned code. */
|
|
15
|
+
const isExactRecord = (valueArg, keysArg) => {
|
|
16
|
+
if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg))
|
|
17
|
+
return false;
|
|
18
|
+
const prototype = Object.getPrototypeOf(valueArg);
|
|
19
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
20
|
+
return false;
|
|
21
|
+
const keys = Reflect.ownKeys(valueArg);
|
|
22
|
+
return keys.length === keysArg.length && keys.every((keyArg) => {
|
|
23
|
+
const descriptor = Object.getOwnPropertyDescriptor(valueArg, keyArg);
|
|
24
|
+
return typeof keyArg === 'string' && keysArg.includes(keyArg)
|
|
25
|
+
&& descriptor?.enumerable === true && Object.hasOwn(descriptor, 'value');
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
const isController = (valueArg) => isExactRecord(valueArg, ['kind', 'id', 'epoch'])
|
|
29
|
+
&& (valueArg.kind === 'cloudly' || valueArg.kind === 'onebox')
|
|
30
|
+
&& isIdentifier(valueArg.id) && isIdentifier(valueArg.epoch);
|
|
31
|
+
const isAuthorityReference = (valueArg) => isExactRecord(valueArg, ['id', 'generation', 'digest'])
|
|
32
|
+
&& isIdentifier(valueArg.id) && isPositiveInteger(valueArg.generation) && isDigest(valueArg.digest);
|
|
33
|
+
const isAssignmentReference = (valueArg) => isExactRecord(valueArg, ['assignmentId', 'generation', 'digest'])
|
|
34
|
+
&& isIdentifier(valueArg.assignmentId) && isPositiveInteger(valueArg.generation)
|
|
35
|
+
&& isDigest(valueArg.digest);
|
|
36
|
+
const assignmentKeys = [
|
|
37
|
+
'schemaVersion', 'controller', 'organizationId', 'nodeId', 'runtimeNamespace',
|
|
38
|
+
'assignmentId', 'serviceId', 'replicaId', 'attemptId', 'workerGroupId', 'siteId',
|
|
39
|
+
'generation', 'previous', 'disposition', 'offlinePolicy', 'workload', 'digest',
|
|
40
|
+
];
|
|
41
|
+
const assertAssignment = (valueArg) => {
|
|
42
|
+
if (!isExactRecord(valueArg, assignmentKeys)
|
|
43
|
+
|| valueArg.schemaVersion !== 1 || !isController(valueArg.controller)
|
|
44
|
+
|| !isPositiveInteger(valueArg.generation) || !isDigest(valueArg.digest)
|
|
45
|
+
|| valueArg.offlinePolicy !== 'continue-node-bound'
|
|
46
|
+
|| !['run', 'stop-preserve', 'remove-runtime-preserve-storage'].includes(valueArg.disposition))
|
|
47
|
+
fail('runtime assignment must use its exact v1 schema');
|
|
48
|
+
for (const key of ['organizationId', 'nodeId', 'runtimeNamespace', 'assignmentId',
|
|
49
|
+
'serviceId', 'replicaId', 'attemptId']) {
|
|
50
|
+
if (!isIdentifier(valueArg[key]))
|
|
51
|
+
fail(`runtime assignment ${key} must be a canonical identifier`);
|
|
52
|
+
}
|
|
53
|
+
for (const key of ['workerGroupId', 'siteId']) {
|
|
54
|
+
if (valueArg[key] !== null && !isIdentifier(valueArg[key]))
|
|
55
|
+
fail('invalid runtime topology ID');
|
|
56
|
+
}
|
|
57
|
+
if (valueArg.generation === 1 ? valueArg.previous !== null
|
|
58
|
+
: !isAssignmentReference(valueArg.previous)
|
|
59
|
+
|| valueArg.previous.assignmentId !== valueArg.assignmentId
|
|
60
|
+
|| valueArg.previous.generation !== valueArg.generation - 1) {
|
|
61
|
+
fail('runtime assignment must bind its exact preceding generation');
|
|
62
|
+
}
|
|
63
|
+
const workload = valueArg.workload;
|
|
64
|
+
if (!isExactRecord(workload, ['registryHost', 'repository', 'imageDigest', 'platform',
|
|
65
|
+
'config', 'network', 'secrets', 'storage'])
|
|
66
|
+
|| typeof workload.registryHost !== 'string' || workload.registryHost.length > 253
|
|
67
|
+
|| typeof workload.repository !== 'string' || workload.repository.length > 255
|
|
68
|
+
|| !isDigest(workload.imageDigest)
|
|
69
|
+
|| !['linux/amd64', 'linux/arm64'].includes(workload.platform)
|
|
70
|
+
|| !isAuthorityReference(workload.config)
|
|
71
|
+
|| (workload.network !== null && !isAuthorityReference(workload.network))
|
|
72
|
+
|| (workload.secrets !== null && !isAuthorityReference(workload.secrets))
|
|
73
|
+
|| !Array.isArray(workload.storage)
|
|
74
|
+
|| workload.storage.length > runtimeAssignmentV1Contract.maximumStorageReferences) {
|
|
75
|
+
fail('runtime assignment workload must use exact immutable references');
|
|
76
|
+
}
|
|
77
|
+
buildDigestPinnedImageReference(workload.registryHost, workload.repository, workload.imageDigest);
|
|
78
|
+
let previousId = '';
|
|
79
|
+
for (let index = 0; index < workload.storage.length; index++) {
|
|
80
|
+
const descriptor = Object.getOwnPropertyDescriptor(workload.storage, String(index));
|
|
81
|
+
if (!descriptor || !Object.hasOwn(descriptor, 'value')
|
|
82
|
+
|| !isAuthorityReference(descriptor.value) || descriptor.value.id <= previousId) {
|
|
83
|
+
fail('runtime assignment storage references must be dense, sorted and unique');
|
|
84
|
+
}
|
|
85
|
+
previousId = descriptor.value.id;
|
|
86
|
+
}
|
|
87
|
+
canonicalizeStrictJson(valueArg, fail);
|
|
88
|
+
return valueArg;
|
|
89
|
+
};
|
|
90
|
+
const digestPayload = async (domainArg, valueArg) => {
|
|
91
|
+
const { digest: _digest, ...payload } = valueArg;
|
|
92
|
+
return `sha256:${await createCanonicalJsonSha256Hex(canonicalizeStrictJson({ domain: domainArg, payload }, fail), fail)}`;
|
|
93
|
+
};
|
|
94
|
+
/** Digest helpers validate structure and exclude only the object's own digest. */
|
|
95
|
+
export const computeRuntimeAssignmentV1Digest = async (assignmentArg) => digestPayload(runtimeAssignmentV1Contract.assignmentDigestDomain, assertAssignment(assignmentArg));
|
|
96
|
+
export const validateRuntimeAssignmentV1 = async (valueArg) => {
|
|
97
|
+
try {
|
|
98
|
+
const assignment = snapshotAssignment(valueArg);
|
|
99
|
+
return assignment.digest === await computeRuntimeAssignmentV1Digest(assignment)
|
|
100
|
+
? [] : ['runtime assignment digest does not match'];
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return ['runtime assignment must use its exact canonical v1 schema'];
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
const snapshotAssignment = (valueArg) => JSON.parse(canonicalizeStrictJson(assertAssignment(valueArg), fail));
|
|
107
|
+
export const getRuntimeAssignmentV1Reference = (assignmentArg) => ({
|
|
108
|
+
assignmentId: assignmentArg.assignmentId,
|
|
109
|
+
generation: assignmentArg.generation,
|
|
110
|
+
digest: assignmentArg.digest,
|
|
111
|
+
});
|
|
112
|
+
const canonical = (valueArg) => canonicalizeStrictJson(valueArg, fail);
|
|
113
|
+
const immutableAttempt = (assignmentArg) => {
|
|
114
|
+
const { generation, previous, disposition, digest, ...attempt } = assignmentArg;
|
|
115
|
+
return canonical(attempt);
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* Pure CAS decision, not authentication or execution. The caller must persist
|
|
119
|
+
* acceptance atomically before effects, serialize replica ownership, and retain
|
|
120
|
+
* removed assignments as tombstones. New attempts require separate stop/fencing
|
|
121
|
+
* admission for every older writer. An omitted assignment causes no transition.
|
|
122
|
+
* Persist only the returned detached snapshot, never the mutable input object.
|
|
123
|
+
*/
|
|
124
|
+
export const evaluateRuntimeAssignmentV1 = async (incomingArg, acceptedArg, trustedScopeArg) => {
|
|
125
|
+
try {
|
|
126
|
+
incomingArg = snapshotAssignment(incomingArg);
|
|
127
|
+
acceptedArg = acceptedArg === null ? null : snapshotAssignment(acceptedArg);
|
|
128
|
+
if (!isExactRecord(trustedScopeArg, ['controller', 'organizationId', 'nodeId', 'runtimeNamespace'])
|
|
129
|
+
|| !isController(trustedScopeArg.controller)
|
|
130
|
+
|| !isIdentifier(trustedScopeArg.organizationId) || !isIdentifier(trustedScopeArg.nodeId)
|
|
131
|
+
|| !isIdentifier(trustedScopeArg.runtimeNamespace))
|
|
132
|
+
return { status: 'reject', reason: 'invalid' };
|
|
133
|
+
trustedScopeArg = JSON.parse(canonical(trustedScopeArg));
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return { status: 'reject', reason: 'invalid' };
|
|
137
|
+
}
|
|
138
|
+
if ((await validateRuntimeAssignmentV1(incomingArg)).length
|
|
139
|
+
|| (acceptedArg !== null && (await validateRuntimeAssignmentV1(acceptedArg)).length)) {
|
|
140
|
+
return { status: 'reject', reason: 'invalid' };
|
|
141
|
+
}
|
|
142
|
+
const matchesScope = (assignmentArg) => assignmentArg.controller.kind === trustedScopeArg.controller.kind
|
|
143
|
+
&& assignmentArg.controller.id === trustedScopeArg.controller.id
|
|
144
|
+
&& assignmentArg.controller.epoch === trustedScopeArg.controller.epoch
|
|
145
|
+
&& assignmentArg.organizationId === trustedScopeArg.organizationId
|
|
146
|
+
&& assignmentArg.nodeId === trustedScopeArg.nodeId
|
|
147
|
+
&& assignmentArg.runtimeNamespace === trustedScopeArg.runtimeNamespace;
|
|
148
|
+
if (!matchesScope(incomingArg) || (acceptedArg !== null && !matchesScope(acceptedArg))) {
|
|
149
|
+
return { status: 'reject', reason: 'scope-mismatch' };
|
|
150
|
+
}
|
|
151
|
+
if (acceptedArg === null) {
|
|
152
|
+
return incomingArg.generation === 1
|
|
153
|
+
&& incomingArg.disposition === 'run'
|
|
154
|
+
? { status: 'accept', assignment: incomingArg }
|
|
155
|
+
: { status: 'reject', reason: 'predecessor-mismatch' };
|
|
156
|
+
}
|
|
157
|
+
if (incomingArg.assignmentId !== acceptedArg.assignmentId) {
|
|
158
|
+
return { status: 'reject', reason: 'attempt-mismatch' };
|
|
159
|
+
}
|
|
160
|
+
if (incomingArg.generation < acceptedArg.generation)
|
|
161
|
+
return { status: 'reject', reason: 'stale' };
|
|
162
|
+
if (incomingArg.generation === acceptedArg.generation) {
|
|
163
|
+
return incomingArg.digest === acceptedArg.digest
|
|
164
|
+
? { status: 'replay', assignment: acceptedArg } : { status: 'reject', reason: 'conflict' };
|
|
165
|
+
}
|
|
166
|
+
if (canonical(incomingArg.previous) !== canonical(getRuntimeAssignmentV1Reference(acceptedArg))) {
|
|
167
|
+
return { status: 'reject', reason: 'predecessor-mismatch' };
|
|
168
|
+
}
|
|
169
|
+
if (immutableAttempt(incomingArg) !== immutableAttempt(acceptedArg)) {
|
|
170
|
+
return { status: 'reject', reason: 'attempt-mismatch' };
|
|
171
|
+
}
|
|
172
|
+
const order = {
|
|
173
|
+
run: 0, 'stop-preserve': 1, 'remove-runtime-preserve-storage': 2,
|
|
174
|
+
};
|
|
175
|
+
if (order[incomingArg.disposition] !== order[acceptedArg.disposition] + 1) {
|
|
176
|
+
return { status: 'reject', reason: 'disposition-regression' };
|
|
177
|
+
}
|
|
178
|
+
return { status: 'accept', assignment: incomingArg };
|
|
179
|
+
};
|
|
180
|
+
const assertObservation = (valueArg) => {
|
|
181
|
+
if (!isExactRecord(valueArg, ['schemaVersion', 'assignment', 'reporterSessionId',
|
|
182
|
+
'sequence', 'observedAt', 'phase', 'ready', 'runtime', 'digest'])
|
|
183
|
+
|| valueArg.schemaVersion !== 1 || !isAssignmentReference(valueArg.assignment)
|
|
184
|
+
|| !isIdentifier(valueArg.reporterSessionId) || !isPositiveInteger(valueArg.sequence)
|
|
185
|
+
|| !isPositiveInteger(valueArg.observedAt) || !isDigest(valueArg.digest)
|
|
186
|
+
|| typeof valueArg.ready !== 'boolean'
|
|
187
|
+
|| !['pending', 'running', 'stopped', 'removed', 'failed', 'unknown'].includes(valueArg.phase)) {
|
|
188
|
+
fail('runtime observation must use its exact v1 schema');
|
|
189
|
+
}
|
|
190
|
+
if (valueArg.runtime !== null && (!isExactRecord(valueArg.runtime, [
|
|
191
|
+
'sandboxId', 'containerId', 'imageDigest',
|
|
192
|
+
]) || !isIdentifier(valueArg.runtime.sandboxId) || !isIdentifier(valueArg.runtime.containerId)
|
|
193
|
+
|| !isDigest(valueArg.runtime.imageDigest)))
|
|
194
|
+
fail('runtime identity must be canonical');
|
|
195
|
+
if ((valueArg.ready && valueArg.phase !== 'running')
|
|
196
|
+
|| (valueArg.phase === 'running' && valueArg.runtime === null)
|
|
197
|
+
|| (valueArg.phase === 'removed' && valueArg.runtime !== null)) {
|
|
198
|
+
fail('runtime observation readiness and identity contradict its phase');
|
|
199
|
+
}
|
|
200
|
+
canonical(valueArg);
|
|
201
|
+
return valueArg;
|
|
202
|
+
};
|
|
203
|
+
export const computeRuntimeAssignmentObservationV1Digest = async (observationArg) => digestPayload(runtimeAssignmentV1Contract.observationDigestDomain, assertObservation(observationArg));
|
|
204
|
+
/** Checks exact assignment evidence; transport must separately bind reporter/session. */
|
|
205
|
+
export const validateRuntimeAssignmentObservationV1 = async (observationArg, assignmentArg) => {
|
|
206
|
+
try {
|
|
207
|
+
const observation = JSON.parse(canonical(assertObservation(observationArg)));
|
|
208
|
+
assignmentArg = snapshotAssignment(assignmentArg);
|
|
209
|
+
if ((await validateRuntimeAssignmentV1(assignmentArg)).length)
|
|
210
|
+
return ['invalid assignment'];
|
|
211
|
+
if (canonical(observation.assignment) !== canonical(getRuntimeAssignmentV1Reference(assignmentArg))) {
|
|
212
|
+
return ['runtime observation does not match the exact assignment'];
|
|
213
|
+
}
|
|
214
|
+
if (observation.digest !== await computeRuntimeAssignmentObservationV1Digest(observation)) {
|
|
215
|
+
return ['runtime observation digest does not match'];
|
|
216
|
+
}
|
|
217
|
+
if (observation.ready && (assignmentArg.disposition !== 'run'
|
|
218
|
+
|| observation.runtime?.imageDigest !== assignmentArg.workload.imageDigest)) {
|
|
219
|
+
return ['runtime readiness does not match authorized execution'];
|
|
220
|
+
}
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return ['runtime observation must use its exact canonical v1 schema'];
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
/**
|
|
228
|
+
* Receiver must authorize the assignment's node before calling this helper.
|
|
229
|
+
* Compare/persist the returned snapshot atomically with the old receipt. A new
|
|
230
|
+
* session continues the durable counter; it does not reset observation history.
|
|
231
|
+
* Across dispositions, the old receipt must reference the exact predecessor;
|
|
232
|
+
* receivers must process each revision's observations in order.
|
|
233
|
+
* These observations are never sufficient authority to relocate a storage writer.
|
|
234
|
+
*/
|
|
235
|
+
export const evaluateRuntimeAssignmentObservationV1 = async (incomingArg, acceptedArg, assignmentArg, trustedReporterSessionIdArg) => {
|
|
236
|
+
try {
|
|
237
|
+
incomingArg = JSON.parse(canonical(assertObservation(incomingArg)));
|
|
238
|
+
acceptedArg = acceptedArg === null ? null
|
|
239
|
+
: JSON.parse(canonical(assertObservation(acceptedArg)));
|
|
240
|
+
assignmentArg = snapshotAssignment(assignmentArg);
|
|
241
|
+
if (!isIdentifier(trustedReporterSessionIdArg)
|
|
242
|
+
|| (await validateRuntimeAssignmentObservationV1(incomingArg, assignmentArg)).length
|
|
243
|
+
|| (acceptedArg !== null && (acceptedArg.digest !== await computeRuntimeAssignmentObservationV1Digest(acceptedArg)
|
|
244
|
+
|| acceptedArg.assignment.assignmentId !== assignmentArg.assignmentId
|
|
245
|
+
|| (canonical(acceptedArg.assignment) !== canonical(getRuntimeAssignmentV1Reference(assignmentArg))
|
|
246
|
+
&& canonical(acceptedArg.assignment) !== canonical(assignmentArg.previous)))))
|
|
247
|
+
return { status: 'reject', reason: 'invalid' };
|
|
248
|
+
if (incomingArg.reporterSessionId !== trustedReporterSessionIdArg) {
|
|
249
|
+
return { status: 'reject', reason: 'session-mismatch' };
|
|
250
|
+
}
|
|
251
|
+
if (acceptedArg !== null && incomingArg.sequence <= acceptedArg.sequence) {
|
|
252
|
+
if (incomingArg.sequence < acceptedArg.sequence)
|
|
253
|
+
return { status: 'reject', reason: 'stale' };
|
|
254
|
+
return incomingArg.digest === acceptedArg.digest
|
|
255
|
+
? { status: 'replay', observation: acceptedArg }
|
|
256
|
+
: { status: 'reject', reason: 'conflict' };
|
|
257
|
+
}
|
|
258
|
+
if (incomingArg.sequence !== (acceptedArg?.sequence ?? 0) + 1) {
|
|
259
|
+
return { status: 'reject', reason: 'sequence-gap' };
|
|
260
|
+
}
|
|
261
|
+
return { status: 'accept', observation: incomingArg };
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return { status: 'reject', reason: 'invalid' };
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicnVudGltZWFzc2lnbm1lbnQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9kYXRhL3J1bnRpbWVhc3NpZ25tZW50LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUNBLE9BQU8sRUFBRSwrQkFBK0IsRUFBRSxjQUFjLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQUN0RixPQUFPLEVBQUUsc0JBQXNCLEVBQUUsNEJBQTRCLEVBQUUsTUFBTSw2QkFBNkIsQ0FBQztBQTJGbkcsTUFBTSxDQUFDLE1BQU0sMkJBQTJCLEdBQUcsTUFBTSxDQUFDLE1BQU0sQ0FBQztJQUN2RCxzQkFBc0IsRUFBRSxrQ0FBa0M7SUFDMUQsdUJBQXVCLEVBQUUsOENBQThDO0lBQ3ZFLHdCQUF3QixFQUFFLEVBQUU7SUFDNUIsUUFBUSxFQUFFLFdBQVc7SUFDckIsT0FBTyxFQUFFLHlDQUF5QztDQUMxQyxDQUFDLENBQUM7QUFFWixNQUFNLElBQUksR0FBa0MsQ0FBQyxVQUFVLEVBQUUsRUFBRSxHQUFHLE1BQU0sSUFBSSxLQUFLLENBQUMsVUFBVSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFDN0YsTUFBTSxZQUFZLEdBQUcsQ0FBQyxRQUFpQixFQUFzQixFQUFFLENBQzdELE9BQU8sUUFBUSxLQUFLLFFBQVEsSUFBSSxxQ0FBcUMsQ0FBQyxJQUFJLENBQUMsUUFBUSxDQUFDLENBQUM7QUFDdkYsTUFBTSxpQkFBaUIsR0FBRyxDQUFDLFFBQWlCLEVBQXNCLEVBQUUsQ0FDbEUsT0FBTyxRQUFRLEtBQUssUUFBUSxJQUFJLE1BQU0sQ0FBQyxhQUFhLENBQUMsUUFBUSxDQUFDLElBQUksUUFBUSxHQUFHLENBQUMsQ0FBQztBQUNqRixNQUFNLFFBQVEsR0FBRyxDQUFDLFFBQWlCLEVBQTZCLEVBQUUsQ0FDaEUsT0FBTyxRQUFRLEtBQUssUUFBUSxJQUFJLGNBQWMsQ0FBQyxRQUFRLENBQUMsQ0FBQztBQUUzRCwrRUFBK0U7QUFDL0UsTUFBTSxhQUFhLEdBQUcs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
|
|
@@ -4,15 +4,54 @@ import type { ICluster } from '../data/cluster.js';
|
|
|
4
4
|
import type { IClusterNode, IClusterNodeMetrics, INodeAction, INodeMetricsSample, ISparkNodeRuntimeInfo, TNodeActionName } from '../data/clusternode.js';
|
|
5
5
|
import type { IDeployment } from '../data/deployment.js';
|
|
6
6
|
import type { IIdentity } from '../data/user.js';
|
|
7
|
-
|
|
7
|
+
import type { INodeCredentialMetadataV1, INodeCredentialMutationPreconditionsV1, INodeCredentialMutationResultV1, TNodeCredentialPurpose } from '../data/nodecredential.js';
|
|
8
|
+
/** Requires verified platform-infrastructure admin authority; returns public data only. */
|
|
9
|
+
export interface IRequest_Any_Cloudly_GetNodeConfig extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_GetNodeConfig> {
|
|
8
10
|
method: 'getNodeConfig';
|
|
9
11
|
request: {
|
|
12
|
+
identity: IIdentity;
|
|
10
13
|
nodeId: string;
|
|
11
14
|
};
|
|
12
15
|
response: {
|
|
13
16
|
configData: IClusterNode;
|
|
14
17
|
};
|
|
15
18
|
}
|
|
19
|
+
/** These credential methods require verified infrastructure-admin authority. */
|
|
20
|
+
export interface IReq_Any_Cloudly_GetNodeCredential extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Any_Cloudly_GetNodeCredential> {
|
|
21
|
+
method: 'getNodeCredential';
|
|
22
|
+
request: {
|
|
23
|
+
identity: IIdentity;
|
|
24
|
+
nodeId: string;
|
|
25
|
+
purpose: TNodeCredentialPurpose;
|
|
26
|
+
};
|
|
27
|
+
response: {
|
|
28
|
+
metadata: INodeCredentialMetadataV1 | null;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export interface IReq_Any_Cloudly_RotateNodeCredential extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Any_Cloudly_RotateNodeCredential> {
|
|
32
|
+
method: 'rotateNodeCredential';
|
|
33
|
+
request: {
|
|
34
|
+
identity: IIdentity;
|
|
35
|
+
mutation: INodeCredentialMutationPreconditionsV1 & {
|
|
36
|
+
/**
|
|
37
|
+
* SHA-256(token UTF-8 bytes), exactly 64 lowercase hexadecimal characters.
|
|
38
|
+
* The node must durably retain its pending CSPRNG credential and mutation
|
|
39
|
+
* identity before this request. A metadata-only retry never issues plaintext.
|
|
40
|
+
* Treat this request field as sensitive; never include it in hooks or logs.
|
|
41
|
+
*/
|
|
42
|
+
newTokenHash: string;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
response: INodeCredentialMutationResultV1;
|
|
46
|
+
}
|
|
47
|
+
export interface IReq_Any_Cloudly_RevokeNodeCredential extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_Any_Cloudly_RevokeNodeCredential> {
|
|
48
|
+
method: 'revokeNodeCredential';
|
|
49
|
+
request: {
|
|
50
|
+
identity: IIdentity;
|
|
51
|
+
mutation: INodeCredentialMutationPreconditionsV1;
|
|
52
|
+
};
|
|
53
|
+
response: INodeCredentialMutationResultV1;
|
|
54
|
+
}
|
|
16
55
|
export interface IRequest_Any_Cloudly_GetNodesByCluster {
|
|
17
56
|
method: 'getNodesByCluster';
|
|
18
57
|
request: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serve.zone/interfaces",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "28.0.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.",
|
|
6
6
|
"exports": {
|
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@git.zone/tsbuild": "^4.4.2",
|
|
31
|
-
"@git.zone/tsdoc": "^2.
|
|
31
|
+
"@git.zone/tsdoc": "^2.2.0",
|
|
32
32
|
"@git.zone/tsrun": "^2.0.6",
|
|
33
33
|
"@git.zone/tstest": "^4.0.0",
|
|
34
|
-
"@types/node": "^26.
|
|
34
|
+
"@types/node": "^26.4.1"
|
|
35
35
|
},
|
|
36
36
|
"files": [
|
|
37
37
|
"ts/**/*",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
".smartconfig.json",
|
|
42
42
|
"readme.md",
|
|
43
43
|
"changelog.md",
|
|
44
|
-
"license"
|
|
44
|
+
"license.md"
|
|
45
45
|
],
|
|
46
46
|
"repository": {
|
|
47
47
|
"type": "git",
|
package/readme.md
CHANGED
|
@@ -47,6 +47,67 @@ validator; unlike `@serve.zone/interfaces/runtime`, it does not load the general
|
|
|
47
47
|
runtime graph, `plugins.js`, SmartCrypto, TypedRequest, Corestore, or settlement
|
|
48
48
|
runtime modules.
|
|
49
49
|
|
|
50
|
+
### Node Credential Administration
|
|
51
|
+
|
|
52
|
+
`requests.node` defines `getNodeCredential`, `rotateNodeCredential` and
|
|
53
|
+
`revokeNodeCredential` for verified platform-infrastructure administrators.
|
|
54
|
+
`getNodeConfig` now also requires `identity`; callers using the previous
|
|
55
|
+
unauthenticated request shape must update. Its response remains the public
|
|
56
|
+
`IClusterNode`, never a persisted backend document.
|
|
57
|
+
|
|
58
|
+
`data.INodeCredentialMetadataV1` separates `spark` and `pallet` purposes and
|
|
59
|
+
exposes lifecycle state, generation, rotation-required status and session epoch,
|
|
60
|
+
but no bearer, hash, socket peer or controller process identity. Infrastructure
|
|
61
|
+
credentials do not grant organization or workload permissions.
|
|
62
|
+
|
|
63
|
+
Mutations require the exact generation and session epoch plus a stable mutation
|
|
64
|
+
ID. Rotation takes the SHA-256 hash of a node-generated CSPRNG credential that the
|
|
65
|
+
node has already durably retained with that mutation ID. Lost-response retries
|
|
66
|
+
repeat the complete request and receive the original metadata, never newly minted
|
|
67
|
+
plaintext. A replay result is historical and cannot authorize a current session.
|
|
68
|
+
Treat rotation request hashes as sensitive in transport hooks and logs.
|
|
69
|
+
|
|
70
|
+
These types do not implement authentication, storage, enrollment or endpoints.
|
|
71
|
+
Cloudly must verify current administrator authority, validate complete inputs,
|
|
72
|
+
atomically persist the credential transition and immutable actor-bound receipt,
|
|
73
|
+
and fence subsequent control effects against current durable credential/session
|
|
74
|
+
authority. Node identity storage and transport integration must be qualified
|
|
75
|
+
before enabling rotation or Pallet enrollment.
|
|
76
|
+
|
|
77
|
+
### Node-Bound Runtime Assignments
|
|
78
|
+
|
|
79
|
+
The `data` namespace exports `IRuntimeAssignmentV1`, canonical digest helpers,
|
|
80
|
+
`evaluateRuntimeAssignmentV1`, and assignment observation validators/evaluators
|
|
81
|
+
for Cloudly/Onebox controllers and Pallet nodes. These are shared contracts;
|
|
82
|
+
they do not install a runtime, schedule containers or expose an RPC endpoint.
|
|
83
|
+
|
|
84
|
+
Each assignment binds a controller incarnation, organization, stable node and
|
|
85
|
+
replica IDs, one immutable execution attempt, image/configuration and opaque
|
|
86
|
+
network/storage/secret references. Group and site IDs are nullable topology
|
|
87
|
+
provenance. All fields except the digest itself participate in a domain-separated
|
|
88
|
+
SHA-256 digest. Digests provide integrity and compare-and-swap identity;
|
|
89
|
+
authentication must separately establish the trusted controller/node scope.
|
|
90
|
+
|
|
91
|
+
An attempt starts at generation one with `run`, then advances through
|
|
92
|
+
`stop-preserve` and `remove-runtime-preserve-storage` using exact predecessor
|
|
93
|
+
generation/digest references. Exact retries return `replay`; conflicts, stale
|
|
94
|
+
revisions, gaps, changed execution specifications and resurrection are rejected.
|
|
95
|
+
Consumers must atomically persist the evaluator's detached snapshot before effects,
|
|
96
|
+
retain removal tombstones and storage, and serialize ownership of each replica.
|
|
97
|
+
Omission causes no change. A new attempt requires separate admission proving any
|
|
98
|
+
older exclusive writer stopped or fenced. The supported offline policy is
|
|
99
|
+
`continue-node-bound`; no heartbeat timeout or lease expiry proves a writer absent.
|
|
100
|
+
|
|
101
|
+
Observations bind the exact assignment, authenticated reporter session, durable
|
|
102
|
+
per-assignment sequence and digest. Reconnect does not reset the counter. Observation
|
|
103
|
+
history must reference the same assignment revision or its exact predecessor;
|
|
104
|
+
receivers process each disposition's observations in order. Session
|
|
105
|
+
authorization, clock/freshness policy and durable receipt persistence belong to
|
|
106
|
+
the receiving service. Readiness requires matching running image evidence and
|
|
107
|
+
`run` intent; assignment acceptance alone is never readiness. A valid observation
|
|
108
|
+
can describe drift (such as a removed runtime while intent remains `run`), and
|
|
109
|
+
does not authorize storage relocation or prove fencing.
|
|
110
|
+
|
|
50
111
|
### Offline SmartDB Settlement And Packaging
|
|
51
112
|
|
|
52
113
|
The Node-only `@serve.zone/interfaces/runtime` export includes isolated V1
|
|
@@ -1108,7 +1169,7 @@ The package is authored as ESM TypeScript and built with `tsbuild tsfolders`.
|
|
|
1108
1169
|
|
|
1109
1170
|
## License and Legal Information
|
|
1110
1171
|
|
|
1111
|
-
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license) file.
|
|
1172
|
+
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license.md](./license.md) file.
|
|
1112
1173
|
|
|
1113
1174
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
|
1114
1175
|
|
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/data/index.ts
CHANGED
|
@@ -20,7 +20,9 @@ export * from './immutableimage.js';
|
|
|
20
20
|
export * from './isolatedrestore.js';
|
|
21
21
|
export * from './isolatedrestore.golden.js';
|
|
22
22
|
export * from './mail.js';
|
|
23
|
+
export * from './nodecredential.js';
|
|
23
24
|
export * from './registry.js';
|
|
25
|
+
export * from './runtimeassignment.js';
|
|
24
26
|
export * from './secret.js';
|
|
25
27
|
export * from './baremetal.js';
|
|
26
28
|
export * from './baseos.js';
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Independent infrastructure credentials; neither purpose grants workload RBAC. */
|
|
2
|
+
export type TNodeCredentialPurpose = 'spark' | 'pallet';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Public inspection and mutation-result metadata. Never includes bearer material,
|
|
6
|
+
* hashes, socket peer IDs or controller process IDs. On replay this is historical
|
|
7
|
+
* evidence, not an assertion that a credential or reporter session is still live.
|
|
8
|
+
*/
|
|
9
|
+
export interface INodeCredentialMetadataV1 {
|
|
10
|
+
schemaVersion: 1;
|
|
11
|
+
nodeId: string;
|
|
12
|
+
purpose: TNodeCredentialPurpose;
|
|
13
|
+
state: 'active' | 'revoked';
|
|
14
|
+
generation: number;
|
|
15
|
+
/** Migrated Spark credentials remain usable only until their staged rotation. */
|
|
16
|
+
requiresRotation: boolean;
|
|
17
|
+
sessionEpoch: number;
|
|
18
|
+
sessionId: string | null;
|
|
19
|
+
createdAt: number;
|
|
20
|
+
updatedAt: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Infrastructure-admin mutation preconditions. Both counters are exact CAS
|
|
25
|
+
* preconditions, not minimums. Retrying must retain this complete input and the
|
|
26
|
+
* original mutationId; a changed request with the same ID must fail closed.
|
|
27
|
+
*/
|
|
28
|
+
export interface INodeCredentialMutationPreconditionsV1 {
|
|
29
|
+
schemaVersion: 1;
|
|
30
|
+
nodeId: string;
|
|
31
|
+
purpose: TNodeCredentialPurpose;
|
|
32
|
+
mutationId: string;
|
|
33
|
+
expectedGeneration: number;
|
|
34
|
+
expectedSessionEpoch: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface INodeCredentialMutationResultV1 {
|
|
38
|
+
metadata: INodeCredentialMetadataV1;
|
|
39
|
+
/** A replay returns the original result without restoring its authority. */
|
|
40
|
+
replayed: boolean;
|
|
41
|
+
}
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import type { TSha256Digest, TImmutableContainerPlatform } from './immutableimage.js';
|
|
2
|
+
import { buildDigestPinnedImageReference, isSha256Digest } from './immutableimage.js';
|
|
3
|
+
import { canonicalizeStrictJson, createCanonicalJsonSha256Hex } from '../private/canonicaljson.js';
|
|
4
|
+
|
|
5
|
+
export interface IRuntimeControllerV1 {
|
|
6
|
+
kind: 'cloudly' | 'onebox';
|
|
7
|
+
id: string;
|
|
8
|
+
/** Persisted authority incarnation; changing it requires explicit re-admission. */
|
|
9
|
+
epoch: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Content-bound reference, not a credential, mount path or permission grant. */
|
|
13
|
+
export interface IRuntimeAuthorityReferenceV1 {
|
|
14
|
+
id: string;
|
|
15
|
+
generation: number;
|
|
16
|
+
digest: TSha256Digest;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface IRuntimeAssignmentReferenceV1 {
|
|
20
|
+
assignmentId: string;
|
|
21
|
+
generation: number;
|
|
22
|
+
digest: TSha256Digest;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Derived from authenticated transport and locally configured ownership. */
|
|
26
|
+
export interface IRuntimeAssignmentScopeV1 {
|
|
27
|
+
controller: IRuntimeControllerV1;
|
|
28
|
+
organizationId: string;
|
|
29
|
+
nodeId: string;
|
|
30
|
+
runtimeNamespace: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type TRuntimeAssignmentDispositionV1 =
|
|
34
|
+
| 'run'
|
|
35
|
+
| 'stop-preserve'
|
|
36
|
+
| 'remove-runtime-preserve-storage';
|
|
37
|
+
|
|
38
|
+
export interface IRuntimeAssignmentV1 extends IRuntimeAssignmentScopeV1 {
|
|
39
|
+
schemaVersion: 1;
|
|
40
|
+
assignmentId: string;
|
|
41
|
+
serviceId: string;
|
|
42
|
+
/** Stable logical replica; independent of container IDs and execution attempts. */
|
|
43
|
+
replicaId: string;
|
|
44
|
+
/** One execution attempt, immutable for this assignment. */
|
|
45
|
+
attemptId: string;
|
|
46
|
+
/** Logical topology provenance; placement authority remains nodeId. */
|
|
47
|
+
workerGroupId: string | null;
|
|
48
|
+
siteId: string | null;
|
|
49
|
+
generation: number;
|
|
50
|
+
/** Exact CAS predecessor; null only at generation 1. */
|
|
51
|
+
previous: IRuntimeAssignmentReferenceV1 | null;
|
|
52
|
+
disposition: TRuntimeAssignmentDispositionV1;
|
|
53
|
+
/** No automatic relocation or expiry-based claim of a stopped writer. */
|
|
54
|
+
offlinePolicy: 'continue-node-bound';
|
|
55
|
+
workload: {
|
|
56
|
+
registryHost: string;
|
|
57
|
+
repository: string;
|
|
58
|
+
imageDigest: TSha256Digest;
|
|
59
|
+
platform: TImmutableContainerPlatform;
|
|
60
|
+
/** Immutable complete invocation/configuration owned by the controller. */
|
|
61
|
+
config: IRuntimeAuthorityReferenceV1;
|
|
62
|
+
network: IRuntimeAuthorityReferenceV1 | null;
|
|
63
|
+
secrets: IRuntimeAuthorityReferenceV1 | null;
|
|
64
|
+
/** Sorted unique reference IDs. Empty means no persistent storage claims. */
|
|
65
|
+
storage: IRuntimeAuthorityReferenceV1[];
|
|
66
|
+
};
|
|
67
|
+
digest: TSha256Digest;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type TRuntimeAssignmentDecisionV1 =
|
|
71
|
+
| { status: 'accept' | 'replay'; assignment: IRuntimeAssignmentV1 }
|
|
72
|
+
| { status: 'reject'; reason: 'invalid' | 'scope-mismatch' | 'conflict' | 'stale'
|
|
73
|
+
| 'predecessor-mismatch' | 'attempt-mismatch' | 'disposition-regression' };
|
|
74
|
+
|
|
75
|
+
/** Value-free execution evidence. Receipt/acceptance alone never means ready. */
|
|
76
|
+
export interface IRuntimeAssignmentObservationV1 {
|
|
77
|
+
schemaVersion: 1;
|
|
78
|
+
assignment: IRuntimeAssignmentReferenceV1;
|
|
79
|
+
/** Authenticated reporter session; its authority is verified by the receiver. */
|
|
80
|
+
reporterSessionId: string;
|
|
81
|
+
/** Durable per-assignment counter; never resets when a reporter reconnects. */
|
|
82
|
+
sequence: number;
|
|
83
|
+
observedAt: number;
|
|
84
|
+
phase: 'pending' | 'running' | 'stopped' | 'removed' | 'failed' | 'unknown';
|
|
85
|
+
ready: boolean;
|
|
86
|
+
runtime: {
|
|
87
|
+
sandboxId: string;
|
|
88
|
+
containerId: string;
|
|
89
|
+
imageDigest: TSha256Digest;
|
|
90
|
+
} | null;
|
|
91
|
+
digest: TSha256Digest;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const runtimeAssignmentV1Contract = Object.freeze({
|
|
95
|
+
assignmentDigestDomain: 'serve.zone/runtime-assignment-v1',
|
|
96
|
+
observationDigestDomain: 'serve.zone/runtime-assignment-observation-v1',
|
|
97
|
+
maximumStorageReferences: 64,
|
|
98
|
+
omission: 'no-effect',
|
|
99
|
+
removal: 'retain-assignment-tombstone-and-storage',
|
|
100
|
+
} as const);
|
|
101
|
+
|
|
102
|
+
const fail: (messageArg: string) => never = (messageArg) => { throw new Error(messageArg); };
|
|
103
|
+
const isIdentifier = (valueArg: unknown): valueArg is string =>
|
|
104
|
+
typeof valueArg === 'string' && /^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/.test(valueArg);
|
|
105
|
+
const isPositiveInteger = (valueArg: unknown): valueArg is number =>
|
|
106
|
+
typeof valueArg === 'number' && Number.isSafeInteger(valueArg) && valueArg > 0;
|
|
107
|
+
const isDigest = (valueArg: unknown): valueArg is TSha256Digest =>
|
|
108
|
+
typeof valueArg === 'string' && isSha256Digest(valueArg);
|
|
109
|
+
|
|
110
|
+
/** Reject accessors before reading any fields; do not run input-owned code. */
|
|
111
|
+
const isExactRecord = (
|
|
112
|
+
valueArg: unknown,
|
|
113
|
+
keysArg: readonly string[],
|
|
114
|
+
): valueArg is Record<string, unknown> => {
|
|
115
|
+
if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) return false;
|
|
116
|
+
const prototype = Object.getPrototypeOf(valueArg);
|
|
117
|
+
if (prototype !== Object.prototype && prototype !== null) return false;
|
|
118
|
+
const keys = Reflect.ownKeys(valueArg);
|
|
119
|
+
return keys.length === keysArg.length && keys.every((keyArg) => {
|
|
120
|
+
const descriptor = Object.getOwnPropertyDescriptor(valueArg, keyArg);
|
|
121
|
+
return typeof keyArg === 'string' && keysArg.includes(keyArg)
|
|
122
|
+
&& descriptor?.enumerable === true && Object.hasOwn(descriptor, 'value');
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const isController = (valueArg: unknown): valueArg is IRuntimeControllerV1 =>
|
|
127
|
+
isExactRecord(valueArg, ['kind', 'id', 'epoch'])
|
|
128
|
+
&& (valueArg.kind === 'cloudly' || valueArg.kind === 'onebox')
|
|
129
|
+
&& isIdentifier(valueArg.id) && isIdentifier(valueArg.epoch);
|
|
130
|
+
const isAuthorityReference = (valueArg: unknown): valueArg is IRuntimeAuthorityReferenceV1 =>
|
|
131
|
+
isExactRecord(valueArg, ['id', 'generation', 'digest'])
|
|
132
|
+
&& isIdentifier(valueArg.id) && isPositiveInteger(valueArg.generation) && isDigest(valueArg.digest);
|
|
133
|
+
const isAssignmentReference = (valueArg: unknown): valueArg is IRuntimeAssignmentReferenceV1 =>
|
|
134
|
+
isExactRecord(valueArg, ['assignmentId', 'generation', 'digest'])
|
|
135
|
+
&& isIdentifier(valueArg.assignmentId) && isPositiveInteger(valueArg.generation)
|
|
136
|
+
&& isDigest(valueArg.digest);
|
|
137
|
+
|
|
138
|
+
const assignmentKeys = [
|
|
139
|
+
'schemaVersion', 'controller', 'organizationId', 'nodeId', 'runtimeNamespace',
|
|
140
|
+
'assignmentId', 'serviceId', 'replicaId', 'attemptId', 'workerGroupId', 'siteId',
|
|
141
|
+
'generation', 'previous', 'disposition', 'offlinePolicy', 'workload', 'digest',
|
|
142
|
+
] as const;
|
|
143
|
+
|
|
144
|
+
const assertAssignment = (valueArg: unknown): IRuntimeAssignmentV1 => {
|
|
145
|
+
if (!isExactRecord(valueArg, assignmentKeys)
|
|
146
|
+
|| valueArg.schemaVersion !== 1 || !isController(valueArg.controller)
|
|
147
|
+
|| !isPositiveInteger(valueArg.generation) || !isDigest(valueArg.digest)
|
|
148
|
+
|| valueArg.offlinePolicy !== 'continue-node-bound'
|
|
149
|
+
|| !['run', 'stop-preserve', 'remove-runtime-preserve-storage'].includes(
|
|
150
|
+
valueArg.disposition as string,
|
|
151
|
+
)) fail('runtime assignment must use its exact v1 schema');
|
|
152
|
+
for (const key of ['organizationId', 'nodeId', 'runtimeNamespace', 'assignmentId',
|
|
153
|
+
'serviceId', 'replicaId', 'attemptId']) {
|
|
154
|
+
if (!isIdentifier(valueArg[key])) fail(`runtime assignment ${key} must be a canonical identifier`);
|
|
155
|
+
}
|
|
156
|
+
for (const key of ['workerGroupId', 'siteId']) {
|
|
157
|
+
if (valueArg[key] !== null && !isIdentifier(valueArg[key])) fail('invalid runtime topology ID');
|
|
158
|
+
}
|
|
159
|
+
if (valueArg.generation === 1 ? valueArg.previous !== null
|
|
160
|
+
: !isAssignmentReference(valueArg.previous)
|
|
161
|
+
|| valueArg.previous.assignmentId !== valueArg.assignmentId
|
|
162
|
+
|| valueArg.previous.generation !== (valueArg.generation as number) - 1) {
|
|
163
|
+
fail('runtime assignment must bind its exact preceding generation');
|
|
164
|
+
}
|
|
165
|
+
const workload = valueArg.workload;
|
|
166
|
+
if (!isExactRecord(workload, ['registryHost', 'repository', 'imageDigest', 'platform',
|
|
167
|
+
'config', 'network', 'secrets', 'storage'])
|
|
168
|
+
|| typeof workload.registryHost !== 'string' || workload.registryHost.length > 253
|
|
169
|
+
|| typeof workload.repository !== 'string' || workload.repository.length > 255
|
|
170
|
+
|| !isDigest(workload.imageDigest)
|
|
171
|
+
|| !['linux/amd64', 'linux/arm64'].includes(workload.platform as string)
|
|
172
|
+
|| !isAuthorityReference(workload.config)
|
|
173
|
+
|| (workload.network !== null && !isAuthorityReference(workload.network))
|
|
174
|
+
|| (workload.secrets !== null && !isAuthorityReference(workload.secrets))
|
|
175
|
+
|| !Array.isArray(workload.storage)
|
|
176
|
+
|| workload.storage.length > runtimeAssignmentV1Contract.maximumStorageReferences) {
|
|
177
|
+
fail('runtime assignment workload must use exact immutable references');
|
|
178
|
+
}
|
|
179
|
+
buildDigestPinnedImageReference(workload.registryHost as string,
|
|
180
|
+
workload.repository as string, workload.imageDigest as TSha256Digest);
|
|
181
|
+
let previousId = '';
|
|
182
|
+
for (let index = 0; index < (workload.storage as unknown[]).length; index++) {
|
|
183
|
+
const descriptor = Object.getOwnPropertyDescriptor(workload.storage, String(index));
|
|
184
|
+
if (!descriptor || !Object.hasOwn(descriptor, 'value')
|
|
185
|
+
|| !isAuthorityReference(descriptor.value) || descriptor.value.id <= previousId) {
|
|
186
|
+
fail('runtime assignment storage references must be dense, sorted and unique');
|
|
187
|
+
}
|
|
188
|
+
previousId = descriptor.value.id;
|
|
189
|
+
}
|
|
190
|
+
canonicalizeStrictJson(valueArg, fail);
|
|
191
|
+
return valueArg as unknown as IRuntimeAssignmentV1;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const digestPayload = async (domainArg: string, valueArg: { digest: TSha256Digest }): Promise<TSha256Digest> => {
|
|
195
|
+
const { digest: _digest, ...payload } = valueArg;
|
|
196
|
+
return `sha256:${await createCanonicalJsonSha256Hex(
|
|
197
|
+
canonicalizeStrictJson({ domain: domainArg, payload }, fail), fail,
|
|
198
|
+
)}`;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/** Digest helpers validate structure and exclude only the object's own digest. */
|
|
202
|
+
export const computeRuntimeAssignmentV1Digest = async (
|
|
203
|
+
assignmentArg: IRuntimeAssignmentV1,
|
|
204
|
+
): Promise<TSha256Digest> => digestPayload(
|
|
205
|
+
runtimeAssignmentV1Contract.assignmentDigestDomain, assertAssignment(assignmentArg),
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
export const validateRuntimeAssignmentV1 = async (valueArg: unknown): Promise<string[]> => {
|
|
209
|
+
try {
|
|
210
|
+
const assignment = snapshotAssignment(valueArg);
|
|
211
|
+
return assignment.digest === await computeRuntimeAssignmentV1Digest(assignment)
|
|
212
|
+
? [] : ['runtime assignment digest does not match'];
|
|
213
|
+
} catch {
|
|
214
|
+
return ['runtime assignment must use its exact canonical v1 schema'];
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const snapshotAssignment = (valueArg: unknown): IRuntimeAssignmentV1 =>
|
|
219
|
+
JSON.parse(canonicalizeStrictJson(assertAssignment(valueArg), fail)) as IRuntimeAssignmentV1;
|
|
220
|
+
|
|
221
|
+
export const getRuntimeAssignmentV1Reference = (
|
|
222
|
+
assignmentArg: IRuntimeAssignmentV1,
|
|
223
|
+
): IRuntimeAssignmentReferenceV1 => ({
|
|
224
|
+
assignmentId: assignmentArg.assignmentId,
|
|
225
|
+
generation: assignmentArg.generation,
|
|
226
|
+
digest: assignmentArg.digest,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const canonical = (valueArg: unknown): string => canonicalizeStrictJson(valueArg, fail);
|
|
230
|
+
const immutableAttempt = (assignmentArg: IRuntimeAssignmentV1): string => {
|
|
231
|
+
const { generation, previous, disposition, digest, ...attempt } = assignmentArg;
|
|
232
|
+
return canonical(attempt);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Pure CAS decision, not authentication or execution. The caller must persist
|
|
237
|
+
* acceptance atomically before effects, serialize replica ownership, and retain
|
|
238
|
+
* removed assignments as tombstones. New attempts require separate stop/fencing
|
|
239
|
+
* admission for every older writer. An omitted assignment causes no transition.
|
|
240
|
+
* Persist only the returned detached snapshot, never the mutable input object.
|
|
241
|
+
*/
|
|
242
|
+
export const evaluateRuntimeAssignmentV1 = async (
|
|
243
|
+
incomingArg: IRuntimeAssignmentV1,
|
|
244
|
+
acceptedArg: IRuntimeAssignmentV1 | null,
|
|
245
|
+
trustedScopeArg: IRuntimeAssignmentScopeV1,
|
|
246
|
+
): Promise<TRuntimeAssignmentDecisionV1> => {
|
|
247
|
+
try {
|
|
248
|
+
incomingArg = snapshotAssignment(incomingArg);
|
|
249
|
+
acceptedArg = acceptedArg === null ? null : snapshotAssignment(acceptedArg);
|
|
250
|
+
if (!isExactRecord(trustedScopeArg, ['controller', 'organizationId', 'nodeId', 'runtimeNamespace'])
|
|
251
|
+
|| !isController(trustedScopeArg.controller)
|
|
252
|
+
|| !isIdentifier(trustedScopeArg.organizationId) || !isIdentifier(trustedScopeArg.nodeId)
|
|
253
|
+
|| !isIdentifier(trustedScopeArg.runtimeNamespace)) return { status: 'reject', reason: 'invalid' };
|
|
254
|
+
trustedScopeArg = JSON.parse(canonical(trustedScopeArg)) as IRuntimeAssignmentScopeV1;
|
|
255
|
+
} catch {
|
|
256
|
+
return { status: 'reject', reason: 'invalid' };
|
|
257
|
+
}
|
|
258
|
+
if ((await validateRuntimeAssignmentV1(incomingArg)).length
|
|
259
|
+
|| (acceptedArg !== null && (await validateRuntimeAssignmentV1(acceptedArg)).length)) {
|
|
260
|
+
return { status: 'reject', reason: 'invalid' };
|
|
261
|
+
}
|
|
262
|
+
const matchesScope = (assignmentArg: IRuntimeAssignmentV1): boolean =>
|
|
263
|
+
assignmentArg.controller.kind === trustedScopeArg.controller.kind
|
|
264
|
+
&& assignmentArg.controller.id === trustedScopeArg.controller.id
|
|
265
|
+
&& assignmentArg.controller.epoch === trustedScopeArg.controller.epoch
|
|
266
|
+
&& assignmentArg.organizationId === trustedScopeArg.organizationId
|
|
267
|
+
&& assignmentArg.nodeId === trustedScopeArg.nodeId
|
|
268
|
+
&& assignmentArg.runtimeNamespace === trustedScopeArg.runtimeNamespace;
|
|
269
|
+
if (!matchesScope(incomingArg) || (acceptedArg !== null && !matchesScope(acceptedArg))) {
|
|
270
|
+
return { status: 'reject', reason: 'scope-mismatch' };
|
|
271
|
+
}
|
|
272
|
+
if (acceptedArg === null) {
|
|
273
|
+
return incomingArg.generation === 1
|
|
274
|
+
&& incomingArg.disposition === 'run'
|
|
275
|
+
? { status: 'accept', assignment: incomingArg }
|
|
276
|
+
: { status: 'reject', reason: 'predecessor-mismatch' };
|
|
277
|
+
}
|
|
278
|
+
if (incomingArg.assignmentId !== acceptedArg.assignmentId) {
|
|
279
|
+
return { status: 'reject', reason: 'attempt-mismatch' };
|
|
280
|
+
}
|
|
281
|
+
if (incomingArg.generation < acceptedArg.generation) return { status: 'reject', reason: 'stale' };
|
|
282
|
+
if (incomingArg.generation === acceptedArg.generation) {
|
|
283
|
+
return incomingArg.digest === acceptedArg.digest
|
|
284
|
+
? { status: 'replay', assignment: acceptedArg } : { status: 'reject', reason: 'conflict' };
|
|
285
|
+
}
|
|
286
|
+
if (canonical(incomingArg.previous) !== canonical(getRuntimeAssignmentV1Reference(acceptedArg))) {
|
|
287
|
+
return { status: 'reject', reason: 'predecessor-mismatch' };
|
|
288
|
+
}
|
|
289
|
+
if (immutableAttempt(incomingArg) !== immutableAttempt(acceptedArg)) {
|
|
290
|
+
return { status: 'reject', reason: 'attempt-mismatch' };
|
|
291
|
+
}
|
|
292
|
+
const order: Record<TRuntimeAssignmentDispositionV1, number> = {
|
|
293
|
+
run: 0, 'stop-preserve': 1, 'remove-runtime-preserve-storage': 2,
|
|
294
|
+
};
|
|
295
|
+
if (order[incomingArg.disposition] !== order[acceptedArg.disposition] + 1) {
|
|
296
|
+
return { status: 'reject', reason: 'disposition-regression' };
|
|
297
|
+
}
|
|
298
|
+
return { status: 'accept', assignment: incomingArg };
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const assertObservation = (valueArg: unknown): IRuntimeAssignmentObservationV1 => {
|
|
302
|
+
if (!isExactRecord(valueArg, ['schemaVersion', 'assignment', 'reporterSessionId',
|
|
303
|
+
'sequence', 'observedAt', 'phase', 'ready', 'runtime', 'digest'])
|
|
304
|
+
|| valueArg.schemaVersion !== 1 || !isAssignmentReference(valueArg.assignment)
|
|
305
|
+
|| !isIdentifier(valueArg.reporterSessionId) || !isPositiveInteger(valueArg.sequence)
|
|
306
|
+
|| !isPositiveInteger(valueArg.observedAt) || !isDigest(valueArg.digest)
|
|
307
|
+
|| typeof valueArg.ready !== 'boolean'
|
|
308
|
+
|| !['pending', 'running', 'stopped', 'removed', 'failed', 'unknown'].includes(valueArg.phase as string)) {
|
|
309
|
+
fail('runtime observation must use its exact v1 schema');
|
|
310
|
+
}
|
|
311
|
+
if (valueArg.runtime !== null && (!isExactRecord(valueArg.runtime, [
|
|
312
|
+
'sandboxId', 'containerId', 'imageDigest',
|
|
313
|
+
]) || !isIdentifier(valueArg.runtime.sandboxId) || !isIdentifier(valueArg.runtime.containerId)
|
|
314
|
+
|| !isDigest(valueArg.runtime.imageDigest))) fail('runtime identity must be canonical');
|
|
315
|
+
if ((valueArg.ready && valueArg.phase !== 'running')
|
|
316
|
+
|| (valueArg.phase === 'running' && valueArg.runtime === null)
|
|
317
|
+
|| (valueArg.phase === 'removed' && valueArg.runtime !== null)) {
|
|
318
|
+
fail('runtime observation readiness and identity contradict its phase');
|
|
319
|
+
}
|
|
320
|
+
canonical(valueArg);
|
|
321
|
+
return valueArg as unknown as IRuntimeAssignmentObservationV1;
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
export const computeRuntimeAssignmentObservationV1Digest = async (
|
|
325
|
+
observationArg: IRuntimeAssignmentObservationV1,
|
|
326
|
+
): Promise<TSha256Digest> => digestPayload(
|
|
327
|
+
runtimeAssignmentV1Contract.observationDigestDomain, assertObservation(observationArg),
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
/** Checks exact assignment evidence; transport must separately bind reporter/session. */
|
|
331
|
+
export const validateRuntimeAssignmentObservationV1 = async (
|
|
332
|
+
observationArg: unknown,
|
|
333
|
+
assignmentArg: IRuntimeAssignmentV1,
|
|
334
|
+
): Promise<string[]> => {
|
|
335
|
+
try {
|
|
336
|
+
const observation = JSON.parse(canonical(assertObservation(observationArg))) as IRuntimeAssignmentObservationV1;
|
|
337
|
+
assignmentArg = snapshotAssignment(assignmentArg);
|
|
338
|
+
if ((await validateRuntimeAssignmentV1(assignmentArg)).length) return ['invalid assignment'];
|
|
339
|
+
if (canonical(observation.assignment) !== canonical(getRuntimeAssignmentV1Reference(assignmentArg))) {
|
|
340
|
+
return ['runtime observation does not match the exact assignment'];
|
|
341
|
+
}
|
|
342
|
+
if (observation.digest !== await computeRuntimeAssignmentObservationV1Digest(observation)) {
|
|
343
|
+
return ['runtime observation digest does not match'];
|
|
344
|
+
}
|
|
345
|
+
if (observation.ready && (assignmentArg.disposition !== 'run'
|
|
346
|
+
|| observation.runtime?.imageDigest !== assignmentArg.workload.imageDigest)) {
|
|
347
|
+
return ['runtime readiness does not match authorized execution'];
|
|
348
|
+
}
|
|
349
|
+
return [];
|
|
350
|
+
} catch {
|
|
351
|
+
return ['runtime observation must use its exact canonical v1 schema'];
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
export type TRuntimeObservationDecisionV1 =
|
|
356
|
+
| { status: 'accept' | 'replay'; observation: IRuntimeAssignmentObservationV1 }
|
|
357
|
+
| { status: 'reject'; reason: 'invalid' | 'session-mismatch' | 'stale' | 'conflict' | 'sequence-gap' };
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Receiver must authorize the assignment's node before calling this helper.
|
|
361
|
+
* Compare/persist the returned snapshot atomically with the old receipt. A new
|
|
362
|
+
* session continues the durable counter; it does not reset observation history.
|
|
363
|
+
* Across dispositions, the old receipt must reference the exact predecessor;
|
|
364
|
+
* receivers must process each revision's observations in order.
|
|
365
|
+
* These observations are never sufficient authority to relocate a storage writer.
|
|
366
|
+
*/
|
|
367
|
+
export const evaluateRuntimeAssignmentObservationV1 = async (
|
|
368
|
+
incomingArg: IRuntimeAssignmentObservationV1,
|
|
369
|
+
acceptedArg: IRuntimeAssignmentObservationV1 | null,
|
|
370
|
+
assignmentArg: IRuntimeAssignmentV1,
|
|
371
|
+
trustedReporterSessionIdArg: string,
|
|
372
|
+
): Promise<TRuntimeObservationDecisionV1> => {
|
|
373
|
+
try {
|
|
374
|
+
incomingArg = JSON.parse(canonical(assertObservation(incomingArg))) as IRuntimeAssignmentObservationV1;
|
|
375
|
+
acceptedArg = acceptedArg === null ? null
|
|
376
|
+
: JSON.parse(canonical(assertObservation(acceptedArg))) as IRuntimeAssignmentObservationV1;
|
|
377
|
+
assignmentArg = snapshotAssignment(assignmentArg);
|
|
378
|
+
if (!isIdentifier(trustedReporterSessionIdArg)
|
|
379
|
+
|| (await validateRuntimeAssignmentObservationV1(incomingArg, assignmentArg)).length
|
|
380
|
+
|| (acceptedArg !== null && (
|
|
381
|
+
acceptedArg.digest !== await computeRuntimeAssignmentObservationV1Digest(acceptedArg)
|
|
382
|
+
|| acceptedArg.assignment.assignmentId !== assignmentArg.assignmentId
|
|
383
|
+
|| (canonical(acceptedArg.assignment) !== canonical(getRuntimeAssignmentV1Reference(assignmentArg))
|
|
384
|
+
&& canonical(acceptedArg.assignment) !== canonical(assignmentArg.previous))
|
|
385
|
+
))) return { status: 'reject', reason: 'invalid' };
|
|
386
|
+
if (incomingArg.reporterSessionId !== trustedReporterSessionIdArg) {
|
|
387
|
+
return { status: 'reject', reason: 'session-mismatch' };
|
|
388
|
+
}
|
|
389
|
+
if (acceptedArg !== null && incomingArg.sequence <= acceptedArg.sequence) {
|
|
390
|
+
if (incomingArg.sequence < acceptedArg.sequence) return { status: 'reject', reason: 'stale' };
|
|
391
|
+
return incomingArg.digest === acceptedArg.digest
|
|
392
|
+
? { status: 'replay', observation: acceptedArg }
|
|
393
|
+
: { status: 'reject', reason: 'conflict' };
|
|
394
|
+
}
|
|
395
|
+
if (incomingArg.sequence !== (acceptedArg?.sequence ?? 0) + 1) {
|
|
396
|
+
return { status: 'reject', reason: 'sequence-gap' };
|
|
397
|
+
}
|
|
398
|
+
return { status: 'accept', observation: incomingArg };
|
|
399
|
+
} catch {
|
|
400
|
+
return { status: 'reject', reason: 'invalid' };
|
|
401
|
+
}
|
|
402
|
+
};
|
package/ts/requests/node.ts
CHANGED
|
@@ -4,10 +4,21 @@ import type { ICluster } from '../data/cluster.js';
|
|
|
4
4
|
import type { IClusterNode, IClusterNodeMetrics, INodeAction, INodeMetricsSample, ISparkNodeRuntimeInfo, TNodeActionName } from '../data/clusternode.js';
|
|
5
5
|
import type { IDeployment } from '../data/deployment.js';
|
|
6
6
|
import type { IIdentity } from '../data/user.js';
|
|
7
|
+
import type {
|
|
8
|
+
INodeCredentialMetadataV1,
|
|
9
|
+
INodeCredentialMutationPreconditionsV1,
|
|
10
|
+
INodeCredentialMutationResultV1,
|
|
11
|
+
TNodeCredentialPurpose,
|
|
12
|
+
} from '../data/nodecredential.js';
|
|
7
13
|
|
|
8
|
-
|
|
14
|
+
/** Requires verified platform-infrastructure admin authority; returns public data only. */
|
|
15
|
+
export interface IRequest_Any_Cloudly_GetNodeConfig extends plugins.typedrequestInterfaces.implementsTR<
|
|
16
|
+
plugins.typedrequestInterfaces.ITypedRequest,
|
|
17
|
+
IRequest_Any_Cloudly_GetNodeConfig
|
|
18
|
+
> {
|
|
9
19
|
method: 'getNodeConfig';
|
|
10
20
|
request: {
|
|
21
|
+
identity: IIdentity;
|
|
11
22
|
nodeId: string;
|
|
12
23
|
};
|
|
13
24
|
response: {
|
|
@@ -15,6 +26,54 @@ export interface IRequest_Any_Cloudly_GetNodeConfig {
|
|
|
15
26
|
};
|
|
16
27
|
}
|
|
17
28
|
|
|
29
|
+
/** These credential methods require verified infrastructure-admin authority. */
|
|
30
|
+
export interface IReq_Any_Cloudly_GetNodeCredential extends plugins.typedrequestInterfaces.implementsTR<
|
|
31
|
+
plugins.typedrequestInterfaces.ITypedRequest,
|
|
32
|
+
IReq_Any_Cloudly_GetNodeCredential
|
|
33
|
+
> {
|
|
34
|
+
method: 'getNodeCredential';
|
|
35
|
+
request: {
|
|
36
|
+
identity: IIdentity;
|
|
37
|
+
nodeId: string;
|
|
38
|
+
purpose: TNodeCredentialPurpose;
|
|
39
|
+
};
|
|
40
|
+
response: {
|
|
41
|
+
metadata: INodeCredentialMetadataV1 | null;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface IReq_Any_Cloudly_RotateNodeCredential extends plugins.typedrequestInterfaces.implementsTR<
|
|
46
|
+
plugins.typedrequestInterfaces.ITypedRequest,
|
|
47
|
+
IReq_Any_Cloudly_RotateNodeCredential
|
|
48
|
+
> {
|
|
49
|
+
method: 'rotateNodeCredential';
|
|
50
|
+
request: {
|
|
51
|
+
identity: IIdentity;
|
|
52
|
+
mutation: INodeCredentialMutationPreconditionsV1 & {
|
|
53
|
+
/**
|
|
54
|
+
* SHA-256(token UTF-8 bytes), exactly 64 lowercase hexadecimal characters.
|
|
55
|
+
* The node must durably retain its pending CSPRNG credential and mutation
|
|
56
|
+
* identity before this request. A metadata-only retry never issues plaintext.
|
|
57
|
+
* Treat this request field as sensitive; never include it in hooks or logs.
|
|
58
|
+
*/
|
|
59
|
+
newTokenHash: string;
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
response: INodeCredentialMutationResultV1;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface IReq_Any_Cloudly_RevokeNodeCredential extends plugins.typedrequestInterfaces.implementsTR<
|
|
66
|
+
plugins.typedrequestInterfaces.ITypedRequest,
|
|
67
|
+
IReq_Any_Cloudly_RevokeNodeCredential
|
|
68
|
+
> {
|
|
69
|
+
method: 'revokeNodeCredential';
|
|
70
|
+
request: {
|
|
71
|
+
identity: IIdentity;
|
|
72
|
+
mutation: INodeCredentialMutationPreconditionsV1;
|
|
73
|
+
};
|
|
74
|
+
response: INodeCredentialMutationResultV1;
|
|
75
|
+
}
|
|
76
|
+
|
|
18
77
|
export interface IRequest_Any_Cloudly_GetNodesByCluster {
|
|
19
78
|
method: 'getNodesByCluster';
|
|
20
79
|
request: {
|
/package/{license → license.md}
RENAMED
|
File without changes
|