@serve.zone/interfaces 32.34.0 → 32.36.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.
Files changed (45) hide show
  1. package/changelog.md +71 -0
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/data/clusternode.d.ts +16 -2
  4. package/dist_ts/data/clusternode.js +11 -1
  5. package/dist_ts/data/deploymentlegacyimport.d.ts +92 -0
  6. package/dist_ts/data/deploymentlegacyimport.js +237 -0
  7. package/dist_ts/data/deploymentoperation.d.ts +63 -38
  8. package/dist_ts/data/deploymentoperation.js +12 -2
  9. package/dist_ts/data/deploymentpreflight.d.ts +42 -10
  10. package/dist_ts/data/deploymentpreflight.js +47 -10
  11. package/dist_ts/data/immutableimage.d.ts +26 -2
  12. package/dist_ts/data/immutableimage.js +12 -2
  13. package/dist_ts/data/index.d.ts +5 -0
  14. package/dist_ts/data/index.js +6 -1
  15. package/dist_ts/data/networknode.d.ts +16 -6
  16. package/dist_ts/data/networknode.js +1 -1
  17. package/dist_ts/data/runtimeassignmentexec.d.ts +110 -0
  18. package/dist_ts/data/runtimeassignmentexec.js +164 -0
  19. package/dist_ts/data/runtimeassignmentlogs.d.ts +121 -0
  20. package/dist_ts/data/runtimeassignmentlogs.js +139 -0
  21. package/dist_ts/data/runtimeassignmentstats.d.ts +60 -0
  22. package/dist_ts/data/runtimeassignmentstats.js +93 -0
  23. package/dist_ts/data/runtimelocalvolumearchive.d.ts +103 -0
  24. package/dist_ts/data/runtimelocalvolumearchive.js +111 -0
  25. package/dist_ts/data/user.d.ts +6 -1
  26. package/dist_ts/data/user.js +6 -1
  27. package/dist_ts/requests/runtimesession.d.ts +81 -0
  28. package/dist_ts/requests/service.d.ts +51 -0
  29. package/package.json +2 -2
  30. package/readme.md +109 -1
  31. package/ts/00_commitinfo_data.ts +1 -1
  32. package/ts/data/clusternode.ts +17 -2
  33. package/ts/data/deploymentlegacyimport.ts +371 -0
  34. package/ts/data/deploymentoperation.ts +83 -39
  35. package/ts/data/deploymentpreflight.ts +86 -18
  36. package/ts/data/immutableimage.ts +42 -3
  37. package/ts/data/index.ts +5 -0
  38. package/ts/data/networknode.ts +12 -5
  39. package/ts/data/runtimeassignmentexec.ts +271 -0
  40. package/ts/data/runtimeassignmentlogs.ts +249 -0
  41. package/ts/data/runtimeassignmentstats.ts +149 -0
  42. package/ts/data/runtimelocalvolumearchive.ts +207 -0
  43. package/ts/data/user.ts +10 -0
  44. package/ts/requests/runtimesession.ts +102 -0
  45. package/ts/requests/service.ts +66 -0
