@serve.zone/interfaces 32.56.0 → 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 +28 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- 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 +58 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/index.ts +1 -0
- package/ts/data/runtimesessionrefusal.ts +153 -0
package/changelog.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2026-10-14 - 32.57.0
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- `runtimeSessionRefusalContract`, `IRuntimeSessionRefusal` and
|
|
8
|
+
`TRuntimeSessionMomentaryRefusalReason`: the `TypedResponseError.errorData` a controller or relay
|
|
9
|
+
answers a refused runtime session registration, read or report with, exactly
|
|
10
|
+
`{ code: 'runtime-session-report-refused', reason }` — the wire shape Cloudly ≥33.19.1 emits
|
|
11
|
+
for budget and stopping refusals and Pallet 36.3.14 reads, now owned here. `momentaryReasons`
|
|
12
|
+
(`controller-deadline`, `controller-unavailable`) are the controller's own momentary state,
|
|
13
|
+
after which a registering node offers again; every other reason, and an error without this
|
|
14
|
+
payload, is a verdict. The distinction governs only whether a node defers a registration;
|
|
15
|
+
report refusal reasons are unchanged. A reason is lowercase letter words joined by single
|
|
16
|
+
hyphens, at most 64 characters, so it names a check and never carries a value. The rule
|
|
17
|
+
producers adopt: Cloudly states `controller-unavailable` for every transient failure of its own
|
|
18
|
+
on a registration (today only when stopping), and a relay states only `controller-unavailable`,
|
|
19
|
+
only when it cannot carry.
|
|
20
|
+
- `buildRuntimeSessionRefusal(reason)` (throws on a non-word reason),
|
|
21
|
+
`validateRuntimeSessionRefusal(value, path?)`, `readRuntimeSessionRefusal(value)` (exactly
|
|
22
|
+
`code` and `reason`, own data properties only) and `readMomentaryRuntimeSessionRefusal(value)`.
|
|
23
|
+
The readers refuse any extra member, so a producer gates a new member on the peer's interfaces
|
|
24
|
+
version.
|
|
25
|
+
- `runtimeSessionRequestBounds`: Cloudly's 6 s `controllerBudgetMs` for every node request, a
|
|
26
|
+
relay's 9 s `relayRegistrationBudgetMs` from a registration's arrival to its answer (beyond
|
|
27
|
+
Cloudly's budget by more than a second, below TypedSocket's 10 s restore deadline) and its 8 s
|
|
28
|
+
`relaySessionForwardMs` for session reads and reports, so Cloudly's stated refusal reaches the
|
|
29
|
+
node before a relay gives up.
|
|
30
|
+
|
|
3
31
|
## 2026-10-13 - 32.56.0
|
|
4
32
|
|
|
5
33
|
### Features
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@serve.zone/interfaces',
|
|
6
|
-
version: '32.
|
|
6
|
+
version: '32.57.0',
|
|
7
7
|
description: 'Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.'
|
|
8
8
|
};
|
|
9
9
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSx3QkFBd0I7SUFDOUIsT0FBTyxFQUFFLFNBQVM7SUFDbEIsV0FBVyxFQUFFLHVGQUF1RjtDQUNyRyxDQUFBIn0=
|
package/dist_ts/data/index.d.ts
CHANGED
|
@@ -65,6 +65,7 @@ export * from './runtimestorageclaim.js';
|
|
|
65
65
|
export * from './runtimelocalstorageclaim.js';
|
|
66
66
|
export * from './runtimelocalvolumearchive.js';
|
|
67
67
|
export * from './runtimesession.js';
|
|
68
|
+
export * from './runtimesessionrefusal.js';
|
|
68
69
|
export * from './runtimeterminalreceipt.js';
|
|
69
70
|
export * from './secret.js';
|
|
70
71
|
export * from './baremetal.js';
|
package/dist_ts/data/index.js
CHANGED
|
@@ -61,6 +61,7 @@ export * from './runtimestorageclaim.js';
|
|
|
61
61
|
export * from './runtimelocalstorageclaim.js';
|
|
62
62
|
export * from './runtimelocalvolumearchive.js';
|
|
63
63
|
export * from './runtimesession.js';
|
|
64
|
+
export * from './runtimesessionrefusal.js';
|
|
64
65
|
export * from './runtimeterminalreceipt.js';
|
|
65
66
|
export * from './secret.js';
|
|
66
67
|
export * from './baremetal.js';
|
|
@@ -83,4 +84,4 @@ export * from './traffic.js';
|
|
|
83
84
|
export * from './user.js';
|
|
84
85
|
export * from './version.js';
|
|
85
86
|
export * from './webpush.js';
|
|
86
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
87
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9kYXRhL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLGNBQWMsb0JBQW9CLENBQUM7QUFDbkMsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLGlCQUFpQixDQUFDO0FBQ2hDLGNBQWMscUJBQXFCLENBQUM7QUFDcEMsY0FBYyxnQ0FBZ0MsQ0FBQztBQUMvQyxjQUFjLDZCQUE2QixDQUFDO0FBQzVDLGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsZ0JBQWdCLENBQUM7QUFDL0IsY0FBYyw0QkFBNEIsQ0FBQztBQUMzQyxjQUFjLDhCQUE4QixDQUFDO0FBQzdDLGNBQWMsZUFBZSxDQUFDO0FBQzlCLGNBQWMsdUJBQXVCLENBQUM7QUFDdEMsY0FBYyxpQkFBaUIsQ0FBQztBQUNoQyxjQUFjLHlCQUF5QixDQUFDO0FBQ3hDLGNBQWMsNkJBQTZCLENBQUM7QUFDNUMsY0FBYyw0QkFBNEIsQ0FBQztBQUMzQyxjQUFjLDBCQUEwQixDQUFDO0FBQ3pDLGNBQWMsMEJBQTBCLENBQUM7QUFDekMsY0FBYyxnQkFBZ0IsQ0FBQztBQUMvQixjQUFjLFVBQVUsQ0FBQztBQUN6QixjQUFjLGFBQWEsQ0FBQztBQUM1QixjQUFjLGFBQWEsQ0FBQztBQUM1QixjQUFjLFlBQVksQ0FBQztBQUMzQixjQUFjLHVCQUF1QixDQUFDO0FBQ3RDLGNBQWMsbUJBQW1CLENBQUM7QUFDbEMsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyxnQkFBZ0IsQ0FBQztBQUMvQixjQUFjLFlBQVksQ0FBQztBQUMzQixjQUFjLHFCQUFxQixDQUFDO0FBQ3BDLGNBQWMsc0JBQXNCLENBQUM7QUFDckMsY0FBYyw2QkFBNkIsQ0FBQztBQUM1QyxjQUFjLFdBQVcsQ0FBQztBQUMxQixjQUFjLHFCQUFxQixDQUFDO0FBQ3BDLGNBQWMscUJBQXFCLENBQUM7QUFDcEMsY0FBYyx5QkFBeUIsQ0FBQztBQUN4QyxjQUFjLHFCQUFxQixDQUFDO0FBQ3BDLGNBQWMseUJBQXlCLENBQUM7QUFDeEMsY0FBYyx5QkFBeUIsQ0FBQztBQUN4QyxjQUFjLHVCQUF1QixDQUFDO0FBQ3RDLGNBQWMsd0JBQXdCLENBQUM7QUFDdkMsY0FBYyxzQkFBc0IsQ0FBQztBQUNyQyxjQUFjLDhCQUE4QixDQUFDO0FBQzdDLGNBQWMsbUJBQW1CLENBQUM7QUFDbEMsY0FBYyxrQkFBa0IsQ0FBQztBQUNqQyxjQUFjLHdCQUF3QixDQUFDO0FBR3ZDLE9BQU8sRUFBRSwwQkFBMEIsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBRzlELE9BQU8sRUFBRSxxQ0FBcUMsRUFBRSxNQUFNLDZCQUE2QixDQUFDO0FBRXBGLE9BQU8sRUFBRSwwQ0FBMEMsRUFBRSwwQ0FBMEMsRUFBRSxNQUFNLGtDQUFrQyxDQUFDO0FBQzFJLGNBQWMsd0JBQXdCLENBQUM7QUFDdkMsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLHFCQUFxQixDQUFDO0FBQ3BDLGNBQWMsZUFBZSxDQUFDO0FBQzlCLGNBQWMscUJBQXFCLENBQUM7QUFDcEMsY0FBYyx3QkFBd0IsQ0FBQztBQUN2QyxjQUFjLDRCQUE0QixDQUFDO0FBQzNDLGNBQWMsNEJBQTRCLENBQUM7QUFDM0MsY0FBYyw2QkFBNkIsQ0FBQztBQUM1QyxjQUFjLG9CQUFvQixDQUFDO0FBQ25DLGNBQWMsMEJBQTBCLENBQUM7QUFDekMsY0FBYywrQkFBK0IsQ0FBQztBQUM5QyxjQUFjLGdDQUFnQyxDQUFDO0FBQy9DLGNBQWMscUJBQXFCLENBQUM7QUFDcEMsY0FBYyw0QkFBNEIsQ0FBQztBQUMzQyxjQUFjLDZCQUE2QixDQUFDO0FBQzVDLGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsZ0JBQWdCLENBQUM7QUFDL0IsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxrQkFBa0IsQ0FBQztBQUNqQyxjQUFjLDBCQUEwQixDQUFDO0FBQ3pDLGNBQWMsZUFBZSxDQUFDO0FBQzlCLGNBQWMsNEJBQTRCLENBQUM7QUFDM0MsY0FBYyxzQkFBc0IsQ0FBQztBQUNyQyxjQUFjLGNBQWMsQ0FBQztBQUM3QixjQUFjLHdCQUF3QixDQUFDO0FBQ3ZDLGNBQWMsK0JBQStCLENBQUM7QUFDOUMsY0FBYyxtQkFBbUIsQ0FBQztBQUNsQyxjQUFjLHFCQUFxQixDQUFDO0FBQ3BDLGNBQWMsa0NBQWtDLENBQUM7QUFDakQsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxvQkFBb0IsQ0FBQztBQUNuQyxjQUFjLGNBQWMsQ0FBQztBQUM3QixjQUFjLFdBQVcsQ0FBQztBQUMxQixjQUFjLGNBQWMsQ0FBQztBQUM3QixjQUFjLGNBQWMsQ0FBQyJ9
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The refusal a controller states when it does not take a node's runtime session request.
|
|
3
|
+
*
|
|
4
|
+
* It travels as `TypedResponseError.errorData` on `registerPalletRuntimeSession` and on every
|
|
5
|
+
* session-scoped read and report a node sends its controller. `code` is a frozen wire value.
|
|
6
|
+
* `momentaryReasons` are the controller's own momentary state, not a judgement of the node: a
|
|
7
|
+
* registering node waits and offers the same registration again, keeping everything it runs.
|
|
8
|
+
* Every other reason, and an error that carries no such payload, is a verdict — on a registration
|
|
9
|
+
* it ends the node's session. Momentary against verdict governs only whether a node defers a
|
|
10
|
+
* registration: a node retries its session reads and reports regardless of the reason, so the
|
|
11
|
+
* reasons a report is refused with (`state-conflict`, `*-write-conflict`, a capacity limit such as
|
|
12
|
+
* `log-intake-saturated`) are unchanged by this contract.
|
|
13
|
+
*
|
|
14
|
+
* Who states which reason today, on a registration (Cloudly ≥33.19.1, `isBudgetRefusal`):
|
|
15
|
+
* - Cloudly states `controller-deadline` when the request ran out of
|
|
16
|
+
* `runtimeSessionRequestBounds.controllerBudgetMs`, and `controller-unavailable` when it is
|
|
17
|
+
* stopping. Every other failure, a transient one of its own included, is answered without a
|
|
18
|
+
* payload and so reads as a verdict.
|
|
19
|
+
* - No relay states a refusal of its own yet.
|
|
20
|
+
*
|
|
21
|
+
* The rule producers adopt:
|
|
22
|
+
* - Cloudly states `controller-unavailable` on a registration for every transient failure of its
|
|
23
|
+
* own: its database connection lost or unavailable, a registration write conflict, a race with a
|
|
24
|
+
* credential being applied.
|
|
25
|
+
* - A relay states only `controller-unavailable`, and only when it cannot carry the request or its
|
|
26
|
+
* answer: no Cloudly link, or its own bound (`runtimeSessionRequestBounds`) ran out. Every
|
|
27
|
+
* answer Cloudly gave it forwards untouched; it never states a verdict or `controller-deadline`.
|
|
28
|
+
* - A verdict names the check that refused: a revoked or inactive credential, a deleted, retiring
|
|
29
|
+
* or foreign node, an invalid body. It never carries a momentary reason.
|
|
30
|
+
*
|
|
31
|
+
* The payload is exactly `{ code, reason }`. Pallet 36.3.14 reads the two members and tolerates
|
|
32
|
+
* others, but every reader built on `readRuntimeSessionRefusal` refuses a payload with any other
|
|
33
|
+
* member, and a refused payload reads as no refusal — a momentary refusal would turn into a
|
|
34
|
+
* verdict. A producer therefore never adds a member without gating it on the peer's spoken
|
|
35
|
+
* interfaces version.
|
|
36
|
+
*/
|
|
37
|
+
export declare const runtimeSessionRefusalContract: Readonly<{
|
|
38
|
+
readonly code: 'runtime-session-report-refused';
|
|
39
|
+
readonly momentaryReasons: readonly ["controller-deadline", "controller-unavailable"];
|
|
40
|
+
/**
|
|
41
|
+
* A reason is one or more words of lowercase ASCII letters joined by single hyphens, at most
|
|
42
|
+
* this many characters. It names a check, never a value: no digit, case, dot, colon or other
|
|
43
|
+
* separator can carry an identifier, address, digest, count or credential across.
|
|
44
|
+
*/
|
|
45
|
+
readonly maximumReasonLength: 64;
|
|
46
|
+
}>;
|
|
47
|
+
export type TRuntimeSessionMomentaryRefusalReason = typeof runtimeSessionRefusalContract.momentaryReasons[number];
|
|
48
|
+
/**
|
|
49
|
+
* `TypedResponseError.errorData` of a refused runtime session request. `reason` is a word naming
|
|
50
|
+
* the check that refused (see `runtimeSessionRefusalContract`), never a value.
|
|
51
|
+
*/
|
|
52
|
+
export interface IRuntimeSessionRefusal {
|
|
53
|
+
code: typeof runtimeSessionRefusalContract.code;
|
|
54
|
+
reason: string;
|
|
55
|
+
}
|
|
56
|
+
/** Validates one runtime session refusal and names every reason it is refused. */
|
|
57
|
+
export declare const validateRuntimeSessionRefusal: (valueArg: unknown, pathArg?: string) => string[];
|
|
58
|
+
/** The payload a controller or relay answers a refused request with; throws on a non-word reason. */
|
|
59
|
+
export declare const buildRuntimeSessionRefusal: (reasonArg: string) => IRuntimeSessionRefusal;
|
|
60
|
+
/**
|
|
61
|
+
* The refusal inside an error payload, or `null` when the value is not exactly one.
|
|
62
|
+
*
|
|
63
|
+
* It reads a value that survived serialization, the way `protocol.readProtocolRefusal` does: own
|
|
64
|
+
* data properties only, exactly `code` and `reason`, nothing that runs.
|
|
65
|
+
*/
|
|
66
|
+
export declare const readRuntimeSessionRefusal: (valueArg: unknown) => IRuntimeSessionRefusal | null;
|
|
67
|
+
/**
|
|
68
|
+
* The momentary reason an error payload states, or `null` when it is a verdict: any other reason,
|
|
69
|
+
* a payload that is not exactly a refusal, or no payload at all.
|
|
70
|
+
*/
|
|
71
|
+
export declare const readMomentaryRuntimeSessionRefusal: (valueArg: unknown) => TRuntimeSessionMomentaryRefusalReason | null;
|
|
72
|
+
/**
|
|
73
|
+
* How long each hop may hold a node's runtime session request, so the hop nearest the decision
|
|
74
|
+
* answers first and the node always reads a stated refusal rather than its own timeout.
|
|
75
|
+
*/
|
|
76
|
+
export declare const runtimeSessionRequestBounds: Readonly<{
|
|
77
|
+
/**
|
|
78
|
+
* Cloudly's budget for every node request, registration included. When it runs out Cloudly
|
|
79
|
+
* answers `controller-deadline`.
|
|
80
|
+
*/
|
|
81
|
+
readonly controllerBudgetMs: 6000;
|
|
82
|
+
/**
|
|
83
|
+
* A relay's bound on a registration, from its arrival at the relay to the answer, including any
|
|
84
|
+
* wait for the relay's own Cloudly link. It exceeds `controllerBudgetMs` by more than a second so
|
|
85
|
+
* Cloudly's own `controller-deadline` reaches the node through the relay, and stays below
|
|
86
|
+
* TypedSocket's 10 s connection-restore deadline (`TYPEDSOCKET_MAX_CONNECTION_RESTORE_TIMEOUT_MS`,
|
|
87
|
+
* which a client can only lower) so the node reads an answer before its restore hook times out.
|
|
88
|
+
* When it runs out the relay answers `controller-unavailable`.
|
|
89
|
+
*/
|
|
90
|
+
readonly relayRegistrationBudgetMs: 9000;
|
|
91
|
+
/**
|
|
92
|
+
* A relay's bound on forwarding a session read or report over its established Cloudly link.
|
|
93
|
+
* It exceeds `controllerBudgetMs` so Cloudly's answer arrives first; when it runs out the relay
|
|
94
|
+
* answers `controller-unavailable`.
|
|
95
|
+
*/
|
|
96
|
+
readonly relaySessionForwardMs: 8000;
|
|
97
|
+
}>;
|
|
@@ -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.
|
package/ts/00_commitinfo_data.ts
CHANGED
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';
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { readExactDataRecord } from '../private/dataproperties.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The refusal a controller states when it does not take a node's runtime session request.
|
|
5
|
+
*
|
|
6
|
+
* It travels as `TypedResponseError.errorData` on `registerPalletRuntimeSession` and on every
|
|
7
|
+
* session-scoped read and report a node sends its controller. `code` is a frozen wire value.
|
|
8
|
+
* `momentaryReasons` are the controller's own momentary state, not a judgement of the node: a
|
|
9
|
+
* registering node waits and offers the same registration again, keeping everything it runs.
|
|
10
|
+
* Every other reason, and an error that carries no such payload, is a verdict — on a registration
|
|
11
|
+
* it ends the node's session. Momentary against verdict governs only whether a node defers a
|
|
12
|
+
* registration: a node retries its session reads and reports regardless of the reason, so the
|
|
13
|
+
* reasons a report is refused with (`state-conflict`, `*-write-conflict`, a capacity limit such as
|
|
14
|
+
* `log-intake-saturated`) are unchanged by this contract.
|
|
15
|
+
*
|
|
16
|
+
* Who states which reason today, on a registration (Cloudly ≥33.19.1, `isBudgetRefusal`):
|
|
17
|
+
* - Cloudly states `controller-deadline` when the request ran out of
|
|
18
|
+
* `runtimeSessionRequestBounds.controllerBudgetMs`, and `controller-unavailable` when it is
|
|
19
|
+
* stopping. Every other failure, a transient one of its own included, is answered without a
|
|
20
|
+
* payload and so reads as a verdict.
|
|
21
|
+
* - No relay states a refusal of its own yet.
|
|
22
|
+
*
|
|
23
|
+
* The rule producers adopt:
|
|
24
|
+
* - Cloudly states `controller-unavailable` on a registration for every transient failure of its
|
|
25
|
+
* own: its database connection lost or unavailable, a registration write conflict, a race with a
|
|
26
|
+
* credential being applied.
|
|
27
|
+
* - A relay states only `controller-unavailable`, and only when it cannot carry the request or its
|
|
28
|
+
* answer: no Cloudly link, or its own bound (`runtimeSessionRequestBounds`) ran out. Every
|
|
29
|
+
* answer Cloudly gave it forwards untouched; it never states a verdict or `controller-deadline`.
|
|
30
|
+
* - A verdict names the check that refused: a revoked or inactive credential, a deleted, retiring
|
|
31
|
+
* or foreign node, an invalid body. It never carries a momentary reason.
|
|
32
|
+
*
|
|
33
|
+
* The payload is exactly `{ code, reason }`. Pallet 36.3.14 reads the two members and tolerates
|
|
34
|
+
* others, but every reader built on `readRuntimeSessionRefusal` refuses a payload with any other
|
|
35
|
+
* member, and a refused payload reads as no refusal — a momentary refusal would turn into a
|
|
36
|
+
* verdict. A producer therefore never adds a member without gating it on the peer's spoken
|
|
37
|
+
* interfaces version.
|
|
38
|
+
*/
|
|
39
|
+
export const runtimeSessionRefusalContract = Object.freeze({
|
|
40
|
+
code: 'runtime-session-report-refused',
|
|
41
|
+
momentaryReasons: Object.freeze(['controller-deadline', 'controller-unavailable'] as const),
|
|
42
|
+
/**
|
|
43
|
+
* A reason is one or more words of lowercase ASCII letters joined by single hyphens, at most
|
|
44
|
+
* this many characters. It names a check, never a value: no digit, case, dot, colon or other
|
|
45
|
+
* separator can carry an identifier, address, digest, count or credential across.
|
|
46
|
+
*/
|
|
47
|
+
maximumReasonLength: 64,
|
|
48
|
+
} as const);
|
|
49
|
+
|
|
50
|
+
export type TRuntimeSessionMomentaryRefusalReason =
|
|
51
|
+
typeof runtimeSessionRefusalContract.momentaryReasons[number];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* `TypedResponseError.errorData` of a refused runtime session request. `reason` is a word naming
|
|
55
|
+
* the check that refused (see `runtimeSessionRefusalContract`), never a value.
|
|
56
|
+
*/
|
|
57
|
+
export interface IRuntimeSessionRefusal {
|
|
58
|
+
code: typeof runtimeSessionRefusalContract.code;
|
|
59
|
+
reason: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const isRefusalReason = (valueArg: unknown): valueArg is string => (
|
|
63
|
+
typeof valueArg === 'string'
|
|
64
|
+
&& valueArg.length <= runtimeSessionRefusalContract.maximumReasonLength
|
|
65
|
+
&& /^[a-z]+(?:-[a-z]+)*$/.test(valueArg)
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const reasonRule = 'must be lowercase letter words joined by single hyphens, at most '
|
|
69
|
+
+ `${runtimeSessionRefusalContract.maximumReasonLength} characters`;
|
|
70
|
+
|
|
71
|
+
/** Validates one runtime session refusal and names every reason it is refused. */
|
|
72
|
+
export const validateRuntimeSessionRefusal = (
|
|
73
|
+
valueArg: unknown,
|
|
74
|
+
pathArg = 'runtime session refusal',
|
|
75
|
+
): string[] => {
|
|
76
|
+
const refusal = readExactDataRecord(valueArg, ['code', 'reason']);
|
|
77
|
+
if (refusal === null) {
|
|
78
|
+
return [`${pathArg} must use its exact schema`];
|
|
79
|
+
}
|
|
80
|
+
const errors: string[] = [];
|
|
81
|
+
if (refusal.code !== runtimeSessionRefusalContract.code) {
|
|
82
|
+
errors.push(`${pathArg} code must be ${runtimeSessionRefusalContract.code}`);
|
|
83
|
+
}
|
|
84
|
+
if (!isRefusalReason(refusal.reason)) {
|
|
85
|
+
errors.push(`${pathArg} reason ${reasonRule}`);
|
|
86
|
+
}
|
|
87
|
+
return errors;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** The payload a controller or relay answers a refused request with; throws on a non-word reason. */
|
|
91
|
+
export const buildRuntimeSessionRefusal = (reasonArg: string): IRuntimeSessionRefusal => {
|
|
92
|
+
if (!isRefusalReason(reasonArg)) {
|
|
93
|
+
throw new Error(`runtime session refusal reason ${reasonRule}`);
|
|
94
|
+
}
|
|
95
|
+
return Object.freeze({ code: runtimeSessionRefusalContract.code, reason: reasonArg });
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The refusal inside an error payload, or `null` when the value is not exactly one.
|
|
100
|
+
*
|
|
101
|
+
* It reads a value that survived serialization, the way `protocol.readProtocolRefusal` does: own
|
|
102
|
+
* data properties only, exactly `code` and `reason`, nothing that runs.
|
|
103
|
+
*/
|
|
104
|
+
export const readRuntimeSessionRefusal = (valueArg: unknown): IRuntimeSessionRefusal | null => {
|
|
105
|
+
const refusal = readExactDataRecord(valueArg, ['code', 'reason']);
|
|
106
|
+
if (refusal === null || validateRuntimeSessionRefusal(refusal).length > 0) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
return Object.freeze({ code: runtimeSessionRefusalContract.code, reason: refusal.reason as string });
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The momentary reason an error payload states, or `null` when it is a verdict: any other reason,
|
|
114
|
+
* a payload that is not exactly a refusal, or no payload at all.
|
|
115
|
+
*/
|
|
116
|
+
export const readMomentaryRuntimeSessionRefusal = (
|
|
117
|
+
valueArg: unknown,
|
|
118
|
+
): TRuntimeSessionMomentaryRefusalReason | null => {
|
|
119
|
+
const refusal = readRuntimeSessionRefusal(valueArg);
|
|
120
|
+
if (refusal === null) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
const momentary = runtimeSessionRefusalContract.momentaryReasons
|
|
124
|
+
.find((reasonArg) => reasonArg === refusal.reason);
|
|
125
|
+
return momentary ?? null;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* How long each hop may hold a node's runtime session request, so the hop nearest the decision
|
|
130
|
+
* answers first and the node always reads a stated refusal rather than its own timeout.
|
|
131
|
+
*/
|
|
132
|
+
export const runtimeSessionRequestBounds = Object.freeze({
|
|
133
|
+
/**
|
|
134
|
+
* Cloudly's budget for every node request, registration included. When it runs out Cloudly
|
|
135
|
+
* answers `controller-deadline`.
|
|
136
|
+
*/
|
|
137
|
+
controllerBudgetMs: 6_000,
|
|
138
|
+
/**
|
|
139
|
+
* A relay's bound on a registration, from its arrival at the relay to the answer, including any
|
|
140
|
+
* wait for the relay's own Cloudly link. It exceeds `controllerBudgetMs` by more than a second so
|
|
141
|
+
* Cloudly's own `controller-deadline` reaches the node through the relay, and stays below
|
|
142
|
+
* TypedSocket's 10 s connection-restore deadline (`TYPEDSOCKET_MAX_CONNECTION_RESTORE_TIMEOUT_MS`,
|
|
143
|
+
* which a client can only lower) so the node reads an answer before its restore hook times out.
|
|
144
|
+
* When it runs out the relay answers `controller-unavailable`.
|
|
145
|
+
*/
|
|
146
|
+
relayRegistrationBudgetMs: 9_000,
|
|
147
|
+
/**
|
|
148
|
+
* A relay's bound on forwarding a session read or report over its established Cloudly link.
|
|
149
|
+
* It exceeds `controllerBudgetMs` so Cloudly's answer arrives first; when it runs out the relay
|
|
150
|
+
* answers `controller-unavailable`.
|
|
151
|
+
*/
|
|
152
|
+
relaySessionForwardMs: 8_000,
|
|
153
|
+
} as const);
|