@serve.zone/interfaces 28.4.0 → 28.5.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/changelog.md +9 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/index.d.ts +1 -0
- package/dist_ts/data/index.js +2 -1
- package/dist_ts/data/runtimeconfig.d.ts +122 -0
- package/dist_ts/data/runtimeconfig.js +278 -0
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/index.ts +1 -0
- package/ts/data/runtimeconfig.ts +385 -0
package/changelog.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2026-09-09 - 28.5.0
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- add runtime config v1 contract (data/runtimeconfig)
|
|
8
|
+
- Introduce runtime config v1 interfaces for execution intent, target ports, readiness, log policy, and immutable image metadata.
|
|
9
|
+
- Add exact-schema snapshot validation, canonical digest computation, digest validation, and assignment binding helpers.
|
|
10
|
+
- Export the runtime config data module and add tests for digest vectors, schema bounds, assignment binding, and detached async snapshots.
|
|
11
|
+
|
|
3
12
|
## 2026-09-09 - 28.4.0
|
|
4
13
|
|
|
5
14
|
### Features
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@serve.zone/interfaces',
|
|
6
|
-
version: '28.
|
|
6
|
+
version: '28.5.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
|
@@ -24,6 +24,7 @@ export * from './nodecredential.js';
|
|
|
24
24
|
export * from './nodeenrollment.js';
|
|
25
25
|
export * from './registry.js';
|
|
26
26
|
export * from './runtimeassignment.js';
|
|
27
|
+
export * from './runtimeconfig.js';
|
|
27
28
|
export * from './secret.js';
|
|
28
29
|
export * from './baremetal.js';
|
|
29
30
|
export * from './baseos.js';
|
package/dist_ts/data/index.js
CHANGED
|
@@ -24,6 +24,7 @@ export * from './nodecredential.js';
|
|
|
24
24
|
export * from './nodeenrollment.js';
|
|
25
25
|
export * from './registry.js';
|
|
26
26
|
export * from './runtimeassignment.js';
|
|
27
|
+
export * from './runtimeconfig.js';
|
|
27
28
|
export * from './secret.js';
|
|
28
29
|
export * from './baremetal.js';
|
|
29
30
|
export * from './baseos.js';
|
|
@@ -38,4 +39,4 @@ export * from './traffic.js';
|
|
|
38
39
|
export * from './user.js';
|
|
39
40
|
export * from './version.js';
|
|
40
41
|
export * from './webpush.js';
|
|
41
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
42
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9kYXRhL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLGNBQWMsb0JBQW9CLENBQUM7QUFDbkMsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxlQUFlLENBQUM7QUFDOUIsY0FBYyx1QkFBdUIsQ0FBQztBQUN0QyxjQUFjLGlCQUFpQixDQUFDO0FBQ2hDLGNBQWMseUJBQXlCLENBQUM7QUFDeEMsY0FBYywwQkFBMEIsQ0FBQztBQUN6QyxjQUFjLDBCQUEwQixDQUFDO0FBQ3pDLGNBQWMsZ0JBQWdCLENBQUM7QUFDL0IsY0FBYyxVQUFVLENBQUM7QUFDekIsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxZQUFZLENBQUM7QUFDM0IsY0FBYyx1QkFBdUIsQ0FBQztBQUN0QyxjQUFjLGNBQWMsQ0FBQztBQUM3QixjQUFjLGdCQUFnQixDQUFDO0FBQy9CLGNBQWMsWUFBWSxDQUFDO0FBQzNCLGNBQWMscUJBQXFCLENBQUM7QUFDcEMsY0FBYyxzQkFBc0IsQ0FBQztBQUNyQyxjQUFjLDZCQUE2QixDQUFDO0FBQzVDLGNBQWMsV0FBVyxDQUFDO0FBQzFCLGNBQWMscUJBQXFCLENBQUM7QUFDcEMsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLGVBQWUsQ0FBQztBQUM5QixjQUFjLHdCQUF3QixDQUFDO0FBQ3ZDLGNBQWMsb0JBQW9CLENBQUM7QUFDbkMsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxnQkFBZ0IsQ0FBQztBQUMvQixjQUFjLGFBQWEsQ0FBQztBQUM1QixjQUFjLGFBQWEsQ0FBQztBQUM1QixjQUFjLGtCQUFrQixDQUFDO0FBQ2pDLGNBQWMsZUFBZSxDQUFDO0FBQzlCLGNBQWMsY0FBYyxDQUFDO0FBQzdCLGNBQWMsbUJBQW1CLENBQUM7QUFDbEMsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxvQkFBb0IsQ0FBQztBQUNuQyxjQUFjLGNBQWMsQ0FBQztBQUM3QixjQUFjLFdBQVcsQ0FBQztBQUMxQixjQUFjLGNBQWMsQ0FBQztBQUM3QixjQUFjLGNBQWMsQ0FBQyJ9
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { IImmutableContainerInvocationPlatformEvidenceV1, IImmutableContainerInvocationV1, TSha256Digest } from './immutableimage.js';
|
|
2
|
+
import type { IRuntimeAssignmentV1, IRuntimeAuthorityReferenceV1 } from './runtimeassignment.js';
|
|
3
|
+
/** A container target only; declaration does not publish a host or gateway port. */
|
|
4
|
+
export interface IRuntimeConfigTargetPortV1 {
|
|
5
|
+
name: string;
|
|
6
|
+
port: number;
|
|
7
|
+
/** Inclusive; equal to port for a single port. */
|
|
8
|
+
portEnd: number;
|
|
9
|
+
protocol: 'tcp' | 'udp';
|
|
10
|
+
}
|
|
11
|
+
export interface IRuntimeConfigReadinessTimingV1 {
|
|
12
|
+
/** Measured from the evidenced container start; no probes before this delay. */
|
|
13
|
+
initialDelayMs: number;
|
|
14
|
+
/** Failures during this startup grace cannot mark the workload unhealthy. */
|
|
15
|
+
startupGraceMs: number;
|
|
16
|
+
/** Probe start-to-start interval; probes must never overlap. */
|
|
17
|
+
intervalMs: number;
|
|
18
|
+
/** Per-probe deadline, including connection, TLS and HTTP response headers. */
|
|
19
|
+
timeoutMs: number;
|
|
20
|
+
/** Consecutive successful probes needed before ready becomes true. */
|
|
21
|
+
successThreshold: number;
|
|
22
|
+
/** Consecutive failures after startup grace needed to declare unhealthy. */
|
|
23
|
+
failureThreshold: number;
|
|
24
|
+
}
|
|
25
|
+
export type TRuntimeConfigReadinessV1 = IRuntimeConfigReadinessTimingV1 & ({
|
|
26
|
+
kind: 'process';
|
|
27
|
+
} | {
|
|
28
|
+
kind: 'tcp';
|
|
29
|
+
targetPort: string;
|
|
30
|
+
port: number;
|
|
31
|
+
} | {
|
|
32
|
+
kind: 'http';
|
|
33
|
+
targetPort: string;
|
|
34
|
+
port: number;
|
|
35
|
+
scheme: 'http' | 'https';
|
|
36
|
+
/**
|
|
37
|
+
* Host header name and HTTPS SNI/certificate hostname. Connect only to the
|
|
38
|
+
* evidenced container endpoint, never resolve this name as a probe target.
|
|
39
|
+
* HTTPS verifies system trust and this hostname; no insecure TLS mode.
|
|
40
|
+
*/
|
|
41
|
+
host: string;
|
|
42
|
+
/** Canonical origin-form request target, including any nonsecret query. */
|
|
43
|
+
path: string;
|
|
44
|
+
/** Sorted unique final HTTP response status codes. */
|
|
45
|
+
expectedStatusCodes: number[];
|
|
46
|
+
/** A redirect response is evaluated as received; it cannot redirect probes. */
|
|
47
|
+
followRedirects: false;
|
|
48
|
+
});
|
|
49
|
+
export interface IRuntimeConfigLogPolicyV1 {
|
|
50
|
+
/** Maximum UTF-8 bytes in one captured stdout/stderr line. */
|
|
51
|
+
maximumLineBytes: number;
|
|
52
|
+
/** Maximum bytes waiting for delivery; overflow requires explicit evidence. */
|
|
53
|
+
maximumBufferedBytes: number;
|
|
54
|
+
/** Retention of archived logs; durable payloads belong to SmartBucket. */
|
|
55
|
+
retentionMs: number;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Controller-owned resolved nonsecret execution intent. OCI command, effective
|
|
59
|
+
* environment, working directory and identity must be resolved before sealing;
|
|
60
|
+
* consumers must not append inherited image defaults or choose runtime defaults.
|
|
61
|
+
* Secrets, network identity and storage claims remain separately assignment-bound
|
|
62
|
+
* owner references. This document contains no registry credentials or host paths.
|
|
63
|
+
*/
|
|
64
|
+
export interface IRuntimeConfigV1 extends IRuntimeAuthorityReferenceV1 {
|
|
65
|
+
schemaVersion: 1;
|
|
66
|
+
organizationId: string;
|
|
67
|
+
serviceId: string;
|
|
68
|
+
image: {
|
|
69
|
+
registryHost: string;
|
|
70
|
+
repository: string;
|
|
71
|
+
/** Root descriptor digest, which may be an OCI platform index. */
|
|
72
|
+
imageDigest: TSha256Digest;
|
|
73
|
+
invocation: IImmutableContainerInvocationV1;
|
|
74
|
+
};
|
|
75
|
+
/** Complete effective public environment; secret values must never be inlined. */
|
|
76
|
+
environment: Record<string, string>;
|
|
77
|
+
workingDirectory: string;
|
|
78
|
+
cpuMillis: number;
|
|
79
|
+
memoryBytes: number;
|
|
80
|
+
readonlyRootfs: boolean;
|
|
81
|
+
runAsUser: number;
|
|
82
|
+
runAsGroup: number;
|
|
83
|
+
/** Sorted unique names, with no overlapping ranges for the same transport. */
|
|
84
|
+
targetPorts: IRuntimeConfigTargetPortV1[];
|
|
85
|
+
readiness: TRuntimeConfigReadinessV1;
|
|
86
|
+
/**
|
|
87
|
+
* SmartData owns log metadata and SmartBucket owns archived payloads. This
|
|
88
|
+
* policy does not authorize persistent local files. Any CRI log capture uses
|
|
89
|
+
* disposable process-local transport artifacts and must evidence bounded
|
|
90
|
+
* buffering, backpressure and lost data; it must not silently truncate logs.
|
|
91
|
+
*/
|
|
92
|
+
logs: IRuntimeConfigLogPolicyV1;
|
|
93
|
+
}
|
|
94
|
+
export interface IRuntimeConfigAssignmentBindingV1 {
|
|
95
|
+
config: IRuntimeConfigV1;
|
|
96
|
+
assignment: IRuntimeAssignmentV1;
|
|
97
|
+
platformEvidence: IImmutableContainerInvocationPlatformEvidenceV1;
|
|
98
|
+
}
|
|
99
|
+
export declare const runtimeConfigV1Contract: Readonly<{
|
|
100
|
+
readonly digestDomain: "serve.zone/runtime-config-v1";
|
|
101
|
+
readonly maximumCanonicalBytes: 262144;
|
|
102
|
+
readonly maximumEnvironmentEntries: 128;
|
|
103
|
+
readonly maximumTargetPorts: 64;
|
|
104
|
+
readonly maximumReadinessDurationMs: 3600000;
|
|
105
|
+
readonly maximumLogRetentionMs: 31536000000;
|
|
106
|
+
}>;
|
|
107
|
+
/**
|
|
108
|
+
* Detached exact-schema snapshot for synchronous persistence assertions. Digest
|
|
109
|
+
* integrity, scope authorization, admission and runtime readiness are not implied.
|
|
110
|
+
*/
|
|
111
|
+
export declare const snapshotRuntimeConfigV1: (valueArg: unknown) => IRuntimeConfigV1;
|
|
112
|
+
/** Domain-separated canonical JSON excludes only the config's own digest. */
|
|
113
|
+
export declare const computeRuntimeConfigV1Digest: (configArg: IRuntimeConfigV1) => Promise<TSha256Digest>;
|
|
114
|
+
export declare const validateRuntimeConfigV1: (valueArg: unknown) => Promise<string[]>;
|
|
115
|
+
/**
|
|
116
|
+
* Bind valid content to one exact assignment and select its platform evidence.
|
|
117
|
+
* No assignment digest is embedded in the config, avoiding circular hashes.
|
|
118
|
+
* This proves content consistency only: the caller must authenticate the
|
|
119
|
+
* controller, durably admit the attempt, resolve other owner references, and
|
|
120
|
+
* verify runtime and readiness evidence before effects or route publication.
|
|
121
|
+
*/
|
|
122
|
+
export declare const bindRuntimeConfigV1ToAssignment: (configArg: unknown, assignmentArg: unknown) => Promise<IRuntimeConfigAssignmentBindingV1>;
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { buildDigestPinnedImageReference, isSha256Digest, validateImmutableContainerInvocation, verifyImmutableContainerInvocationDigest, } from './immutableimage.js';
|
|
2
|
+
import { snapshotRuntimeAssignmentV1, validateRuntimeAssignmentV1 } from './runtimeassignment.js';
|
|
3
|
+
import { canonicalizeStrictJson, createCanonicalJsonSha256Hex } from '../private/canonicaljson.js';
|
|
4
|
+
export const runtimeConfigV1Contract = Object.freeze({
|
|
5
|
+
digestDomain: 'serve.zone/runtime-config-v1',
|
|
6
|
+
maximumCanonicalBytes: 262_144,
|
|
7
|
+
maximumEnvironmentEntries: 128,
|
|
8
|
+
maximumTargetPorts: 64,
|
|
9
|
+
maximumReadinessDurationMs: 3_600_000,
|
|
10
|
+
maximumLogRetentionMs: 31_536_000_000,
|
|
11
|
+
});
|
|
12
|
+
const fail = (messageArg) => { throw new Error(messageArg); };
|
|
13
|
+
const encoder = new TextEncoder();
|
|
14
|
+
const isInteger = (valueArg, minimumArg, maximumArg) => typeof valueArg === 'number' && Number.isSafeInteger(valueArg) && !Object.is(valueArg, -0)
|
|
15
|
+
&& valueArg >= minimumArg && valueArg <= maximumArg;
|
|
16
|
+
const isIdentifier = (valueArg) => typeof valueArg === 'string' && /^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/.test(valueArg);
|
|
17
|
+
const isDigest = (valueArg) => typeof valueArg === 'string' && isSha256Digest(valueArg);
|
|
18
|
+
const isPortName = (valueArg) => typeof valueArg === 'string' && /^[a-z0-9][a-z0-9_.-]{0,62}$/.test(valueArg);
|
|
19
|
+
const isExactRecord = (valueArg, keysArg) => {
|
|
20
|
+
if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg))
|
|
21
|
+
return false;
|
|
22
|
+
const keys = Object.keys(valueArg);
|
|
23
|
+
return keys.length === keysArg.length && keys.every((keyArg) => keysArg.includes(keyArg));
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Bound traversal before canonicalization and copy descriptor values so input
|
|
27
|
+
* getters/toJSON never run. The canonical helper supplies scalar Unicode and
|
|
28
|
+
* safe-integer validation. Detached output is obtained before any async work.
|
|
29
|
+
*/
|
|
30
|
+
const snapshotBoundedJson = (valueArg) => {
|
|
31
|
+
let nodes = 0;
|
|
32
|
+
let stringBytes = 0;
|
|
33
|
+
const ancestors = new Set();
|
|
34
|
+
const accountString = (value) => {
|
|
35
|
+
if (value.length > runtimeConfigV1Contract.maximumCanonicalBytes)
|
|
36
|
+
fail('runtime config string is oversized');
|
|
37
|
+
stringBytes += encoder.encode(value).byteLength;
|
|
38
|
+
if (stringBytes > runtimeConfigV1Contract.maximumCanonicalBytes)
|
|
39
|
+
fail('runtime config is oversized');
|
|
40
|
+
};
|
|
41
|
+
const copy = (value, depth) => {
|
|
42
|
+
if (++nodes > 8192 || depth > 12)
|
|
43
|
+
fail('runtime config exceeds structural bounds');
|
|
44
|
+
if (typeof value === 'string') {
|
|
45
|
+
accountString(value);
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'number')
|
|
49
|
+
return value;
|
|
50
|
+
if (!value || typeof value !== 'object')
|
|
51
|
+
fail('runtime config contains a non-JSON value');
|
|
52
|
+
if (ancestors.has(value))
|
|
53
|
+
fail('runtime config contains a cycle');
|
|
54
|
+
ancestors.add(value);
|
|
55
|
+
try {
|
|
56
|
+
const array = Array.isArray(value);
|
|
57
|
+
const prototype = Object.getPrototypeOf(value);
|
|
58
|
+
if (array ? prototype !== Array.prototype : prototype !== Object.prototype && prototype !== null) {
|
|
59
|
+
fail('runtime config requires plain records and arrays');
|
|
60
|
+
}
|
|
61
|
+
const keys = Reflect.ownKeys(value);
|
|
62
|
+
if (keys.length > 257)
|
|
63
|
+
fail('runtime config object exceeds key bounds');
|
|
64
|
+
if (array) {
|
|
65
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(value, 'length');
|
|
66
|
+
const length = lengthDescriptor?.value;
|
|
67
|
+
if (!isInteger(length, 0, 256) || keys.length !== length + 1) {
|
|
68
|
+
fail('runtime config requires bounded dense arrays');
|
|
69
|
+
}
|
|
70
|
+
const result = [];
|
|
71
|
+
for (let index = 0; index < length; index++) {
|
|
72
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
73
|
+
if (!descriptor || !descriptor.enumerable || !Object.hasOwn(descriptor, 'value')) {
|
|
74
|
+
fail('runtime config array contains a missing or non-data entry');
|
|
75
|
+
}
|
|
76
|
+
result.push(copy(descriptor.value, depth + 1));
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
const result = Object.create(null);
|
|
81
|
+
for (const key of keys) {
|
|
82
|
+
if (typeof key !== 'string')
|
|
83
|
+
fail('runtime config contains a symbol key');
|
|
84
|
+
accountString(key);
|
|
85
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
86
|
+
if (!descriptor || !descriptor.enumerable || !Object.hasOwn(descriptor, 'value')) {
|
|
87
|
+
fail('runtime config requires enumerable data properties');
|
|
88
|
+
}
|
|
89
|
+
result[key] = copy(descriptor.value, depth + 1);
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
ancestors.delete(value);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
const canonical = canonicalizeStrictJson(copy(valueArg, 0), fail);
|
|
98
|
+
if (encoder.encode(canonical).byteLength > runtimeConfigV1Contract.maximumCanonicalBytes) {
|
|
99
|
+
fail('runtime config canonical JSON is oversized');
|
|
100
|
+
}
|
|
101
|
+
return JSON.parse(canonical);
|
|
102
|
+
};
|
|
103
|
+
const timingKeys = ['initialDelayMs', 'startupGraceMs', 'intervalMs', 'timeoutMs',
|
|
104
|
+
'successThreshold', 'failureThreshold'];
|
|
105
|
+
const assertReadiness = (valueArg, portsArg) => {
|
|
106
|
+
if (!valueArg || typeof valueArg !== 'object')
|
|
107
|
+
fail('runtime config readiness is required');
|
|
108
|
+
const value = valueArg;
|
|
109
|
+
const keys = value.kind === 'process' ? ['kind', ...timingKeys]
|
|
110
|
+
: value.kind === 'tcp' ? ['kind', ...timingKeys, 'targetPort', 'port']
|
|
111
|
+
: value.kind === 'http' ? ['kind', ...timingKeys, 'targetPort', 'port',
|
|
112
|
+
'scheme', 'host', 'path', 'expectedStatusCodes', 'followRedirects'] : [];
|
|
113
|
+
if (keys.length === 0 || !isExactRecord(value, keys)
|
|
114
|
+
|| !isInteger(value.initialDelayMs, 0, runtimeConfigV1Contract.maximumReadinessDurationMs)
|
|
115
|
+
|| !isInteger(value.startupGraceMs, 0, runtimeConfigV1Contract.maximumReadinessDurationMs)
|
|
116
|
+
|| !isInteger(value.intervalMs, 100, runtimeConfigV1Contract.maximumReadinessDurationMs)
|
|
117
|
+
|| !isInteger(value.timeoutMs, 1, 60_000)
|
|
118
|
+
|| !isInteger(value.successThreshold, 1, 100)
|
|
119
|
+
|| !isInteger(value.failureThreshold, 1, 100)
|
|
120
|
+
|| value.timeoutMs > value.intervalMs || value.initialDelayMs > value.startupGraceMs) {
|
|
121
|
+
fail('runtime config readiness must use explicit bounded timing and exact schema');
|
|
122
|
+
}
|
|
123
|
+
if (value.kind === 'process')
|
|
124
|
+
return;
|
|
125
|
+
const target = portsArg.find((portArg) => portArg.name === value.targetPort);
|
|
126
|
+
if (!isPortName(value.targetPort) || !target || target.protocol !== 'tcp'
|
|
127
|
+
|| !isInteger(value.port, target.port, target.portEnd)) {
|
|
128
|
+
fail('runtime config readiness must select a declared TCP target and port');
|
|
129
|
+
}
|
|
130
|
+
if (value.kind === 'tcp')
|
|
131
|
+
return;
|
|
132
|
+
if ((value.scheme !== 'http' && value.scheme !== 'https') || value.followRedirects !== false
|
|
133
|
+
|| typeof value.host !== 'string' || value.host.length > 253
|
|
134
|
+
|| !/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/.test(value.host)
|
|
135
|
+
|| typeof value.path !== 'string' || value.path.length > 4096
|
|
136
|
+
|| !value.path.startsWith('/') || value.path.startsWith('//')
|
|
137
|
+
|| /[^\x21-\x7e]|[\\#]/.test(value.path)
|
|
138
|
+
|| /%(?![0-9A-F]{2})/.test(value.path)
|
|
139
|
+
|| !Array.isArray(value.expectedStatusCodes) || value.expectedStatusCodes.length === 0
|
|
140
|
+
|| value.expectedStatusCodes.length > 32) {
|
|
141
|
+
fail('runtime config HTTP readiness must use an exact bounded request policy');
|
|
142
|
+
}
|
|
143
|
+
const parsed = new URL(value.path, 'http://runtime.invalid');
|
|
144
|
+
if (`${parsed.pathname}${parsed.search}` !== value.path)
|
|
145
|
+
fail('runtime config HTTP path must be canonical');
|
|
146
|
+
let previousCode = 0;
|
|
147
|
+
for (const code of value.expectedStatusCodes) {
|
|
148
|
+
if (!isInteger(code, 200, 599) || code <= previousCode) {
|
|
149
|
+
fail('runtime config HTTP status expectations must be sorted unique final codes');
|
|
150
|
+
}
|
|
151
|
+
previousCode = code;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
const assertConfig = (valueArg) => {
|
|
155
|
+
if (!isExactRecord(valueArg, ['schemaVersion', 'id', 'generation', 'organizationId', 'serviceId',
|
|
156
|
+
'image', 'environment', 'workingDirectory', 'cpuMillis', 'memoryBytes', 'readonlyRootfs',
|
|
157
|
+
'runAsUser', 'runAsGroup', 'targetPorts', 'readiness', 'logs', 'digest'])
|
|
158
|
+
|| valueArg.schemaVersion !== 1 || !isIdentifier(valueArg.id)
|
|
159
|
+
|| !isInteger(valueArg.generation, 1, Number.MAX_SAFE_INTEGER)
|
|
160
|
+
|| !isIdentifier(valueArg.organizationId) || !isIdentifier(valueArg.serviceId)
|
|
161
|
+
|| !isDigest(valueArg.digest))
|
|
162
|
+
fail('runtime config must use its exact v1 schema');
|
|
163
|
+
const image = valueArg.image;
|
|
164
|
+
if (!isExactRecord(image, ['registryHost', 'repository', 'imageDigest', 'invocation'])
|
|
165
|
+
|| typeof image.registryHost !== 'string' || image.registryHost.length > 253
|
|
166
|
+
|| typeof image.repository !== 'string' || image.repository.length > 255
|
|
167
|
+
|| !isDigest(image.imageDigest)
|
|
168
|
+
|| validateImmutableContainerInvocation(image.invocation).length !== 0) {
|
|
169
|
+
fail('runtime config image must contain exact immutable invocation evidence');
|
|
170
|
+
}
|
|
171
|
+
buildDigestPinnedImageReference(image.registryHost, image.repository, image.imageDigest);
|
|
172
|
+
const environment = valueArg.environment;
|
|
173
|
+
if (!environment || typeof environment !== 'object' || Array.isArray(environment)
|
|
174
|
+
|| Object.keys(environment).length > runtimeConfigV1Contract.maximumEnvironmentEntries) {
|
|
175
|
+
fail('runtime config requires an explicit bounded nonsecret environment');
|
|
176
|
+
}
|
|
177
|
+
for (const [key, value] of Object.entries(environment)) {
|
|
178
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(key) || typeof value !== 'string'
|
|
179
|
+
|| value.includes('\u0000') || encoder.encode(value).byteLength > 8192) {
|
|
180
|
+
fail('runtime config environment must contain bounded string entries');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const directory = valueArg.workingDirectory;
|
|
184
|
+
if (typeof directory !== 'string' || !directory.startsWith('/')
|
|
185
|
+
|| encoder.encode(directory).byteLength > 4096 || /[\u0000-\u001f\u007f]/.test(directory)
|
|
186
|
+
|| (directory !== '/' && directory.split('/').slice(1).some((partArg) => !partArg || partArg === '.' || partArg === '..'))
|
|
187
|
+
|| !isInteger(valueArg.cpuMillis, 1, 1_000_000)
|
|
188
|
+
|| !isInteger(valueArg.memoryBytes, 16 * 1024 * 1024, 2 ** 50)
|
|
189
|
+
|| typeof valueArg.readonlyRootfs !== 'boolean'
|
|
190
|
+
|| !isInteger(valueArg.runAsUser, 0, 2 ** 31 - 1)
|
|
191
|
+
|| !isInteger(valueArg.runAsGroup, 0, 2 ** 31 - 1)) {
|
|
192
|
+
fail('runtime config requires canonical directory, explicit resources and numeric identity');
|
|
193
|
+
}
|
|
194
|
+
const ports = valueArg.targetPorts;
|
|
195
|
+
if (!Array.isArray(ports) || ports.length > runtimeConfigV1Contract.maximumTargetPorts) {
|
|
196
|
+
fail('runtime config target ports must be a bounded array');
|
|
197
|
+
}
|
|
198
|
+
let previousName = '';
|
|
199
|
+
const validatedPorts = [];
|
|
200
|
+
for (const port of ports) {
|
|
201
|
+
if (!isExactRecord(port, ['name', 'port', 'portEnd', 'protocol'])
|
|
202
|
+
|| !isPortName(port.name) || port.name <= previousName
|
|
203
|
+
|| !isInteger(port.port, 1, 65535) || !isInteger(port.portEnd, port.port, 65535)
|
|
204
|
+
|| (port.protocol !== 'tcp' && port.protocol !== 'udp')
|
|
205
|
+
|| validatedPorts.some((previousArg) => previousArg.protocol === port.protocol
|
|
206
|
+
&& port.port <= previousArg.portEnd && port.portEnd >= previousArg.port)) {
|
|
207
|
+
fail('runtime config target ports require sorted unique names and nonoverlapping transport ranges');
|
|
208
|
+
}
|
|
209
|
+
previousName = port.name;
|
|
210
|
+
validatedPorts.push(port);
|
|
211
|
+
}
|
|
212
|
+
assertReadiness(valueArg.readiness, validatedPorts);
|
|
213
|
+
const logs = valueArg.logs;
|
|
214
|
+
if (!isExactRecord(logs, ['maximumLineBytes', 'maximumBufferedBytes', 'retentionMs'])
|
|
215
|
+
|| !isInteger(logs.maximumLineBytes, 1, 1_048_576)
|
|
216
|
+
|| !isInteger(logs.maximumBufferedBytes, 1, 67_108_864)
|
|
217
|
+
|| logs.maximumBufferedBytes < logs.maximumLineBytes
|
|
218
|
+
|| !isInteger(logs.retentionMs, 1, runtimeConfigV1Contract.maximumLogRetentionMs)) {
|
|
219
|
+
fail('runtime config logs require explicit bounded line, buffer and retention limits');
|
|
220
|
+
}
|
|
221
|
+
return valueArg;
|
|
222
|
+
};
|
|
223
|
+
/**
|
|
224
|
+
* Detached exact-schema snapshot for synchronous persistence assertions. Digest
|
|
225
|
+
* integrity, scope authorization, admission and runtime readiness are not implied.
|
|
226
|
+
*/
|
|
227
|
+
export const snapshotRuntimeConfigV1 = (valueArg) => assertConfig(snapshotBoundedJson(valueArg));
|
|
228
|
+
/** Domain-separated canonical JSON excludes only the config's own digest. */
|
|
229
|
+
export const computeRuntimeConfigV1Digest = async (configArg) => {
|
|
230
|
+
const { digest: _digest, ...payload } = snapshotRuntimeConfigV1(configArg);
|
|
231
|
+
return `sha256:${await createCanonicalJsonSha256Hex(canonicalizeStrictJson({
|
|
232
|
+
domain: runtimeConfigV1Contract.digestDomain, payload,
|
|
233
|
+
}, fail), fail)}`;
|
|
234
|
+
};
|
|
235
|
+
const validateSnapshot = async (configArg) => {
|
|
236
|
+
if (!await verifyImmutableContainerInvocationDigest(configArg.image.invocation)) {
|
|
237
|
+
return ['runtime config immutable invocation digest does not match'];
|
|
238
|
+
}
|
|
239
|
+
return configArg.digest === await computeRuntimeConfigV1Digest(configArg)
|
|
240
|
+
? [] : ['runtime config digest does not match'];
|
|
241
|
+
};
|
|
242
|
+
export const validateRuntimeConfigV1 = async (valueArg) => {
|
|
243
|
+
try {
|
|
244
|
+
return await validateSnapshot(snapshotRuntimeConfigV1(valueArg));
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return ['runtime config must use its exact canonical v1 schema'];
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
/**
|
|
251
|
+
* Bind valid content to one exact assignment and select its platform evidence.
|
|
252
|
+
* No assignment digest is embedded in the config, avoiding circular hashes.
|
|
253
|
+
* This proves content consistency only: the caller must authenticate the
|
|
254
|
+
* controller, durably admit the attempt, resolve other owner references, and
|
|
255
|
+
* verify runtime and readiness evidence before effects or route publication.
|
|
256
|
+
*/
|
|
257
|
+
export const bindRuntimeConfigV1ToAssignment = async (configArg, assignmentArg) => {
|
|
258
|
+
const config = snapshotRuntimeConfigV1(configArg);
|
|
259
|
+
const assignment = snapshotRuntimeAssignmentV1(snapshotBoundedJson(assignmentArg));
|
|
260
|
+
if ((await validateSnapshot(config)).length !== 0
|
|
261
|
+
|| (await validateRuntimeAssignmentV1(assignment)).length !== 0) {
|
|
262
|
+
fail('runtime config binding requires valid config, invocation and assignment digests');
|
|
263
|
+
}
|
|
264
|
+
const reference = assignment.workload.config;
|
|
265
|
+
if (reference.id !== config.id || reference.generation !== config.generation
|
|
266
|
+
|| reference.digest !== config.digest || assignment.organizationId !== config.organizationId
|
|
267
|
+
|| assignment.serviceId !== config.serviceId
|
|
268
|
+
|| assignment.workload.registryHost !== config.image.registryHost
|
|
269
|
+
|| assignment.workload.repository !== config.image.repository
|
|
270
|
+
|| assignment.workload.imageDigest !== config.image.imageDigest) {
|
|
271
|
+
fail('runtime config does not match the exact assignment reference, ownership and image');
|
|
272
|
+
}
|
|
273
|
+
const platformEvidence = config.image.invocation.evidence.find((entryArg) => entryArg.platform === assignment.workload.platform);
|
|
274
|
+
if (!platformEvidence)
|
|
275
|
+
fail('runtime config lacks exact assignment platform evidence');
|
|
276
|
+
return { config, assignment, platformEvidence };
|
|
277
|
+
};
|
|
278
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicnVudGltZWNvbmZpZy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL2RhdGEvcnVudGltZWNvbmZpZy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFLQSxPQUFPLEVBQ0wsK0JBQStCLEVBQy9CLGNBQWMsRUFDZCxvQ0FBb0MsRUFDcEMsd0NBQXdDLEdBQ3pDLE1BQU0scUJBQXFCLENBQUM7QUFFN0IsT0FBTyxFQUFFLDJCQUEyQixFQUFFLDJCQUEyQixFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFDbEcsT0FBTyxFQUFFLHNCQUFzQixFQUFFLDRCQUE0QixFQUFFLE1BQU0sNkJBQTZCLENBQUM7QUFzR25HLE1BQU0sQ0FBQyxNQUFNLHVCQUF1QixHQUFHLE1BQU0sQ0FBQyxNQUFNLENBQUM7SUFDbkQsWUFBWSxFQUFFLDhCQUE4QjtJQUM1QyxxQkFBcUIsRUFBRSxPQUFPO0lBQzlCLHlCQUF5QixFQUFFLEdBQUc7SUFDOUIsa0JBQWtCLEVBQUUsRUFBRTtJQUN0QiwwQkFBMEIsRUFBRSxTQUFTO0lBQ3JDLHFCQUFxQixFQUFFLGNBQWM7Q0FDN0IsQ0FBQyxDQUFDO0FBRVosTUFBTSxJQUFJLEdBQWtDLENBQUMsVUFBVSxFQUFFLEVBQUUsR0FBRyxNQUFNLElBQUksS0FBSyxDQUFDLFVBQVUsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBQzdGLE1BQU0sT0FBTyxHQUFHLElBQUksV0FBVyxFQUFFLENBQUM7QUFDbEMsTUFBTSxTQUFTLEdBQUcsQ0FBQyxRQUFpQixFQUFFLFVBQWtCLEVBQUUsVUFBa0IsRUFBc0IsRUFBRSxDQUNsRyxPQUFPLFFBQVEsS0FBSyxRQUFRLElBQUksTUFBTSxDQUFDLGFBQWEsQ0FBQyxRQUFRLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxFQUFFLENBQUMsUUFBUSxFQUFFLENBQUMsQ0FBQyxDQUFDO09BQ3ZGLFFBQVEsSUFBSSxVQUFVLElBQUksUUFBUSxJQUFJLFVBQVUsQ0FBQztBQUN0RCxNQUFN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
|
package/package.json
CHANGED
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/data/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ export * from './nodecredential.js';
|
|
|
24
24
|
export * from './nodeenrollment.js';
|
|
25
25
|
export * from './registry.js';
|
|
26
26
|
export * from './runtimeassignment.js';
|
|
27
|
+
export * from './runtimeconfig.js';
|
|
27
28
|
export * from './secret.js';
|
|
28
29
|
export * from './baremetal.js';
|
|
29
30
|
export * from './baseos.js';
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IImmutableContainerInvocationPlatformEvidenceV1,
|
|
3
|
+
IImmutableContainerInvocationV1,
|
|
4
|
+
TSha256Digest,
|
|
5
|
+
} from './immutableimage.js';
|
|
6
|
+
import {
|
|
7
|
+
buildDigestPinnedImageReference,
|
|
8
|
+
isSha256Digest,
|
|
9
|
+
validateImmutableContainerInvocation,
|
|
10
|
+
verifyImmutableContainerInvocationDigest,
|
|
11
|
+
} from './immutableimage.js';
|
|
12
|
+
import type { IRuntimeAssignmentV1, IRuntimeAuthorityReferenceV1 } from './runtimeassignment.js';
|
|
13
|
+
import { snapshotRuntimeAssignmentV1, validateRuntimeAssignmentV1 } from './runtimeassignment.js';
|
|
14
|
+
import { canonicalizeStrictJson, createCanonicalJsonSha256Hex } from '../private/canonicaljson.js';
|
|
15
|
+
|
|
16
|
+
/** A container target only; declaration does not publish a host or gateway port. */
|
|
17
|
+
export interface IRuntimeConfigTargetPortV1 {
|
|
18
|
+
name: string;
|
|
19
|
+
port: number;
|
|
20
|
+
/** Inclusive; equal to port for a single port. */
|
|
21
|
+
portEnd: number;
|
|
22
|
+
protocol: 'tcp' | 'udp';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface IRuntimeConfigReadinessTimingV1 {
|
|
26
|
+
/** Measured from the evidenced container start; no probes before this delay. */
|
|
27
|
+
initialDelayMs: number;
|
|
28
|
+
/** Failures during this startup grace cannot mark the workload unhealthy. */
|
|
29
|
+
startupGraceMs: number;
|
|
30
|
+
/** Probe start-to-start interval; probes must never overlap. */
|
|
31
|
+
intervalMs: number;
|
|
32
|
+
/** Per-probe deadline, including connection, TLS and HTTP response headers. */
|
|
33
|
+
timeoutMs: number;
|
|
34
|
+
/** Consecutive successful probes needed before ready becomes true. */
|
|
35
|
+
successThreshold: number;
|
|
36
|
+
/** Consecutive failures after startup grace needed to declare unhealthy. */
|
|
37
|
+
failureThreshold: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type TRuntimeConfigReadinessV1 = IRuntimeConfigReadinessTimingV1 & (
|
|
41
|
+
| { kind: 'process' }
|
|
42
|
+
| { kind: 'tcp'; targetPort: string; port: number }
|
|
43
|
+
| {
|
|
44
|
+
kind: 'http';
|
|
45
|
+
targetPort: string;
|
|
46
|
+
port: number;
|
|
47
|
+
scheme: 'http' | 'https';
|
|
48
|
+
/**
|
|
49
|
+
* Host header name and HTTPS SNI/certificate hostname. Connect only to the
|
|
50
|
+
* evidenced container endpoint, never resolve this name as a probe target.
|
|
51
|
+
* HTTPS verifies system trust and this hostname; no insecure TLS mode.
|
|
52
|
+
*/
|
|
53
|
+
host: string;
|
|
54
|
+
/** Canonical origin-form request target, including any nonsecret query. */
|
|
55
|
+
path: string;
|
|
56
|
+
/** Sorted unique final HTTP response status codes. */
|
|
57
|
+
expectedStatusCodes: number[];
|
|
58
|
+
/** A redirect response is evaluated as received; it cannot redirect probes. */
|
|
59
|
+
followRedirects: false;
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
export interface IRuntimeConfigLogPolicyV1 {
|
|
64
|
+
/** Maximum UTF-8 bytes in one captured stdout/stderr line. */
|
|
65
|
+
maximumLineBytes: number;
|
|
66
|
+
/** Maximum bytes waiting for delivery; overflow requires explicit evidence. */
|
|
67
|
+
maximumBufferedBytes: number;
|
|
68
|
+
/** Retention of archived logs; durable payloads belong to SmartBucket. */
|
|
69
|
+
retentionMs: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Controller-owned resolved nonsecret execution intent. OCI command, effective
|
|
74
|
+
* environment, working directory and identity must be resolved before sealing;
|
|
75
|
+
* consumers must not append inherited image defaults or choose runtime defaults.
|
|
76
|
+
* Secrets, network identity and storage claims remain separately assignment-bound
|
|
77
|
+
* owner references. This document contains no registry credentials or host paths.
|
|
78
|
+
*/
|
|
79
|
+
export interface IRuntimeConfigV1 extends IRuntimeAuthorityReferenceV1 {
|
|
80
|
+
schemaVersion: 1;
|
|
81
|
+
organizationId: string;
|
|
82
|
+
serviceId: string;
|
|
83
|
+
image: {
|
|
84
|
+
registryHost: string;
|
|
85
|
+
repository: string;
|
|
86
|
+
/** Root descriptor digest, which may be an OCI platform index. */
|
|
87
|
+
imageDigest: TSha256Digest;
|
|
88
|
+
invocation: IImmutableContainerInvocationV1;
|
|
89
|
+
};
|
|
90
|
+
/** Complete effective public environment; secret values must never be inlined. */
|
|
91
|
+
environment: Record<string, string>;
|
|
92
|
+
workingDirectory: string;
|
|
93
|
+
cpuMillis: number;
|
|
94
|
+
memoryBytes: number;
|
|
95
|
+
readonlyRootfs: boolean;
|
|
96
|
+
runAsUser: number;
|
|
97
|
+
runAsGroup: number;
|
|
98
|
+
/** Sorted unique names, with no overlapping ranges for the same transport. */
|
|
99
|
+
targetPorts: IRuntimeConfigTargetPortV1[];
|
|
100
|
+
readiness: TRuntimeConfigReadinessV1;
|
|
101
|
+
/**
|
|
102
|
+
* SmartData owns log metadata and SmartBucket owns archived payloads. This
|
|
103
|
+
* policy does not authorize persistent local files. Any CRI log capture uses
|
|
104
|
+
* disposable process-local transport artifacts and must evidence bounded
|
|
105
|
+
* buffering, backpressure and lost data; it must not silently truncate logs.
|
|
106
|
+
*/
|
|
107
|
+
logs: IRuntimeConfigLogPolicyV1;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface IRuntimeConfigAssignmentBindingV1 {
|
|
111
|
+
config: IRuntimeConfigV1;
|
|
112
|
+
assignment: IRuntimeAssignmentV1;
|
|
113
|
+
platformEvidence: IImmutableContainerInvocationPlatformEvidenceV1;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const runtimeConfigV1Contract = Object.freeze({
|
|
117
|
+
digestDomain: 'serve.zone/runtime-config-v1',
|
|
118
|
+
maximumCanonicalBytes: 262_144,
|
|
119
|
+
maximumEnvironmentEntries: 128,
|
|
120
|
+
maximumTargetPorts: 64,
|
|
121
|
+
maximumReadinessDurationMs: 3_600_000,
|
|
122
|
+
maximumLogRetentionMs: 31_536_000_000,
|
|
123
|
+
} as const);
|
|
124
|
+
|
|
125
|
+
const fail: (messageArg: string) => never = (messageArg) => { throw new Error(messageArg); };
|
|
126
|
+
const encoder = new TextEncoder();
|
|
127
|
+
const isInteger = (valueArg: unknown, minimumArg: number, maximumArg: number): valueArg is number =>
|
|
128
|
+
typeof valueArg === 'number' && Number.isSafeInteger(valueArg) && !Object.is(valueArg, -0)
|
|
129
|
+
&& valueArg >= minimumArg && valueArg <= maximumArg;
|
|
130
|
+
const isIdentifier = (valueArg: unknown): valueArg is string =>
|
|
131
|
+
typeof valueArg === 'string' && /^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/.test(valueArg);
|
|
132
|
+
const isDigest = (valueArg: unknown): valueArg is TSha256Digest =>
|
|
133
|
+
typeof valueArg === 'string' && isSha256Digest(valueArg);
|
|
134
|
+
const isPortName = (valueArg: unknown): valueArg is string =>
|
|
135
|
+
typeof valueArg === 'string' && /^[a-z0-9][a-z0-9_.-]{0,62}$/.test(valueArg);
|
|
136
|
+
const isExactRecord = (
|
|
137
|
+
valueArg: unknown, keysArg: readonly string[],
|
|
138
|
+
): valueArg is Record<string, unknown> => {
|
|
139
|
+
if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) return false;
|
|
140
|
+
const keys = Object.keys(valueArg);
|
|
141
|
+
return keys.length === keysArg.length && keys.every((keyArg) => keysArg.includes(keyArg));
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Bound traversal before canonicalization and copy descriptor values so input
|
|
146
|
+
* getters/toJSON never run. The canonical helper supplies scalar Unicode and
|
|
147
|
+
* safe-integer validation. Detached output is obtained before any async work.
|
|
148
|
+
*/
|
|
149
|
+
const snapshotBoundedJson = (valueArg: unknown): unknown => {
|
|
150
|
+
let nodes = 0;
|
|
151
|
+
let stringBytes = 0;
|
|
152
|
+
const ancestors = new Set<object>();
|
|
153
|
+
const accountString = (value: string): void => {
|
|
154
|
+
if (value.length > runtimeConfigV1Contract.maximumCanonicalBytes) fail('runtime config string is oversized');
|
|
155
|
+
stringBytes += encoder.encode(value).byteLength;
|
|
156
|
+
if (stringBytes > runtimeConfigV1Contract.maximumCanonicalBytes) fail('runtime config is oversized');
|
|
157
|
+
};
|
|
158
|
+
const copy = (value: unknown, depth: number): unknown => {
|
|
159
|
+
if (++nodes > 8192 || depth > 12) fail('runtime config exceeds structural bounds');
|
|
160
|
+
if (typeof value === 'string') { accountString(value); return value; }
|
|
161
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'number') return value;
|
|
162
|
+
if (!value || typeof value !== 'object') fail('runtime config contains a non-JSON value');
|
|
163
|
+
if (ancestors.has(value)) fail('runtime config contains a cycle');
|
|
164
|
+
ancestors.add(value);
|
|
165
|
+
try {
|
|
166
|
+
const array = Array.isArray(value);
|
|
167
|
+
const prototype = Object.getPrototypeOf(value);
|
|
168
|
+
if (array ? prototype !== Array.prototype : prototype !== Object.prototype && prototype !== null) {
|
|
169
|
+
fail('runtime config requires plain records and arrays');
|
|
170
|
+
}
|
|
171
|
+
const keys = Reflect.ownKeys(value);
|
|
172
|
+
if (keys.length > 257) fail('runtime config object exceeds key bounds');
|
|
173
|
+
if (array) {
|
|
174
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(value, 'length');
|
|
175
|
+
const length = lengthDescriptor?.value;
|
|
176
|
+
if (!isInteger(length, 0, 256) || keys.length !== length + 1) {
|
|
177
|
+
fail('runtime config requires bounded dense arrays');
|
|
178
|
+
}
|
|
179
|
+
const result: unknown[] = [];
|
|
180
|
+
for (let index = 0; index < length; index++) {
|
|
181
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
182
|
+
if (!descriptor || !descriptor.enumerable || !Object.hasOwn(descriptor, 'value')) {
|
|
183
|
+
fail('runtime config array contains a missing or non-data entry');
|
|
184
|
+
}
|
|
185
|
+
result.push(copy(descriptor.value, depth + 1));
|
|
186
|
+
}
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
const result: Record<string, unknown> = Object.create(null);
|
|
190
|
+
for (const key of keys) {
|
|
191
|
+
if (typeof key !== 'string') fail('runtime config contains a symbol key');
|
|
192
|
+
accountString(key);
|
|
193
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
194
|
+
if (!descriptor || !descriptor.enumerable || !Object.hasOwn(descriptor, 'value')) {
|
|
195
|
+
fail('runtime config requires enumerable data properties');
|
|
196
|
+
}
|
|
197
|
+
result[key] = copy(descriptor.value, depth + 1);
|
|
198
|
+
}
|
|
199
|
+
return result;
|
|
200
|
+
} finally { ancestors.delete(value); }
|
|
201
|
+
};
|
|
202
|
+
const canonical = canonicalizeStrictJson(copy(valueArg, 0), fail);
|
|
203
|
+
if (encoder.encode(canonical).byteLength > runtimeConfigV1Contract.maximumCanonicalBytes) {
|
|
204
|
+
fail('runtime config canonical JSON is oversized');
|
|
205
|
+
}
|
|
206
|
+
return JSON.parse(canonical);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const timingKeys = ['initialDelayMs', 'startupGraceMs', 'intervalMs', 'timeoutMs',
|
|
210
|
+
'successThreshold', 'failureThreshold'] as const;
|
|
211
|
+
const assertReadiness = (valueArg: unknown, portsArg: IRuntimeConfigTargetPortV1[]): void => {
|
|
212
|
+
if (!valueArg || typeof valueArg !== 'object') fail('runtime config readiness is required');
|
|
213
|
+
const value = valueArg as Record<string, unknown>;
|
|
214
|
+
const keys = value.kind === 'process' ? ['kind', ...timingKeys]
|
|
215
|
+
: value.kind === 'tcp' ? ['kind', ...timingKeys, 'targetPort', 'port']
|
|
216
|
+
: value.kind === 'http' ? ['kind', ...timingKeys, 'targetPort', 'port',
|
|
217
|
+
'scheme', 'host', 'path', 'expectedStatusCodes', 'followRedirects'] : [];
|
|
218
|
+
if (keys.length === 0 || !isExactRecord(value, keys)
|
|
219
|
+
|| !isInteger(value.initialDelayMs, 0, runtimeConfigV1Contract.maximumReadinessDurationMs)
|
|
220
|
+
|| !isInteger(value.startupGraceMs, 0, runtimeConfigV1Contract.maximumReadinessDurationMs)
|
|
221
|
+
|| !isInteger(value.intervalMs, 100, runtimeConfigV1Contract.maximumReadinessDurationMs)
|
|
222
|
+
|| !isInteger(value.timeoutMs, 1, 60_000)
|
|
223
|
+
|| !isInteger(value.successThreshold, 1, 100)
|
|
224
|
+
|| !isInteger(value.failureThreshold, 1, 100)
|
|
225
|
+
|| value.timeoutMs > value.intervalMs || value.initialDelayMs > value.startupGraceMs) {
|
|
226
|
+
fail('runtime config readiness must use explicit bounded timing and exact schema');
|
|
227
|
+
}
|
|
228
|
+
if (value.kind === 'process') return;
|
|
229
|
+
const target = portsArg.find((portArg) => portArg.name === value.targetPort);
|
|
230
|
+
if (!isPortName(value.targetPort) || !target || target.protocol !== 'tcp'
|
|
231
|
+
|| !isInteger(value.port, target.port, target.portEnd)) {
|
|
232
|
+
fail('runtime config readiness must select a declared TCP target and port');
|
|
233
|
+
}
|
|
234
|
+
if (value.kind === 'tcp') return;
|
|
235
|
+
if ((value.scheme !== 'http' && value.scheme !== 'https') || value.followRedirects !== false
|
|
236
|
+
|| typeof value.host !== 'string' || value.host.length > 253
|
|
237
|
+
|| !/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/.test(value.host)
|
|
238
|
+
|| typeof value.path !== 'string' || value.path.length > 4096
|
|
239
|
+
|| !value.path.startsWith('/') || value.path.startsWith('//')
|
|
240
|
+
|| /[^\x21-\x7e]|[\\#]/.test(value.path)
|
|
241
|
+
|| /%(?![0-9A-F]{2})/.test(value.path)
|
|
242
|
+
|| !Array.isArray(value.expectedStatusCodes) || value.expectedStatusCodes.length === 0
|
|
243
|
+
|| value.expectedStatusCodes.length > 32) {
|
|
244
|
+
fail('runtime config HTTP readiness must use an exact bounded request policy');
|
|
245
|
+
}
|
|
246
|
+
const parsed = new URL(value.path, 'http://runtime.invalid');
|
|
247
|
+
if (`${parsed.pathname}${parsed.search}` !== value.path) fail('runtime config HTTP path must be canonical');
|
|
248
|
+
let previousCode = 0;
|
|
249
|
+
for (const code of value.expectedStatusCodes) {
|
|
250
|
+
if (!isInteger(code, 200, 599) || code <= previousCode) {
|
|
251
|
+
fail('runtime config HTTP status expectations must be sorted unique final codes');
|
|
252
|
+
}
|
|
253
|
+
previousCode = code;
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const assertConfig = (valueArg: unknown): IRuntimeConfigV1 => {
|
|
258
|
+
if (!isExactRecord(valueArg, ['schemaVersion', 'id', 'generation', 'organizationId', 'serviceId',
|
|
259
|
+
'image', 'environment', 'workingDirectory', 'cpuMillis', 'memoryBytes', 'readonlyRootfs',
|
|
260
|
+
'runAsUser', 'runAsGroup', 'targetPorts', 'readiness', 'logs', 'digest'])
|
|
261
|
+
|| valueArg.schemaVersion !== 1 || !isIdentifier(valueArg.id)
|
|
262
|
+
|| !isInteger(valueArg.generation, 1, Number.MAX_SAFE_INTEGER)
|
|
263
|
+
|| !isIdentifier(valueArg.organizationId) || !isIdentifier(valueArg.serviceId)
|
|
264
|
+
|| !isDigest(valueArg.digest)) fail('runtime config must use its exact v1 schema');
|
|
265
|
+
const image = valueArg.image;
|
|
266
|
+
if (!isExactRecord(image, ['registryHost', 'repository', 'imageDigest', 'invocation'])
|
|
267
|
+
|| typeof image.registryHost !== 'string' || image.registryHost.length > 253
|
|
268
|
+
|| typeof image.repository !== 'string' || image.repository.length > 255
|
|
269
|
+
|| !isDigest(image.imageDigest)
|
|
270
|
+
|| validateImmutableContainerInvocation(image.invocation).length !== 0) {
|
|
271
|
+
fail('runtime config image must contain exact immutable invocation evidence');
|
|
272
|
+
}
|
|
273
|
+
buildDigestPinnedImageReference(image.registryHost as string, image.repository as string,
|
|
274
|
+
image.imageDigest as TSha256Digest);
|
|
275
|
+
const environment = valueArg.environment;
|
|
276
|
+
if (!environment || typeof environment !== 'object' || Array.isArray(environment)
|
|
277
|
+
|| Object.keys(environment).length > runtimeConfigV1Contract.maximumEnvironmentEntries) {
|
|
278
|
+
fail('runtime config requires an explicit bounded nonsecret environment');
|
|
279
|
+
}
|
|
280
|
+
for (const [key, value] of Object.entries(environment)) {
|
|
281
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(key) || typeof value !== 'string'
|
|
282
|
+
|| value.includes('\u0000') || encoder.encode(value).byteLength > 8192) {
|
|
283
|
+
fail('runtime config environment must contain bounded string entries');
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const directory = valueArg.workingDirectory;
|
|
287
|
+
if (typeof directory !== 'string' || !directory.startsWith('/')
|
|
288
|
+
|| encoder.encode(directory).byteLength > 4096 || /[\u0000-\u001f\u007f]/.test(directory)
|
|
289
|
+
|| (directory !== '/' && directory.split('/').slice(1).some((partArg) => !partArg || partArg === '.' || partArg === '..'))
|
|
290
|
+
|| !isInteger(valueArg.cpuMillis, 1, 1_000_000)
|
|
291
|
+
|| !isInteger(valueArg.memoryBytes, 16 * 1024 * 1024, 2 ** 50)
|
|
292
|
+
|| typeof valueArg.readonlyRootfs !== 'boolean'
|
|
293
|
+
|| !isInteger(valueArg.runAsUser, 0, 2 ** 31 - 1)
|
|
294
|
+
|| !isInteger(valueArg.runAsGroup, 0, 2 ** 31 - 1)) {
|
|
295
|
+
fail('runtime config requires canonical directory, explicit resources and numeric identity');
|
|
296
|
+
}
|
|
297
|
+
const ports = valueArg.targetPorts;
|
|
298
|
+
if (!Array.isArray(ports) || ports.length > runtimeConfigV1Contract.maximumTargetPorts) {
|
|
299
|
+
fail('runtime config target ports must be a bounded array');
|
|
300
|
+
}
|
|
301
|
+
let previousName = '';
|
|
302
|
+
const validatedPorts: IRuntimeConfigTargetPortV1[] = [];
|
|
303
|
+
for (const port of ports) {
|
|
304
|
+
if (!isExactRecord(port, ['name', 'port', 'portEnd', 'protocol'])
|
|
305
|
+
|| !isPortName(port.name) || port.name <= previousName
|
|
306
|
+
|| !isInteger(port.port, 1, 65535) || !isInteger(port.portEnd, port.port, 65535)
|
|
307
|
+
|| (port.protocol !== 'tcp' && port.protocol !== 'udp')
|
|
308
|
+
|| validatedPorts.some((previousArg) => previousArg.protocol === port.protocol
|
|
309
|
+
&& (port.port as number) <= previousArg.portEnd && (port.portEnd as number) >= previousArg.port)) {
|
|
310
|
+
fail('runtime config target ports require sorted unique names and nonoverlapping transport ranges');
|
|
311
|
+
}
|
|
312
|
+
previousName = port.name;
|
|
313
|
+
validatedPorts.push(port as unknown as IRuntimeConfigTargetPortV1);
|
|
314
|
+
}
|
|
315
|
+
assertReadiness(valueArg.readiness, validatedPorts);
|
|
316
|
+
const logs = valueArg.logs;
|
|
317
|
+
if (!isExactRecord(logs, ['maximumLineBytes', 'maximumBufferedBytes', 'retentionMs'])
|
|
318
|
+
|| !isInteger(logs.maximumLineBytes, 1, 1_048_576)
|
|
319
|
+
|| !isInteger(logs.maximumBufferedBytes, 1, 67_108_864)
|
|
320
|
+
|| logs.maximumBufferedBytes < logs.maximumLineBytes
|
|
321
|
+
|| !isInteger(logs.retentionMs, 1, runtimeConfigV1Contract.maximumLogRetentionMs)) {
|
|
322
|
+
fail('runtime config logs require explicit bounded line, buffer and retention limits');
|
|
323
|
+
}
|
|
324
|
+
return valueArg as unknown as IRuntimeConfigV1;
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Detached exact-schema snapshot for synchronous persistence assertions. Digest
|
|
329
|
+
* integrity, scope authorization, admission and runtime readiness are not implied.
|
|
330
|
+
*/
|
|
331
|
+
export const snapshotRuntimeConfigV1 = (valueArg: unknown): IRuntimeConfigV1 =>
|
|
332
|
+
assertConfig(snapshotBoundedJson(valueArg));
|
|
333
|
+
|
|
334
|
+
/** Domain-separated canonical JSON excludes only the config's own digest. */
|
|
335
|
+
export const computeRuntimeConfigV1Digest = async (configArg: IRuntimeConfigV1): Promise<TSha256Digest> => {
|
|
336
|
+
const { digest: _digest, ...payload } = snapshotRuntimeConfigV1(configArg);
|
|
337
|
+
return `sha256:${await createCanonicalJsonSha256Hex(canonicalizeStrictJson({
|
|
338
|
+
domain: runtimeConfigV1Contract.digestDomain, payload,
|
|
339
|
+
}, fail), fail)}`;
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const validateSnapshot = async (configArg: IRuntimeConfigV1): Promise<string[]> => {
|
|
343
|
+
if (!await verifyImmutableContainerInvocationDigest(configArg.image.invocation)) {
|
|
344
|
+
return ['runtime config immutable invocation digest does not match'];
|
|
345
|
+
}
|
|
346
|
+
return configArg.digest === await computeRuntimeConfigV1Digest(configArg)
|
|
347
|
+
? [] : ['runtime config digest does not match'];
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
export const validateRuntimeConfigV1 = async (valueArg: unknown): Promise<string[]> => {
|
|
351
|
+
try { return await validateSnapshot(snapshotRuntimeConfigV1(valueArg)); }
|
|
352
|
+
catch { return ['runtime config must use its exact canonical v1 schema']; }
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Bind valid content to one exact assignment and select its platform evidence.
|
|
357
|
+
* No assignment digest is embedded in the config, avoiding circular hashes.
|
|
358
|
+
* This proves content consistency only: the caller must authenticate the
|
|
359
|
+
* controller, durably admit the attempt, resolve other owner references, and
|
|
360
|
+
* verify runtime and readiness evidence before effects or route publication.
|
|
361
|
+
*/
|
|
362
|
+
export const bindRuntimeConfigV1ToAssignment = async (
|
|
363
|
+
configArg: unknown, assignmentArg: unknown,
|
|
364
|
+
): Promise<IRuntimeConfigAssignmentBindingV1> => {
|
|
365
|
+
const config = snapshotRuntimeConfigV1(configArg);
|
|
366
|
+
const assignment = snapshotRuntimeAssignmentV1(snapshotBoundedJson(assignmentArg));
|
|
367
|
+
if ((await validateSnapshot(config)).length !== 0
|
|
368
|
+
|| (await validateRuntimeAssignmentV1(assignment)).length !== 0) {
|
|
369
|
+
fail('runtime config binding requires valid config, invocation and assignment digests');
|
|
370
|
+
}
|
|
371
|
+
const reference = assignment.workload.config;
|
|
372
|
+
if (reference.id !== config.id || reference.generation !== config.generation
|
|
373
|
+
|| reference.digest !== config.digest || assignment.organizationId !== config.organizationId
|
|
374
|
+
|| assignment.serviceId !== config.serviceId
|
|
375
|
+
|| assignment.workload.registryHost !== config.image.registryHost
|
|
376
|
+
|| assignment.workload.repository !== config.image.repository
|
|
377
|
+
|| assignment.workload.imageDigest !== config.image.imageDigest) {
|
|
378
|
+
fail('runtime config does not match the exact assignment reference, ownership and image');
|
|
379
|
+
}
|
|
380
|
+
const platformEvidence = config.image.invocation.evidence.find(
|
|
381
|
+
(entryArg) => entryArg.platform === assignment.workload.platform,
|
|
382
|
+
);
|
|
383
|
+
if (!platformEvidence) fail('runtime config lacks exact assignment platform evidence');
|
|
384
|
+
return { config, assignment, platformEvidence };
|
|
385
|
+
};
|