@@ -0,0 +1,249 @@
1
+ import type { IRuntimeAssignment } from './runtimeassignment.js';
2
+ import { snapshotRuntimeAssignment, validateRuntimeAssignment } from './runtimeassignment.js';
3
+ import type { IRuntimeConfigLogPolicy } from './runtimeconfig.js';
4
+ import type { IRuntimeSessionBinding } from './runtimesession.js';
5
+ import { snapshotRuntimeSessionBinding } from './runtimesession.js';
6
+ import {
7
+ captureNetwork, networkCounter, networkEqual, networkId, networkList, networkRecord, requireNetwork,
8
+ } from '../private/networkcontract.js';
9
+ import { isRuntimeLogPolicy } from '../private/runtimepolicy.js';
10
+
11
+ /** The container output a log line was written to. */
12
+ export type TRuntimeAssignmentLogStream = 'stdout' | 'stderr';
13
+
14
+ /**
15
+ * One piece of one line a container wrote, as the runtime captured it.
16
+ *
17
+ * `text` holds no line terminator and no `\n`; the node decodes the captured bytes as UTF-8 and
18
+ * replaces an invalid sequence with U+FFFD. A line longer than one entry travels as consecutive
19
+ * entries of the same stream: every piece but the last states `partial: true`, and the receiver
20
+ * joins them. The joined line is at most the config's `logs.maximumLineBytes` UTF-8 bytes; the node
21
+ * cuts a longer one there and follows its last piece with a `line-cut` loss of the same stream.
22
+ */
23
+ export interface IRuntimeAssignmentLogLine {
24
+ kind: 'line';
25
+ stream: TRuntimeAssignmentLogStream;
26
+ /** When the runtime captured the piece, in nanoseconds since the Unix epoch, as decimal digits. */
27
+ timestampNs: string;
28
+ text: string;
29
+ /** The line continues in the next `line` entry of the same stream. */
30
+ partial: boolean;
31
+ }
32
+
33
+ /**
34
+ * Why output did not reach the controller.
35
+ *
36
+ * - `buffer-overflow`: the node's buffer held `logs.maximumBufferedBytes` unacknowledged bytes and
37
+ * the oldest were dropped to keep capturing;
38
+ * - `line-cut`: the rest of a line beyond `logs.maximumLineBytes`;
39
+ * - `capture-restart`: the node's capture restarted without its buffer, for example after a reboot
40
+ * emptied its disposable capture directory; how much was lost is unknown;
41
+ * - `not-captured`: the container was started without output capture, so none of its output is
42
+ * available; how much there was is unknown.
43
+ */
44
+ export type TRuntimeAssignmentLogLossReason = 'buffer-overflow' | 'line-cut' | 'capture-restart' | 'not-captured';
45
+
46
+ /**
47
+ * Evidence of output that was lost, in the position of the stream where it was lost.
48
+ *
49
+ * `bytes` and `lines` count exactly what was dropped; both are `null` for `capture-restart` and
50
+ * `not-captured`, whose extent the node cannot know, and both are stated for the other reasons.
51
+ * A `line-cut` counts the cut bytes and no line, because the line itself was delivered; it names
52
+ * the stream of that line. A `buffer-overflow` counts the dropped bytes and the whole lines among
53
+ * them, and names a stream only when every dropped byte came from it.
54
+ */
55
+ export interface IRuntimeAssignmentLogLoss {
56
+ kind: 'lost';
57
+ reason: TRuntimeAssignmentLogLossReason;
58
+ stream: TRuntimeAssignmentLogStream | null;
59
+ /** When the node detected the loss, in nanoseconds since the Unix epoch, as decimal digits. */
60
+ timestampNs: string;
61
+ bytes: number | null;
62
+ lines: number | null;
63
+ }
64
+
65
+ export type TRuntimeAssignmentLogEntry = IRuntimeAssignmentLogLine | IRuntimeAssignmentLogLoss;
66
+
67
+ /**
68
+ * One delivery of one attempt's captured output, in capture order.
69
+ *
70
+ * `captureId` names one capture of the attempt on its node: the node starts a new capture, with a
71
+ * new id and `sequence` 1, whenever it cannot continue the previous one, and opens it with a
72
+ * `capture-restart` loss. Within a capture `sequence` counts batches from 1 without gaps, and the
73
+ * node sends the next batch only after the controller acknowledged the previous one, so a batch is
74
+ * the one unit of retry. `final` states that no batch follows in this capture: the container is gone
75
+ * and its output is drained. Only a final batch may be empty.
76
+ */
77
+ export interface IRuntimeAssignmentLogBatch {
78
+ assignmentId: string;
79
+ attemptId: string;
80
+ captureId: string;
81
+ sequence: number;
82
+ entries: TRuntimeAssignmentLogEntry[];
83
+ final: boolean;
84
+ }
85
+
86
+ /** Pallet push over its current authenticated session. */
87
+ export interface IReportRuntimeAssignmentLogsRequest {
88
+ session: IRuntimeSessionBinding;
89
+ batch: IRuntimeAssignmentLogBatch;
90
+ }
91
+
92
+ /**
93
+ * The controller holds the batch durably. `replay` answers a batch it already holds, which it does
94
+ * not store again. It never refuses a batch for a gap in `sequence`: the node discards only what
95
+ * was acknowledged, so a missing batch can only be the controller's own loss, which it records.
96
+ */
97
+ export interface IReportRuntimeAssignmentLogsResponse {
98
+ status: 'accepted' | 'replay';
99
+ assignmentId: string;
100
+ captureId: string;
101
+ sequence: number;
102
+ }
103
+
104
+ export const runtimeAssignmentLogContract = Object.freeze({
105
+ error: 'Runtime assignment log contract rejected.',
106
+ /** Pallet pushes; the controller persists metadata and payloads and acknowledges by sequence. */
107
+ delivery: 'node-pushes-controller-acknowledges-each-batch',
108
+ /** The most entries in one batch. */
109
+ maximumEntriesPerBatch: 1024,
110
+ /** The most UTF-8 bytes of `text` in one entry; a longer line travels as `partial` pieces. */
111
+ maximumEntryTextBytes: 65_536,
112
+ /**
113
+ * The canonical JSON of a whole report is at most one mebibyte, the bound every runtime network
114
+ * contract captures with; the node closes a batch before the next entry would cross it.
115
+ */
116
+ maximumReportCanonicalBytes: 1_048_576,
117
+ } as const);
118
+
119
+ const fail = (): never => { throw new Error(runtimeAssignmentLogContract.error); };
120
+ const safe = <T>(operationArg: () => T): T => { try { return operationArg(); } catch { return fail(); } };
121
+ const encoder = new TextEncoder();
122
+ const streams = ['stdout', 'stderr'] as const;
123
+ const lossReasons = ['buffer-overflow', 'line-cut', 'capture-restart', 'not-captured'] as const;
124
+ const statuses = ['accepted', 'replay'] as const;
125
+
126
+ const literalOf = <TLiteral extends string>(valueArg: unknown, allowedArg: readonly TLiteral[]): TLiteral => {
127
+ requireNetwork((allowedArg as readonly unknown[]).includes(valueArg));
128
+ return valueArg as TLiteral;
129
+ };
130
+ /** Nanoseconds since the Unix epoch as canonical decimal digits: no sign, no leading zero. */
131
+ const timestampOf = (valueArg: unknown): string => {
132
+ requireNetwork(typeof valueArg === 'string' && /^[1-9][0-9]{0,19}$/.test(valueArg)
133
+ && BigInt(valueArg) <= 18_446_744_073_709_551_615n);
134
+ return valueArg;
135
+ };
136
+ const countOf = (valueArg: unknown): number => {
137
+ requireNetwork(typeof valueArg === 'number' && Number.isSafeInteger(valueArg) && valueArg >= 0
138
+ && !Object.is(valueArg, -0));
139
+ return valueArg;
140
+ };
141
+
142
+ const lineOf = (valueArg: unknown): IRuntimeAssignmentLogLine => {
143
+ const value = networkRecord(valueArg, ['kind', 'stream', 'timestampNs', 'text', 'partial']);
144
+ requireNetwork(value.kind === 'line' && typeof value.text === 'string' && !value.text.includes('\n')
145
+ && encoder.encode(value.text).byteLength <= runtimeAssignmentLogContract.maximumEntryTextBytes
146
+ && typeof value.partial === 'boolean');
147
+ return {
148
+ kind: 'line', stream: literalOf(value.stream, streams), timestampNs: timestampOf(value.timestampNs),
149
+ text: value.text, partial: value.partial,
150
+ };
151
+ };
152
+
153
+ const lossOf = (valueArg: unknown): IRuntimeAssignmentLogLoss => {
154
+ const value = networkRecord(valueArg, ['kind', 'reason', 'stream', 'timestampNs', 'bytes', 'lines']);
155
+ requireNetwork(value.kind === 'lost');
156
+ const reason = literalOf(value.reason, lossReasons);
157
+ const stream = value.stream === null ? null : literalOf(value.stream, streams);
158
+ const unknownExtent = reason === 'capture-restart' || reason === 'not-captured';
159
+ if (unknownExtent) {
160
+ requireNetwork(value.bytes === null && value.lines === null && stream === null);
161
+ return { kind: 'lost', reason, stream, timestampNs: timestampOf(value.timestampNs), bytes: null, lines: null };
162
+ }
163
+ const bytes = networkCounter(value.bytes);
164
+ const lines = countOf(value.lines);
165
+ requireNetwork(reason !== 'line-cut' || (stream !== null && lines === 0));
166
+ return { kind: 'lost', reason, stream, timestampNs: timestampOf(value.timestampNs), bytes, lines };
167
+ };
168
+
169
+ const entryOf = (valueArg: unknown): TRuntimeAssignmentLogEntry => {
170
+ requireNetwork(valueArg !== null && typeof valueArg === 'object');
171
+ return (valueArg as Record<string, unknown>).kind === 'lost' ? lossOf(valueArg) : lineOf(valueArg);
172
+ };
173
+
174
+ const batchOf = (valueArg: unknown): IRuntimeAssignmentLogBatch => {
175
+ const value = networkRecord(valueArg, ['assignmentId', 'attemptId', 'captureId', 'sequence', 'entries', 'final']);
176
+ const entries = networkList(value.entries, runtimeAssignmentLogContract.maximumEntriesPerBatch, entryOf);
177
+ requireNetwork(typeof value.final === 'boolean' && (entries.length > 0 || value.final));
178
+ return {
179
+ assignmentId: networkId(value.assignmentId), attemptId: networkId(value.attemptId),
180
+ captureId: networkId(value.captureId), sequence: networkCounter(value.sequence), entries, final: value.final,
181
+ };
182
+ };
183
+
184
+ /** Detached exact-schema snapshot of one batch, bounded like a whole report. */
185
+ export const snapshotRuntimeAssignmentLogBatch = (valueArg: unknown): IRuntimeAssignmentLogBatch =>
186
+ safe(() => batchOf(captureNetwork(valueArg)));
187
+
188
+ export const snapshotReportRuntimeAssignmentLogsRequest = (
189
+ valueArg: unknown,
190
+ ): IReportRuntimeAssignmentLogsRequest => safe(() => {
191
+ const value = networkRecord(captureNetwork(valueArg), ['session', 'batch']);
192
+ return { session: snapshotRuntimeSessionBinding(value.session), batch: batchOf(value.batch) };
193
+ });
194
+
195
+ export const snapshotReportRuntimeAssignmentLogsResponse = (
196
+ valueArg: unknown,
197
+ ): IReportRuntimeAssignmentLogsResponse => safe(() => {
198
+ const value = networkRecord(captureNetwork(valueArg), ['status', 'assignmentId', 'captureId', 'sequence']);
199
+ return {
200
+ status: literalOf(value.status, statuses), assignmentId: networkId(value.assignmentId),
201
+ captureId: networkId(value.captureId), sequence: networkCounter(value.sequence),
202
+ };
203
+ });
204
+
205
+ /**
206
+ * Controller-side content binding, not authentication. `trustedSessionArg` is the controller's
207
+ * CURRENT session with the node the report arrived on, never read from the request. `assignmentArg`
208
+ * is the controller's own record of the batch's assignment, any generation of it, and
209
+ * `logPolicyArg` the `logs` policy of the config it runs with. The request's session must equal the
210
+ * trusted one, the assignment must be this node's and carry the batch's attempt, and every line
211
+ * the batch holds, its pieces joined, must fit the policy's `maximumLineBytes`. A line whose first
212
+ * pieces travelled in an earlier batch is judged by the pieces this batch holds.
213
+ */
214
+ export const bindReportRuntimeAssignmentLogsRequest = async (
215
+ valueArg: unknown, assignmentArg: unknown, logPolicyArg: IRuntimeConfigLogPolicy, trustedSessionArg: unknown,
216
+ ): Promise<{ request: IReportRuntimeAssignmentLogsRequest; assignment: IRuntimeAssignment }> => {
217
+ const request = snapshotReportRuntimeAssignmentLogsRequest(valueArg);
218
+ const assignment = safe(() => snapshotRuntimeAssignment(assignmentArg));
219
+ const trustedSession = safe(() => snapshotRuntimeSessionBinding(trustedSessionArg));
220
+ if ((await validateRuntimeAssignment(assignment)).length) fail();
221
+ safe(() => {
222
+ requireNetwork(isRuntimeLogPolicy(logPolicyArg));
223
+ requireNetwork(networkEqual(request.session, trustedSession)
224
+ && networkEqual(assignment.controller, trustedSession.controller)
225
+ && assignment.nodeId === trustedSession.nodeId
226
+ && assignment.runtimeNamespace === trustedSession.runtimeNamespace
227
+ && assignment.assignmentId === request.batch.assignmentId
228
+ && assignment.attemptId === request.batch.attemptId);
229
+ const joined: Record<TRuntimeAssignmentLogStream, number> = { stdout: 0, stderr: 0 };
230
+ for (const entry of request.batch.entries) {
231
+ if (entry.kind !== 'line') continue;
232
+ joined[entry.stream] += encoder.encode(entry.text).byteLength;
233
+ requireNetwork(joined[entry.stream] <= logPolicyArg.maximumLineBytes);
234
+ if (!entry.partial) joined[entry.stream] = 0;
235
+ }
236
+ });
237
+ return { request, assignment };
238
+ };
239
+
240
+ /** Matches an acknowledgement to the exact batch that was sent. */
241
+ export const bindReportRuntimeAssignmentLogsResponse = (
242
+ valueArg: unknown, requestArg: unknown,
243
+ ): IReportRuntimeAssignmentLogsResponse => safe(() => {
244
+ const response = snapshotReportRuntimeAssignmentLogsResponse(valueArg);
245
+ const { batch } = snapshotReportRuntimeAssignmentLogsRequest(requestArg);
246
+ requireNetwork(response.assignmentId === batch.assignmentId && response.captureId === batch.captureId
247
+ && response.sequence === batch.sequence);
248
+ return response;
249
+ });
@@ -0,0 +1,149 @@
1
+ import type { IRuntimeAssignment, IRuntimeAssignmentReference } from './runtimeassignment.js';
2
+ import { getRuntimeAssignmentReference, snapshotRuntimeAssignment, validateRuntimeAssignment } from './runtimeassignment.js';
3
+ import type { IRuntimeSessionBinding } from './runtimesession.js';
4
+ import { snapshotRuntimeSessionBinding } from './runtimesession.js';
5
+ import {
6
+ captureNetwork, networkCounter, networkDigest, networkEqual, networkId, networkRecord, requireNetwork,
7
+ } from '../private/networkcontract.js';
8
+
9
+ /** Bytes the workload's network namespace received and sent since the sandbox started. */
10
+ export interface IRuntimeAssignmentNetworkStats {
11
+ receivedBytes: number;
12
+ transmittedBytes: number;
13
+ }
14
+
15
+ /**
16
+ * One resource sample of one running attempt, as the runtime reports it.
17
+ *
18
+ * `cpuUsageCoreNanoseconds` is cumulative CPU time across every core since the container started,
19
+ * as decimal digits because it outgrows a JSON number; a reader derives utilisation from two
20
+ * samples and their `timestampNs`. `memoryLimitBytes` is the limit the node enforces, the sealed
21
+ * config's `memoryBytes`. `network` is `null` when the runtime reports no interface of the
22
+ * sandbox, and `writableLayerBytes` is `null` when it reports no usage of the container's writable
23
+ * layer.
24
+ */
25
+ export interface IRuntimeAssignmentStatsSample {
26
+ /** When the runtime took the sample, in nanoseconds since the Unix epoch, as decimal digits. */
27
+ timestampNs: string;
28
+ cpuUsageCoreNanoseconds: string;
29
+ memoryWorkingSetBytes: number;
30
+ memoryLimitBytes: number;
31
+ network: IRuntimeAssignmentNetworkStats | null;
32
+ writableLayerBytes: number | null;
33
+ }
34
+
35
+ /** Controller read over the node's current authenticated session. */
36
+ export interface IReadRuntimeAssignmentStatsRequest {
37
+ session: IRuntimeSessionBinding;
38
+ /** The exact current revision of an assignment on this node. */
39
+ assignment: IRuntimeAssignmentReference;
40
+ }
41
+
42
+ /**
43
+ * `sampled` carries one sample; `not-running` states that the attempt has no running container to
44
+ * sample, and carries none.
45
+ */
46
+ export interface IReadRuntimeAssignmentStatsResponse {
47
+ status: 'sampled' | 'not-running';
48
+ assignment: IRuntimeAssignmentReference;
49
+ sample: IRuntimeAssignmentStatsSample | null;
50
+ }
51
+
52
+ export const runtimeAssignmentStatsContract = Object.freeze({
53
+ error: 'Runtime assignment stats contract rejected.',
54
+ /** The node answers one read per assignment at a time and refuses a concurrent one. */
55
+ concurrency: 'one-read-per-assignment',
56
+ } as const);
57
+
58
+ const fail = (): never => { throw new Error(runtimeAssignmentStatsContract.error); };
59
+ const safe = <T>(operationArg: () => T): T => { try { return operationArg(); } catch { return fail(); } };
60
+
61
+ /** An unsigned 64-bit counter as canonical decimal digits: no sign, no leading zero. */
62
+ const decimalOf = (valueArg: unknown): string => {
63
+ requireNetwork(typeof valueArg === 'string' && /^(0|[1-9][0-9]{0,19})$/.test(valueArg)
64
+ && BigInt(valueArg) <= 18_446_744_073_709_551_615n);
65
+ return valueArg;
66
+ };
67
+ const bytesOf = (valueArg: unknown): number => {
68
+ requireNetwork(typeof valueArg === 'number' && Number.isSafeInteger(valueArg) && valueArg >= 0
69
+ && !Object.is(valueArg, -0));
70
+ return valueArg;
71
+ };
72
+ const referenceOf = (valueArg: unknown): IRuntimeAssignmentReference => {
73
+ const value = networkRecord(valueArg, ['assignmentId', 'generation', 'digest']);
74
+ return {
75
+ assignmentId: networkId(value.assignmentId), generation: networkCounter(value.generation),
76
+ digest: networkDigest(value.digest),
77
+ };
78
+ };
79
+
80
+ const sampleOf = (valueArg: unknown): IRuntimeAssignmentStatsSample => {
81
+ const value = networkRecord(valueArg, ['timestampNs', 'cpuUsageCoreNanoseconds', 'memoryWorkingSetBytes',
82
+ 'memoryLimitBytes', 'network', 'writableLayerBytes']);
83
+ const timestampNs = decimalOf(value.timestampNs);
84
+ requireNetwork(timestampNs !== '0');
85
+ let network: IRuntimeAssignmentNetworkStats | null = null;
86
+ if (value.network !== null) {
87
+ const item = networkRecord(value.network, ['receivedBytes', 'transmittedBytes']);
88
+ network = { receivedBytes: bytesOf(item.receivedBytes), transmittedBytes: bytesOf(item.transmittedBytes) };
89
+ }
90
+ return {
91
+ timestampNs,
92
+ cpuUsageCoreNanoseconds: decimalOf(value.cpuUsageCoreNanoseconds),
93
+ memoryWorkingSetBytes: bytesOf(value.memoryWorkingSetBytes),
94
+ memoryLimitBytes: networkCounter(value.memoryLimitBytes),
95
+ network,
96
+ writableLayerBytes: value.writableLayerBytes === null ? null : bytesOf(value.writableLayerBytes),
97
+ };
98
+ };
99
+
100
+ export const snapshotReadRuntimeAssignmentStatsRequest = (
101
+ valueArg: unknown,
102
+ ): IReadRuntimeAssignmentStatsRequest => safe(() => {
103
+ const value = networkRecord(captureNetwork(valueArg), ['session', 'assignment']);
104
+ return { session: snapshotRuntimeSessionBinding(value.session), assignment: referenceOf(value.assignment) };
105
+ });
106
+
107
+ export const snapshotReadRuntimeAssignmentStatsResponse = (
108
+ valueArg: unknown,
109
+ ): IReadRuntimeAssignmentStatsResponse => safe(() => {
110
+ const value = networkRecord(captureNetwork(valueArg), ['status', 'assignment', 'sample']);
111
+ requireNetwork((value.status === 'sampled' && value.sample !== null)
112
+ || (value.status === 'not-running' && value.sample === null));
113
+ return {
114
+ status: value.status as IReadRuntimeAssignmentStatsResponse['status'],
115
+ assignment: referenceOf(value.assignment),
116
+ sample: value.sample === null ? null : sampleOf(value.sample),
117
+ };
118
+ });
119
+
120
+ /**
121
+ * Node-side content binding, not authentication. `trustedSessionArg` is the node's CURRENT
122
+ * session, and the request must have arrived on that session's own connection. `assignmentArg` is
123
+ * the node's current admitted revision of the named assignment: the request must name exactly it,
124
+ * and it must belong to the session's controller, node and runtime namespace.
125
+ */
126
+ export const bindReadRuntimeAssignmentStatsRequest = async (
127
+ valueArg: unknown, assignmentArg: unknown, trustedSessionArg: unknown,
128
+ ): Promise<{ request: IReadRuntimeAssignmentStatsRequest; assignment: IRuntimeAssignment }> => {
129
+ const request = snapshotReadRuntimeAssignmentStatsRequest(valueArg);
130
+ const assignment = safe(() => snapshotRuntimeAssignment(assignmentArg));
131
+ const trustedSession = safe(() => snapshotRuntimeSessionBinding(trustedSessionArg));
132
+ if ((await validateRuntimeAssignment(assignment)).length) fail();
133
+ safe(() => requireNetwork(networkEqual(request.session, trustedSession)
134
+ && networkEqual(assignment.controller, trustedSession.controller)
135
+ && assignment.nodeId === trustedSession.nodeId
136
+ && assignment.runtimeNamespace === trustedSession.runtimeNamespace
137
+ && networkEqual(request.assignment, getRuntimeAssignmentReference(assignment))));
138
+ return { request, assignment };
139
+ };
140
+
141
+ /** Matches a sample to the exact assignment revision the read named. */
142
+ export const bindReadRuntimeAssignmentStatsResponse = (
143
+ valueArg: unknown, requestArg: unknown,
144
+ ): IReadRuntimeAssignmentStatsResponse => safe(() => {
145
+ const response = snapshotReadRuntimeAssignmentStatsResponse(valueArg);
146
+ const request = snapshotReadRuntimeAssignmentStatsRequest(requestArg);
147
+ requireNetwork(networkEqual(response.assignment, request.assignment));
148
+ return response;
149
+ });
@@ -0,0 +1,207 @@
1
+ import type * as plugins from '../plugins.js';
2
+ import type { TSha256Digest } from './immutableimage.js';
3
+ import type { IRuntimeAuthorityReference } from './runtimeassignment.js';
4
+ import type { IRuntimeLocalStorageClaim } from './runtimelocalstorageclaim.js';
5
+ import {
6
+ getRuntimeLocalStorageClaimReference, snapshotRuntimeLocalStorageClaim, validateRuntimeLocalStorageClaim,
7
+ } from './runtimelocalstorageclaim.js';
8
+ import type { IRuntimeSessionBinding } from './runtimesession.js';
9
+ import { snapshotRuntimeSessionBinding } from './runtimesession.js';
10
+ import {
11
+ captureNetwork, networkDigest, networkEqual, networkRecord, networkReference, requireNetwork,
12
+ } from '../private/networkcontract.js';
13
+
14
+ /**
15
+ * The one archive format a volume travels in: a POSIX.1-2001 pax archive of the volume's tree.
16
+ *
17
+ * The first entry is the volume's root, `./`, and every other name is relative below it. An entry is
18
+ * a directory, a regular file, a symbolic link (stored, never followed), a hard link to an earlier
19
+ * entry of the same archive, or a FIFO. Ownership is numeric: `uid` and `gid`, never user or group
20
+ * names, so the tree means the same on every host. Mode bits, including setuid, setgid and sticky,
21
+ * and the modification time travel exactly. A socket carries no bytes and belongs to the process
22
+ * that created it, so it is omitted; a device node, extended attributes and ACLs are not part of
23
+ * the format, and a volume that holds a device node is refused. The archive ends with the format's
24
+ * two zero blocks, so its length is a whole number of 512-byte blocks.
25
+ */
26
+ export type TRuntimeLocalVolumeArchiveFormat = 'pax';
27
+
28
+ /**
29
+ * A local controller reads one volume as an archive. The node refuses while a running container
30
+ * holds the volume: it reads a volume no container holds, or one whose holding container is
31
+ * stopped, which a node never restarts, and no container is granted the volume while it reads.
32
+ */
33
+ export interface IReadRuntimeLocalVolumeArchiveRequest {
34
+ session: IRuntimeSessionBinding;
35
+ /** The reference of the node's current generation of a bound `directory` claim. */
36
+ claim: IRuntimeAuthorityReference;
37
+ }
38
+
39
+ /**
40
+ * The archive arrives on the stream the TypedRequest answers beside this body. `holder` states
41
+ * whether the volume was read unheld or under a stopped holder. The stream carries no integrity:
42
+ * the node learns the archive's length and digest only while it writes it.
43
+ */
44
+ export interface IReadRuntimeLocalVolumeArchiveResponse {
45
+ claim: IRuntimeAuthorityReference;
46
+ format: TRuntimeLocalVolumeArchiveFormat;
47
+ holder: 'none' | 'stopped';
48
+ }
49
+
50
+ /**
51
+ * A local controller writes one archive as the bytes of an `import` claim that waits in
52
+ * `import-required`: the online form of an operator's offline import. The node receives the stream
53
+ * beside this body, whose integrity must be exactly `byteLength` and `digest`, unpacks it into a
54
+ * private staging tree, and publishes that tree as the volume only after the whole archive
55
+ * verified. The tree keeps the ownership the archive carries, as every imported tree does.
56
+ */
57
+ export interface IWriteRuntimeLocalVolumeArchiveRequest {
58
+ session: IRuntimeSessionBinding;
59
+ claim: IRuntimeAuthorityReference;
60
+ format: TRuntimeLocalVolumeArchiveFormat;
61
+ byteLength: number;
62
+ digest: TSha256Digest;
63
+ }
64
+
65
+ /**
66
+ * `receiving` permits sending and confirms nothing more. The stream's acceptance receipt is what
67
+ * confirms that the node published the volume as `ready`; a rejected or aborted stream leaves the
68
+ * claim in `import-required` with nothing published.
69
+ */
70
+ export interface IWriteRuntimeLocalVolumeArchiveResponse {
71
+ claim: IRuntimeAuthorityReference;
72
+ status: 'receiving';
73
+ }
74
+
75
+ /**
76
+ * Volume archives are a local controller's alone (`controller.kind: 'onebox'`), on the connection
77
+ * that holds its session, and a node moves one archive per volume at a time: a read or write of a
78
+ * volume that is already moving one is refused.
79
+ */
80
+ export const runtimeLocalVolumeArchiveContract = Object.freeze({
81
+ error: 'Runtime local volume archive contract rejected.',
82
+ controllerKind: 'onebox',
83
+ format: 'pax',
84
+ entryKinds: ['directory', 'file', 'symlink', 'hardlink', 'fifo'],
85
+ omittedEntryKinds: ['socket'],
86
+ ownership: 'numeric-uid-gid',
87
+ blockBytes: 512,
88
+ /** The shortest archive: the root directory's header and the two closing zero blocks. */
89
+ minimumArchiveBytes: 1536,
90
+ concurrency: 'one-archive-per-volume',
91
+ } as const);
92
+
93
+ const fail = (): never => { throw new Error(runtimeLocalVolumeArchiveContract.error); };
94
+ const safe = <T>(operationArg: () => T): T => { try { return operationArg(); } catch { return fail(); } };
95
+
96
+ const archiveBytesOf = (valueArg: unknown): number => {
97
+ requireNetwork(typeof valueArg === 'number' && Number.isSafeInteger(valueArg)
98
+ && valueArg >= runtimeLocalVolumeArchiveContract.minimumArchiveBytes
99
+ && valueArg % runtimeLocalVolumeArchiveContract.blockBytes === 0);
100
+ return valueArg;
101
+ };
102
+
103
+ export const snapshotReadRuntimeLocalVolumeArchiveRequest = (
104
+ valueArg: unknown,
105
+ ): IReadRuntimeLocalVolumeArchiveRequest => safe(() => {
106
+ const value = networkRecord(captureNetwork(valueArg), ['session', 'claim']);
107
+ return { session: snapshotRuntimeSessionBinding(value.session), claim: networkReference(value.claim) };
108
+ });
109
+
110
+ export const snapshotReadRuntimeLocalVolumeArchiveResponse = (
111
+ valueArg: unknown,
112
+ ): IReadRuntimeLocalVolumeArchiveResponse => safe(() => {
113
+ const value = networkRecord(captureNetwork(valueArg), ['claim', 'format', 'holder']);
114
+ requireNetwork(value.format === 'pax' && (value.holder === 'none' || value.holder === 'stopped'));
115
+ return { claim: networkReference(value.claim), format: 'pax', holder: value.holder };
116
+ });
117
+
118
+ export const snapshotWriteRuntimeLocalVolumeArchiveRequest = (
119
+ valueArg: unknown,
120
+ ): IWriteRuntimeLocalVolumeArchiveRequest => safe(() => {
121
+ const value = networkRecord(captureNetwork(valueArg), ['session', 'claim', 'format', 'byteLength', 'digest']);
122
+ requireNetwork(value.format === 'pax');
123
+ return {
124
+ session: snapshotRuntimeSessionBinding(value.session), claim: networkReference(value.claim), format: 'pax',
125
+ byteLength: archiveBytesOf(value.byteLength), digest: networkDigest(value.digest),
126
+ };
127
+ });
128
+
129
+ export const snapshotWriteRuntimeLocalVolumeArchiveResponse = (
130
+ valueArg: unknown,
131
+ ): IWriteRuntimeLocalVolumeArchiveResponse => safe(() => {
132
+ const value = networkRecord(captureNetwork(valueArg), ['claim', 'status']);
133
+ requireNetwork(value.status === 'receiving');
134
+ return { claim: networkReference(value.claim), status: 'receiving' };
135
+ });
136
+
137
+ /** The exact integrity the write's stream must carry: the request's length and digest. */
138
+ export const getRuntimeLocalVolumeArchiveIntegrity = (
139
+ requestArg: unknown,
140
+ ): plugins.typedrequestInterfaces.IVirtualStreamIntegrity => {
141
+ const request = snapshotWriteRuntimeLocalVolumeArchiveRequest(requestArg);
142
+ return { algorithm: 'sha256', byteLength: request.byteLength, digest: request.digest };
143
+ };
144
+
145
+ /**
146
+ * The claim a request names must be the node's current generation of a bound `directory` claim of
147
+ * the session's controller, cluster, node and runtime namespace, and the session must be a local
148
+ * controller's current one.
149
+ */
150
+ const bindClaim = async (
151
+ requestArg: { session: IRuntimeSessionBinding; claim: IRuntimeAuthorityReference },
152
+ claimArg: unknown, trustedSessionArg: unknown,
153
+ ): Promise<IRuntimeLocalStorageClaim> => {
154
+ const trustedSession = safe(() => snapshotRuntimeSessionBinding(trustedSessionArg));
155
+ const claim = safe(() => snapshotRuntimeLocalStorageClaim(claimArg));
156
+ if ((await validateRuntimeLocalStorageClaim(claim)).length) fail();
157
+ safe(() => requireNetwork(networkEqual(requestArg.session, trustedSession)
158
+ && trustedSession.controller.kind === runtimeLocalVolumeArchiveContract.controllerKind
159
+ && networkEqual(requestArg.claim, getRuntimeLocalStorageClaimReference(claim))
160
+ && claim.lifecycle === 'bound' && claim.source.kind === 'directory'
161
+ && networkEqual(claim.controller, trustedSession.controller)
162
+ && claim.clusterId === trustedSession.clusterId
163
+ && claim.nodeId === trustedSession.nodeId
164
+ && claim.runtimeNamespace === trustedSession.runtimeNamespace));
165
+ return claim;
166
+ };
167
+
168
+ /**
169
+ * Node-side content binding, not authentication. `claimArg` is the node's current generation of the
170
+ * named claim and `trustedSessionArg` its CURRENT session, on whose own connection the request must
171
+ * have arrived. Whether the volume is `ready` and who holds it are the node's ledger checks.
172
+ */
173
+ export const bindReadRuntimeLocalVolumeArchiveRequest = async (
174
+ valueArg: unknown, claimArg: unknown, trustedSessionArg: unknown,
175
+ ): Promise<{ request: IReadRuntimeLocalVolumeArchiveRequest; claim: IRuntimeLocalStorageClaim }> => {
176
+ const request = snapshotReadRuntimeLocalVolumeArchiveRequest(valueArg);
177
+ return { request, claim: await bindClaim(request, claimArg, trustedSessionArg) };
178
+ };
179
+
180
+ /**
181
+ * As the read binding, and the claim must be an `import` claim. That it still waits in
182
+ * `import-required` is the node's ledger check.
183
+ */
184
+ export const bindWriteRuntimeLocalVolumeArchiveRequest = async (
185
+ valueArg: unknown, claimArg: unknown, trustedSessionArg: unknown,
186
+ ): Promise<{ request: IWriteRuntimeLocalVolumeArchiveRequest; claim: IRuntimeLocalStorageClaim }> => {
187
+ const request = snapshotWriteRuntimeLocalVolumeArchiveRequest(valueArg);
188
+ const claim = await bindClaim(request, claimArg, trustedSessionArg);
189
+ if (claim.initialization !== 'import') fail();
190
+ return { request, claim };
191
+ };
192
+
193
+ export const bindReadRuntimeLocalVolumeArchiveResponse = (
194
+ valueArg: unknown, requestArg: unknown,
195
+ ): IReadRuntimeLocalVolumeArchiveResponse => safe(() => {
196
+ const response = snapshotReadRuntimeLocalVolumeArchiveResponse(valueArg);
197
+ requireNetwork(networkEqual(response.claim, snapshotReadRuntimeLocalVolumeArchiveRequest(requestArg).claim));
198
+ return response;
199
+ });
200
+
201
+ export const bindWriteRuntimeLocalVolumeArchiveResponse = (
202
+ valueArg: unknown, requestArg: unknown,
203
+ ): IWriteRuntimeLocalVolumeArchiveResponse => safe(() => {
204
+ const response = snapshotWriteRuntimeLocalVolumeArchiveResponse(valueArg);
205
+ requireNetwork(networkEqual(response.claim, snapshotWriteRuntimeLocalVolumeArchiveRequest(requestArg).claim));
206
+ return response;
207
+ });
package/ts/data/user.ts CHANGED
@@ -13,6 +13,11 @@ export type TServiceDeploymentCapability =
13
13
  | 'deployment:adopt-existing'
