@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 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.56.0',
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=
@@ -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';
@@ -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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9kYXRhL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLGNBQWMsb0JBQW9CLENBQUM7QUFDbkMsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLGlCQUFpQixDQUFDO0FBQ2hDLGNBQWMscUJBQXFCLENBQUM7QUFDcEMsY0FBYyxnQ0FBZ0MsQ0FBQztBQUMvQyxjQUFjLDZCQUE2QixDQUFDO0FBQzVDLGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsZ0JBQWdCLENBQUM7QUFDL0IsY0FBYyw0QkFBNEIsQ0FBQztBQUMzQyxjQUFjLDhCQUE4QixDQUFDO0FBQzdDLGNBQWMsZUFBZSxDQUFDO0FBQzlCLGNBQWMsdUJBQXVCLENBQUM7QUFDdEMsY0FBYyxpQkFBaUIsQ0FBQztBQUNoQyxjQUFjLHlCQUF5QixDQUFDO0FBQ3hDLGNBQWMsNkJBQTZCLENBQUM7QUFDNUMsY0FBYyw0QkFBNEIsQ0FBQztBQUMzQyxjQUFjLDBCQUEwQixDQUFDO0FBQ3pDLGNBQWMsMEJBQTBCLENBQUM7QUFDekMsY0FBYyxnQkFBZ0IsQ0FBQztBQUMvQixjQUFjLFVBQVUsQ0FBQztBQUN6QixjQUFjLGFBQWEsQ0FBQztBQUM1QixjQUFjLGFBQWEsQ0FBQztBQUM1QixjQUFjLFlBQVksQ0FBQztBQUMzQixjQUFjLHVCQUF1QixDQUFDO0FBQ3RDLGNBQWMsbUJBQW1CLENBQUM7QUFDbEMsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyxnQkFBZ0IsQ0FBQztBQUMvQixjQUFjLFlBQVksQ0FBQztBQUMzQixjQUFjLHFCQUFxQixDQUFDO0FBQ3BDLGNBQWMsc0JBQXNCLENBQUM7QUFDckMsY0FBYyw2QkFBNkIsQ0FBQztBQUM1QyxjQUFjLFdBQVcsQ0FBQztBQUMxQixjQUFjLHFCQUFxQixDQUFDO0FBQ3BDLGNBQWMscUJBQXFCLENBQUM7QUFDcEMsY0FBYyx5QkFBeUIsQ0FBQztBQUN4QyxjQUFjLHFCQUFxQixDQUFDO0FBQ3BDLGNBQWMseUJBQXlCLENBQUM7QUFDeEMsY0FBYyx5QkFBeUIsQ0FBQztBQUN4QyxjQUFjLHVCQUF1QixDQUFDO0FBQ3RDLGNBQWMsd0JBQXdCLENBQUM7QUFDdkMsY0FBYyxzQkFBc0IsQ0FBQztBQUNyQyxjQUFjLDhCQUE4QixDQUFDO0FBQzdDLGNBQWMsbUJBQW1CLENBQUM7QUFDbEMsY0FBYyxrQkFBa0IsQ0FBQztBQUNqQyxjQUFjLHdCQUF3QixDQUFDO0FBR3ZDLE9BQU8sRUFBRSwwQkFBMEIsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBRzlELE9BQU8sRUFBRSxxQ0FBcUMsRUFBRSxNQUFNLDZCQUE2QixDQUFDO0FBRXBGLE9BQU8sRUFBRSwwQ0FBMEMsRUFBRSwwQ0FBMEMsRUFBRSxNQUFNLGtDQUFrQyxDQUFDO0FBQzFJLGNBQWMsd0JBQXdCLENBQUM7QUFDdkMsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLHFCQUFxQixDQUFDO0FBQ3BDLGNBQWMsZUFBZSxDQUFDO0FBQzlCLGNBQWMscUJBQXFCLENBQUM7QUFDcEMsY0FBYyx3QkFBd0IsQ0FBQztBQUN2QyxjQUFjLDRCQUE0QixDQUFDO0FBQzNDLGNBQWMsNEJBQTRCLENBQUM7QUFDM0MsY0FBYyw2QkFBNkIsQ0FBQztBQUM1QyxjQUFjLG9CQUFvQixDQUFDO0FBQ25DLGNBQWMsMEJBQTBCLENBQUM7QUFDekMsY0FBYywrQkFBK0IsQ0FBQztBQUM5QyxjQUFjLGdDQUFnQyxDQUFDO0FBQy9DLGNBQWMscUJBQXFCLENBQUM7QUFDcEMsY0FBYyw2QkFBNkIsQ0FBQztBQUM1QyxjQUFjLGFBQWEsQ0FBQztBQUM1QixjQUFjLGdCQUFnQixDQUFDO0FBQy9CLGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsa0JBQWtCLENBQUM7QUFDakMsY0FBYywwQkFBMEIsQ0FBQztBQUN6QyxjQUFjLGVBQWUsQ0FBQztBQUM5QixjQUFjLDRCQUE0QixDQUFDO0FBQzNDLGNBQWMsc0JBQXNCLENBQUM7QUFDckMsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyx3QkFBd0IsQ0FBQztBQUN2QyxjQUFjLCtCQUErQixDQUFDO0FBQzlDLGNBQWMsbUJBQW1CLENBQUM7QUFDbEMsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLGtDQUFrQyxDQUFDO0FBQ2pELGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsb0JBQW9CLENBQUM7QUFDbkMsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyxXQUFXLENBQUM7QUFDMUIsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyxjQUFjLENBQUMifQ==
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serve.zone/interfaces",
3
- "version": "32.56.0",
3
+ "version": "32.57.0",
4
4
  "private": false,
5
5
  "description": "Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.",
6
6
  "exports": {
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.
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/interfaces',
6
- version: '32.56.0',
6
+ version: '32.57.0',
7
7
  description: 'Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.'
8
8
  }
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);