@serve.zone/interfaces 32.55.1 → 32.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +50 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/appstore/catalog.d.ts +1 -1
- package/dist_ts/appstore/catalog.js +2 -1
- package/dist_ts/appstore/types.d.ts +36 -0
- package/dist_ts/appstore/types.js +11 -1
- package/dist_ts/appstore/versionconfig.d.ts +11 -3
- package/dist_ts/appstore/versionconfig.js +146 -7
- package/dist_ts/data/index.d.ts +1 -0
- package/dist_ts/data/index.js +2 -1
- package/dist_ts/data/runtimesessionrefusal.d.ts +97 -0
- package/dist_ts/data/runtimesessionrefusal.js +127 -0
- package/package.json +1 -1
- package/readme.md +75 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/appstore/catalog.ts +1 -0
- package/ts/appstore/types.ts +38 -0
- package/ts/appstore/versionconfig.ts +163 -5
- package/ts/data/index.ts +1 -0
- package/ts/data/runtimesessionrefusal.ts +153 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { readExactDataRecord } from '../private/dataproperties.js';
|
|
2
|
+
/**
|
|
3
|
+
* The refusal a controller states when it does not take a node's runtime session request.
|
|
4
|
+
*
|
|
5
|
+
* It travels as `TypedResponseError.errorData` on `registerPalletRuntimeSession` and on every
|
|
6
|
+
* session-scoped read and report a node sends its controller. `code` is a frozen wire value.
|
|
7
|
+
* `momentaryReasons` are the controller's own momentary state, not a judgement of the node: a
|
|
8
|
+
* registering node waits and offers the same registration again, keeping everything it runs.
|
|
9
|
+
* Every other reason, and an error that carries no such payload, is a verdict — on a registration
|
|
10
|
+
* it ends the node's session. Momentary against verdict governs only whether a node defers a
|
|
11
|
+
* registration: a node retries its session reads and reports regardless of the reason, so the
|
|
12
|
+
* reasons a report is refused with (`state-conflict`, `*-write-conflict`, a capacity limit such as
|
|
13
|
+
* `log-intake-saturated`) are unchanged by this contract.
|
|
14
|
+
*
|
|
15
|
+
* Who states which reason today, on a registration (Cloudly ≥33.19.1, `isBudgetRefusal`):
|
|
16
|
+
* - Cloudly states `controller-deadline` when the request ran out of
|
|
17
|
+
* `runtimeSessionRequestBounds.controllerBudgetMs`, and `controller-unavailable` when it is
|
|
18
|
+
* stopping. Every other failure, a transient one of its own included, is answered without a
|
|
19
|
+
* payload and so reads as a verdict.
|
|
20
|
+
* - No relay states a refusal of its own yet.
|
|
21
|
+
*
|
|
22
|
+
* The rule producers adopt:
|
|
23
|
+
* - Cloudly states `controller-unavailable` on a registration for every transient failure of its
|
|
24
|
+
* own: its database connection lost or unavailable, a registration write conflict, a race with a
|
|
25
|
+
* credential being applied.
|
|
26
|
+
* - A relay states only `controller-unavailable`, and only when it cannot carry the request or its
|
|
27
|
+
* answer: no Cloudly link, or its own bound (`runtimeSessionRequestBounds`) ran out. Every
|
|
28
|
+
* answer Cloudly gave it forwards untouched; it never states a verdict or `controller-deadline`.
|
|
29
|
+
* - A verdict names the check that refused: a revoked or inactive credential, a deleted, retiring
|
|
30
|
+
* or foreign node, an invalid body. It never carries a momentary reason.
|
|
31
|
+
*
|
|
32
|
+
* The payload is exactly `{ code, reason }`. Pallet 36.3.14 reads the two members and tolerates
|
|
33
|
+
* others, but every reader built on `readRuntimeSessionRefusal` refuses a payload with any other
|
|
34
|
+
* member, and a refused payload reads as no refusal — a momentary refusal would turn into a
|
|
35
|
+
* verdict. A producer therefore never adds a member without gating it on the peer's spoken
|
|
36
|
+
* interfaces version.
|
|
37
|
+
*/
|
|
38
|
+
export const runtimeSessionRefusalContract = Object.freeze({
|
|
39
|
+
code: 'runtime-session-report-refused',
|
|
40
|
+
momentaryReasons: Object.freeze(['controller-deadline', 'controller-unavailable']),
|
|
41
|
+
/**
|
|
42
|
+
* A reason is one or more words of lowercase ASCII letters joined by single hyphens, at most
|
|
43
|
+
* this many characters. It names a check, never a value: no digit, case, dot, colon or other
|
|
44
|
+
* separator can carry an identifier, address, digest, count or credential across.
|
|
45
|
+
*/
|
|
46
|
+
maximumReasonLength: 64,
|
|
47
|
+
});
|
|
48
|
+
const isRefusalReason = (valueArg) => (typeof valueArg === 'string'
|
|
49
|
+
&& valueArg.length <= runtimeSessionRefusalContract.maximumReasonLength
|
|
50
|
+
&& /^[a-z]+(?:-[a-z]+)*$/.test(valueArg));
|
|
51
|
+
const reasonRule = 'must be lowercase letter words joined by single hyphens, at most '
|
|
52
|
+
+ `${runtimeSessionRefusalContract.maximumReasonLength} characters`;
|
|
53
|
+
/** Validates one runtime session refusal and names every reason it is refused. */
|
|
54
|
+
export const validateRuntimeSessionRefusal = (valueArg, pathArg = 'runtime session refusal') => {
|
|
55
|
+
const refusal = readExactDataRecord(valueArg, ['code', 'reason']);
|
|
56
|
+
if (refusal === null) {
|
|
57
|
+
return [`${pathArg} must use its exact schema`];
|
|
58
|
+
}
|
|
59
|
+
const errors = [];
|
|
60
|
+
if (refusal.code !== runtimeSessionRefusalContract.code) {
|
|
61
|
+
errors.push(`${pathArg} code must be ${runtimeSessionRefusalContract.code}`);
|
|
62
|
+
}
|
|
63
|
+
if (!isRefusalReason(refusal.reason)) {
|
|
64
|
+
errors.push(`${pathArg} reason ${reasonRule}`);
|
|
65
|
+
}
|
|
66
|
+
return errors;
|
|
67
|
+
};
|
|
68
|
+
/** The payload a controller or relay answers a refused request with; throws on a non-word reason. */
|
|
69
|
+
export const buildRuntimeSessionRefusal = (reasonArg) => {
|
|
70
|
+
if (!isRefusalReason(reasonArg)) {
|
|
71
|
+
throw new Error(`runtime session refusal reason ${reasonRule}`);
|
|
72
|
+
}
|
|
73
|
+
return Object.freeze({ code: runtimeSessionRefusalContract.code, reason: reasonArg });
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* The refusal inside an error payload, or `null` when the value is not exactly one.
|
|
77
|
+
*
|
|
78
|
+
* It reads a value that survived serialization, the way `protocol.readProtocolRefusal` does: own
|
|
79
|
+
* data properties only, exactly `code` and `reason`, nothing that runs.
|
|
80
|
+
*/
|
|
81
|
+
export const readRuntimeSessionRefusal = (valueArg) => {
|
|
82
|
+
const refusal = readExactDataRecord(valueArg, ['code', 'reason']);
|
|
83
|
+
if (refusal === null || validateRuntimeSessionRefusal(refusal).length > 0) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
return Object.freeze({ code: runtimeSessionRefusalContract.code, reason: refusal.reason });
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* The momentary reason an error payload states, or `null` when it is a verdict: any other reason,
|
|
90
|
+
* a payload that is not exactly a refusal, or no payload at all.
|
|
91
|
+
*/
|
|
92
|
+
export const readMomentaryRuntimeSessionRefusal = (valueArg) => {
|
|
93
|
+
const refusal = readRuntimeSessionRefusal(valueArg);
|
|
94
|
+
if (refusal === null) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const momentary = runtimeSessionRefusalContract.momentaryReasons
|
|
98
|
+
.find((reasonArg) => reasonArg === refusal.reason);
|
|
99
|
+
return momentary ?? null;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* How long each hop may hold a node's runtime session request, so the hop nearest the decision
|
|
103
|
+
* answers first and the node always reads a stated refusal rather than its own timeout.
|
|
104
|
+
*/
|
|
105
|
+
export const runtimeSessionRequestBounds = Object.freeze({
|
|
106
|
+
/**
|
|
107
|
+
* Cloudly's budget for every node request, registration included. When it runs out Cloudly
|
|
108
|
+
* answers `controller-deadline`.
|
|
109
|
+
*/
|
|
110
|
+
controllerBudgetMs: 6_000,
|
|
111
|
+
/**
|
|
112
|
+
* A relay's bound on a registration, from its arrival at the relay to the answer, including any
|
|
113
|
+
* wait for the relay's own Cloudly link. It exceeds `controllerBudgetMs` by more than a second so
|
|
114
|
+
* Cloudly's own `controller-deadline` reaches the node through the relay, and stays below
|
|
115
|
+
* TypedSocket's 10 s connection-restore deadline (`TYPEDSOCKET_MAX_CONNECTION_RESTORE_TIMEOUT_MS`,
|
|
116
|
+
* which a client can only lower) so the node reads an answer before its restore hook times out.
|
|
117
|
+
* When it runs out the relay answers `controller-unavailable`.
|
|
118
|
+
*/
|
|
119
|
+
relayRegistrationBudgetMs: 9_000,
|
|
120
|
+
/**
|
|
121
|
+
* A relay's bound on forwarding a session read or report over its established Cloudly link.
|
|
122
|
+
* It exceeds `controllerBudgetMs` so Cloudly's answer arrives first; when it runs out the relay
|
|
123
|
+
* answers `controller-unavailable`.
|
|
124
|
+
*/
|
|
125
|
+
relaySessionForwardMs: 8_000,
|
|
126
|
+
});
|
|
127
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicnVudGltZXNlc3Npb25yZWZ1c2FsLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHMvZGF0YS9ydW50aW1lc2Vzc2lvbnJlZnVzYWwudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLG1CQUFtQixFQUFFLE1BQU0sOEJBQThCLENBQUM7QUFFbkU7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBbUNHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sNkJBQTZCLEdBQUcsTUFBTSxDQUFDLE1BQU0sQ0FBQztJQUN6RCxJQUFJLEVBQUUsZ0NBQWdDO0lBQ3RDLGdCQUFnQixFQUFFLE1BQU0sQ0FBQyxNQUFNLENBQUMsQ0FBQyxxQkFBcUIsRUFBRSx3QkFBd0IsQ0FBVSxDQUFDO0lBQzNGOzs7O09BSUc7SUFDSCxtQkFBbUIsRUFBRSxFQUFFO0NBQ2YsQ0FBQyxDQUFDO0FBY1osTUFBTSxlQUFlLEdBQUcsQ0FBQyxRQUFpQixFQUFzQixFQUFFLENBQUMsQ0FDakUsT0FBTyxRQUFRLEtBQUssUUFBUTtPQUN6QixRQUFRLENBQUMsTUFBTSxJQUFJLDZCQUE2QixDQUFDLG1CQUFtQjtPQUNwRSxzQkFBc0IsQ0FBQyxJQUFJLENBQUMsUUFBUSxDQUFDLENBQ3pDLENBQUM7QUFFRixNQUFNLFVBQVUsR0FBRyxtRUFBbUU7TUFDbEYsR0FBRyw2QkFBNkIsQ0FBQyxtQkFBbUIsYUFBYSxDQUFDO0FBRXRFLGtGQUFrRjtBQUNsRixNQUFNLENBQUMsTUFBTSw2QkFBNkIsR0FBRyxDQUMzQyxRQUFpQixFQUNqQixPQUFPLEdBQUcseUJBQXlCLEVBQ3pCLEVBQUU7SUFDWixNQUFNLE9BQU8sR0FBRyxtQkFBbUIsQ0FBQyxRQUFRLEVBQUUsQ0FBQyxNQUFNLEVBQUUsUUFBUSxDQUFDLENBQUMsQ0FBQztJQUNsRSxJQUFJLE9BQU8sS0FBSyxJQUFJLEVBQUUsQ0FBQztRQUNyQixPQUFPLENBQUMsR0FBRyxPQUFPLDRCQUE0QixD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
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -1452,6 +1452,64 @@ carries the same offer, judged in the same order before the JWT, and answers wit
|
|
|
1452
1452
|
the server's own offer. Both registration methods reject HTTP transport. Sensitive requests and registry
|
|
1453
1453
|
responses must be excluded from hooks, logs, diagnostics and durable journals.
|
|
1454
1454
|
|
|
1455
|
+
### Runtime Session Refusals
|
|
1456
|
+
|
|
1457
|
+
A controller that does not take a node's registration, session read or report
|
|
1458
|
+
answers `TypedResponseError` with `data.IRuntimeSessionRefusal` as its
|
|
1459
|
+
`errorData`: exactly `{ code: 'runtime-session-report-refused', reason }`
|
|
1460
|
+
(`data.runtimeSessionRefusalContract.code`, a frozen wire value). The reason is a
|
|
1461
|
+
word naming the check that refused, never a value: lowercase ASCII letter words
|
|
1462
|
+
joined by single hyphens, at most 64 characters
|
|
1463
|
+
(`runtimeSessionRefusalContract.maximumReasonLength`), so no identifier, address,
|
|
1464
|
+
digest, count or credential can cross in it.
|
|
1465
|
+
|
|
1466
|
+
Two reasons are the controller's own momentary state
|
|
1467
|
+
(`runtimeSessionRefusalContract.momentaryReasons`): a registering node waits and
|
|
1468
|
+
offers the same registration again, keeping what it runs. Every other reason,
|
|
1469
|
+
and an error without this payload, is a verdict; on a registration it ends the
|
|
1470
|
+
node's session. Momentary against verdict governs only whether a node defers a
|
|
1471
|
+
registration: a node retries its session reads and reports regardless of the
|
|
1472
|
+
reason, so the reasons a report is refused with (`state-conflict`,
|
|
1473
|
+
`*-write-conflict`, a capacity limit such as `log-intake-saturated`) are
|
|
1474
|
+
unchanged by this contract.
|
|
1475
|
+
|
|
1476
|
+
On a registration today (Cloudly ≥33.19.1), Cloudly attaches the payload only
|
|
1477
|
+
when the refusal is no judgement of the node: its budget ran out
|
|
1478
|
+
(`controller-deadline`) or it is stopping (`controller-unavailable`). Every
|
|
1479
|
+
other failure, a transient one of its own included, is answered without a
|
|
1480
|
+
payload and so reads as a verdict. No relay states a refusal of its own yet.
|
|
1481
|
+
|
|
1482
|
+
The rule producers adopt:
|
|
1483
|
+
|
|
1484
|
+
| Reason | Stated by | When |
|
|
1485
|
+
| --- | --- | --- |
|
|
1486
|
+
| `controller-deadline` | Cloudly | the request ran out of `controllerBudgetMs` (stated today) |
|
|
1487
|
+
| `controller-unavailable` | Cloudly | it is stopping (stated today); any other transient failure of its own on a registration: database connection lost or unavailable, a registration write conflict, a race with a credential being applied |
|
|
1488
|
+
| `controller-unavailable` | relay | only when it cannot carry the request or its answer: no Cloudly link, or its own bound ran out |
|
|
1489
|
+
| any other word | Cloudly | a verdict: revoked or inactive credential, deleted, retiring or foreign node, invalid body |
|
|
1490
|
+
|
|
1491
|
+
A relay forwards every answer Cloudly gave untouched and states nothing but
|
|
1492
|
+
`controller-unavailable` itself. `data.buildRuntimeSessionRefusal(reason)` builds
|
|
1493
|
+
the payload and throws on a reason that is not a word;
|
|
1494
|
+
`data.validateRuntimeSessionRefusal` names every violation;
|
|
1495
|
+
`data.readRuntimeSessionRefusal(errorData)` answers the refusal or `null`, and
|
|
1496
|
+
`data.readMomentaryRuntimeSessionRefusal(errorData)` answers the momentary reason
|
|
1497
|
+
or `null` for a verdict.
|
|
1498
|
+
|
|
1499
|
+
The readers are exact-key. Pallet 36.3.14 reads `code` and `reason` and tolerates
|
|
1500
|
+
other members, but a reader built on this contract refuses a payload carrying
|
|
1501
|
+
any other member, and a refused payload reads as no refusal, which turns a
|
|
1502
|
+
momentary refusal into a verdict. A producer never adds a member without gating
|
|
1503
|
+
it on the peer's spoken interfaces version.
|
|
1504
|
+
|
|
1505
|
+
`data.runtimeSessionRequestBounds` orders the hops so the one nearest the
|
|
1506
|
+
decision answers first and a node always reads a stated refusal rather than its
|
|
1507
|
+
own timeout: Cloudly's `controllerBudgetMs` (6 s, every node request including
|
|
1508
|
+
registration); a relay's `relayRegistrationBudgetMs` (9 s from arrival to
|
|
1509
|
+
answer, more than a second beyond Cloudly's budget and below TypedSocket's 10 s
|
|
1510
|
+
connection-restore deadline); and a relay's `relaySessionForwardMs` (8 s) for a
|
|
1511
|
+
session read or report forwarded over its established Cloudly link.
|
|
1512
|
+
|
|
1455
1513
|
### Assignment-Bound Runtime Secrets
|
|
1456
1514
|
|
|
1457
1515
|
`@serve.zone/interfaces/runtime` exports the Pallet assignment-secret contract.
|
|
@@ -3611,9 +3669,25 @@ The catalog validator judges every supported version config in full; no consumer
|
|
|
3611
3669
|
|
|
3612
3670
|
- `image`, then the port model (`appstore.validateAppStoreVersionPortModel`): `port` or `targetPorts` is required, and `targetPorts`, `domains` and `publicPortMappings` are read as exactly their types and resolved through `data.normalizeServicePortConfig`.
|
|
3613
3671
|
- `containerArgs` and the environment declarations.
|
|
3614
|
-
- Storage (`appstore.validateAppStoreVersionStorage`): the exact config member set (`appstore.appStoreVersionConfigKeys`), `platformRequirements`, `storageClasses` and `storageRequests` (declared together, every class used), the storage feature ids `requiresFeatures` must name, portable legacy `volumes`, and every environment, secret-file and mount destination used once.
|
|
3672
|
+
- Storage (`appstore.validateAppStoreVersionStorage`): the exact config member set (`appstore.appStoreVersionConfigKeys`), `platformRequirements`, `storageClasses` and `storageRequests` (declared together, every class used), `backupExclusions`, the storage feature ids `requiresFeatures` must name, portable legacy `volumes`, and every environment, secret-file and mount destination used once.
|
|
3615
3673
|
- Platform OIDC, then Docker `publishedPorts` (`appstore.validateAppStorePublishedPorts`).
|
|
3616
3674
|
|
|
3675
|
+
A version declares data it keeps only for observability, such as workload logs, as `backupExclusions` (`IAppStoreBackupExclusion[]`): a backup leaves those object keys out, and a restore neither brings them back nor removes them. Each entry names one objectStorage request of the version (`storageRequest`: an explicit request id, or `legacy-s3` when the version declares `platformRequirements.s3`; a filesystem request is refused), 1 to 16 `keyPrefixes` and a one-line `reason` an operator reads:
|
|
3676
|
+
|
|
3677
|
+
```typescript
|
|
3678
|
+
const cloudlyVersion: Partial<appstore.IAppStoreVersionConfig> = {
|
|
3679
|
+
platformRequirements: { s3: true },
|
|
3680
|
+
backupExclusions: [{
|
|
3681
|
+
storageRequest: 'legacy-s3',
|
|
3682
|
+
keyPrefixes: ['runtime-logs/'],
|
|
3683
|
+
reason: 'Workload logs; a restore brings back state, not past logs.',
|
|
3684
|
+
}],
|
|
3685
|
+
requiresFeatures: [appstore.appStoreStorageFeatureIds.objectStorageBackupExclusions],
|
|
3686
|
+
};
|
|
3687
|
+
```
|
|
3688
|
+
|
|
3689
|
+
Entries are in strictly increasing `storageRequest` order with each request at most once. A key prefix is 1 to 512 UTF-8 bytes, ends in `/`, does not start with `/`, has no empty, `.` or `..` segment and no control character; the prefixes of an entry are strictly increasing and none lies inside another. `appstore.appStoreBackupExclusionLimits` states the bounds, and `appstore.validateAppStoreBackupExclusionKeyPrefixes(value, label?)` is the one prefix-list rule: the version-config check applies it to each entry, and a reader of a list carrying such prefixes elsewhere, such as a backup manifest, judges it there too (an empty list means valid, otherwise one `Invalid <label>` reason). A version declaring any entry names `storage.object-storage.backup-exclusions` (`appStoreStorageFeatureIds.objectStorageBackupExclusions`) in `requiresFeatures`, so a host that does not leave the keys out of its backups lists the version without installing it, and an older host never reads the member.
|
|
3690
|
+
|
|
3617
3691
|
Each storage request is judged on its own by the storage contract behind `platform.normalizeStorageRequest`: members, capacity quantities and limits, protection, delivery and mount path, with that contract's messages and bounds, and a legacy volume's mount path as the request it becomes. The version config adds what only holds between requests: each request's class is declared, of its kind and backs a declared limit (`hardQuota`), backup or snapshots; ids, environment keys, secret files and mounts are used once; the legacy names `legacyFilesystem`, `legacyObjectStorage` and `legacy-s3` stay reserved. Two App Store narrowings apply on top: a request id is a stable lowercase identifier (it shares a namespace with the derived `legacy-volume-*` ids), and a file delivery targets one file directly below `/run/secrets`.
|
|
3618
3692
|
|
|
3619
3693
|
Each of these validators stops at the first violation and answers it as a one-element list starting `Invalid <label>`, so a config is refused for the same reason everywhere.
|
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/appstore/catalog.ts
CHANGED
package/ts/appstore/types.ts
CHANGED
|
@@ -62,6 +62,8 @@ export const appStoreStorageFeatureIds = {
|
|
|
62
62
|
filesystem: 'storage.filesystem',
|
|
63
63
|
objectStorage: 'storage.object-storage',
|
|
64
64
|
objectStorageFile: 'storage.object-storage.file',
|
|
65
|
+
/** The version declares `backupExclusions`; a consumer naming it leaves those keys out of backups. */
|
|
66
|
+
objectStorageBackupExclusions: 'storage.object-storage.backup-exclusions',
|
|
65
67
|
} as const;
|
|
66
68
|
|
|
67
69
|
export type TAppStoreStorageFeatureId =
|
|
@@ -127,6 +129,35 @@ export type TAppStoreStorageRequest =
|
|
|
127
129
|
| IAppStoreFilesystemStorageRequest
|
|
128
130
|
| IAppStoreObjectStorageRequest;
|
|
129
131
|
|
|
132
|
+
/** The bounds of `IAppStoreVersionConfig.backupExclusions`. */
|
|
133
|
+
export const appStoreBackupExclusionLimits = Object.freeze({
|
|
134
|
+
/** At most one entry per object-storage request; a version config holds at most 64 requests. */
|
|
135
|
+
maximumEntries: 64,
|
|
136
|
+
maximumKeyPrefixes: 16,
|
|
137
|
+
maximumKeyPrefixBytes: 512,
|
|
138
|
+
maximumReasonLength: 512,
|
|
139
|
+
} as const);
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Keys of one object-storage request that the app keeps only for observability, such as its
|
|
143
|
+
* workload logs. A backup leaves them out; a restore neither brings them back nor removes them.
|
|
144
|
+
*/
|
|
145
|
+
export interface IAppStoreBackupExclusion {
|
|
146
|
+
/**
|
|
147
|
+
* An objectStorage request of this version: an explicit request id, or `legacy-s3` when the
|
|
148
|
+
* version declares `platformRequirements.s3`. A filesystem request cannot declare exclusions.
|
|
149
|
+
*/
|
|
150
|
+
storageRequest: string;
|
|
151
|
+
/**
|
|
152
|
+
* 1 to 16 object key prefixes in strictly increasing UTF-16 code-unit order, none a prefix of
|
|
153
|
+
* another. Each is 1 to 512 UTF-8 bytes, ends in `/`, does not start with `/`, has no empty,
|
|
154
|
+
* `.` or `..` segment and no control character or unpaired surrogate.
|
|
155
|
+
*/
|
|
156
|
+
keyPrefixes: string[];
|
|
157
|
+
/** One operator-readable line, e.g. "Workload logs; a restore brings back state, not past logs." */
|
|
158
|
+
reason: string;
|
|
159
|
+
}
|
|
160
|
+
|
|
130
161
|
export interface IAppStoreInlineSource {
|
|
131
162
|
type: 'inline';
|
|
132
163
|
}
|
|
@@ -402,6 +433,13 @@ export interface IAppStoreVersionConfig {
|
|
|
402
433
|
storageClasses?: Record<string, IAppStoreStorageClass>;
|
|
403
434
|
/** Stable named filesystem and managed object-storage requests. */
|
|
404
435
|
storageRequests?: TAppStoreStorageRequest[];
|
|
436
|
+
/**
|
|
437
|
+
* Data the app keeps only for observability, in strictly increasing `storageRequest` order with
|
|
438
|
+
* each request at most once. A backup leaves it out; a restore neither brings it back nor
|
|
439
|
+
* removes it. A version declaring any entry names
|
|
440
|
+
* `appStoreStorageFeatureIds.objectStorageBackupExclusions` in `requiresFeatures`.
|
|
441
|
+
*/
|
|
442
|
+
backupExclusions?: IAppStoreBackupExclusion[];
|
|
405
443
|
publishedPorts?: IAppStorePublishedPort[];
|
|
406
444
|
platformRequirements?: IAppStorePlatformRequirements;
|
|
407
445
|
/**
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
* `platform.normalizeStorageRequest`), reporting through this module's rejection. What this module
|
|
16
16
|
* adds are the rules a request can only break inside a version config: its class is declared and of
|
|
17
17
|
* its kind, the class backs a declared limit or protection, ids and destinations are used once, the
|
|
18
|
-
* legacy names stay reserved,
|
|
18
|
+
* legacy names stay reserved, a backup exclusion names an object-storage request, and the storage
|
|
19
|
+
* feature ids are named.
|
|
19
20
|
*/
|
|
20
21
|
import { normalizeServiceAbsolutePath, validateServiceContainerArgs } from '../data/service.js';
|
|
21
22
|
import { normalizeServicePortConfig } from '../data/serviceports.js';
|
|
@@ -26,16 +27,22 @@ import type {
|
|
|
26
27
|
TServiceTargetPortRef,
|
|
27
28
|
TServiceTargetProtocol,
|
|
28
29
|
} from '../data/serviceports.js';
|
|
30
|
+
import { compareWireTokens } from '../private/canonicaljson.js';
|
|
29
31
|
import { readDataElements, readDataProperties } from '../private/dataproperties.js';
|
|
30
32
|
import type { TCompleteKeyList } from '../private/keylist.js';
|
|
31
33
|
import { createStorageContractReaders } from '../private/storagecontract.js';
|
|
32
34
|
import type { TObjectStorageDelivery } from '../platform/storage.js';
|
|
33
35
|
import {
|
|
36
|
+
appStoreBackupExclusionLimits,
|
|
34
37
|
appStoreStorageFeatureIds,
|
|
35
38
|
validateAppStoreEnvironmentDeclarations,
|
|
36
39
|
validateAppStoreVersionPlatformOidc,
|
|
37
40
|
} from './types.js';
|
|
38
|
-
import type {
|
|
41
|
+
import type {
|
|
42
|
+
IAppStoreBackupExclusion,
|
|
43
|
+
IAppStoreVersionConfig,
|
|
44
|
+
TAppStoreStorageRequest,
|
|
45
|
+
} from './types.js';
|
|
39
46
|
|
|
40
47
|
const versionConfigMemberList = [
|
|
41
48
|
'image',
|
|
@@ -48,6 +55,7 @@ const versionConfigMemberList = [
|
|
|
48
55
|
'volumes',
|
|
49
56
|
'storageClasses',
|
|
50
57
|
'storageRequests',
|
|
58
|
+
'backupExclusions',
|
|
51
59
|
'publishedPorts',
|
|
52
60
|
'platformRequirements',
|
|
53
61
|
'platformOidc',
|
|
@@ -92,6 +100,8 @@ const legacyObjectStorageClassKey = 'legacyObjectStorage';
|
|
|
92
100
|
const legacyObjectStorageRequestId = 'legacy-s3';
|
|
93
101
|
const knownStorageFeatureIds: readonly string[] = Object.values(appStoreStorageFeatureIds);
|
|
94
102
|
const platformRequirementKeys = ['mongodb', 's3', 'clickhouse', 'valkey', 'mariadb', 'mail'] as const;
|
|
103
|
+
const controlCharacterPattern = /[\u0000-\u001f\u007f]/;
|
|
104
|
+
const utf8Encoder = new TextEncoder();
|
|
95
105
|
|
|
96
106
|
/** Thrown inside this module only; every exported validator answers it as its result. */
|
|
97
107
|
class AppStoreVersionConfigRejection extends Error {}
|
|
@@ -601,6 +611,7 @@ function checkExplicitStorageRequests(
|
|
|
601
611
|
function checkFeatureDeclaration(
|
|
602
612
|
requiresFeaturesArg: unknown,
|
|
603
613
|
explicitRequestsArg: readonly TAppStoreStorageRequest[],
|
|
614
|
+
backupExclusionsArg: readonly IAppStoreBackupExclusion[],
|
|
604
615
|
labelArg: string,
|
|
605
616
|
): void {
|
|
606
617
|
const derivedFeatures = new Set<string>();
|
|
@@ -614,6 +625,9 @@ function checkFeatureDeclaration(
|
|
|
614
625
|
if (explicitRequestsArg.some((requestArg) => requestArg.kind === 'objectStorage' && requestArg.delivery.type === 'file')) {
|
|
615
626
|
derivedFeatures.add(appStoreStorageFeatureIds.objectStorageFile);
|
|
616
627
|
}
|
|
628
|
+
if (backupExclusionsArg.length > 0) {
|
|
629
|
+
derivedFeatures.add(appStoreStorageFeatureIds.objectStorageBackupExclusions);
|
|
630
|
+
}
|
|
617
631
|
|
|
618
632
|
const declaredEntries = requiresFeaturesArg === undefined
|
|
619
633
|
? []
|
|
@@ -641,6 +655,123 @@ function checkFeatureDeclaration(
|
|
|
641
655
|
}
|
|
642
656
|
}
|
|
643
657
|
|
|
658
|
+
const hasUnpairedSurrogate = (valueArg: string): boolean => {
|
|
659
|
+
for (let index = 0; index < valueArg.length; index++) {
|
|
660
|
+
const codeUnit = valueArg.charCodeAt(index);
|
|
661
|
+
if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
|
|
662
|
+
const following = valueArg.charCodeAt(index + 1);
|
|
663
|
+
if (!(following >= 0xdc00 && following <= 0xdfff)) return true;
|
|
664
|
+
index++;
|
|
665
|
+
} else if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) {
|
|
666
|
+
return true;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return false;
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
/** One object key prefix: a relative key path that names a whole subtree. */
|
|
673
|
+
function checkBackupExclusionKeyPrefix(valueArg: unknown, labelArg: string): string {
|
|
674
|
+
if (typeof valueArg !== 'string') return reject(`Invalid ${labelArg}: expected a string`);
|
|
675
|
+
if (controlCharacterPattern.test(valueArg) || hasUnpairedSurrogate(valueArg)) {
|
|
676
|
+
reject(`Invalid ${labelArg}: must not contain a control character or an unpaired surrogate`);
|
|
677
|
+
}
|
|
678
|
+
const byteLength = utf8Encoder.encode(valueArg).byteLength;
|
|
679
|
+
if (byteLength < 1 || byteLength > appStoreBackupExclusionLimits.maximumKeyPrefixBytes) {
|
|
680
|
+
reject(`Invalid ${labelArg}: must be 1 to ${appStoreBackupExclusionLimits.maximumKeyPrefixBytes} UTF-8 bytes`);
|
|
681
|
+
}
|
|
682
|
+
if (valueArg.startsWith('/')) reject(`Invalid ${labelArg}: must not start with '/'`);
|
|
683
|
+
if (!valueArg.endsWith('/')) reject(`Invalid ${labelArg}: must end with '/'`);
|
|
684
|
+
if (valueArg.slice(0, -1).split('/').some((segmentArg) => segmentArg === '' || segmentArg === '.' || segmentArg === '..')) {
|
|
685
|
+
reject(`Invalid ${labelArg}: must not contain an empty, '.' or '..' segment`);
|
|
686
|
+
}
|
|
687
|
+
return valueArg;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* One exclusion's key prefix list: 1 to 16 prefixes, strictly increasing, none inside another.
|
|
692
|
+
* `validateAppStoreBackupExclusionKeyPrefixes` exports it, so every reader of a prefix list judges
|
|
693
|
+
* it by this one rule.
|
|
694
|
+
*/
|
|
695
|
+
function checkBackupExclusionKeyPrefixes(valueArg: unknown, labelArg: string): string[] {
|
|
696
|
+
const { maximumKeyPrefixes } = appStoreBackupExclusionLimits;
|
|
697
|
+
const prefixEntries = readDataElements(valueArg, maximumKeyPrefixes);
|
|
698
|
+
if (prefixEntries === null || prefixEntries.length === 0) {
|
|
699
|
+
return reject(`Invalid ${labelArg}: expected an array of 1 to ${maximumKeyPrefixes} key prefixes`);
|
|
700
|
+
}
|
|
701
|
+
const keyPrefixes: string[] = [];
|
|
702
|
+
prefixEntries.forEach((prefixArg, indexArg) => {
|
|
703
|
+
const prefixLabel = `${labelArg}[${indexArg}]`;
|
|
704
|
+
const prefix = checkBackupExclusionKeyPrefix(prefixArg, prefixLabel);
|
|
705
|
+
const previousPrefix = keyPrefixes[keyPrefixes.length - 1];
|
|
706
|
+
if (previousPrefix !== undefined && compareWireTokens(previousPrefix, prefix) >= 0) {
|
|
707
|
+
reject(`Invalid ${labelArg}: must be strictly increasing`);
|
|
708
|
+
}
|
|
709
|
+
const covering = keyPrefixes.find((existingArg) => prefix.startsWith(existingArg));
|
|
710
|
+
if (covering !== undefined) {
|
|
711
|
+
reject(`Invalid ${prefixLabel}: '${prefix}' lies inside '${covering}'`);
|
|
712
|
+
}
|
|
713
|
+
keyPrefixes.push(prefix);
|
|
714
|
+
});
|
|
715
|
+
return keyPrefixes;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* The members of each backup exclusion, its key prefixes and the entry order. Which request an
|
|
720
|
+
* entry names is judged once the config's requests are known (`checkBackupExclusionTargets`).
|
|
721
|
+
*/
|
|
722
|
+
function checkBackupExclusions(valueArg: unknown, labelArg: string): IAppStoreBackupExclusion[] {
|
|
723
|
+
if (valueArg === undefined) return [];
|
|
724
|
+
const { maximumEntries, maximumReasonLength } = appStoreBackupExclusionLimits;
|
|
725
|
+
const entries = readDataElements(valueArg, maximumEntries);
|
|
726
|
+
if (entries === null || entries.length === 0) {
|
|
727
|
+
return reject(`Invalid ${labelArg}: backupExclusions must contain between 1 and ${maximumEntries} entries`);
|
|
728
|
+
}
|
|
729
|
+
let previousRequest: string | null = null;
|
|
730
|
+
return entries.map((entryArg, indexArg): IAppStoreBackupExclusion => {
|
|
731
|
+
const entryLabel = `${labelArg} backupExclusions[${indexArg}]`;
|
|
732
|
+
const entry = readRecord(entryArg, entryLabel);
|
|
733
|
+
assertExactKeys(entry, ['storageRequest', 'keyPrefixes', 'reason'], entryLabel);
|
|
734
|
+
const { storageRequest, reason } = entry;
|
|
735
|
+
if (typeof storageRequest !== 'string') return reject(`Invalid ${entryLabel}.storageRequest: expected a string`);
|
|
736
|
+
if (previousRequest !== null && compareWireTokens(previousRequest, storageRequest) >= 0) {
|
|
737
|
+
reject(previousRequest === storageRequest
|
|
738
|
+
? `Invalid ${labelArg}: backupExclusions name storage request '${storageRequest}' more than once`
|
|
739
|
+
: `Invalid ${labelArg}: backupExclusions must be in strictly increasing storageRequest order`);
|
|
740
|
+
}
|
|
741
|
+
previousRequest = storageRequest;
|
|
742
|
+
|
|
743
|
+
const keyPrefixes = checkBackupExclusionKeyPrefixes(entry.keyPrefixes, `${entryLabel}.keyPrefixes`);
|
|
744
|
+
|
|
745
|
+
if (typeof reason !== 'string'
|
|
746
|
+
|| reason.length === 0
|
|
747
|
+
|| reason.length > maximumReasonLength
|
|
748
|
+
|| reason.trim() !== reason
|
|
749
|
+
|| controlCharacterPattern.test(reason)
|
|
750
|
+
|| hasUnpairedSurrogate(reason)) {
|
|
751
|
+
reject(`Invalid ${entryLabel}.reason: must be one trimmed line of 1 to ${maximumReasonLength} characters`);
|
|
752
|
+
}
|
|
753
|
+
return { storageRequest, keyPrefixes, reason };
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/** Each backup exclusion names an object-storage request of the normalized config. */
|
|
758
|
+
function checkBackupExclusionTargets(
|
|
759
|
+
exclusionsArg: readonly IAppStoreBackupExclusion[],
|
|
760
|
+
objectStorageRequestIdsArg: ReadonlySet<string>,
|
|
761
|
+
filesystemRequestIdsArg: ReadonlySet<string>,
|
|
762
|
+
labelArg: string,
|
|
763
|
+
): void {
|
|
764
|
+
exclusionsArg.forEach((exclusionArg, indexArg) => {
|
|
765
|
+
const { storageRequest } = exclusionArg;
|
|
766
|
+
if (objectStorageRequestIdsArg.has(storageRequest)) return;
|
|
767
|
+
const requestLabel = `${labelArg} backupExclusions[${indexArg}].storageRequest`;
|
|
768
|
+
if (filesystemRequestIdsArg.has(storageRequest)) {
|
|
769
|
+
reject(`Invalid ${requestLabel}: '${storageRequest}' is a filesystem request; only an objectStorage request declares backup exclusions`);
|
|
770
|
+
}
|
|
771
|
+
reject(`Invalid ${requestLabel}: '${storageRequest}' does not name an objectStorage request of this version`);
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
|
|
644
775
|
interface ILegacyVolumeFacts {
|
|
645
776
|
name?: string;
|
|
646
777
|
mountPath: string;
|
|
@@ -710,7 +841,8 @@ function checkStorage(configArg: Record<string, unknown>, labelArg: string): voi
|
|
|
710
841
|
destinations,
|
|
711
842
|
labelArg,
|
|
712
843
|
);
|
|
713
|
-
|
|
844
|
+
const backupExclusions = checkBackupExclusions(configArg.backupExclusions, labelArg);
|
|
845
|
+
checkFeatureDeclaration(configArg.requiresFeatures, explicitRequests, backupExclusions, labelArg);
|
|
714
846
|
const legacyS3 = platformRequirements.s3 === true;
|
|
715
847
|
if (legacyS3 && explicitRequests.some((requestArg) => requestArg.kind === 'objectStorage')) {
|
|
716
848
|
reject(`Invalid ${labelArg}: platformRequirements.s3 cannot coexist with explicit objectStorage requests`);
|
|
@@ -727,6 +859,7 @@ function checkStorage(configArg: Record<string, unknown>, labelArg: string): voi
|
|
|
727
859
|
const allMounts: Array<{ id: string; mountPath: string }> = explicitRequests.flatMap((requestArg) => (
|
|
728
860
|
requestArg.kind === 'filesystem' ? [{ id: requestArg.id, mountPath: requestArg.mountPath }] : []
|
|
729
861
|
));
|
|
862
|
+
const legacyVolumeRequestIds: string[] = [];
|
|
730
863
|
legacyVolumes.forEach((volumeArg, indexArg) => {
|
|
731
864
|
const requestId = legacyVolumeRequestId(volumeArg, indexArg, labelArg);
|
|
732
865
|
if (requestIds.has(requestId)) reject(`Invalid ${labelArg}: storage request id '${requestId}' is duplicated`);
|
|
@@ -734,6 +867,7 @@ function checkStorage(configArg: Record<string, unknown>, labelArg: string): voi
|
|
|
734
867
|
requestIds.add(requestId);
|
|
735
868
|
mountPaths.push(volumeArg.mountPath);
|
|
736
869
|
allMounts.push({ id: requestId, mountPath: volumeArg.mountPath });
|
|
870
|
+
legacyVolumeRequestIds.push(requestId);
|
|
737
871
|
});
|
|
738
872
|
|
|
739
873
|
if (legacyS3) {
|
|
@@ -744,6 +878,18 @@ function checkStorage(configArg: Record<string, unknown>, labelArg: string): voi
|
|
|
744
878
|
reject(`Invalid ${labelArg}: storage request id '${legacyObjectStorageRequestId}' is reserved for legacy S3 normalization`);
|
|
745
879
|
}
|
|
746
880
|
}
|
|
881
|
+
checkBackupExclusionTargets(
|
|
882
|
+
backupExclusions,
|
|
883
|
+
new Set([
|
|
884
|
+
...explicitRequests.flatMap((requestArg) => (requestArg.kind === 'objectStorage' ? [requestArg.id] : [])),
|
|
885
|
+
...(legacyS3 ? [legacyObjectStorageRequestId] : []),
|
|
886
|
+
]),
|
|
887
|
+
new Set([
|
|
888
|
+
...explicitRequests.flatMap((requestArg) => (requestArg.kind === 'filesystem' ? [requestArg.id] : [])),
|
|
889
|
+
...legacyVolumeRequestIds,
|
|
890
|
+
]),
|
|
891
|
+
labelArg,
|
|
892
|
+
);
|
|
747
893
|
|
|
748
894
|
const checkedMounts: string[] = [];
|
|
749
895
|
for (const mount of allMounts) {
|
|
@@ -777,12 +923,24 @@ export const validateAppStorePublishedPorts = (publishedPortsArg: unknown, label
|
|
|
777
923
|
|
|
778
924
|
/**
|
|
779
925
|
* The config's member set and its storage: legacy `volumes`, `storageClasses`, `storageRequests`,
|
|
780
|
-
* `platformRequirements`, the storage feature ids `requiresFeatures` must name,
|
|
781
|
-
* environment, secret-file and mount destination being used once.
|
|
926
|
+
* `platformRequirements`, `backupExclusions`, the storage feature ids `requiresFeatures` must name,
|
|
927
|
+
* and every environment, secret-file and mount destination being used once.
|
|
782
928
|
*/
|
|
783
929
|
export const validateAppStoreVersionStorage = (configArg: unknown, labelArg = defaultLabel): string[] =>
|
|
784
930
|
firstRejection(() => checkStorage(readConfig(configArg, labelArg), labelArg));
|
|
785
931
|
|
|
932
|
+
/**
|
|
933
|
+
* One backup exclusion's `keyPrefixes`, the rule `validateAppStoreVersionStorage` applies to each
|
|
934
|
+
* `IAppStoreBackupExclusion`: 1 to 16 prefixes in strictly increasing UTF-16 code-unit order, none
|
|
935
|
+
* inside another, each 1 to 512 UTF-8 bytes ending in `/`, without a leading `/`, an empty, `.` or
|
|
936
|
+
* `..` segment, a control character or an unpaired surrogate. A reader of a list that carries such
|
|
937
|
+
* prefixes elsewhere, such as a backup manifest, judges it here.
|
|
938
|
+
*/
|
|
939
|
+
export const validateAppStoreBackupExclusionKeyPrefixes = (
|
|
940
|
+
valueArg: unknown,
|
|
941
|
+
labelArg = 'keyPrefixes',
|
|
942
|
+
): string[] => firstRejection(() => checkBackupExclusionKeyPrefixes(valueArg, labelArg));
|
|
943
|
+
|
|
786
944
|
/**
|
|
787
945
|
* Every member rule of a version config, in the order a host applies them: image, port model,
|
|
788
946
|
* container arguments, environment declarations, storage, platform OIDC, published ports.
|
package/ts/data/index.ts
CHANGED
|
@@ -66,6 +66,7 @@ export * from './runtimestorageclaim.js';
|
|
|
66
66
|
export * from './runtimelocalstorageclaim.js';
|
|
67
67
|
export * from './runtimelocalvolumearchive.js';
|
|
68
68
|
export * from './runtimesession.js';
|
|
69
|
+
export * from './runtimesessionrefusal.js';
|
|
69
70
|
export * from './runtimeterminalreceipt.js';
|
|
70
71
|
export * from './secret.js';
|
|
71
72
|
export * from './baremetal.js';
|