14
14
  | 'deployment:status'
15
15
  | 'deployment:create-greenfield'
16
+ /**
17
+ * Reserve a greenfield successor whose image Cloudly imports from an attested legacy service,
18
+ * and have Cloudly import it. Granted on an organization-service-slot grant only.
19
+ */
20
+ | 'deployment:import-legacy'
16
21
  | 'deployment:promote-route'
17
22
  | 'deployment:rollback-image'
18
23
  | 'deployment:rollback-route'
@@ -53,6 +58,7 @@ const serviceDeploymentCapabilities = new Set<TServiceDeploymentCapability>([
53
58
  'deployment:adopt-existing',
54
59
  'deployment:status',
55
60
  'deployment:create-greenfield',
61
+ 'deployment:import-legacy',
56
62
  'deployment:promote-route',
57
63
  'deployment:rollback-image',
58
64
  'deployment:rollback-route',
@@ -106,6 +112,10 @@ export const validateServiceDeploymentGrant = (
106
112
  && capabilities.includes('deployment:adopt-existing')) {
107
113
  errors.push('deployment:adopt-existing requires an existing-service grant');
108
114
  }
115
+ if (capabilities && grant.scope !== 'organization-service-slot'
116
+ && capabilities.includes('deployment:import-legacy')) {
117
+ errors.push('deployment:import-legacy requires an organization-service-slot grant');
118
+ }
109
119
  return errors;
110
120
  };
111
121