@serve.zone/interfaces 32.56.0 → 32.58.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 +54 -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 +43 -0
- package/dist_ts/appstore/types.js +15 -1
- package/dist_ts/appstore/versionconfig.d.ts +12 -3
- package/dist_ts/appstore/versionconfig.js +131 -17
- 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 +3 -3
- package/readme.md +82 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/appstore/catalog.ts +1 -0
- package/ts/appstore/types.ts +46 -0
- package/ts/appstore/versionconfig.ts +144 -13
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serve.zone/interfaces",
|
|
3
|
-
"version": "32.
|
|
3
|
+
"version": "32.58.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.",
|
|
6
6
|
"exports": {
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"@git.zone/tsbuild": "^6.0.2",
|
|
31
31
|
"@git.zone/tsdoc": "^3.0.2",
|
|
32
32
|
"@git.zone/tsrun": "^3.0.3",
|
|
33
|
-
"@git.zone/tstest": "^
|
|
34
|
-
"@types/node": "26.6.
|
|
33
|
+
"@git.zone/tstest": "^8.0.0",
|
|
34
|
+
"@types/node": "26.6.5",
|
|
35
35
|
"typescript": "^6.0.3"
|
|
36
36
|
},
|
|
37
37
|
"files": [
|
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,7 +3669,7 @@ 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), `backupExclusions`, 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`, `databaseBackupExclusions`, 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
|
|
|
3617
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:
|
|
@@ -3630,6 +3688,29 @@ const cloudlyVersion: Partial<appstore.IAppStoreVersionConfig> = {
|
|
|
3630
3688
|
|
|
3631
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.
|
|
3632
3690
|
|
|
3691
|
+
Database collections of the same kind, such as a workload log ledger, are declared as `databaseBackupExclusions` (`IAppStoreDatabaseBackupExclusion[]`): a backup exports those collections with their indexes and without their documents, so a restore brings them back empty. Each entry names one database the version provisions (`database`: `mongodb`, which requires `platformRequirements.mongodb: true`, the Corestore `database` resource), 1 to 64 `collections` and a one-line `reason`:
|
|
3692
|
+
|
|
3693
|
+
```typescript
|
|
3694
|
+
const cloudlyVersion: Partial<appstore.IAppStoreVersionConfig> = {
|
|
3695
|
+
platformRequirements: { mongodb: true, s3: true },
|
|
3696
|
+
databaseBackupExclusions: [{
|
|
3697
|
+
database: 'mongodb',
|
|
3698
|
+
collections: [
|
|
3699
|
+
'cloudly_runtime_log_attempts',
|
|
3700
|
+
'cloudly_runtime_log_batches',
|
|
3701
|
+
'cloudly_runtime_log_captures',
|
|
3702
|
+
'cloudly_runtime_log_inline_payloads',
|
|
3703
|
+
'cloudly_runtime_log_node_discards',
|
|
3704
|
+
'cloudly_runtime_log_queue',
|
|
3705
|
+
],
|
|
3706
|
+
reason: 'Workload log ledger; a restore brings back state, not past logs.',
|
|
3707
|
+
}],
|
|
3708
|
+
requiresFeatures: [appstore.appStoreStorageFeatureIds.databaseBackupExclusions],
|
|
3709
|
+
};
|
|
3710
|
+
```
|
|
3711
|
+
|
|
3712
|
+
Entries are in strictly increasing `database` order with each database at most once. A collection name is 1 to 128 UTF-8 bytes, does not start with `.` or `system.` (a system collection is never excluded), and has no `/`, `\`, `$`, control character (C0, DEL or C1) or unpaired surrogate; the names of an entry are strictly increasing. `appstore.appStoreDatabaseBackupExclusionLimits` states the bounds, and `appstore.validateAppStoreDatabaseBackupExclusionCollections(value, label?)` is the one collection-list rule: the version-config check applies it to each entry, and a reader of such a list elsewhere, such as Corestore's export request, judges it there too. A version declaring any entry names `storage.database.backup-exclusions` (`appStoreStorageFeatureIds.databaseBackupExclusions`) in `requiresFeatures`, so a host whose database backups cannot leave documents out lists the version without installing it, and an older host never reads the member.
|
|
3713
|
+
|
|
3633
3714
|
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`.
|
|
3634
3715
|
|
|
3635
3716
|
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
|
@@ -64,6 +64,11 @@ export const appStoreStorageFeatureIds = {
|
|
|
64
64
|
objectStorageFile: 'storage.object-storage.file',
|
|
65
65
|
/** The version declares `backupExclusions`; a consumer naming it leaves those keys out of backups. */
|
|
66
66
|
objectStorageBackupExclusions: 'storage.object-storage.backup-exclusions',
|
|
67
|
+
/**
|
|
68
|
+
* The version declares `databaseBackupExclusions`; a consumer naming it exports those collections
|
|
69
|
+
* without their documents.
|
|
70
|
+
*/
|
|
71
|
+
databaseBackupExclusions: 'storage.database.backup-exclusions',
|
|
67
72
|
} as const;
|
|
68
73
|
|
|
69
74
|
export type TAppStoreStorageFeatureId =
|
|
@@ -158,6 +163,40 @@ export interface IAppStoreBackupExclusion {
|
|
|
158
163
|
reason: string;
|
|
159
164
|
}
|
|
160
165
|
|
|
166
|
+
/**
|
|
167
|
+
* A database a version can declare backup exclusions for: `mongodb`, the document database
|
|
168
|
+
* `platformRequirements.mongodb` provisions (a Corestore `database` resource).
|
|
169
|
+
*/
|
|
170
|
+
export type TAppStoreBackupExclusionDatabase = 'mongodb';
|
|
171
|
+
|
|
172
|
+
/** The bounds of `IAppStoreVersionConfig.databaseBackupExclusions`. */
|
|
173
|
+
export const appStoreDatabaseBackupExclusionLimits = Object.freeze({
|
|
174
|
+
/** At most one entry per database; a version provisions at most one (`mongodb`). */
|
|
175
|
+
maximumEntries: 1,
|
|
176
|
+
maximumCollections: 64,
|
|
177
|
+
/** Keeps a whole collection list inside one 16 KiB control document. */
|
|
178
|
+
maximumCollectionNameBytes: 128,
|
|
179
|
+
maximumReasonLength: 512,
|
|
180
|
+
} as const);
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Collections of one database that the app keeps only for observability, such as its workload
|
|
184
|
+
* log ledger. A backup exports them with their indexes and without their documents; a restore
|
|
185
|
+
* therefore brings them back empty.
|
|
186
|
+
*/
|
|
187
|
+
export interface IAppStoreDatabaseBackupExclusion {
|
|
188
|
+
/** The database: `mongodb`, which the version provisions through `platformRequirements.mongodb`. */
|
|
189
|
+
database: TAppStoreBackupExclusionDatabase;
|
|
190
|
+
/**
|
|
191
|
+
* 1 to 64 collection names in strictly increasing UTF-16 code-unit order. Each is 1 to 128 UTF-8
|
|
192
|
+
* bytes, does not start with `.` or `system.`, and has no `/`, `\`, `$`, control character or
|
|
193
|
+
* unpaired surrogate.
|
|
194
|
+
*/
|
|
195
|
+
collections: string[];
|
|
196
|
+
/** One operator-readable line, e.g. "Workload log ledger; a restore brings back state, not past logs." */
|
|
197
|
+
reason: string;
|
|
198
|
+
}
|
|
199
|
+
|
|
161
200
|
export interface IAppStoreInlineSource {
|
|
162
201
|
type: 'inline';
|
|
163
202
|
}
|
|
@@ -440,6 +479,13 @@ export interface IAppStoreVersionConfig {
|
|
|
440
479
|
* `appStoreStorageFeatureIds.objectStorageBackupExclusions` in `requiresFeatures`.
|
|
441
480
|
*/
|
|
442
481
|
backupExclusions?: IAppStoreBackupExclusion[];
|
|
482
|
+
/**
|
|
483
|
+
* Database collections the app keeps only for observability, in strictly increasing `database`
|
|
484
|
+
* order with each database at most once. A backup exports them without their documents; a
|
|
485
|
+
* restore brings them back empty. A version declaring any entry names
|
|
486
|
+
* `appStoreStorageFeatureIds.databaseBackupExclusions` in `requiresFeatures`.
|
|
487
|
+
*/
|
|
488
|
+
databaseBackupExclusions?: IAppStoreDatabaseBackupExclusion[];
|
|
443
489
|
publishedPorts?: IAppStorePublishedPort[];
|
|
444
490
|
platformRequirements?: IAppStorePlatformRequirements;
|
|
445
491
|
/**
|
|
@@ -15,8 +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, a backup exclusion names an object-storage request,
|
|
19
|
-
* feature ids are named.
|
|
18
|
+
* legacy names stay reserved, a backup exclusion names an object-storage request, a database backup
|
|
19
|
+
* exclusion names a provisioned database, and the storage feature ids are named.
|
|
20
20
|
*/
|
|
21
21
|
import { normalizeServiceAbsolutePath, validateServiceContainerArgs } from '../data/service.js';
|
|
22
22
|
import { normalizeServicePortConfig } from '../data/serviceports.js';
|
|
@@ -34,13 +34,16 @@ import { createStorageContractReaders } from '../private/storagecontract.js';
|
|
|
34
34
|
import type { TObjectStorageDelivery } from '../platform/storage.js';
|
|
35
35
|
import {
|
|
36
36
|
appStoreBackupExclusionLimits,
|
|
37
|
+
appStoreDatabaseBackupExclusionLimits,
|
|
37
38
|
appStoreStorageFeatureIds,
|
|
38
39
|
validateAppStoreEnvironmentDeclarations,
|
|
39
40
|
validateAppStoreVersionPlatformOidc,
|
|
40
41
|
} from './types.js';
|
|
41
42
|
import type {
|
|
42
43
|
IAppStoreBackupExclusion,
|
|
44
|
+
IAppStoreDatabaseBackupExclusion,
|
|
43
45
|
IAppStoreVersionConfig,
|
|
46
|
+
TAppStoreBackupExclusionDatabase,
|
|
44
47
|
TAppStoreStorageRequest,
|
|
45
48
|
} from './types.js';
|
|
46
49
|
|
|
@@ -56,6 +59,7 @@ const versionConfigMemberList = [
|
|
|
56
59
|
'storageClasses',
|
|
57
60
|
'storageRequests',
|
|
58
61
|
'backupExclusions',
|
|
62
|
+
'databaseBackupExclusions',
|
|
59
63
|
'publishedPorts',
|
|
60
64
|
'platformRequirements',
|
|
61
65
|
'platformOidc',
|
|
@@ -101,6 +105,12 @@ const legacyObjectStorageRequestId = 'legacy-s3';
|
|
|
101
105
|
const knownStorageFeatureIds: readonly string[] = Object.values(appStoreStorageFeatureIds);
|
|
102
106
|
const platformRequirementKeys = ['mongodb', 's3', 'clickhouse', 'valkey', 'mariadb', 'mail'] as const;
|
|
103
107
|
const controlCharacterPattern = /[\u0000-\u001f\u007f]/;
|
|
108
|
+
/** The C0 and C1 controls and DEL: what the database engine refuses in a namespace component. */
|
|
109
|
+
const collectionNameControlPattern = /[\u0000-\u001f\u007f-\u009f]/;
|
|
110
|
+
/** Each database a version can declare exclusions for, with the platform requirement provisioning it. */
|
|
111
|
+
const backupExclusionDatabaseRequirements: Readonly<Record<TAppStoreBackupExclusionDatabase, 'mongodb'>> = {
|
|
112
|
+
mongodb: 'mongodb',
|
|
113
|
+
};
|
|
104
114
|
const utf8Encoder = new TextEncoder();
|
|
105
115
|
|
|
106
116
|
/** Thrown inside this module only; every exported validator answers it as its result. */
|
|
@@ -612,6 +622,7 @@ function checkFeatureDeclaration(
|
|
|
612
622
|
requiresFeaturesArg: unknown,
|
|
613
623
|
explicitRequestsArg: readonly TAppStoreStorageRequest[],
|
|
614
624
|
backupExclusionsArg: readonly IAppStoreBackupExclusion[],
|
|
625
|
+
databaseBackupExclusionsArg: readonly IAppStoreDatabaseBackupExclusion[],
|
|
615
626
|
labelArg: string,
|
|
616
627
|
): void {
|
|
617
628
|
const derivedFeatures = new Set<string>();
|
|
@@ -628,6 +639,9 @@ function checkFeatureDeclaration(
|
|
|
628
639
|
if (backupExclusionsArg.length > 0) {
|
|
629
640
|
derivedFeatures.add(appStoreStorageFeatureIds.objectStorageBackupExclusions);
|
|
630
641
|
}
|
|
642
|
+
if (databaseBackupExclusionsArg.length > 0) {
|
|
643
|
+
derivedFeatures.add(appStoreStorageFeatureIds.databaseBackupExclusions);
|
|
644
|
+
}
|
|
631
645
|
|
|
632
646
|
const declaredEntries = requiresFeaturesArg === undefined
|
|
633
647
|
? []
|
|
@@ -669,6 +683,19 @@ const hasUnpairedSurrogate = (valueArg: string): boolean => {
|
|
|
669
683
|
return false;
|
|
670
684
|
};
|
|
671
685
|
|
|
686
|
+
/** An exclusion's `reason`: one trimmed line an operator reads. */
|
|
687
|
+
function checkBackupExclusionReason(valueArg: unknown, maximumLengthArg: number, entryLabelArg: string): string {
|
|
688
|
+
if (typeof valueArg !== 'string'
|
|
689
|
+
|| valueArg.length === 0
|
|
690
|
+
|| valueArg.length > maximumLengthArg
|
|
691
|
+
|| valueArg.trim() !== valueArg
|
|
692
|
+
|| controlCharacterPattern.test(valueArg)
|
|
693
|
+
|| hasUnpairedSurrogate(valueArg)) {
|
|
694
|
+
return reject(`Invalid ${entryLabelArg}.reason: must be one trimmed line of 1 to ${maximumLengthArg} characters`);
|
|
695
|
+
}
|
|
696
|
+
return valueArg;
|
|
697
|
+
}
|
|
698
|
+
|
|
672
699
|
/** One object key prefix: a relative key path that names a whole subtree. */
|
|
673
700
|
function checkBackupExclusionKeyPrefix(valueArg: unknown, labelArg: string): string {
|
|
674
701
|
if (typeof valueArg !== 'string') return reject(`Invalid ${labelArg}: expected a string`);
|
|
@@ -741,16 +768,99 @@ function checkBackupExclusions(valueArg: unknown, labelArg: string): IAppStoreBa
|
|
|
741
768
|
previousRequest = storageRequest;
|
|
742
769
|
|
|
743
770
|
const keyPrefixes = checkBackupExclusionKeyPrefixes(entry.keyPrefixes, `${entryLabel}.keyPrefixes`);
|
|
771
|
+
return { storageRequest, keyPrefixes, reason: checkBackupExclusionReason(reason, maximumReasonLength, entryLabel) };
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* One collection name a database backup exclusion carries: a name the database engine stores
|
|
777
|
+
* (`database` resources run on NoSQLDB, a MongoDB-compatible engine) that is not a system collection.
|
|
778
|
+
*/
|
|
779
|
+
function checkDatabaseBackupExclusionCollection(valueArg: unknown, labelArg: string): string {
|
|
780
|
+
const { maximumCollectionNameBytes } = appStoreDatabaseBackupExclusionLimits;
|
|
781
|
+
if (typeof valueArg !== 'string') return reject(`Invalid ${labelArg}: expected a string`);
|
|
782
|
+
if (collectionNameControlPattern.test(valueArg) || hasUnpairedSurrogate(valueArg)) {
|
|
783
|
+
reject(`Invalid ${labelArg}: must not contain a control character or an unpaired surrogate`);
|
|
784
|
+
}
|
|
785
|
+
const byteLength = utf8Encoder.encode(valueArg).byteLength;
|
|
786
|
+
if (byteLength < 1 || byteLength > maximumCollectionNameBytes) {
|
|
787
|
+
reject(`Invalid ${labelArg}: must be 1 to ${maximumCollectionNameBytes} UTF-8 bytes`);
|
|
788
|
+
}
|
|
789
|
+
if (valueArg.startsWith('.')) reject(`Invalid ${labelArg}: must not start with '.'`);
|
|
790
|
+
if (valueArg.startsWith('system.')) reject(`Invalid ${labelArg}: must not name a 'system.' collection`);
|
|
791
|
+
if (/[/\\$]/.test(valueArg)) reject(`Invalid ${labelArg}: must not contain '/', '\\' or '$'`);
|
|
792
|
+
return valueArg;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* One exclusion's collection list: 1 to 64 names, strictly increasing.
|
|
797
|
+
* `validateAppStoreDatabaseBackupExclusionCollections` exports it, so every reader of a collection
|
|
798
|
+
* list judges it by this one rule.
|
|
799
|
+
*/
|
|
800
|
+
function checkDatabaseBackupExclusionCollections(valueArg: unknown, labelArg: string): string[] {
|
|
801
|
+
const { maximumCollections } = appStoreDatabaseBackupExclusionLimits;
|
|
802
|
+
const collectionEntries = readDataElements(valueArg, maximumCollections);
|
|
803
|
+
if (collectionEntries === null || collectionEntries.length === 0) {
|
|
804
|
+
return reject(`Invalid ${labelArg}: expected an array of 1 to ${maximumCollections} collection names`);
|
|
805
|
+
}
|
|
806
|
+
const collections: string[] = [];
|
|
807
|
+
collectionEntries.forEach((collectionArg, indexArg) => {
|
|
808
|
+
const collection = checkDatabaseBackupExclusionCollection(collectionArg, `${labelArg}[${indexArg}]`);
|
|
809
|
+
const previousCollection = collections[collections.length - 1];
|
|
810
|
+
if (previousCollection !== undefined && compareWireTokens(previousCollection, collection) >= 0) {
|
|
811
|
+
reject(`Invalid ${labelArg}: must be strictly increasing`);
|
|
812
|
+
}
|
|
813
|
+
collections.push(collection);
|
|
814
|
+
});
|
|
815
|
+
return collections;
|
|
816
|
+
}
|
|
744
817
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
818
|
+
/**
|
|
819
|
+
* The members of each database backup exclusion, its collections and the entry order. Whether the
|
|
820
|
+
* version provisions the database an entry names is judged with its platform requirements
|
|
821
|
+
* (`checkDatabaseBackupExclusionTargets`).
|
|
822
|
+
*/
|
|
823
|
+
function checkDatabaseBackupExclusions(valueArg: unknown, labelArg: string): IAppStoreDatabaseBackupExclusion[] {
|
|
824
|
+
if (valueArg === undefined) return [];
|
|
825
|
+
const { maximumEntries, maximumReasonLength } = appStoreDatabaseBackupExclusionLimits;
|
|
826
|
+
const entries = readDataElements(valueArg, maximumEntries);
|
|
827
|
+
if (entries === null || entries.length === 0) {
|
|
828
|
+
return reject(`Invalid ${labelArg}: databaseBackupExclusions must contain between 1 and ${maximumEntries} entries`);
|
|
829
|
+
}
|
|
830
|
+
let previousDatabase: string | null = null;
|
|
831
|
+
return entries.map((entryArg, indexArg): IAppStoreDatabaseBackupExclusion => {
|
|
832
|
+
const entryLabel = `${labelArg} databaseBackupExclusions[${indexArg}]`;
|
|
833
|
+
const entry = readRecord(entryArg, entryLabel);
|
|
834
|
+
assertExactKeys(entry, ['database', 'collections', 'reason'], entryLabel);
|
|
835
|
+
const { database } = entry;
|
|
836
|
+
if (typeof database !== 'string' || !Object.hasOwn(backupExclusionDatabaseRequirements, database)) {
|
|
837
|
+
return reject(`Invalid ${entryLabel}.database: expected one of ${Object.keys(backupExclusionDatabaseRequirements).join(', ')}`);
|
|
838
|
+
}
|
|
839
|
+
if (previousDatabase !== null && compareWireTokens(previousDatabase, database) >= 0) {
|
|
840
|
+
reject(previousDatabase === database
|
|
841
|
+
? `Invalid ${labelArg}: databaseBackupExclusions name database '${database}' more than once`
|
|
842
|
+
: `Invalid ${labelArg}: databaseBackupExclusions must be in strictly increasing database order`);
|
|
843
|
+
}
|
|
844
|
+
previousDatabase = database;
|
|
845
|
+
return {
|
|
846
|
+
database: database as TAppStoreBackupExclusionDatabase,
|
|
847
|
+
collections: checkDatabaseBackupExclusionCollections(entry.collections, `${entryLabel}.collections`),
|
|
848
|
+
reason: checkBackupExclusionReason(entry.reason, maximumReasonLength, entryLabel),
|
|
849
|
+
};
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/** Each database backup exclusion names a database the version provisions. */
|
|
854
|
+
function checkDatabaseBackupExclusionTargets(
|
|
855
|
+
exclusionsArg: readonly IAppStoreDatabaseBackupExclusion[],
|
|
856
|
+
platformRequirementsArg: Record<string, unknown>,
|
|
857
|
+
labelArg: string,
|
|
858
|
+
): void {
|
|
859
|
+
exclusionsArg.forEach((exclusionArg, indexArg) => {
|
|
860
|
+
const requirement = backupExclusionDatabaseRequirements[exclusionArg.database];
|
|
861
|
+
if (platformRequirementsArg[requirement] !== true) {
|
|
862
|
+
reject(`Invalid ${labelArg} databaseBackupExclusions[${indexArg}].database: '${exclusionArg.database}' requires platformRequirements.${requirement}`);
|
|
752
863
|
}
|
|
753
|
-
return { storageRequest, keyPrefixes, reason };
|
|
754
864
|
});
|
|
755
865
|
}
|
|
756
866
|
|
|
@@ -842,7 +952,15 @@ function checkStorage(configArg: Record<string, unknown>, labelArg: string): voi
|
|
|
842
952
|
labelArg,
|
|
843
953
|
);
|
|
844
954
|
const backupExclusions = checkBackupExclusions(configArg.backupExclusions, labelArg);
|
|
845
|
-
|
|
955
|
+
const databaseBackupExclusions = checkDatabaseBackupExclusions(configArg.databaseBackupExclusions, labelArg);
|
|
956
|
+
checkFeatureDeclaration(
|
|
957
|
+
configArg.requiresFeatures,
|
|
958
|
+
explicitRequests,
|
|
959
|
+
backupExclusions,
|
|
960
|
+
databaseBackupExclusions,
|
|
961
|
+
labelArg,
|
|
962
|
+
);
|
|
963
|
+
checkDatabaseBackupExclusionTargets(databaseBackupExclusions, platformRequirements, labelArg);
|
|
846
964
|
const legacyS3 = platformRequirements.s3 === true;
|
|
847
965
|
if (legacyS3 && explicitRequests.some((requestArg) => requestArg.kind === 'objectStorage')) {
|
|
848
966
|
reject(`Invalid ${labelArg}: platformRequirements.s3 cannot coexist with explicit objectStorage requests`);
|
|
@@ -923,8 +1041,9 @@ export const validateAppStorePublishedPorts = (publishedPortsArg: unknown, label
|
|
|
923
1041
|
|
|
924
1042
|
/**
|
|
925
1043
|
* The config's member set and its storage: legacy `volumes`, `storageClasses`, `storageRequests`,
|
|
926
|
-
* `platformRequirements`, `backupExclusions`, the storage feature ids
|
|
927
|
-
* and every environment, secret-file and mount destination being used
|
|
1044
|
+
* `platformRequirements`, `backupExclusions`, `databaseBackupExclusions`, the storage feature ids
|
|
1045
|
+
* `requiresFeatures` must name, and every environment, secret-file and mount destination being used
|
|
1046
|
+
* once.
|
|
928
1047
|
*/
|
|
929
1048
|
export const validateAppStoreVersionStorage = (configArg: unknown, labelArg = defaultLabel): string[] =>
|
|
930
1049
|
firstRejection(() => checkStorage(readConfig(configArg, labelArg), labelArg));
|
|
@@ -941,6 +1060,18 @@ export const validateAppStoreBackupExclusionKeyPrefixes = (
|
|
|
941
1060
|
labelArg = 'keyPrefixes',
|
|
942
1061
|
): string[] => firstRejection(() => checkBackupExclusionKeyPrefixes(valueArg, labelArg));
|
|
943
1062
|
|
|
1063
|
+
/**
|
|
1064
|
+
* One database backup exclusion's `collections`, the rule `validateAppStoreVersionStorage` applies
|
|
1065
|
+
* to each `IAppStoreDatabaseBackupExclusion`: 1 to 64 names in strictly increasing UTF-16
|
|
1066
|
+
* code-unit order, each 1 to 128 UTF-8 bytes, not starting with `.` or `system.`, without `/`,
|
|
1067
|
+
* `\`, `$`, a control character or an unpaired surrogate. A reader of a list that carries such
|
|
1068
|
+
* names elsewhere, such as Corestore's export request or a backup's provenance, judges it here.
|
|
1069
|
+
*/
|
|
1070
|
+
export const validateAppStoreDatabaseBackupExclusionCollections = (
|
|
1071
|
+
valueArg: unknown,
|
|
1072
|
+
labelArg = 'collections',
|
|
1073
|
+
): string[] => firstRejection(() => checkDatabaseBackupExclusionCollections(valueArg, labelArg));
|
|
1074
|
+
|
|
944
1075
|
/**
|
|
945
1076
|
* Every member rule of a version config, in the order a host applies them: image, port model,
|
|
946
1077
|
* 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';
|