@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
@@ -5,6 +5,8 @@ import type {
5
5
  } from './immutableimage.js';
6
6
  import { normalizeImmutableReleaseTag, normalizeSha256Digest } from './immutableimage.js';
7
7
  import type { TServiceDeploymentCapability } from './user.js';
8
+ import type { ILegacyImageImportReference } from './deploymentlegacyimport.js';
9
+ import { validateLegacyImageImportReference } from './deploymentlegacyimport.js';
8
10
  import { validateProtocolOfferOn, type IProtocolOffer } from '../protocol.js';
9
11
  import type {
10
12
  IDeploymentRouteRequest,
@@ -84,6 +86,19 @@ export const deploymentPreflightBlockerCodes = [
84
86
  'RELAY_NAME_RESERVED',
85
87
  'ROUTE_VERIFICATION_FAILED',
86
88
  'TLS_VERIFICATION_FAILED',
89
+ /**
90
+ * A legacy import names no attestation Cloudly holds for that legacy service in the requested
91
+ * organization. Evidence is captured only while the legacy runtime runs, so there is no later
92
+ * substitute for a missing one.
93
+ */
94
+ 'LEGACY_IMPORT_ATTESTATION_MISSING',
95
+ /**
96
+ * The attested legacy image is no longer what the request or the registry states: the asserted
97
+ * root digest differs, the legacy tag moved, or a manifest the attestation names is gone.
98
+ */
99
+ 'LEGACY_IMPORT_SOURCE_MISMATCH',
100
+ /** The declared target platforms are not exactly the platforms the attested image carries. */
101
+ 'LEGACY_IMPORT_PLATFORM_MISMATCH',
87
102
  ] as const;
88
103
 
89
104
  export type TDeploymentPreflightBlockerCode = typeof deploymentPreflightBlockerCodes[number];
@@ -93,7 +108,9 @@ export interface IDeploymentPreflightBlocker {
93
108
  message: string;
94
109
  }
95
110
 
96
- export type TDeploymentPreflightWarningCode = 'RECOVERY_REDEPLOY_OVER_FAILED_RUNTIME';
111
+ export type TDeploymentPreflightWarningCode =
112
+ | 'RECOVERY_REDEPLOY_OVER_FAILED_RUNTIME'
113
+ | 'PLATFORM_BINDING_READINESS_DEFERRED';
97
114
 
98
115
  /**
99
116
  * Auditable non-blocking preflight finding. Warnings never veto a decision.
@@ -102,7 +119,16 @@ export type TDeploymentPreflightWarningCode = 'RECOVERY_REDEPLOY_OVER_FAILED_RUN
102
119
  * RUNTIME_ATTESTATION_MISMATCH blockers because the current runtime provides
103
120
  * no consistent attested digest and the service's most recent deployment
104
121
  * operation ended 'failed' or 'cleaned' (a recovery redeploy over a known-bad
105
- * runtime). No other blocker is ever relaxed.
122
+ * runtime).
123
+ * `PLATFORM_BINDING_READINESS_DEFERRED` documents that the promote preflight
124
+ * of a held greenfield service that was never promoted accepted a Corestore
125
+ * `database` or `objectstorage` binding that is enabled but not yet `ready`.
126
+ * A held service receives no credential material, so its binding can become
127
+ * ready only after promotion releases the hold; for a binding that adopts a
128
+ * legacy resource the Corestore inventory proved the resource authorized for
129
+ * exactly that binding. The route preflight still requires the binding
130
+ * `ready`, so no traffic reaches the service before its binding is.
131
+ * No other blocker is ever relaxed.
106
132
  */
107
133
  export interface IDeploymentPreflightWarning {
108
134
  code: TDeploymentPreflightWarningCode;
@@ -193,21 +219,39 @@ export interface IDeploymentReleaseEvidence {
193
219
  attestations: IDeploymentReleaseEvidenceReference[];
194
220
  }
195
221
 
196
- export interface IDeploymentPreflightRequestData {
222
+ /**
223
+ * A preflight states where the deployment's image comes from, exactly as its reservation will:
224
+ * a clean source revision with its release evidence, or — for a greenfield successor only — a
225
+ * legacy import naming an attested legacy image, which has no revision, worktree or build
226
+ * evidence to state.
227
+ */
228
+ export type IDeploymentPreflightRequestData = IDeploymentPreflightRequestDataBase & (
229
+ | {
230
+ sourceRevision: string;
231
+ sourceDirty: boolean;
232
+ releaseEvidence?: IDeploymentReleaseEvidence;
233
+ legacyImport?: never;
234
+ }
235
+ | {
236
+ legacyImport: ILegacyImageImportReference;
237
+ sourceRevision?: never;
238
+ sourceDirty?: never;
239
+ releaseEvidence?: never;
240
+ }
241
+ );
242
+
243
+ interface IDeploymentPreflightRequestDataBase {
197
244
  requestId: string;
198
245
  serviceId: string;
199
246
  organizationId?: string;
200
247
  mode?: TDeploymentOperationMode;
201
248
  phase?: TDeploymentOperationPhase;
202
249
  operationId?: string;
203
- sourceRevision: string;
204
- sourceDirty: boolean;
205
250
  version: string;
206
251
  intentDigest: string;
207
252
  proposedConfiguration: IDeploymentPreflightProposedConfiguration;
208
253
  ociIndexDigest?: string;
209
254
  releaseTag?: string;
210
- releaseEvidence?: IDeploymentReleaseEvidence;
211
255
  routes?: IDeploymentRouteRequest[];
212
256
  /** Legacy v14 discriminator. Equivalent to mode: 'greenfield'. */
213
257
  greenfield?: true;
@@ -249,13 +293,15 @@ export interface IDeploymentPreflightReport {
249
293
  mode?: TDeploymentOperationMode;
250
294
  phase?: TDeploymentOperationPhase;
251
295
  operation?: IServiceDeploymentOperation;
296
+ /** The request's image origin as judged: a source revision, or a legacy import. */
252
297
  source: {
253
- revision: string;
254
- dirty: boolean;
255
298
  version: string;
256
299
  intentDigest: TSha256Digest;
257
300
  configurationDigest: TSha256Digest;
258
- };
301
+ } & (
302
+ | { revision: string; dirty: boolean; legacyImport?: never }
303
+ | { legacyImport: ILegacyImageImportReference; revision?: never; dirty?: never }
304
+ );
259
305
  currentImage: {
260
306
  tag?: string;
261
307
  digest?: TSha256Digest;
@@ -339,16 +385,35 @@ export const validateDeploymentPreflightRequest = (
339
385
  if (typeof requestArg.serviceId !== 'string' || !requestArg.serviceId.trim() || requestArg.serviceId.length > 200) {
340
386
  blockers.push({ code: 'INVALID_REQUEST', message: 'A bounded service identifier is required.' });
341
387
  }
342
- if (typeof requestArg.sourceRevision !== 'string' || !fullGitRevisionRegex.test(requestArg.sourceRevision)) {
343
- blockers.push({ code: 'SOURCE_REVISION_INVALID', message: 'Source revision must be a full lower-case Git commit SHA.' });
344
- if (typeof requestArg.sourceRevision !== 'string') {
345
- blockers.push({ code: 'INVALID_REQUEST', message: 'Source revision is malformed.' });
388
+ const legacyImport = requestArg.legacyImport !== undefined;
389
+ if (legacyImport) {
390
+ const referenceErrors = validateLegacyImageImportReference(requestArg.legacyImport);
391
+ if (referenceErrors.length > 0) {
392
+ blockers.push({
393
+ code: 'INVALID_REQUEST',
394
+ message: `The legacy import reference is malformed: ${referenceErrors.join('; ')}.`,
395
+ });
396
+ }
397
+ if (requestArg.sourceRevision !== undefined
398
+ || requestArg.sourceDirty !== undefined
399
+ || requestArg.releaseEvidence !== undefined) {
400
+ blockers.push({
401
+ code: 'INVALID_REQUEST',
402
+ message: 'A legacy import states no source revision, worktree state or release evidence.',
403
+ });
404
+ }
405
+ } else {
406
+ if (typeof requestArg.sourceRevision !== 'string' || !fullGitRevisionRegex.test(requestArg.sourceRevision)) {
407
+ blockers.push({ code: 'SOURCE_REVISION_INVALID', message: 'Source revision must be a full lower-case Git commit SHA.' });
408
+ if (typeof requestArg.sourceRevision !== 'string') {
409
+ blockers.push({ code: 'INVALID_REQUEST', message: 'Source revision is malformed.' });
410
+ }
411
+ }
412
+ if (typeof requestArg.sourceDirty !== 'boolean') {
413
+ blockers.push({ code: 'INVALID_REQUEST', message: 'Source worktree state must be reported.' });
414
+ } else if (requestArg.sourceDirty) {
415
+ blockers.push({ code: 'SOURCE_WORKTREE_DIRTY', message: 'The source worktree contains uncommitted changes.' });
346
416
  }
347
- }
348
- if (typeof requestArg.sourceDirty !== 'boolean') {
349
- blockers.push({ code: 'INVALID_REQUEST', message: 'Source worktree state must be reported.' });
350
- } else if (requestArg.sourceDirty) {
351
- blockers.push({ code: 'SOURCE_WORKTREE_DIRTY', message: 'The source worktree contains uncommitted changes.' });
352
417
  }
353
418
  if (typeof requestArg.version !== 'string'
354
419
  || requestArg.version.length > 100
@@ -397,6 +462,9 @@ export const validateDeploymentPreflightRequest = (
397
462
  && (requestArg.greenfield !== true || mode !== 'greenfield')) {
398
463
  blockers.push({ code: 'INVALID_REQUEST', message: 'Legacy greenfield mode is contradictory.' });
399
464
  }
465
+ if (legacyImport && mode !== 'greenfield') {
466
+ blockers.push({ code: 'INVALID_REQUEST', message: 'A legacy import reserves a greenfield successor.' });
467
+ }
400
468
  if (requestArg.phase !== undefined
401
469
  && requestArg.phase !== 'reserve'
402
470
  && requestArg.phase !== 'promote'
@@ -173,8 +173,7 @@ export interface IImageRelease {
173
173
  };
174
174
  }
175
175
 
176
- export interface IImageReleaseTrustedEvidence {
177
- source: 'cloudly-registry';
176
+ interface IImageReleaseTrustedEvidenceBase {
178
177
  /** Deterministic ID for an authenticated registry acceptance event/retry. */
179
178
  evidenceId: string;
180
179
  operationId: string;
@@ -187,6 +186,30 @@ export interface IImageReleaseTrustedEvidence {
187
186
  recordedAt: number;
188
187
  }
189
188
 
189
+ /** The deployment machine user's own authenticated registry push to its operation's tag. */
190
+ export interface IImageReleaseRegistryPushEvidence extends IImageReleaseTrustedEvidenceBase {
191
+ source: 'cloudly-registry';
192
+ }
193
+
194
+ /**
195
+ * Cloudly's own import of an attested legacy image into a legacy-import operation's tag.
196
+ *
197
+ * Nothing was built or pushed: the source fields name the legacy registry root the attestation
198
+ * proved ran, and `registryRootDigest` is that root or the index Cloudly wrapped around it.
199
+ */
200
+ export interface IImageReleaseLegacyImportEvidence extends IImageReleaseTrustedEvidenceBase {
201
+ source: 'cloudly-legacy-import';
202
+ legacyServiceId: string;
203
+ attestationId: string;
204
+ sourceRepository: string;
205
+ sourceTag: string;
206
+ sourceRootDigest: TSha256Digest;
207
+ }
208
+
209
+ export type IImageReleaseTrustedEvidence =
210
+ | IImageReleaseRegistryPushEvidence
211
+ | IImageReleaseLegacyImportEvidence;
212
+
190
213
  export const selectTrustedImageReleaseEvidence = (argsArg: {
191
214
  release: IImageRelease;
192
215
  operationId: string;
@@ -197,7 +220,14 @@ export const selectTrustedImageReleaseEvidence = (argsArg: {
197
220
  repository: string;
198
221
  tag: string;
199
222
  digest: TSha256Digest;
223
+ /**
224
+ * The one evidence source the operation's image may have come from: a legacy-import operation
225
+ * accepts only Cloudly's import, every other operation only its actor's registry push. Omitted,
226
+ * it is the registry push, the only source an operation without a legacy import has.
227
+ */
228
+ evidenceSource?: IImageReleaseTrustedEvidence['source'];
200
229
  }): { evidence?: IImageReleaseTrustedEvidence; errors: string[] } => {
230
+ const evidenceSource = argsArg.evidenceSource ?? 'cloudly-registry';
201
231
  const evidenceFingerprint = (evidenceArg: IImageReleaseTrustedEvidence) => [
202
232
  evidenceArg.source,
203
233
  evidenceArg.evidenceId,
@@ -209,6 +239,15 @@ export const selectTrustedImageReleaseEvidence = (argsArg: {
209
239
  evidenceArg.registryRootDigest,
210
240
  evidenceArg.manifestMediaType,
211
241
  evidenceArg.recordedAt,
242
+ ...(evidenceArg.source === 'cloudly-legacy-import'
243
+ ? [
244
+ evidenceArg.legacyServiceId,
245
+ evidenceArg.attestationId,
246
+ evidenceArg.sourceRepository,
247
+ evidenceArg.sourceTag,
248
+ evidenceArg.sourceRootDigest,
249
+ ]
250
+ : []),
212
251
  ].join('\u0000');
213
252
  const uniqueEvidence = new Map<string, IImageReleaseTrustedEvidence>();
214
253
  const errors: string[] = [];
@@ -221,7 +260,7 @@ export const selectTrustedImageReleaseEvidence = (argsArg: {
221
260
  }
222
261
  }
223
262
  const evidenceMatches = [...uniqueEvidence.values()].filter((evidenceArg) => (
224
- evidenceArg.source === 'cloudly-registry'
263
+ evidenceArg.source === evidenceSource
225
264
  && evidenceArg.operationId === argsArg.operationId
226
265
  && evidenceArg.actorUserId === argsArg.actorUserId
227
266
  && evidenceArg.registryHost === argsArg.registryHost
package/ts/data/index.ts CHANGED
@@ -12,6 +12,7 @@ export * from './coremail.js';
12
12
  export * from './coremail.runtime.js';
13
13
  export * from './deployment.js';
14
14
  export * from './deploymentadoption.js';
15
+ export * from './deploymentlegacyimport.js';
15
16
  export * from './deploymentmachineuser.js';
16
17
  export * from './deploymentoperation.js';
17
18
  export * from './deploymentpreflight.js';
@@ -52,9 +53,13 @@ export * from './privatenetwork.js';
52
53
  export * from './registry.js';
53
54
  export * from './releaseversion.js';
54
55
  export * from './runtimeassignment.js';
56
+ export * from './runtimeassignmentexec.js';
57
+ export * from './runtimeassignmentlogs.js';
58
+ export * from './runtimeassignmentstats.js';
55
59
  export * from './runtimeconfig.js';
56
60
  export * from './runtimestorageclaim.js';
57
61
  export * from './runtimelocalstorageclaim.js';
62
+ export * from './runtimelocalvolumearchive.js';
58
63
  export * from './runtimesession.js';
59
64
  export * from './runtimeterminalreceipt.js';
60
65
  export * from './secret.js';
@@ -4,13 +4,20 @@ import { captureNetwork, networkCounter, networkNodeId, networkRecord, networkSa
4
4
  type TEgressOwner = IRuntimeNetworkProtectedAuthority['egressAuthorities'][number];
5
5
 
6
6
  /**
7
- * One unmet condition of a node's runtime network readiness, in the order the node producer meets
8
- * them: the node joins the protected authority's egress, every egress owner reports current
9
- * protection, a handoff is reserved, the router selection carries it, the node's acknowledged
10
- * projection carries it, and the node is a managed VPN member. `protection-receipts-pending` names
11
- * the egress owners whose current receipts are still missing.
7
+ * One unmet condition of a node's runtime network readiness. The first is liveness: the node is
8
+ * observed running. The rest follow the order the node producer meets them: the node joins the
9
+ * protected authority's egress, every egress owner reports current protection, a handoff is
10
+ * reserved, the router selection carries it, the node's acknowledged projection carries it, and the
11
+ * node is a managed VPN member. `protection-receipts-pending` names the egress owners whose current
12
+ * receipts are still missing.
12
13
  */
13
14
  export type TRuntimeNetworkNodeReadinessGap =
15
+ /**
16
+ * No accepted report newer than `clusterNodeObservationFreshnessMs` shows the node running.
17
+ * `lastObservedAt` is when Cloudly accepted the node's last report, or `null` when the node's
18
+ * status is not `online`, so no report shows it running.
19
+ */
20
+ | { condition: 'node-observation-stale'; lastObservedAt: number | null }
14
21
  | { condition: 'egress-pending' | 'handoff-pending' | 'selection-pending' | 'projection-pending' | 'vpn-member-pending' }
15
22
  | { condition: 'protection-receipts-pending'; owners: [TEgressOwner, ...TEgressOwner[]] };
16
23
 
@@ -0,0 +1,271 @@
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
+ /** A terminal's size in character cells. */
10
+ export interface IRuntimeTerminalSize {
11
+ columns: number;
12
+ rows: number;
13
+ }
14
+
15
+ /**
16
+ * A controller's request to run one command inside one running attempt's container.
17
+ *
18
+ * `command` is the argument vector the runtime executes directly: no shell parses it unless its
19
+ * first element names one. With `tty` the process runs on a terminal of `terminalSize`, which is
20
+ * stated exactly then; its output arrives on one stream. Without it stdout and stderr arrive on
21
+ * separate streams. Standard input is the optional stream the TypedRequest carries beside this
22
+ * body; its end is the process's end of input.
23
+ */
24
+ export interface IOpenRuntimeAssignmentExecRequest {
25
+ session: IRuntimeSessionBinding;
26
+ /** The exact current revision of a `run` assignment on this node. */
27
+ assignment: IRuntimeAssignmentReference;
28
+ command: string[];
29
+ tty: boolean;
30
+ terminalSize: IRuntimeTerminalSize | null;
31
+ }
32
+
33
+ /**
34
+ * The node's answer once the process runs. `execId` names the session in later requests;
35
+ * `idleTimeoutMs` is how long the session may carry no byte in either direction before the node
36
+ * ends it.
37
+ */
38
+ export interface IOpenRuntimeAssignmentExecResponse {
39
+ execId: string;
40
+ assignment: IRuntimeAssignmentReference;
41
+ tty: boolean;
42
+ idleTimeoutMs: number;
43
+ }
44
+
45
+ /** A new terminal size for a `tty` session. */
46
+ export interface IResizeRuntimeAssignmentExecRequest {
47
+ session: IRuntimeSessionBinding;
48
+ execId: string;
49
+ terminalSize: IRuntimeTerminalSize;
50
+ }
51
+
52
+ export interface IResizeRuntimeAssignmentExecResponse {
53
+ execId: string;
54
+ }
55
+
56
+ /** Ends a session and releases its slot, whether or not its process has exited. */
57
+ export interface ICloseRuntimeAssignmentExecRequest {
58
+ session: IRuntimeSessionBinding;
59
+ execId: string;
60
+ }
61
+
62
+ /**
63
+ * Why the node ended a process before it exited: `closed` by this request, `idle-timeout` after
64
+ * `idleTimeoutMs` without a byte, `container-stopped` because its container stopped.
65
+ */
66
+ export type TRuntimeAssignmentExecTermination = 'closed' | 'idle-timeout' | 'container-stopped';
67
+
68
+ /**
69
+ * How the session ended. `exited` carries the process's `exitCode`, 128 plus the signal number
70
+ * for a process a signal ended; `terminated` states why the node ended it and carries no code.
71
+ */
72
+ export interface ICloseRuntimeAssignmentExecResponse {
73
+ execId: string;
74
+ outcome: 'exited' | 'terminated';
75
+ exitCode: number | null;
76
+ termination: TRuntimeAssignmentExecTermination | null;
77
+ }
78
+
79
+ /**
80
+ * Exec runs only for a controller on the node's own host (`controller.kind: 'onebox'`, which only a
81
+ * local controller binding carries), and only on the connection that holds its session. A session
82
+ * lives until it is closed, or until the node ends it: after `idleTimeoutMs` without a byte, when
83
+ * its container stops, or when the controller session it was opened under ends, in which case its
84
+ * `execId` is gone with it. An ended session keeps its slot and its outcome until it is closed.
85
+ */
86
+ export const runtimeAssignmentExecContract = Object.freeze({
87
+ error: 'Runtime assignment exec contract rejected.',
88
+ controllerKind: 'onebox',
89
+ /** Open sessions per node, ended ones not yet closed included; one more is refused. */
90
+ maximumSessions: 4,
91
+ idleTimeoutMs: 900_000,
92
+ maximumCommandArguments: 256,
93
+ /** The UTF-8 bytes of every argument together. */
94
+ maximumCommandBytes: 65_536,
95
+ /** The largest terminal dimension, the width of the runtime's own size field. */
96
+ maximumTerminalDimension: 65_535,
97
+ maximumExitCode: 255,
98
+ } as const);
99
+
100
+ const fail = (): never => { throw new Error(runtimeAssignmentExecContract.error); };
101
+ const safe = <T>(operationArg: () => T): T => { try { return operationArg(); } catch { return fail(); } };
102
+ const encoder = new TextEncoder();
103
+ const terminations = ['closed', 'idle-timeout', 'container-stopped'] as const;
104
+
105
+ const referenceOf = (valueArg: unknown): IRuntimeAssignmentReference => {
106
+ const value = networkRecord(valueArg, ['assignmentId', 'generation', 'digest']);
107
+ return {
108
+ assignmentId: networkId(value.assignmentId), generation: networkCounter(value.generation),
109
+ digest: networkDigest(value.digest),
110
+ };
111
+ };
112
+ const terminalSizeOf = (valueArg: unknown): IRuntimeTerminalSize => {
113
+ const value = networkRecord(valueArg, ['columns', 'rows']);
114
+ return {
115
+ columns: networkCounter(value.columns, runtimeAssignmentExecContract.maximumTerminalDimension),
116
+ rows: networkCounter(value.rows, runtimeAssignmentExecContract.maximumTerminalDimension),
117
+ };
118
+ };
119
+ const commandOf = (valueArg: unknown): string[] => {
120
+ requireNetwork(Array.isArray(valueArg) && valueArg.length > 0
121
+ && valueArg.length <= runtimeAssignmentExecContract.maximumCommandArguments);
122
+ let bytes = 0;
123
+ const command = (valueArg as unknown[]).map((argumentArg) => {
124
+ requireNetwork(typeof argumentArg === 'string' && !argumentArg.includes('\0'));
125
+ bytes += encoder.encode(argumentArg).byteLength;
126
+ return argumentArg;
127
+ });
128
+ requireNetwork(command[0].length > 0 && bytes <= runtimeAssignmentExecContract.maximumCommandBytes);
129
+ return command;
130
+ };
131
+
132
+ export const snapshotOpenRuntimeAssignmentExecRequest = (
133
+ valueArg: unknown,
134
+ ): IOpenRuntimeAssignmentExecRequest => safe(() => {
135
+ const value = networkRecord(captureNetwork(valueArg), ['session', 'assignment', 'command', 'tty', 'terminalSize']);
136
+ requireNetwork(typeof value.tty === 'boolean' && (value.tty === (value.terminalSize !== null)));
137
+ return {
138
+ session: snapshotRuntimeSessionBinding(value.session), assignment: referenceOf(value.assignment),
139
+ command: commandOf(value.command), tty: value.tty,
140
+ terminalSize: value.terminalSize === null ? null : terminalSizeOf(value.terminalSize),
141
+ };
142
+ });
143
+
144
+ export const snapshotOpenRuntimeAssignmentExecResponse = (
145
+ valueArg: unknown,
146
+ ): IOpenRuntimeAssignmentExecResponse => safe(() => {
147
+ const value = networkRecord(captureNetwork(valueArg), ['execId', 'assignment', 'tty', 'idleTimeoutMs']);
148
+ requireNetwork(typeof value.tty === 'boolean'
149
+ && value.idleTimeoutMs === runtimeAssignmentExecContract.idleTimeoutMs);
150
+ return {
151
+ execId: networkId(value.execId), assignment: referenceOf(value.assignment), tty: value.tty,
152
+ idleTimeoutMs: runtimeAssignmentExecContract.idleTimeoutMs,
153
+ };
154
+ });
155
+
156
+ export const snapshotResizeRuntimeAssignmentExecRequest = (
157
+ valueArg: unknown,
158
+ ): IResizeRuntimeAssignmentExecRequest => safe(() => {
159
+ const value = networkRecord(captureNetwork(valueArg), ['session', 'execId', 'terminalSize']);
160
+ return {
161
+ session: snapshotRuntimeSessionBinding(value.session), execId: networkId(value.execId),
162
+ terminalSize: terminalSizeOf(value.terminalSize),
163
+ };
164
+ });
165
+
166
+ export const snapshotResizeRuntimeAssignmentExecResponse = (
167
+ valueArg: unknown,
168
+ ): IResizeRuntimeAssignmentExecResponse => safe(() => {
169
+ const value = networkRecord(captureNetwork(valueArg), ['execId']);
170
+ return { execId: networkId(value.execId) };
171
+ });
172
+
173
+ export const snapshotCloseRuntimeAssignmentExecRequest = (
174
+ valueArg: unknown,
175
+ ): ICloseRuntimeAssignmentExecRequest => safe(() => {
176
+ const value = networkRecord(captureNetwork(valueArg), ['session', 'execId']);
177
+ return { session: snapshotRuntimeSessionBinding(value.session), execId: networkId(value.execId) };
178
+ });
179
+
180
+ export const snapshotCloseRuntimeAssignmentExecResponse = (
181
+ valueArg: unknown,
182
+ ): ICloseRuntimeAssignmentExecResponse => safe(() => {
183
+ const value = networkRecord(captureNetwork(valueArg), ['execId', 'outcome', 'exitCode', 'termination']);
184
+ const execId = networkId(value.execId);
185
+ if (value.outcome === 'exited') {
186
+ requireNetwork(typeof value.exitCode === 'number' && Number.isSafeInteger(value.exitCode)
187
+ && value.exitCode >= 0 && !Object.is(value.exitCode, -0)
188
+ && value.exitCode <= runtimeAssignmentExecContract.maximumExitCode && value.termination === null);
189
+ return { execId, outcome: 'exited', exitCode: value.exitCode, termination: null };
190
+ }
191
+ requireNetwork(value.outcome === 'terminated' && value.exitCode === null
192
+ && (terminations as readonly unknown[]).includes(value.termination));
193
+ return {
194
+ execId, outcome: 'terminated', exitCode: null,
195
+ termination: value.termination as TRuntimeAssignmentExecTermination,
196
+ };
197
+ });
198
+
199
+ /** Exec is a local controller's alone; `trustedSessionArg` decides, never a request field. */
200
+ const requireLocalSession = (requestSessionArg: IRuntimeSessionBinding, trustedSessionArg: unknown): void => {
201
+ const trustedSession = safe(() => snapshotRuntimeSessionBinding(trustedSessionArg));
202
+ safe(() => requireNetwork(networkEqual(requestSessionArg, trustedSession)
203
+ && trustedSession.controller.kind === runtimeAssignmentExecContract.controllerKind));
204
+ };
205
+
206
+ /**
207
+ * Node-side content binding, not authentication. `trustedSessionArg` is the node's CURRENT
208
+ * session, the request must have arrived on that session's own connection, and that session must
209
+ * be a local controller's. `assignmentArg` is the node's current admitted revision of the named
210
+ * assignment: the request must name exactly it, it must be a `run` assignment, and it must belong
211
+ * to the session's controller, node and runtime namespace. Whether its container runs is the
212
+ * node's own check.
213
+ */
214
+ export const bindOpenRuntimeAssignmentExecRequest = async (
215
+ valueArg: unknown, assignmentArg: unknown, trustedSessionArg: unknown,
216
+ ): Promise<{ request: IOpenRuntimeAssignmentExecRequest; assignment: IRuntimeAssignment }> => {
217
+ const request = snapshotOpenRuntimeAssignmentExecRequest(valueArg);
218
+ requireLocalSession(request.session, trustedSessionArg);
219
+ const assignment = safe(() => snapshotRuntimeAssignment(assignmentArg));
220
+ if ((await validateRuntimeAssignment(assignment)).length) fail();
221
+ safe(() => requireNetwork(assignment.disposition === 'run'
222
+ && networkEqual(assignment.controller, request.session.controller)
223
+ && assignment.nodeId === request.session.nodeId
224
+ && assignment.runtimeNamespace === request.session.runtimeNamespace
225
+ && networkEqual(request.assignment, getRuntimeAssignmentReference(assignment))));
226
+ return { request, assignment };
227
+ };
228
+
229
+ /** Matches an opened session to the request: the same assignment revision and terminal mode. */
230
+ export const bindOpenRuntimeAssignmentExecResponse = (
231
+ valueArg: unknown, requestArg: unknown,
232
+ ): IOpenRuntimeAssignmentExecResponse => safe(() => {
233
+ const response = snapshotOpenRuntimeAssignmentExecResponse(valueArg);
234
+ const request = snapshotOpenRuntimeAssignmentExecRequest(requestArg);
235
+ requireNetwork(networkEqual(response.assignment, request.assignment) && response.tty === request.tty);
236
+ return response;
237
+ });
238
+
239
+ /** Node-side content binding of a resize; which sessions exist and are `tty` is the node's check. */
240
+ export const bindResizeRuntimeAssignmentExecRequest = (
241
+ valueArg: unknown, trustedSessionArg: unknown,
242
+ ): IResizeRuntimeAssignmentExecRequest => {
243
+ const request = snapshotResizeRuntimeAssignmentExecRequest(valueArg);
244
+ requireLocalSession(request.session, trustedSessionArg);
245
+ return request;
246
+ };
247
+
248
+ export const bindResizeRuntimeAssignmentExecResponse = (
249
+ valueArg: unknown, requestArg: unknown,
250
+ ): IResizeRuntimeAssignmentExecResponse => safe(() => {
251
+ const response = snapshotResizeRuntimeAssignmentExecResponse(valueArg);
252
+ requireNetwork(response.execId === snapshotResizeRuntimeAssignmentExecRequest(requestArg).execId);
253
+ return response;
254
+ });
255
+
256
+ /** Node-side content binding of a close; which sessions exist is the node's check. */
257
+ export const bindCloseRuntimeAssignmentExecRequest = (
258
+ valueArg: unknown, trustedSessionArg: unknown,
259
+ ): ICloseRuntimeAssignmentExecRequest => {
260
+ const request = snapshotCloseRuntimeAssignmentExecRequest(valueArg);
261
+ requireLocalSession(request.session, trustedSessionArg);
262
+ return request;
263
+ };
264
+
265
+ export const bindCloseRuntimeAssignmentExecResponse = (
266
+ valueArg: unknown, requestArg: unknown,
267
+ ): ICloseRuntimeAssignmentExecResponse => safe(() => {
268
+ const response = snapshotCloseRuntimeAssignmentExecResponse(valueArg);
269
+ requireNetwork(response.execId === snapshotCloseRuntimeAssignmentExecRequest(requestArg).execId);
270
+ return response;
271
+ });