@serve.zone/interfaces 28.4.0 → 29.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/changelog.md +19 -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/runtimeconfig.d.ts +132 -0
- package/dist_ts/data/runtimeconfig.js +278 -0
- package/dist_ts/data/runtimesession.d.ts +127 -0
- package/dist_ts/data/runtimesession.js +275 -0
- package/dist_ts/requests/index.d.ts +2 -1
- package/dist_ts/requests/index.js +3 -2
- package/dist_ts/requests/runtimesession.d.ts +32 -0
- package/dist_ts/requests/runtimesession.js +2 -0
- package/package.json +1 -1
- package/readme.md +43 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/index.ts +2 -0
- package/ts/data/runtimeconfig.ts +395 -0
- package/ts/data/runtimesession.ts +403 -0
- package/ts/requests/index.ts +2 -0
- package/ts/requests/runtimesession.ts +52 -0
|
@@ -0,0 +1,395 @@
|
|
|
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 policy must be resolved before
|
|
75
|
+
* sealing; consumers must not choose resource limits or identity policies.
|
|
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
|
+
/** null explicitly requests no CPU quota; zero and omitted values are invalid. */
|
|
94
|
+
cpuMillis: number | null;
|
|
95
|
+
memoryBytes: number;
|
|
96
|
+
readonlyRootfs: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Both identity fields must be numbers or both must be null. The null pair
|
|
99
|
+
* explicitly selects the pinned platform image's OCI User, including primary
|
|
100
|
+
* and supplemental group resolution against its immutable root filesystem.
|
|
101
|
+
* An absent OCI User retains the image/runtime specification's root identity.
|
|
102
|
+
* The execution owner must delegate that resolution to the qualified runtime;
|
|
103
|
+
* it must not guess a GID for a UID-only or named image user.
|
|
104
|
+
*/
|
|
105
|
+
runAsUser: number | null;
|
|
106
|
+
/** Numeric override paired with runAsUser, or null for the image identity policy. */
|
|
107
|
+
runAsGroup: number | null;
|
|
108
|
+
/** Sorted unique names, with no overlapping ranges for the same transport. */
|
|
109
|
+
targetPorts: IRuntimeConfigTargetPortV1[];
|
|
110
|
+
readiness: TRuntimeConfigReadinessV1;
|
|
111
|
+
/**
|
|
112
|
+
* SmartData owns log metadata and SmartBucket owns archived payloads. This
|
|
113
|
+
* policy does not authorize persistent local files. Any CRI log capture uses
|
|
114
|
+
* disposable process-local transport artifacts and must evidence bounded
|
|
115
|
+
* buffering, backpressure and lost data; it must not silently truncate logs.
|
|
116
|
+
*/
|
|
117
|
+
logs: IRuntimeConfigLogPolicyV1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface IRuntimeConfigAssignmentBindingV1 {
|
|
121
|
+
config: IRuntimeConfigV1;
|
|
122
|
+
assignment: IRuntimeAssignmentV1;
|
|
123
|
+
platformEvidence: IImmutableContainerInvocationPlatformEvidenceV1;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export const runtimeConfigV1Contract = Object.freeze({
|
|
127
|
+
digestDomain: 'serve.zone/runtime-config-v1',
|
|
128
|
+
maximumCanonicalBytes: 262_144,
|
|
129
|
+
maximumEnvironmentEntries: 128,
|
|
130
|
+
maximumTargetPorts: 64,
|
|
131
|
+
maximumReadinessDurationMs: 3_600_000,
|
|
132
|
+
maximumLogRetentionMs: 31_536_000_000,
|
|
133
|
+
} as const);
|
|
134
|
+
|
|
135
|
+
const fail: (messageArg: string) => never = (messageArg) => { throw new Error(messageArg); };
|
|
136
|
+
const encoder = new TextEncoder();
|
|
137
|
+
const isInteger = (valueArg: unknown, minimumArg: number, maximumArg: number): valueArg is number =>
|
|
138
|
+
typeof valueArg === 'number' && Number.isSafeInteger(valueArg) && !Object.is(valueArg, -0)
|
|
139
|
+
&& valueArg >= minimumArg && valueArg <= maximumArg;
|
|
140
|
+
const isIdentifier = (valueArg: unknown): valueArg is string =>
|
|
141
|
+
typeof valueArg === 'string' && /^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/.test(valueArg);
|
|
142
|
+
const isDigest = (valueArg: unknown): valueArg is TSha256Digest =>
|
|
143
|
+
typeof valueArg === 'string' && isSha256Digest(valueArg);
|
|
144
|
+
const isPortName = (valueArg: unknown): valueArg is string =>
|
|
145
|
+
typeof valueArg === 'string' && /^[a-z0-9][a-z0-9_.-]{0,62}$/.test(valueArg);
|
|
146
|
+
const isExactRecord = (
|
|
147
|
+
valueArg: unknown, keysArg: readonly string[],
|
|
148
|
+
): valueArg is Record<string, unknown> => {
|
|
149
|
+
if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) return false;
|
|
150
|
+
const keys = Object.keys(valueArg);
|
|
151
|
+
return keys.length === keysArg.length && keys.every((keyArg) => keysArg.includes(keyArg));
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Bound traversal before canonicalization and copy descriptor values so input
|
|
156
|
+
* getters/toJSON never run. The canonical helper supplies scalar Unicode and
|
|
157
|
+
* safe-integer validation. Detached output is obtained before any async work.
|
|
158
|
+
*/
|
|
159
|
+
const snapshotBoundedJson = (valueArg: unknown): unknown => {
|
|
160
|
+
let nodes = 0;
|
|
161
|
+
let stringBytes = 0;
|
|
162
|
+
const ancestors = new Set<object>();
|
|
163
|
+
const accountString = (value: string): void => {
|
|
164
|
+
if (value.length > runtimeConfigV1Contract.maximumCanonicalBytes) fail('runtime config string is oversized');
|
|
165
|
+
stringBytes += encoder.encode(value).byteLength;
|
|
166
|
+
if (stringBytes > runtimeConfigV1Contract.maximumCanonicalBytes) fail('runtime config is oversized');
|
|
167
|
+
};
|
|
168
|
+
const copy = (value: unknown, depth: number): unknown => {
|
|
169
|
+
if (++nodes > 8192 || depth > 12) fail('runtime config exceeds structural bounds');
|
|
170
|
+
if (typeof value === 'string') { accountString(value); return value; }
|
|
171
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'number') return value;
|
|
172
|
+
if (!value || typeof value !== 'object') fail('runtime config contains a non-JSON value');
|
|
173
|
+
if (ancestors.has(value)) fail('runtime config contains a cycle');
|
|
174
|
+
ancestors.add(value);
|
|
175
|
+
try {
|
|
176
|
+
const array = Array.isArray(value);
|
|
177
|
+
const prototype = Object.getPrototypeOf(value);
|
|
178
|
+
if (array ? prototype !== Array.prototype : prototype !== Object.prototype && prototype !== null) {
|
|
179
|
+
fail('runtime config requires plain records and arrays');
|
|
180
|
+
}
|
|
181
|
+
const keys = Reflect.ownKeys(value);
|
|
182
|
+
if (keys.length > 257) fail('runtime config object exceeds key bounds');
|
|
183
|
+
if (array) {
|
|
184
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(value, 'length');
|
|
185
|
+
const length = lengthDescriptor?.value;
|
|
186
|
+
if (!isInteger(length, 0, 256) || keys.length !== length + 1) {
|
|
187
|
+
fail('runtime config requires bounded dense arrays');
|
|
188
|
+
}
|
|
189
|
+
const result: unknown[] = [];
|
|
190
|
+
for (let index = 0; index < length; index++) {
|
|
191
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
192
|
+
if (!descriptor || !descriptor.enumerable || !Object.hasOwn(descriptor, 'value')) {
|
|
193
|
+
fail('runtime config array contains a missing or non-data entry');
|
|
194
|
+
}
|
|
195
|
+
result.push(copy(descriptor.value, depth + 1));
|
|
196
|
+
}
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
const result: Record<string, unknown> = Object.create(null);
|
|
200
|
+
for (const key of keys) {
|
|
201
|
+
if (typeof key !== 'string') fail('runtime config contains a symbol key');
|
|
202
|
+
accountString(key);
|
|
203
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
204
|
+
if (!descriptor || !descriptor.enumerable || !Object.hasOwn(descriptor, 'value')) {
|
|
205
|
+
fail('runtime config requires enumerable data properties');
|
|
206
|
+
}
|
|
207
|
+
result[key] = copy(descriptor.value, depth + 1);
|
|
208
|
+
}
|
|
209
|
+
return result;
|
|
210
|
+
} finally { ancestors.delete(value); }
|
|
211
|
+
};
|
|
212
|
+
const canonical = canonicalizeStrictJson(copy(valueArg, 0), fail);
|
|
213
|
+
if (encoder.encode(canonical).byteLength > runtimeConfigV1Contract.maximumCanonicalBytes) {
|
|
214
|
+
fail('runtime config canonical JSON is oversized');
|
|
215
|
+
}
|
|
216
|
+
return JSON.parse(canonical);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const timingKeys = ['initialDelayMs', 'startupGraceMs', 'intervalMs', 'timeoutMs',
|
|
220
|
+
'successThreshold', 'failureThreshold'] as const;
|
|
221
|
+
const assertReadiness = (valueArg: unknown, portsArg: IRuntimeConfigTargetPortV1[]): void => {
|
|
222
|
+
if (!valueArg || typeof valueArg !== 'object') fail('runtime config readiness is required');
|
|
223
|
+
const value = valueArg as Record<string, unknown>;
|
|
224
|
+
const keys = value.kind === 'process' ? ['kind', ...timingKeys]
|
|
225
|
+
: value.kind === 'tcp' ? ['kind', ...timingKeys, 'targetPort', 'port']
|
|
226
|
+
: value.kind === 'http' ? ['kind', ...timingKeys, 'targetPort', 'port',
|
|
227
|
+
'scheme', 'host', 'path', 'expectedStatusCodes', 'followRedirects'] : [];
|
|
228
|
+
if (keys.length === 0 || !isExactRecord(value, keys)
|
|
229
|
+
|| !isInteger(value.initialDelayMs, 0, runtimeConfigV1Contract.maximumReadinessDurationMs)
|
|
230
|
+
|| !isInteger(value.startupGraceMs, 0, runtimeConfigV1Contract.maximumReadinessDurationMs)
|
|
231
|
+
|| !isInteger(value.intervalMs, 100, runtimeConfigV1Contract.maximumReadinessDurationMs)
|
|
232
|
+
|| !isInteger(value.timeoutMs, 1, 60_000)
|
|
233
|
+
|| !isInteger(value.successThreshold, 1, 100)
|
|
234
|
+
|| !isInteger(value.failureThreshold, 1, 100)
|
|
235
|
+
|| value.timeoutMs > value.intervalMs || value.initialDelayMs > value.startupGraceMs) {
|
|
236
|
+
fail('runtime config readiness must use explicit bounded timing and exact schema');
|
|
237
|
+
}
|
|
238
|
+
if (value.kind === 'process') return;
|
|
239
|
+
const target = portsArg.find((portArg) => portArg.name === value.targetPort);
|
|
240
|
+
if (!isPortName(value.targetPort) || !target || target.protocol !== 'tcp'
|
|
241
|
+
|| !isInteger(value.port, target.port, target.portEnd)) {
|
|
242
|
+
fail('runtime config readiness must select a declared TCP target and port');
|
|
243
|
+
}
|
|
244
|
+
if (value.kind === 'tcp') return;
|
|
245
|
+
if ((value.scheme !== 'http' && value.scheme !== 'https') || value.followRedirects !== false
|
|
246
|
+
|| typeof value.host !== 'string' || value.host.length > 253
|
|
247
|
+
|| !/^(?:[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)
|
|
248
|
+
|| typeof value.path !== 'string' || value.path.length > 4096
|
|
249
|
+
|| !value.path.startsWith('/') || value.path.startsWith('//')
|
|
250
|
+
|| /[^\x21-\x7e]|[\\#]/.test(value.path)
|
|
251
|
+
|| /%(?![0-9A-F]{2})/.test(value.path)
|
|
252
|
+
|| !Array.isArray(value.expectedStatusCodes) || value.expectedStatusCodes.length === 0
|
|
253
|
+
|| value.expectedStatusCodes.length > 32) {
|
|
254
|
+
fail('runtime config HTTP readiness must use an exact bounded request policy');
|
|
255
|
+
}
|
|
256
|
+
const parsed = new URL(value.path, 'http://runtime.invalid');
|
|
257
|
+
if (`${parsed.pathname}${parsed.search}` !== value.path) fail('runtime config HTTP path must be canonical');
|
|
258
|
+
let previousCode = 0;
|
|
259
|
+
for (const code of value.expectedStatusCodes) {
|
|
260
|
+
if (!isInteger(code, 200, 599) || code <= previousCode) {
|
|
261
|
+
fail('runtime config HTTP status expectations must be sorted unique final codes');
|
|
262
|
+
}
|
|
263
|
+
previousCode = code;
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const assertConfig = (valueArg: unknown): IRuntimeConfigV1 => {
|
|
268
|
+
if (!isExactRecord(valueArg, ['schemaVersion', 'id', 'generation', 'organizationId', 'serviceId',
|
|
269
|
+
'image', 'environment', 'workingDirectory', 'cpuMillis', 'memoryBytes', 'readonlyRootfs',
|
|
270
|
+
'runAsUser', 'runAsGroup', 'targetPorts', 'readiness', 'logs', 'digest'])
|
|
271
|
+
|| valueArg.schemaVersion !== 1 || !isIdentifier(valueArg.id)
|
|
272
|
+
|| !isInteger(valueArg.generation, 1, Number.MAX_SAFE_INTEGER)
|
|
273
|
+
|| !isIdentifier(valueArg.organizationId) || !isIdentifier(valueArg.serviceId)
|
|
274
|
+
|| !isDigest(valueArg.digest)) fail('runtime config must use its exact v1 schema');
|
|
275
|
+
const image = valueArg.image;
|
|
276
|
+
if (!isExactRecord(image, ['registryHost', 'repository', 'imageDigest', 'invocation'])
|
|
277
|
+
|| typeof image.registryHost !== 'string' || image.registryHost.length > 253
|
|
278
|
+
|| typeof image.repository !== 'string' || image.repository.length > 255
|
|
279
|
+
|| !isDigest(image.imageDigest)
|
|
280
|
+
|| validateImmutableContainerInvocation(image.invocation).length !== 0) {
|
|
281
|
+
fail('runtime config image must contain exact immutable invocation evidence');
|
|
282
|
+
}
|
|
283
|
+
buildDigestPinnedImageReference(image.registryHost as string, image.repository as string,
|
|
284
|
+
image.imageDigest as TSha256Digest);
|
|
285
|
+
const environment = valueArg.environment;
|
|
286
|
+
if (!environment || typeof environment !== 'object' || Array.isArray(environment)
|
|
287
|
+
|| Object.keys(environment).length > runtimeConfigV1Contract.maximumEnvironmentEntries) {
|
|
288
|
+
fail('runtime config requires an explicit bounded nonsecret environment');
|
|
289
|
+
}
|
|
290
|
+
for (const [key, value] of Object.entries(environment)) {
|
|
291
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(key) || typeof value !== 'string'
|
|
292
|
+
|| value.includes('\u0000') || encoder.encode(value).byteLength > 8192) {
|
|
293
|
+
fail('runtime config environment must contain bounded string entries');
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const directory = valueArg.workingDirectory;
|
|
297
|
+
if (typeof directory !== 'string' || !directory.startsWith('/')
|
|
298
|
+
|| encoder.encode(directory).byteLength > 4096 || /[\u0000-\u001f\u007f]/.test(directory)
|
|
299
|
+
|| (directory !== '/' && directory.split('/').slice(1).some((partArg) => !partArg || partArg === '.' || partArg === '..'))
|
|
300
|
+
|| !(valueArg.cpuMillis === null || isInteger(valueArg.cpuMillis, 1, 1_000_000))
|
|
301
|
+
|| !isInteger(valueArg.memoryBytes, 16 * 1024 * 1024, 2 ** 50)
|
|
302
|
+
|| typeof valueArg.readonlyRootfs !== 'boolean'
|
|
303
|
+
|| !((valueArg.runAsUser === null && valueArg.runAsGroup === null)
|
|
304
|
+
|| (isInteger(valueArg.runAsUser, 0, 2 ** 31 - 1) && isInteger(valueArg.runAsGroup, 0, 2 ** 31 - 1)))) {
|
|
305
|
+
fail('runtime config requires canonical directory and explicit resource and identity policies');
|
|
306
|
+
}
|
|
307
|
+
const ports = valueArg.targetPorts;
|
|
308
|
+
if (!Array.isArray(ports) || ports.length > runtimeConfigV1Contract.maximumTargetPorts) {
|
|
309
|
+
fail('runtime config target ports must be a bounded array');
|
|
310
|
+
}
|
|
311
|
+
let previousName = '';
|
|
312
|
+
const validatedPorts: IRuntimeConfigTargetPortV1[] = [];
|
|
313
|
+
for (const port of ports) {
|
|
314
|
+
if (!isExactRecord(port, ['name', 'port', 'portEnd', 'protocol'])
|
|
315
|
+
|| !isPortName(port.name) || port.name <= previousName
|
|
316
|
+
|| !isInteger(port.port, 1, 65535) || !isInteger(port.portEnd, port.port, 65535)
|
|
317
|
+
|| (port.protocol !== 'tcp' && port.protocol !== 'udp')
|
|
318
|
+
|| validatedPorts.some((previousArg) => previousArg.protocol === port.protocol
|
|
319
|
+
&& (port.port as number) <= previousArg.portEnd && (port.portEnd as number) >= previousArg.port)) {
|
|
320
|
+
fail('runtime config target ports require sorted unique names and nonoverlapping transport ranges');
|
|
321
|
+
}
|
|
322
|
+
previousName = port.name;
|
|
323
|
+
validatedPorts.push(port as unknown as IRuntimeConfigTargetPortV1);
|
|
324
|
+
}
|
|
325
|
+
assertReadiness(valueArg.readiness, validatedPorts);
|
|
326
|
+
const logs = valueArg.logs;
|
|
327
|
+
if (!isExactRecord(logs, ['maximumLineBytes', 'maximumBufferedBytes', 'retentionMs'])
|
|
328
|
+
|| !isInteger(logs.maximumLineBytes, 1, 1_048_576)
|
|
329
|
+
|| !isInteger(logs.maximumBufferedBytes, 1, 67_108_864)
|
|
330
|
+
|| logs.maximumBufferedBytes < logs.maximumLineBytes
|
|
331
|
+
|| !isInteger(logs.retentionMs, 1, runtimeConfigV1Contract.maximumLogRetentionMs)) {
|
|
332
|
+
fail('runtime config logs require explicit bounded line, buffer and retention limits');
|
|
333
|
+
}
|
|
334
|
+
return valueArg as unknown as IRuntimeConfigV1;
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Detached exact-schema snapshot for synchronous persistence assertions. Digest
|
|
339
|
+
* integrity, scope authorization, admission and runtime readiness are not implied.
|
|
340
|
+
*/
|
|
341
|
+
export const snapshotRuntimeConfigV1 = (valueArg: unknown): IRuntimeConfigV1 =>
|
|
342
|
+
assertConfig(snapshotBoundedJson(valueArg));
|
|
343
|
+
|
|
344
|
+
/** Domain-separated canonical JSON excludes only the config's own digest. */
|
|
345
|
+
export const computeRuntimeConfigV1Digest = async (configArg: IRuntimeConfigV1): Promise<TSha256Digest> => {
|
|
346
|
+
const { digest: _digest, ...payload } = snapshotRuntimeConfigV1(configArg);
|
|
347
|
+
return `sha256:${await createCanonicalJsonSha256Hex(canonicalizeStrictJson({
|
|
348
|
+
domain: runtimeConfigV1Contract.digestDomain, payload,
|
|
349
|
+
}, fail), fail)}`;
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const validateSnapshot = async (configArg: IRuntimeConfigV1): Promise<string[]> => {
|
|
353
|
+
if (!await verifyImmutableContainerInvocationDigest(configArg.image.invocation)) {
|
|
354
|
+
return ['runtime config immutable invocation digest does not match'];
|
|
355
|
+
}
|
|
356
|
+
return configArg.digest === await computeRuntimeConfigV1Digest(configArg)
|
|
357
|
+
? [] : ['runtime config digest does not match'];
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
export const validateRuntimeConfigV1 = async (valueArg: unknown): Promise<string[]> => {
|
|
361
|
+
try { return await validateSnapshot(snapshotRuntimeConfigV1(valueArg)); }
|
|
362
|
+
catch { return ['runtime config must use its exact canonical v1 schema']; }
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Bind valid content to one exact assignment and select its platform evidence.
|
|
367
|
+
* No assignment digest is embedded in the config, avoiding circular hashes.
|
|
368
|
+
* This proves content consistency only: the caller must authenticate the
|
|
369
|
+
* controller, durably admit the attempt, resolve other owner references, and
|
|
370
|
+
* verify runtime and readiness evidence before effects or route publication.
|
|
371
|
+
*/
|
|
372
|
+
export const bindRuntimeConfigV1ToAssignment = async (
|
|
373
|
+
configArg: unknown, assignmentArg: unknown,
|
|
374
|
+
): Promise<IRuntimeConfigAssignmentBindingV1> => {
|
|
375
|
+
const config = snapshotRuntimeConfigV1(configArg);
|
|
376
|
+
const assignment = snapshotRuntimeAssignmentV1(snapshotBoundedJson(assignmentArg));
|
|
377
|
+
if ((await validateSnapshot(config)).length !== 0
|
|
378
|
+
|| (await validateRuntimeAssignmentV1(assignment)).length !== 0) {
|
|
379
|
+
fail('runtime config binding requires valid config, invocation and assignment digests');
|
|
380
|
+
}
|
|
381
|
+
const reference = assignment.workload.config;
|
|
382
|
+
if (reference.id !== config.id || reference.generation !== config.generation
|
|
383
|
+
|| reference.digest !== config.digest || assignment.organizationId !== config.organizationId
|
|
384
|
+
|| assignment.serviceId !== config.serviceId
|
|
385
|
+
|| assignment.workload.registryHost !== config.image.registryHost
|
|
386
|
+
|| assignment.workload.repository !== config.image.repository
|
|
387
|
+
|| assignment.workload.imageDigest !== config.image.imageDigest) {
|
|
388
|
+
fail('runtime config does not match the exact assignment reference, ownership and image');
|
|
389
|
+
}
|
|
390
|
+
const platformEvidence = config.image.invocation.evidence.find(
|
|
391
|
+
(entryArg) => entryArg.platform === assignment.workload.platform,
|
|
392
|
+
);
|
|
393
|
+
if (!platformEvidence) fail('runtime config lacks exact assignment platform evidence');
|
|
394
|
+
return { config, assignment, platformEvidence };
|
|
395
|
+
};
|