@serve.zone/interfaces 30.0.0 → 30.2.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.
@@ -61,6 +61,14 @@ export interface IReportRuntimeAssignmentObservationRequestV1 {
61
61
 
62
62
  export type TRuntimeObservationAuthorityUseV1 = 'current-session' | 'historical-recovery';
63
63
 
64
+ /** Content comparison result only; all supplied authority must come from its trusted owner. */
65
+ export interface IRuntimeReporterSessionBindingV1 {
66
+ session: IRuntimeSessionBindingV1;
67
+ assignment: IRuntimeAssignmentV1;
68
+ reporterHistory: IRuntimeSessionBindingV1;
69
+ authorityUse: TRuntimeObservationAuthorityUseV1;
70
+ }
71
+
64
72
  export interface IReportRuntimeAssignmentObservationResponseV1 {
65
73
  /** Exact request body after a durable sequence/receipt transaction. */
66
74
  observation: IRuntimeAssignmentObservationV1;
@@ -343,6 +351,32 @@ export const bindApplyRuntimeAssignmentResponseV1 = (
343
351
  return response;
344
352
  });
345
353
 
354
+ /**
355
+ * Synchronous detached content binding, not authentication. trustedSessionArg must
356
+ * be the CURRENT authenticated physical peer; reporterHistoryArg must be immutable
357
+ * server-indexed Pallet-purpose history. Never obtain either from request fields.
358
+ * The owning transaction must recheck current credential/peer authority. Assignment
359
+ * digest validation and report-specific authority remain the caller's responsibility.
360
+ */
361
+ export const bindRuntimeReporterSessionV1 = (
362
+ sessionArg: unknown, reporterSessionIdArg: string, assignmentArg: unknown,
363
+ trustedSessionArg: unknown, reporterHistoryArg: unknown,
364
+ ): IRuntimeReporterSessionBindingV1 => safe(() => {
365
+ const incomingSession = snapshotRuntimeSessionBindingV1(sessionArg);
366
+ const reporterSessionId = identifier(reporterSessionIdArg);
367
+ const assignment = snapshotRuntimeAssignmentV1(capture(assignmentArg));
368
+ const trustedSession = snapshotRuntimeSessionBindingV1(trustedSessionArg);
369
+ const reporterHistory = snapshotRuntimeSessionBindingV1(reporterHistoryArg);
370
+ requireValue(sameSession(incomingSession, trustedSession) && matchesAssignment(trustedSession, assignment)
371
+ && reporterSessionId === reporterHistory.reporterSessionId && sameOwner(trustedSession, reporterHistory));
372
+ const current = sameSession(reporterHistory, trustedSession);
373
+ requireValue(current || (reporterHistory.reporterSessionId !== trustedSession.reporterSessionId
374
+ && reporterHistory.sessionEpoch < trustedSession.sessionEpoch
375
+ && reporterHistory.credentialGeneration <= trustedSession.credentialGeneration));
376
+ return { session: incomingSession, assignment, reporterHistory,
377
+ authorityUse: current ? 'current-session' : 'historical-recovery' };
378
+ });
379
+
346
380
  /**
347
381
  * trustedSessionArg is the CURRENT authenticated physical peer. reporterHistoryArg
348
382
  * must be server-indexed immutable session history, obtained by the original
@@ -359,18 +393,10 @@ export const bindReportRuntimeAssignmentObservationRequestV1 = async (
359
393
  ): Promise<{ request: IReportRuntimeAssignmentObservationRequestV1; authorityUse: TRuntimeObservationAuthorityUseV1 }> => {
360
394
  try {
361
395
  const request = snapshotReportRuntimeAssignmentObservationRequestV1(valueArg);
362
- const assignment = snapshotRuntimeAssignmentV1(capture(assignmentArg));
363
- const trustedSession = snapshotRuntimeSessionBindingV1(trustedSessionArg);
364
- const reporterHistory = snapshotRuntimeSessionBindingV1(reporterHistoryArg);
365
- requireValue(sameSession(request.session, trustedSession) && matchesAssignment(trustedSession, assignment)
366
- && request.observation.reporterSessionId === reporterHistory.reporterSessionId
367
- && sameOwner(trustedSession, reporterHistory));
368
- const current = sameSession(reporterHistory, trustedSession);
369
- requireValue(current || (reporterHistory.reporterSessionId !== trustedSession.reporterSessionId
370
- && reporterHistory.sessionEpoch < trustedSession.sessionEpoch
371
- && reporterHistory.credentialGeneration <= trustedSession.credentialGeneration));
372
- requireValue((await validateRuntimeAssignmentObservationV1(request.observation, assignment)).length === 0);
373
- return { request, authorityUse: current ? 'current-session' : 'historical-recovery' };
396
+ const binding = bindRuntimeReporterSessionV1(request.session, request.observation.reporterSessionId,
397
+ assignmentArg, trustedSessionArg, reporterHistoryArg);
398
+ requireValue((await validateRuntimeAssignmentObservationV1(request.observation, binding.assignment)).length === 0);
399
+ return { request, authorityUse: binding.authorityUse };
374
400
  } catch { return fail(); }
375
401
  };
376
402
 
@@ -0,0 +1,338 @@
1
+ import type { TSha256Digest } from './immutableimage.js';
2
+ import { isSha256Digest } from './immutableimage.js';
3
+ import type { IRuntimeAssignmentReferenceV1, IRuntimeAssignmentV1 } from './runtimeassignment.js';
4
+ import { evaluateRuntimeAssignmentV1, getRuntimeAssignmentV1Reference,
5
+ snapshotRuntimeAssignmentV1 } from './runtimeassignment.js';
6
+ import type { IRuntimeSessionBindingV1, TRuntimeObservationAuthorityUseV1 } from './runtimesession.js';
7
+ import { bindRuntimeReporterSessionV1, snapshotRuntimeSessionBindingV1 } from './runtimesession.js';
8
+ import { canonicalizeStrictJson, createCanonicalJsonSha256Hex } from '../private/canonicaljson.js';
9
+
10
+ /** Exact value-free native terminal evidence; terminal inspection does not collect addresses. */
11
+ export interface IRuntimeAssignmentTerminalEvidenceV1 {
12
+ phase: 'stopped' | 'absent';
13
+ sandboxId: string | null;
14
+ containerId: string | null;
15
+ imageConfigDigest: TSha256Digest | null;
16
+ /** Exactly empty for the qualified native stop/remove path; no route evidence. */
17
+ ipAddresses: string[];
18
+ exitCode: number | null;
19
+ }
20
+
21
+ /**
22
+ * Qualified only when the named Pallet owner has durably joined this exact native
23
+ * operation. Structure and digest alone do not prove that join. Pending, unknown
24
+ * and host-fenced journal entries can never issue this receipt. A new boot permits
25
+ * a new joined terminal operation; it cannot replace the stop/remove chain.
26
+ *
27
+ * This settles only the named Pallet attempt. It grants no readiness, routing,
28
+ * liveness, storage relocation, whole-host, Docker/Coreflow absence, other
29
+ * controller or cross-organization authority. It has no authoritative wall clock.
30
+ */
31
+ export interface IRuntimeAssignmentTerminalReceiptV1 {
32
+ schemaVersion: 1;
33
+ /** Exact generation 2 stop or generation 3 remove revision. */
34
+ assignment: IRuntimeAssignmentReferenceV1;
35
+ /** Exact immutable generation 1 run revision. */
36
+ runAssignment: IRuntimeAssignmentReferenceV1;
37
+ reporterSessionId: string;
38
+ sequence: 1 | 2;
39
+ operation: {
40
+ id: string;
41
+ action: 'stop' | 'remove';
42
+ bootId: string;
43
+ status: 'completed';
44
+ evidence: IRuntimeAssignmentTerminalEvidenceV1;
45
+ };
46
+ /** Null for stop; the exact accepted stop receipt digest for remove. */
47
+ previousReceiptDigest: TSha256Digest | null;
48
+ digest: TSha256Digest;
49
+ }
50
+
51
+ export interface IReportRuntimeAssignmentTerminalReceiptRequestV1 {
52
+ /** Current physical peer binding, even when the immutable receipt is historical. */
53
+ session: IRuntimeSessionBindingV1;
54
+ receipt: IRuntimeAssignmentTerminalReceiptV1;
55
+ }
56
+
57
+ export interface IReportRuntimeAssignmentTerminalReceiptResponseV1 {
58
+ /** Exact immutable body after the owning durable receipt transaction. */
59
+ receipt: IRuntimeAssignmentTerminalReceiptV1;
60
+ /** Historical recovery never restores active readiness, routes or liveness. */
61
+ authorityUse: TRuntimeObservationAuthorityUseV1;
62
+ }
63
+
64
+ export type TRuntimeAssignmentTerminalReceiptDecisionV1 =
65
+ | { status: 'accept' | 'replay'; receipt: IRuntimeAssignmentTerminalReceiptV1 }
66
+ | { status: 'reject'; reason: 'invalid' | 'conflict' | 'sequence-gap' | 'predecessor-mismatch' };
67
+
68
+ export const runtimeAssignmentTerminalReceiptV1Contract = Object.freeze({
69
+ digestDomain: 'serve.zone/runtime-assignment-terminal-receipt-v1',
70
+ maximumReceiptCanonicalBytes: 16_384,
71
+ maximumHistoryCanonicalBytes: 98_304,
72
+ maximumReceipts: 2,
73
+ maximumAssignmentRevisions: 3,
74
+ error: 'Runtime terminal receipt contract rejected.',
75
+ } as const);
76
+
77
+ const fail = (): never => { throw new Error(runtimeAssignmentTerminalReceiptV1Contract.error); };
78
+ function requireValue(conditionArg: unknown): asserts conditionArg { if (!conditionArg) fail(); }
79
+ const safe = <T>(operationArg: () => T): T => { try { return operationArg(); } catch { return fail(); } };
80
+ const canonical = (valueArg: unknown): string => canonicalizeStrictJson(valueArg, fail);
81
+ const encoder = new TextEncoder();
82
+ const identifier = (valueArg: unknown): string => {
83
+ requireValue(typeof valueArg === 'string' && /^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/.test(valueArg));
84
+ return valueArg;
85
+ };
86
+ const digest = (valueArg: unknown): TSha256Digest => {
87
+ requireValue(typeof valueArg === 'string' && isSha256Digest(valueArg));
88
+ return valueArg;
89
+ };
90
+
91
+ /** Bounded inert copy before hashing or calling any nested validator. */
92
+ const capture = (valueArg: unknown, maximumBytesArg: number = runtimeAssignmentTerminalReceiptV1Contract.maximumHistoryCanonicalBytes): unknown => safe(() => {
93
+ let nodes = 0;
94
+ let bytes = 0;
95
+ const ancestors = new Set<object>();
96
+ const account = (value: string): void => {
97
+ requireValue(value.length <= maximumBytesArg);
98
+ bytes += encoder.encode(value).byteLength;
99
+ requireValue(bytes <= maximumBytesArg);
100
+ };
101
+ const copy = (value: unknown, depth: number): unknown => {
102
+ requireValue(++nodes <= 4096 && depth <= 12);
103
+ if (typeof value === 'string') { account(value); return value; }
104
+ if (value === null || typeof value === 'boolean' || typeof value === 'number') return value;
105
+ requireValue(value !== null && typeof value === 'object' && !ancestors.has(value));
106
+ const array = Array.isArray(value);
107
+ const prototype = Object.getPrototypeOf(value);
108
+ requireValue(array ? prototype === Array.prototype : prototype === Object.prototype || prototype === null);
109
+ ancestors.add(value);
110
+ try {
111
+ const keys = Reflect.ownKeys(value);
112
+ requireValue(keys.length <= 65);
113
+ if (array) {
114
+ const length = Object.getOwnPropertyDescriptor(value, 'length')?.value;
115
+ requireValue(Number.isSafeInteger(length) && length >= 0 && length <= 64 && keys.length === length + 1);
116
+ const result: unknown[] = [];
117
+ for (let index = 0; index < length; index++) {
118
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
119
+ requireValue(descriptor?.enumerable && Object.hasOwn(descriptor, 'value'));
120
+ result.push(copy(descriptor.value, depth + 1));
121
+ }
122
+ return result;
123
+ }
124
+ const result: Record<string, unknown> = Object.create(null);
125
+ for (const key of keys) {
126
+ requireValue(typeof key === 'string'); account(key);
127
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
128
+ requireValue(descriptor?.enumerable && Object.hasOwn(descriptor, 'value'));
129
+ result[key] = copy(descriptor.value, depth + 1);
130
+ }
131
+ return result;
132
+ } finally { ancestors.delete(value); }
133
+ };
134
+ const serialized = canonical(copy(valueArg, 0));
135
+ requireValue(encoder.encode(serialized).byteLength <= maximumBytesArg);
136
+ return JSON.parse(serialized);
137
+ });
138
+ const record = (valueArg: unknown, keysArg: string[]): Record<string, unknown> => {
139
+ requireValue(valueArg !== null && typeof valueArg === 'object' && !Array.isArray(valueArg));
140
+ const keys = Object.keys(valueArg);
141
+ requireValue(keys.length === keysArg.length && keys.every((key) => keysArg.includes(key)));
142
+ return valueArg as Record<string, unknown>;
143
+ };
144
+ const reference = (valueArg: unknown): IRuntimeAssignmentReferenceV1 => {
145
+ const value = record(valueArg, ['assignmentId', 'generation', 'digest']);
146
+ requireValue(typeof value.generation === 'number' && Number.isSafeInteger(value.generation)
147
+ && value.generation >= 1 && value.generation <= 3);
148
+ return { assignmentId: identifier(value.assignmentId), generation: value.generation, digest: digest(value.digest) };
149
+ };
150
+ const evidence = (valueArg: unknown): IRuntimeAssignmentTerminalEvidenceV1 => {
151
+ const value = record(valueArg, ['phase', 'sandboxId', 'containerId', 'imageConfigDigest', 'ipAddresses', 'exitCode']);
152
+ requireValue(value.phase === 'absent' || value.phase === 'stopped');
153
+ for (const field of ['sandboxId', 'containerId']) requireValue(value[field] === null
154
+ || (typeof value[field] === 'string' && /^[a-f0-9]{64}$/.test(value[field])));
155
+ requireValue(value.imageConfigDigest === null || (typeof value.imageConfigDigest === 'string'
156
+ && isSha256Digest(value.imageConfigDigest)));
157
+ requireValue(Array.isArray(value.ipAddresses) && value.ipAddresses.length === 0);
158
+ requireValue(value.exitCode === null || (typeof value.exitCode === 'number' && Number.isSafeInteger(value.exitCode)
159
+ && value.exitCode >= -2_147_483_648 && value.exitCode <= 2_147_483_647));
160
+ requireValue(value.phase === 'absent'
161
+ ? value.sandboxId === null && value.containerId === null && value.imageConfigDigest === null && value.exitCode === null
162
+ : value.sandboxId !== null);
163
+ requireValue(value.containerId !== null || (value.imageConfigDigest === null && value.exitCode === null));
164
+ return value as unknown as IRuntimeAssignmentTerminalEvidenceV1;
165
+ };
166
+
167
+ /** Detached exact-schema assertion only; digest, journal provenance and authority remain unverified. */
168
+ export const snapshotRuntimeAssignmentTerminalReceiptV1 = (valueArg: unknown): IRuntimeAssignmentTerminalReceiptV1 => safe(() => {
169
+ const value = record(capture(valueArg, runtimeAssignmentTerminalReceiptV1Contract.maximumReceiptCanonicalBytes),
170
+ ['schemaVersion', 'assignment', 'runAssignment', 'reporterSessionId', 'sequence', 'operation', 'previousReceiptDigest', 'digest']);
171
+ const assignment = reference(value.assignment);
172
+ const runAssignment = reference(value.runAssignment);
173
+ const operation = record(value.operation, ['id', 'action', 'bootId', 'status', 'evidence']);
174
+ const terminalEvidence = evidence(operation.evidence);
175
+ requireValue(value.schemaVersion === 1 && (value.sequence === 1 || value.sequence === 2)
176
+ && operation.status === 'completed' && assignment.generation === value.sequence + 1
177
+ && runAssignment.generation === 1 && runAssignment.assignmentId === assignment.assignmentId);
178
+ requireValue(typeof operation.bootId === 'string'
179
+ && /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(operation.bootId));
180
+ requireValue(value.sequence === 1 ? operation.action === 'stop' && value.previousReceiptDigest === null
181
+ : operation.action === 'remove' && terminalEvidence.phase === 'absent' && value.previousReceiptDigest !== null);
182
+ return { schemaVersion: 1, assignment, runAssignment, reporterSessionId: identifier(value.reporterSessionId),
183
+ sequence: value.sequence, operation: { id: identifier(operation.id), action: operation.action as 'stop' | 'remove',
184
+ bootId: operation.bootId, status: 'completed', evidence: terminalEvidence },
185
+ previousReceiptDigest: value.previousReceiptDigest === null ? null : digest(value.previousReceiptDigest), digest: digest(value.digest) };
186
+ });
187
+
188
+ /** Bounded exact receipt history for synchronous persistence assertions; hashes remain unchecked. */
189
+ export const snapshotRuntimeAssignmentTerminalReceiptHistoryV1 = (valueArg: unknown): IRuntimeAssignmentTerminalReceiptV1[] => safe(() => {
190
+ const values = capture(valueArg);
191
+ requireValue(Array.isArray(values) && values.length <= runtimeAssignmentTerminalReceiptV1Contract.maximumReceipts);
192
+ const receipts = values.map(snapshotRuntimeAssignmentTerminalReceiptV1);
193
+ for (const [index, receipt] of receipts.entries()) {
194
+ requireValue(receipt.sequence === index + 1);
195
+ if (index > 0) requireValue(receipt.previousReceiptDigest === receipts[0].digest
196
+ && canonical(receipt.runAssignment) === canonical(receipts[0].runAssignment)
197
+ && receipt.operation.id !== receipts[0].operation.id);
198
+ }
199
+ return receipts;
200
+ });
201
+
202
+ const assignments = (valueArg: unknown): IRuntimeAssignmentV1[] => {
203
+ const values = capture(valueArg);
204
+ requireValue(Array.isArray(values) && values.length >= 2
205
+ && values.length <= runtimeAssignmentTerminalReceiptV1Contract.maximumAssignmentRevisions);
206
+ return values.map(snapshotRuntimeAssignmentV1);
207
+ };
208
+ const checkAssignmentHistory = async (historyArg: IRuntimeAssignmentV1[]): Promise<void> => {
209
+ const run = historyArg[0];
210
+ const scope = { controller: run.controller, organizationId: run.organizationId,
211
+ nodeId: run.nodeId, runtimeNamespace: run.runtimeNamespace };
212
+ for (const [index, assignment] of historyArg.entries()) {
213
+ requireValue(assignment.generation === index + 1
214
+ && assignment.disposition === ['run', 'stop-preserve', 'remove-runtime-preserve-storage'][index]);
215
+ requireValue((await evaluateRuntimeAssignmentV1(assignment, index === 0 ? null : historyArg[index - 1], scope)).status === 'accept');
216
+ }
217
+ };
218
+ const computeSnapshotDigest = async (receiptArg: IRuntimeAssignmentTerminalReceiptV1): Promise<TSha256Digest> => {
219
+ const { digest: _digest, ...payload } = receiptArg;
220
+ return `sha256:${await createCanonicalJsonSha256Hex(canonical({
221
+ domain: runtimeAssignmentTerminalReceiptV1Contract.digestDomain, payload,
222
+ }), fail)}`;
223
+ };
224
+ /** Domain-separated exact body digest; captures before asynchronous hashing. */
225
+ export const computeRuntimeAssignmentTerminalReceiptV1Digest = async (
226
+ valueArg: IRuntimeAssignmentTerminalReceiptV1,
227
+ ): Promise<TSha256Digest> => {
228
+ try { return await computeSnapshotDigest(snapshotRuntimeAssignmentTerminalReceiptV1(valueArg)); } catch { return fail(); }
229
+ };
230
+ const checkReceipt = async (
231
+ receiptArg: IRuntimeAssignmentTerminalReceiptV1, historyArg: IRuntimeAssignmentV1[],
232
+ previousArg: IRuntimeAssignmentTerminalReceiptV1 | null,
233
+ ): Promise<void> => {
234
+ requireValue(historyArg.length > receiptArg.sequence
235
+ && canonical(receiptArg.runAssignment) === canonical(getRuntimeAssignmentV1Reference(historyArg[0]))
236
+ && canonical(receiptArg.assignment) === canonical(getRuntimeAssignmentV1Reference(historyArg[receiptArg.sequence]))
237
+ && receiptArg.digest === await computeSnapshotDigest(receiptArg));
238
+ if (receiptArg.sequence === 1) requireValue(previousArg === null);
239
+ else {
240
+ requireValue(previousArg !== null && previousArg.sequence === 1);
241
+ await checkReceipt(previousArg, historyArg, null);
242
+ requireValue(receiptArg.previousReceiptDigest === previousArg.digest && receiptArg.operation.id !== previousArg.operation.id);
243
+ }
244
+ };
245
+
246
+ /** Full immutable run/stop/remove chain and exact prior stop receipt are mandatory for removal. */
247
+ export const validateRuntimeAssignmentTerminalReceiptV1 = async (
248
+ valueArg: unknown, assignmentHistoryArg: unknown, previousReceiptArg: unknown,
249
+ ): Promise<string[]> => {
250
+ try {
251
+ const receipt = snapshotRuntimeAssignmentTerminalReceiptV1(valueArg);
252
+ const history = assignments(assignmentHistoryArg);
253
+ const previous = previousReceiptArg === null ? null : snapshotRuntimeAssignmentTerminalReceiptV1(previousReceiptArg);
254
+ await checkAssignmentHistory(history);
255
+ await checkReceipt(receipt, history, previous);
256
+ return [];
257
+ } catch { return [runtimeAssignmentTerminalReceiptV1Contract.error]; }
258
+ };
259
+
260
+ /**
261
+ * Pure bounded CAS decision. Persist under the owning transaction and current
262
+ * credential/peer fence. New acceptance also requires the exact global incumbent;
263
+ * only a previously persisted exact body can replay after the incumbent changes.
264
+ * The helper cannot prove either condition. Retain both bodies after removal.
265
+ */
266
+ export const evaluateRuntimeAssignmentTerminalReceiptV1 = async (
267
+ valueArg: unknown, acceptedHistoryArg: unknown, assignmentHistoryArg: unknown,
268
+ ): Promise<TRuntimeAssignmentTerminalReceiptDecisionV1> => {
269
+ try {
270
+ const receipt = snapshotRuntimeAssignmentTerminalReceiptV1(valueArg);
271
+ const accepted = snapshotRuntimeAssignmentTerminalReceiptHistoryV1(acceptedHistoryArg);
272
+ const history = assignments(assignmentHistoryArg);
273
+ await checkAssignmentHistory(history);
274
+ for (const existing of accepted) await checkReceipt(existing, history, existing.sequence === 1 ? null : accepted[0]);
275
+ if (receipt.digest !== await computeSnapshotDigest(receipt)
276
+ || history.length <= receipt.sequence
277
+ || canonical(receipt.runAssignment) !== canonical(getRuntimeAssignmentV1Reference(history[0]))
278
+ || canonical(receipt.assignment) !== canonical(getRuntimeAssignmentV1Reference(history[receipt.sequence]))) {
279
+ return { status: 'reject', reason: 'invalid' };
280
+ }
281
+ const existing = accepted[receipt.sequence - 1];
282
+ if (existing) return canonical(existing) === canonical(receipt)
283
+ ? { status: 'replay', receipt: existing } : { status: 'reject', reason: 'conflict' };
284
+ if (receipt.sequence !== accepted.length + 1) return { status: 'reject', reason: 'sequence-gap' };
285
+ if (receipt.sequence === 2 && (receipt.previousReceiptDigest !== accepted[0].digest
286
+ || receipt.operation.id === accepted[0].operation.id)) return { status: 'reject', reason: 'predecessor-mismatch' };
287
+ return { status: 'accept', receipt };
288
+ } catch { return { status: 'reject', reason: 'invalid' }; }
289
+ };
290
+
291
+ export const snapshotReportRuntimeAssignmentTerminalReceiptRequestV1 = (
292
+ valueArg: unknown,
293
+ ): IReportRuntimeAssignmentTerminalReceiptRequestV1 => safe(() => {
294
+ const value = record(capture(valueArg), ['session', 'receipt']);
295
+ return { session: snapshotRuntimeSessionBindingV1(value.session), receipt: snapshotRuntimeAssignmentTerminalReceiptV1(value.receipt) };
296
+ });
297
+ export const snapshotReportRuntimeAssignmentTerminalReceiptResponseV1 = (
298
+ valueArg: unknown,
299
+ ): IReportRuntimeAssignmentTerminalReceiptResponseV1 => safe(() => {
300
+ const value = record(capture(valueArg), ['receipt', 'authorityUse']);
301
+ requireValue(value.authorityUse === 'current-session' || value.authorityUse === 'historical-recovery');
302
+ return { receipt: snapshotRuntimeAssignmentTerminalReceiptV1(value.receipt), authorityUse: value.authorityUse };
303
+ });
304
+
305
+ /**
306
+ * CURRENT physical session and immutable server-indexed reporter history are
307
+ * trusted owner inputs, never wire claims. The previous stop receipt must also
308
+ * come from the durable owner. This binds content; it does not authenticate,
309
+ * prove the local operation join, advance CAS, or admit a replacement.
310
+ */
311
+ export const bindReportRuntimeAssignmentTerminalReceiptRequestV1 = async (
312
+ valueArg: unknown, assignmentHistoryArg: unknown, previousReceiptArg: unknown,
313
+ trustedSessionArg: unknown, reporterHistoryArg: unknown,
314
+ ): Promise<{ request: IReportRuntimeAssignmentTerminalReceiptRequestV1; authorityUse: TRuntimeObservationAuthorityUseV1 }> => {
315
+ try {
316
+ const request = snapshotReportRuntimeAssignmentTerminalReceiptRequestV1(valueArg);
317
+ const history = assignments(assignmentHistoryArg);
318
+ const previous = previousReceiptArg === null ? null : snapshotRuntimeAssignmentTerminalReceiptV1(previousReceiptArg);
319
+ requireValue(history.length > request.receipt.sequence);
320
+ const binding = bindRuntimeReporterSessionV1(request.session, request.receipt.reporterSessionId,
321
+ history[request.receipt.sequence], trustedSessionArg, reporterHistoryArg);
322
+ await checkAssignmentHistory(history);
323
+ await checkReceipt(request.receipt, history, previous);
324
+ return { request, authorityUse: binding.authorityUse };
325
+ } catch { return fail(); }
326
+ };
327
+
328
+ /** Exact ACK of the immutable outbox body; no rewritten session, operation or chain. */
329
+ export const bindReportRuntimeAssignmentTerminalReceiptResponseV1 = (
330
+ valueArg: unknown, requestArg: unknown,
331
+ ): IReportRuntimeAssignmentTerminalReceiptResponseV1 => safe(() => {
332
+ const response = snapshotReportRuntimeAssignmentTerminalReceiptResponseV1(valueArg);
333
+ const request = snapshotReportRuntimeAssignmentTerminalReceiptRequestV1(requestArg);
334
+ requireValue(canonical(response.receipt) === canonical(request.receipt)
335
+ && response.authorityUse === (request.receipt.reporterSessionId === request.session.reporterSessionId
336
+ ? 'current-session' : 'historical-recovery'));
337
+ return response;
338
+ });
@@ -0,0 +1,130 @@
1
+ /** Workload permissions are independent of human membership and deployment grants. */
2
+ export type TServiceMachineCapabilityV1 = 'platform:session';
3
+
4
+ /** Exact existing-service authority. Never infer this grant from an API role. */
5
+ export interface IServiceMachineGrantV1 {
6
+ organizationId: string;
7
+ serviceId: string;
8
+ capabilities: TServiceMachineCapabilityV1[];
9
+ }
10
+
11
+ /** Public metadata only. The bearer is delivered through the owning service's SecretSet. */
12
+ export interface IServiceMachineCredentialMetadataV1 {
13
+ schemaVersion: 1;
14
+ credentialId: string;
15
+ userId: string;
16
+ grant: IServiceMachineGrantV1;
17
+ generation: number;
18
+ state: 'active' | 'revoked';
19
+ expiresAt: number;
20
+ createdAt: number;
21
+ updatedAt: number;
22
+ /** Exact immutable encrypted delivery version; null after revocation. */
23
+ delivery: { secretId: string; secretVersionId: string } | null;
24
+ }
25
+
26
+ interface IServiceMachineCredentialMutationBaseV1 {
27
+ schemaVersion: 1;
28
+ organizationId: string;
29
+ serviceId: string;
30
+ /** Retain the complete request and this ID across retries. */
31
+ mutationId: string;
32
+ }
33
+
34
+ /** Creation is absent-only; rotation and revocation require the exact current generation. */
35
+ export type TServiceMachineCredentialMutationV1 = IServiceMachineCredentialMutationBaseV1 & (
36
+ | {
37
+ operation: 'ensure';
38
+ expectedGeneration: null;
39
+ capabilities: TServiceMachineCapabilityV1[];
40
+ expiresAt: number;
41
+ }
42
+ | {
43
+ operation: 'rotate';
44
+ expectedGeneration: number;
45
+ capabilities: TServiceMachineCapabilityV1[];
46
+ expiresAt: number;
47
+ }
48
+ | { operation: 'revoke'; expectedGeneration: number }
49
+ );
50
+
51
+ export interface IServiceMachineCredentialMutationResultV1 {
52
+ /** Historical on replay; replay never restores revoked or superseded authority. */
53
+ credential: IServiceMachineCredentialMetadataV1;
54
+ replayed: boolean;
55
+ }
56
+
57
+ const fail = (): never => { throw new Error('Service credential contract rejected.'); };
58
+ function requireValue(value: unknown): asserts value { if (!value) fail(); }
59
+ const object = (value: unknown, keys: string[]): Record<string, unknown> => {
60
+ requireValue(value !== null && typeof value === 'object' && !Array.isArray(value));
61
+ requireValue(Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
62
+ const actual = Reflect.ownKeys(value);
63
+ requireValue(actual.length === keys.length && keys.every((key) => actual.includes(key)));
64
+ const copy: Record<string, unknown> = Object.create(null);
65
+ for (const key of keys) {
66
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
67
+ requireValue(descriptor && Object.hasOwn(descriptor, 'value') && descriptor.enumerable);
68
+ copy[key] = descriptor.value;
69
+ }
70
+ return copy;
71
+ };
72
+ const identifier = (value: unknown): string => {
73
+ requireValue(typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/.test(value));
74
+ return value;
75
+ };
76
+ const counter = (value: unknown): number => {
77
+ requireValue(typeof value === 'number' && Number.isSafeInteger(value) && value > 0 && value < Number.MAX_SAFE_INTEGER);
78
+ return value;
79
+ };
80
+ const capabilities = (value: unknown): TServiceMachineCapabilityV1[] => {
81
+ requireValue(Array.isArray(value) && Object.getPrototypeOf(value) === Array.prototype);
82
+ const keys = Reflect.ownKeys(value);
83
+ requireValue(keys.length === 2 && keys.includes('length') && keys.includes('0'));
84
+ const entry = Object.getOwnPropertyDescriptor(value, '0');
85
+ requireValue(entry && Object.hasOwn(entry, 'value') && entry.enumerable && entry.value === 'platform:session');
86
+ return ['platform:session'];
87
+ };
88
+ const safe = <T>(operation: () => T): T => { try { return operation(); } catch { return fail(); } };
89
+
90
+ /** Structural copy only; the backend must fence live service ownership and credential authority. */
91
+ export const snapshotServiceMachineGrantV1 = (value: unknown): IServiceMachineGrantV1 => safe(() => {
92
+ const grant = object(value, ['organizationId', 'serviceId', 'capabilities']);
93
+ return { organizationId: identifier(grant.organizationId), serviceId: identifier(grant.serviceId),
94
+ capabilities: capabilities(grant.capabilities) };
95
+ });
96
+
97
+ export const snapshotServiceMachineCredentialMutationV1 = (value: unknown): TServiceMachineCredentialMutationV1 => safe(() => {
98
+ requireValue(value !== null && typeof value === 'object');
99
+ const operation = Object.getOwnPropertyDescriptor(value, 'operation')?.value;
100
+ requireValue(operation === 'ensure' || operation === 'rotate' || operation === 'revoke');
101
+ const request = object(value, ['schemaVersion', 'organizationId', 'serviceId', 'mutationId', 'operation',
102
+ 'expectedGeneration', ...(operation === 'revoke' ? [] : ['capabilities', 'expiresAt'])]);
103
+ requireValue(request.schemaVersion === 1);
104
+ const base = { schemaVersion: 1 as const, organizationId: identifier(request.organizationId),
105
+ serviceId: identifier(request.serviceId), mutationId: identifier(request.mutationId) };
106
+ if (operation === 'revoke') return { ...base, operation, expectedGeneration: counter(request.expectedGeneration) };
107
+ const material = { capabilities: capabilities(request.capabilities), expiresAt: counter(request.expiresAt) };
108
+ if (operation === 'ensure') {
109
+ requireValue(request.expectedGeneration === null);
110
+ return { ...base, ...material, operation, expectedGeneration: null };
111
+ }
112
+ return { ...base, ...material, operation, expectedGeneration: counter(request.expectedGeneration) };
113
+ });
114
+
115
+ export const snapshotServiceMachineCredentialMetadataV1 = (value: unknown): IServiceMachineCredentialMetadataV1 => safe(() => {
116
+ const metadata = object(value, ['schemaVersion', 'credentialId', 'userId', 'grant', 'generation', 'state',
117
+ 'expiresAt', 'createdAt', 'updatedAt', 'delivery']);
118
+ requireValue(metadata.schemaVersion === 1 && (metadata.state === 'active' || metadata.state === 'revoked'));
119
+ const createdAt = counter(metadata.createdAt);
120
+ const updatedAt = counter(metadata.updatedAt);
121
+ requireValue(updatedAt >= createdAt);
122
+ let delivery: IServiceMachineCredentialMetadataV1['delivery'] = null;
123
+ if (metadata.state === 'active') {
124
+ const version = object(metadata.delivery, ['secretId', 'secretVersionId']);
125
+ delivery = { secretId: identifier(version.secretId), secretVersionId: identifier(version.secretVersionId) };
126
+ } else requireValue(metadata.delivery === null);
127
+ return { schemaVersion: 1, credentialId: identifier(metadata.credentialId), userId: identifier(metadata.userId),
128
+ grant: snapshotServiceMachineGrantV1(metadata.grant), generation: counter(metadata.generation),
129
+ state: metadata.state, expiresAt: counter(metadata.expiresAt), createdAt, updatedAt, delivery };
130
+ });
@@ -1,5 +1,30 @@
1
1
  import * as plugins from '../plugins.js';
2
2
  import type { IIdentity, IIdentityCredential, IServiceDeploymentGrant } from '../data/user.js';
3
+ import type {
4
+ IServiceMachineCredentialMetadataV1, IServiceMachineCredentialMutationResultV1,
5
+ TServiceMachineCredentialMutationV1,
6
+ } from '../data/servicecredential.js';
7
+
8
+ export interface IReq_Admin_GetServiceMachineCredentialV1 extends plugins.typedrequestInterfaces.implementsTR<
9
+ plugins.typedrequestInterfaces.ITypedRequest, IReq_Admin_GetServiceMachineCredentialV1
10
+ > {
11
+ method: 'getServiceMachineCredentialV1';
12
+ request: { identity: IIdentityCredential; organizationId: string; serviceId: string };
13
+ response: { credential: IServiceMachineCredentialMetadataV1 | null };
14
+ }
15
+
16
+ /**
17
+ * The backend authorizes the exact service and mutates hashed credential authority,
18
+ * encrypted SecretSet delivery and the replay receipt in one fenced transaction.
19
+ * No bearer is returned. Existing legacy deployment/cluster credentials are separate.
20
+ */
21
+ export interface IReq_Admin_MutateServiceMachineCredentialV1 extends plugins.typedrequestInterfaces.implementsTR<
22
+ plugins.typedrequestInterfaces.ITypedRequest, IReq_Admin_MutateServiceMachineCredentialV1
23
+ > {
24
+ method: 'mutateServiceMachineCredentialV1';
25
+ request: { identity: IIdentityCredential; mutation: TServiceMachineCredentialMutationV1 };
26
+ response: IServiceMachineCredentialMutationResultV1;
27
+ }
3
28
 
4
29
  /** Backend-owned human credential metadata; never includes a password verifier. */
5
30
  export interface IHumanCredentialMetadata {
@@ -1,5 +1,6 @@
1
1
  import * as plugins from '../plugins.js';
2
2
  import type * as data from '../data/runtimesession.js';
3
+ import type * as terminal from '../data/runtimeterminalreceipt.js';
3
4
 
4
5
  /** WSS-only Pallet bootstrap. Derive physical peer/process/idempotency server-side. */
5
6
  export interface IReq_Pallet_Cloudly_RegisterPalletRuntimeSessionV1 extends plugins.typedrequestInterfaces.implementsTR<
@@ -31,6 +32,16 @@ export interface IReq_Pallet_Controller_ReportRuntimeAssignmentObservationV1 ext
31
32
  response: data.IReportRuntimeAssignmentObservationResponseV1;
32
33
  }
33
34
 
35
+ /** Exact joined terminal-operation receipt; observations and boot changes cannot substitute. */
36
+ export interface IReq_Pallet_Controller_ReportRuntimeAssignmentTerminalReceiptV1 extends plugins.typedrequestInterfaces.implementsTR<
37
+ plugins.typedrequestInterfaces.ITypedRequest,
38
+ IReq_Pallet_Controller_ReportRuntimeAssignmentTerminalReceiptV1
39
+ > {
40
+ method: 'reportRuntimeAssignmentTerminalReceiptV1';
41
+ request: terminal.IReportRuntimeAssignmentTerminalReceiptRequestV1;
42
+ response: terminal.IReportRuntimeAssignmentTerminalReceiptResponseV1;
43
+ }
44
+
34
45
  /** Current-peer, exact current assignment authorization; response is secret material. */
35
46
  export interface IReq_Pallet_Controller_GetRuntimeRegistryCredentialV1 extends plugins.typedrequestInterfaces.implementsTR<
36
47
  plugins.typedrequestInterfaces.ITypedRequest,