@serfab/cadre-host 0.8.1 → 0.10.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/dist/auth/index.d.ts +2 -0
- package/dist/auth/index.js +1 -0
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/self-label.d.ts +36 -0
- package/dist/auth/self-label.js +57 -0
- package/dist/auth/self-label.js.map +1 -0
- package/dist/auth/trust-circle.d.ts +36 -9
- package/dist/auth/trust-circle.js +40 -20
- package/dist/auth/trust-circle.js.map +1 -1
- package/dist/auth/types.d.ts +2 -2
- package/dist/bin/host.js +291 -54
- package/dist/bin/host.js.map +1 -1
- package/dist/donation/__tests__/fake-orchestrator.d.ts +103 -0
- package/dist/donation/__tests__/fake-orchestrator.js +180 -0
- package/dist/donation/__tests__/fake-orchestrator.js.map +1 -0
- package/dist/donation/donation-service.d.ts +361 -0
- package/dist/donation/donation-service.js +763 -0
- package/dist/donation/donation-service.js.map +1 -0
- package/dist/donation/donation-store.d.ts +49 -0
- package/dist/donation/donation-store.js +148 -0
- package/dist/donation/donation-store.js.map +1 -0
- package/dist/donation/donation-supervisor.d.ts +167 -0
- package/dist/donation/donation-supervisor.js +341 -0
- package/dist/donation/donation-supervisor.js.map +1 -0
- package/dist/donation/grant-service.d.ts +71 -0
- package/dist/donation/grant-service.js +163 -0
- package/dist/donation/grant-service.js.map +1 -0
- package/dist/donation/grant-store.d.ts +38 -0
- package/dist/donation/grant-store.js +135 -0
- package/dist/donation/grant-store.js.map +1 -0
- package/dist/donation/index.d.ts +20 -0
- package/dist/donation/index.js +17 -0
- package/dist/donation/index.js.map +1 -0
- package/dist/donation/types.d.ts +201 -0
- package/dist/donation/types.js +40 -0
- package/dist/donation/types.js.map +1 -0
- package/dist/index.d.ts +9 -5
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/installer/config.d.ts +26 -0
- package/dist/installer/config.js +13 -0
- package/dist/installer/config.js.map +1 -1
- package/dist/installer/index.d.ts +2 -0
- package/dist/installer/index.js +3 -0
- package/dist/installer/index.js.map +1 -1
- package/dist/installer/wizard.d.ts +4 -0
- package/dist/installer/wizard.js +5 -1
- package/dist/installer/wizard.js.map +1 -1
- package/dist/nat/nat-service.d.ts +5 -5
- package/dist/nat/nat-service.js +9 -9
- package/dist/nat/nat-service.js.map +1 -1
- package/dist/nat/types.d.ts +1 -1
- package/dist/orchestrator/host-process-orchestrator.d.ts +144 -32
- package/dist/orchestrator/host-process-orchestrator.js +397 -128
- package/dist/orchestrator/host-process-orchestrator.js.map +1 -1
- package/dist/orchestrator/index.d.ts +3 -3
- package/dist/orchestrator/index.js +1 -1
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/orchestrator/node-identity.d.ts +26 -0
- package/dist/orchestrator/node-identity.js +36 -0
- package/dist/orchestrator/node-identity.js.map +1 -0
- package/dist/orchestrator/port-allocator.d.ts +14 -0
- package/dist/orchestrator/port-allocator.js +45 -0
- package/dist/orchestrator/port-allocator.js.map +1 -1
- package/dist/orchestrator/state-store.d.ts +5 -5
- package/dist/orchestrator/state-store.js +2 -2
- package/dist/orchestrator/state-store.js.map +1 -1
- package/dist/orchestrator/types.d.ts +8 -8
- package/dist/owner/index.d.ts +11 -0
- package/dist/owner/index.js +11 -0
- package/dist/owner/index.js.map +1 -0
- package/dist/owner/owner-node-client.d.ts +110 -0
- package/dist/{authority/authority-node-client.js → owner/owner-node-client.js} +58 -17
- package/dist/owner/owner-node-client.js.map +1 -0
- package/dist/server/error-handler.d.ts +22 -1
- package/dist/server/error-handler.js +64 -1
- package/dist/server/error-handler.js.map +1 -1
- package/dist/server/events/types.d.ts +3 -0
- package/dist/server/index.d.ts +33 -2
- package/dist/server/index.js +43 -16
- package/dist/server/index.js.map +1 -1
- package/dist/server/routes/grants-admin.d.ts +17 -0
- package/dist/server/routes/grants-admin.js +32 -0
- package/dist/server/routes/grants-admin.js.map +1 -0
- package/dist/server/routes/grants.d.ts +28 -0
- package/dist/server/routes/grants.js +182 -0
- package/dist/server/routes/grants.js.map +1 -0
- package/dist/server/routes/nodes.d.ts +5 -4
- package/dist/server/routes/nodes.js +19 -18
- package/dist/server/routes/nodes.js.map +1 -1
- package/dist/server/routes/settings.d.ts +6 -1
- package/dist/server/routes/settings.js +5 -2
- package/dist/server/routes/settings.js.map +1 -1
- package/dist/server/routes/status.d.ts +8 -4
- package/dist/server/routes/status.js +9 -4
- package/dist/server/routes/status.js.map +1 -1
- package/dist/server/routes/strands.d.ts +20 -0
- package/dist/server/routes/strands.js +38 -0
- package/dist/server/routes/strands.js.map +1 -0
- package/dist/strands/index.d.ts +7 -0
- package/dist/strands/index.js +6 -0
- package/dist/strands/index.js.map +1 -0
- package/dist/strands/strand-service.d.ts +50 -0
- package/dist/strands/strand-service.js +103 -0
- package/dist/strands/strand-service.js.map +1 -0
- package/dist/strands/types.d.ts +93 -0
- package/dist/strands/types.js +23 -0
- package/dist/strands/types.js.map +1 -0
- package/dist/ui/assets/index-BVoqLjyX.css +1 -0
- package/dist/ui/assets/index-C-iks41M.js +17 -0
- package/dist/ui/assets/index-C-iks41M.js.map +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/update/release-key.d.ts +1 -1
- package/dist/update/release-key.js +8 -6
- package/dist/update/release-key.js.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/build-targets.test.ts +26 -0
- package/src/__tests__/global-setup.ts +51 -0
- package/src/__tests__/{host-authority.smoke.test.ts → host-owner.smoke.test.ts} +7 -7
- package/src/__tests__/orchestrator-env-scrub.test.ts +140 -0
- package/src/__tests__/orchestrator-node-identity.test.ts +204 -0
- package/src/__tests__/{orchestrator-authority.test.ts → orchestrator-owner.test.ts} +33 -33
- package/src/__tests__/orchestrator-pin-keys.test.ts +153 -0
- package/src/__tests__/orchestrator-push.test.ts +10 -10
- package/src/__tests__/orchestrator.test.ts +387 -6
- package/src/auth/__tests__/self-label.test.ts +85 -0
- package/src/auth/__tests__/trust-circle-integration.test.ts +152 -147
- package/src/auth/__tests__/trust-circle.test.ts +55 -11
- package/src/auth/index.ts +2 -0
- package/src/auth/self-label.ts +74 -0
- package/src/auth/trust-circle.ts +54 -22
- package/src/auth/types.ts +2 -2
- package/src/bin/host.ts +1299 -1038
- package/src/donation/__tests__/donation-service.test.ts +1068 -0
- package/src/donation/__tests__/donation-supervisor.test.ts +510 -0
- package/src/donation/__tests__/fake-orchestrator.test.ts +168 -0
- package/src/donation/__tests__/fake-orchestrator.ts +225 -0
- package/src/donation/__tests__/grant-service.test.ts +213 -0
- package/src/donation/__tests__/grant-store.test.ts +113 -0
- package/src/donation/donation-service.ts +962 -0
- package/src/donation/donation-store.ts +171 -0
- package/src/donation/donation-supervisor.ts +388 -0
- package/src/donation/grant-service.ts +200 -0
- package/src/donation/grant-store.ts +152 -0
- package/src/donation/index.ts +64 -0
- package/src/donation/types.ts +243 -0
- package/src/index.ts +51 -5
- package/src/installer/__tests__/config.test.ts +27 -1
- package/src/installer/__tests__/installer.smoke.test.ts +31 -1
- package/src/installer/__tests__/wizard.test.ts +5 -1
- package/src/installer/config.ts +34 -0
- package/src/installer/index.ts +5 -0
- package/src/installer/wizard.ts +12 -1
- package/src/nat/__tests__/nat-service.test.ts +5 -5
- package/src/nat/nat-service.ts +10 -10
- package/src/nat/types.ts +1 -1
- package/src/orchestrator/__tests__/node-identity.test.ts +62 -0
- package/src/orchestrator/host-process-orchestrator.ts +401 -134
- package/src/orchestrator/index.ts +3 -3
- package/src/orchestrator/node-identity.ts +40 -0
- package/src/orchestrator/port-allocator.ts +48 -0
- package/src/orchestrator/state-store.ts +7 -7
- package/src/orchestrator/types.ts +8 -8
- package/src/{authority/__tests__/authority-node-client.test.ts → owner/__tests__/owner-node-client.test.ts} +45 -17
- package/src/owner/index.ts +12 -0
- package/src/{authority/authority-node-client.ts → owner/owner-node-client.ts} +83 -23
- package/src/server/__tests__/error-handler.test.ts +22 -0
- package/src/server/__tests__/grants-admin-route.test.ts +73 -0
- package/src/server/__tests__/grants-route.test.ts +263 -0
- package/src/server/__tests__/nodes-route.test.ts +25 -25
- package/src/server/__tests__/server.smoke.test.ts +75 -0
- package/src/server/__tests__/settings-route.test.ts +17 -0
- package/src/server/__tests__/status-route.test.ts +21 -0
- package/src/server/__tests__/strands-route.test.ts +233 -0
- package/src/server/error-handler.ts +67 -1
- package/src/server/events/types.ts +1 -0
- package/src/server/index.ts +75 -17
- package/src/server/routes/grants-admin.ts +41 -0
- package/src/server/routes/grants.ts +210 -0
- package/src/server/routes/nodes.ts +19 -18
- package/src/server/routes/settings.ts +11 -3
- package/src/server/routes/status.ts +18 -8
- package/src/server/routes/strands.ts +54 -0
- package/src/strands/__tests__/strand-service.test.ts +165 -0
- package/src/strands/index.ts +14 -0
- package/src/strands/strand-service.ts +135 -0
- package/src/strands/types.ts +102 -0
- package/src/update/__tests__/release-key.test.ts +16 -3
- package/src/update/release-key.ts +8 -6
- package/dist/authority/authority-node-client.d.ts +0 -77
- package/dist/authority/authority-node-client.js.map +0 -1
- package/dist/authority/index.d.ts +0 -11
- package/dist/authority/index.js +0 -11
- package/dist/authority/index.js.map +0 -1
- package/dist/ui/assets/index-BlMXQPSw.js +0 -14
- package/dist/ui/assets/index-BlMXQPSw.js.map +0 -1
- package/dist/ui/assets/index-BwU5mJGF.css +0 -1
- package/src/authority/index.ts +0 -12
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import debug from 'debug';
|
|
2
|
+
const log = debug('cadre:host:donation-supervisor');
|
|
3
|
+
/**
|
|
4
|
+
* Unit of the doubling respawn backoff: the wait after `n` consecutive failed
|
|
5
|
+
* attempts is `base * 2^n`, capped at {@link DONATION_RESPAWN_BACKOFF_MAX_MS}.
|
|
6
|
+
* So the first retry waits 10s (one attempt is already recorded by then), the
|
|
7
|
+
* second 20s, and so on. A node with no recorded attempt is respawned
|
|
8
|
+
* immediately.
|
|
9
|
+
*/
|
|
10
|
+
export const DONATION_RESPAWN_BACKOFF_BASE_MS = 5000;
|
|
11
|
+
/**
|
|
12
|
+
* Ceiling on the doubling respawn backoff. 5 minutes.
|
|
13
|
+
*
|
|
14
|
+
* NOTE: not reachable at the current {@link DONATION_RESPAWN_MAX_ATTEMPTS} of 5
|
|
15
|
+
* — the longest wait actually served is the one before the 5th attempt,
|
|
16
|
+
* `5s * 2^4` = 80s, and the 5th failure gives up. The cap only starts clamping
|
|
17
|
+
* once a record can reach 6 recorded attempts (`5s * 2^6` = 320s), so raising
|
|
18
|
+
* the attempt cap without revisiting this one changes nothing.
|
|
19
|
+
*/
|
|
20
|
+
export const DONATION_RESPAWN_BACKOFF_MAX_MS = 5 * 60000;
|
|
21
|
+
/**
|
|
22
|
+
* Consecutive respawn attempts after which the host stops trying and marks the
|
|
23
|
+
* donation `error` — a node that will not stay up is a host-side fault the
|
|
24
|
+
* borrower cannot see, so surface it instead of spinning forever.
|
|
25
|
+
*/
|
|
26
|
+
export const DONATION_RESPAWN_MAX_ATTEMPTS = 5;
|
|
27
|
+
/**
|
|
28
|
+
* How long a respawned node must stay up before its attempt budget is refilled.
|
|
29
|
+
* The last respawn produced something that survived, so the next crash starts
|
|
30
|
+
* from a fresh budget rather than inheriting an old crash loop's count.
|
|
31
|
+
*/
|
|
32
|
+
export const DONATION_RESPAWN_HEALTHY_MS = 10 * 60000;
|
|
33
|
+
/** How often the supervisor sweep runs while cadre-host is up. 1 minute. */
|
|
34
|
+
export const DONATION_RESPAWN_SWEEP_MS = 60000;
|
|
35
|
+
/**
|
|
36
|
+
* Statuses the supervisor considers. `provisioning` is a provision still in
|
|
37
|
+
* flight (its own code path owns the child); `error` and `terminated` are
|
|
38
|
+
* terminal — a loan the host gave up on or the borrower ended, neither of which
|
|
39
|
+
* may come back on its own.
|
|
40
|
+
*/
|
|
41
|
+
const SUPERVISED_STATUSES = new Set([
|
|
42
|
+
'awaiting_seed',
|
|
43
|
+
'seeded',
|
|
44
|
+
]);
|
|
45
|
+
/**
|
|
46
|
+
* DonationSupervisor — owns the invariant *a non-terminal donation is expected
|
|
47
|
+
* to be running*.
|
|
48
|
+
*
|
|
49
|
+
* Nothing else re-spawns a donated node: `bin/host.ts` re-spawns only the host's
|
|
50
|
+
* own owner node, the `/api/nodes/:id/{start,restart}` routes refuse non-owner
|
|
51
|
+
* ids, and the stale-`awaiting_seed` reap only terminates. So without this class
|
|
52
|
+
* a crashed, OOM-killed, or reboot-killed donated node stays dead with its
|
|
53
|
+
* record still reading `seeded`, silently costing the borrower a node and their
|
|
54
|
+
* grant a quota slot.
|
|
55
|
+
*
|
|
56
|
+
* One code path ({@link reconcile}) behind three triggers:
|
|
57
|
+
*
|
|
58
|
+
* - **host startup** — one pass after `orchestrator.init()`, which catches every
|
|
59
|
+
* node that died while the host was down;
|
|
60
|
+
* - **exit signal** — the orchestrator's `onStateChange`, so a crash is noticed
|
|
61
|
+
* in milliseconds rather than at the next sweep;
|
|
62
|
+
* - **periodic sweep** — the backstop for deaths no exit event covers (host was
|
|
63
|
+
* down, listener missed, orchestrator re-attached to an already-dead pid).
|
|
64
|
+
*
|
|
65
|
+
* Passes are serialized: `DonationService.respawn` is not itself serialized, and
|
|
66
|
+
* two overlapping respawns of one id both spawn a child while the second drops
|
|
67
|
+
* the first's orchestrator handle (see its docstring). Since two of the three
|
|
68
|
+
* triggers can fire at once, the serialization lives here.
|
|
69
|
+
*/
|
|
70
|
+
export class DonationSupervisor {
|
|
71
|
+
constructor(opts) {
|
|
72
|
+
/** Serialization tail — see the class docstring. */
|
|
73
|
+
this.tail = Promise.resolve();
|
|
74
|
+
/** At most one exit-triggered pass queued at a time (crash storms coalesce). */
|
|
75
|
+
this.exitPassQueued = false;
|
|
76
|
+
/** Set by `stop()` so an in-flight pass abandons its remaining records. */
|
|
77
|
+
this.disposed = false;
|
|
78
|
+
this.service = opts.service;
|
|
79
|
+
this.store = opts.store;
|
|
80
|
+
this.orchestrator = opts.orchestrator;
|
|
81
|
+
this.now = opts.now ?? (() => new Date());
|
|
82
|
+
log('DonationSupervisor initialized');
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* One reconcile pass over the donation store. Serialized against every other
|
|
86
|
+
* pass, so a caller may get a queued pass rather than an immediate one.
|
|
87
|
+
* Returns the donation ids it respawned in *its own* pass.
|
|
88
|
+
*/
|
|
89
|
+
reconcile() {
|
|
90
|
+
const run = () => this.reconcileOnce();
|
|
91
|
+
const next = this.tail.then(run, run);
|
|
92
|
+
// The stored tail swallows outcomes — a failed pass must not reject the
|
|
93
|
+
// next caller's wait, only sequence after it.
|
|
94
|
+
this.tail = next.then(() => undefined, () => undefined);
|
|
95
|
+
return next;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Subscribe to child exits, start the periodic sweep, and run the startup
|
|
99
|
+
* pass. Idempotent — a second call while started is a no-op.
|
|
100
|
+
*/
|
|
101
|
+
start() {
|
|
102
|
+
if (this.timer)
|
|
103
|
+
return;
|
|
104
|
+
this.disposed = false;
|
|
105
|
+
// Subscribe before the first pass so an exit during it is not missed.
|
|
106
|
+
this.unsubscribe = this.orchestrator.onStateChange((info) => { this.onNodeState(info); });
|
|
107
|
+
this.timer = setInterval(() => { this.sweep('timer'); }, DONATION_RESPAWN_SWEEP_MS);
|
|
108
|
+
this.timer.unref();
|
|
109
|
+
this.sweep('startup');
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Stop the timer and unsubscribe. A pass already in flight abandons its
|
|
113
|
+
* remaining records rather than being awaited — every record it has not
|
|
114
|
+
* reached is picked up by the next host start's startup pass.
|
|
115
|
+
*/
|
|
116
|
+
stop() {
|
|
117
|
+
this.disposed = true;
|
|
118
|
+
if (this.timer) {
|
|
119
|
+
clearInterval(this.timer);
|
|
120
|
+
this.timer = undefined;
|
|
121
|
+
}
|
|
122
|
+
this.unsubscribe?.();
|
|
123
|
+
this.unsubscribe = undefined;
|
|
124
|
+
log('DonationSupervisor stopped');
|
|
125
|
+
}
|
|
126
|
+
/** Fire-and-forget pass for the timer / exit / startup triggers. */
|
|
127
|
+
sweep(trigger) {
|
|
128
|
+
void this.reconcile()
|
|
129
|
+
.then((ids) => {
|
|
130
|
+
if (ids.length)
|
|
131
|
+
log('%s sweep respawned %d donation(s): %o', trigger, ids.length, ids);
|
|
132
|
+
})
|
|
133
|
+
.catch((err) => {
|
|
134
|
+
console.error(`donation respawn sweep failed: ${errorMessage(err)}`);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Exit-event trigger. Deliberately cheap: it does not map the handle back to a
|
|
139
|
+
* donation (that needs a store read per event), it just asks for a pass. Owner
|
|
140
|
+
* node exits are not ours — `bin/host.ts` owns re-spawning that one.
|
|
141
|
+
*/
|
|
142
|
+
onNodeState(info) {
|
|
143
|
+
if (info.owner || info.status !== 'stopped')
|
|
144
|
+
return;
|
|
145
|
+
if (this.exitPassQueued)
|
|
146
|
+
return;
|
|
147
|
+
this.exitPassQueued = true;
|
|
148
|
+
void this.reconcile()
|
|
149
|
+
.catch((err) => { console.error(`donation respawn sweep failed: ${errorMessage(err)}`); })
|
|
150
|
+
.finally(() => { this.exitPassQueued = false; });
|
|
151
|
+
}
|
|
152
|
+
/** The unserialized body of one pass. */
|
|
153
|
+
async reconcileOnce() {
|
|
154
|
+
const respawned = [];
|
|
155
|
+
let candidates;
|
|
156
|
+
try {
|
|
157
|
+
candidates = this.store.list().filter((d) => SUPERVISED_STATUSES.has(d.status));
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
// A malformed donations.json throws on every load; log rather than let an
|
|
161
|
+
// unhandled rejection escape the timer.
|
|
162
|
+
log('reconcile could not list donations: %s', errorMessage(err));
|
|
163
|
+
return respawned;
|
|
164
|
+
}
|
|
165
|
+
for (const donation of candidates) {
|
|
166
|
+
if (this.disposed)
|
|
167
|
+
break;
|
|
168
|
+
try {
|
|
169
|
+
const id = await this.reconcileOne(donation);
|
|
170
|
+
if (id)
|
|
171
|
+
respawned.push(id);
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
// One bad record must never end the sweep — the others are still down.
|
|
175
|
+
log('reconcile of donation %s failed: %s', donation.id, errorMessage(err));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return respawned;
|
|
179
|
+
}
|
|
180
|
+
/** Reconcile one record. Returns its id when this pass respawned it. */
|
|
181
|
+
async reconcileOne(donation) {
|
|
182
|
+
// No handle means nothing was ever spawned — `provision` owns that record
|
|
183
|
+
// until it writes one, and replaying it here would race that provision.
|
|
184
|
+
if (!donation.dockerId)
|
|
185
|
+
return undefined;
|
|
186
|
+
if (await this.orchestrator.isRunning(donation.dockerId)) {
|
|
187
|
+
this.refillBudgetIfHealthy(donation);
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
if (!this.backoffElapsed(donation)) {
|
|
191
|
+
log('donation %s is down but still inside its respawn backoff', donation.id);
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
return this.attemptRespawn(donation);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Whether enough time has passed since the last attempt. A record with no
|
|
198
|
+
* attempt history (or an unparsable timestamp — never wedge a node on bad
|
|
199
|
+
* data) is eligible immediately.
|
|
200
|
+
*/
|
|
201
|
+
backoffElapsed(donation) {
|
|
202
|
+
const respawn = donation.respawn;
|
|
203
|
+
if (!respawn)
|
|
204
|
+
return true;
|
|
205
|
+
const last = Date.parse(respawn.lastAttemptAt);
|
|
206
|
+
if (Number.isNaN(last))
|
|
207
|
+
return true;
|
|
208
|
+
const delay = Math.min(DONATION_RESPAWN_BACKOFF_BASE_MS * 2 ** respawn.attempts, DONATION_RESPAWN_BACKOFF_MAX_MS);
|
|
209
|
+
return this.now().getTime() - last >= delay;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Clear the attempt count once a respawned node has been up long enough to
|
|
213
|
+
* count as healthy, so the next crash gets a full budget instead of inheriting
|
|
214
|
+
* the previous crash loop's.
|
|
215
|
+
*
|
|
216
|
+
* `updatedAt` is deliberately left alone: it is the age the stale-`awaiting_seed`
|
|
217
|
+
* reap measures, and a liveness observation is not borrower activity.
|
|
218
|
+
*/
|
|
219
|
+
refillBudgetIfHealthy(donation) {
|
|
220
|
+
if (!this.budgetRefillDue(donation))
|
|
221
|
+
return;
|
|
222
|
+
// Re-read before writing: `donation` comes from the snapshot this pass
|
|
223
|
+
// started from, which can be several awaits old, and `store.put` replaces
|
|
224
|
+
// the whole row — writing the stale copy back would undo a concurrent seed
|
|
225
|
+
// or terminate that landed mid-pass.
|
|
226
|
+
const current = this.store.get(donation.id);
|
|
227
|
+
if (!current?.respawn || !this.budgetRefillDue(current))
|
|
228
|
+
return;
|
|
229
|
+
try {
|
|
230
|
+
this.store.put({ ...current, respawn: { ...current.respawn, attempts: 0 } });
|
|
231
|
+
log('donation %s is healthy again — respawn budget refilled', donation.id);
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
// Costs one attempt off the next crash's budget, nothing more.
|
|
235
|
+
log('failed to refill respawn budget for %s: %s', donation.id, errorMessage(err));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Whether a record carries a crash-loop count old enough to clear. An
|
|
240
|
+
* unparsable timestamp is treated as not-due — unlike the backoff check,
|
|
241
|
+
* being wrong here would erase real attempt history rather than free a node.
|
|
242
|
+
*/
|
|
243
|
+
budgetRefillDue(donation) {
|
|
244
|
+
const respawn = donation.respawn;
|
|
245
|
+
if (!respawn || respawn.attempts === 0)
|
|
246
|
+
return false;
|
|
247
|
+
const last = Date.parse(respawn.lastAttemptAt);
|
|
248
|
+
if (Number.isNaN(last))
|
|
249
|
+
return false;
|
|
250
|
+
return this.now().getTime() - last > DONATION_RESPAWN_HEALTHY_MS;
|
|
251
|
+
}
|
|
252
|
+
/** One respawn attempt, with the give-up check on failure. */
|
|
253
|
+
async attemptRespawn(donation) {
|
|
254
|
+
try {
|
|
255
|
+
const result = await this.service.respawn(donation.id);
|
|
256
|
+
switch (result.outcome) {
|
|
257
|
+
case 'not_respawnable':
|
|
258
|
+
// A record written before the spawn inputs were persisted. Not
|
|
259
|
+
// respawnable and never will be; skip it every pass (cheap — no
|
|
260
|
+
// orchestrator call is reached) rather than fail the sweep.
|
|
261
|
+
log('donation %s is down but not respawnable (no persisted spawn inputs)', donation.id);
|
|
262
|
+
return undefined;
|
|
263
|
+
case 'abandoned':
|
|
264
|
+
// The loan ended while the spawn was in flight and the ending won.
|
|
265
|
+
// Nothing threw, so no attempt is persisted and the give-up path is
|
|
266
|
+
// never entered — correct: this is not a failed attempt.
|
|
267
|
+
log('respawn of donation %s was abandoned (record went %s mid-spawn)', donation.id, result.status ?? 'missing');
|
|
268
|
+
return undefined;
|
|
269
|
+
case 'respawned':
|
|
270
|
+
log('respawned donation %s → %s', donation.id, result.donation.dockerId);
|
|
271
|
+
return donation.id;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
catch (err) {
|
|
275
|
+
const message = errorMessage(err);
|
|
276
|
+
// `respawn` persists the incremented counter; re-read it rather than
|
|
277
|
+
// recomputing, so a give-up decision is made on what is actually on disk.
|
|
278
|
+
const attempts = this.store.get(donation.id)?.respawn?.attempts ?? 0;
|
|
279
|
+
log('respawn attempt %d for donation %s failed: %s', attempts, donation.id, message);
|
|
280
|
+
if (attempts >= DONATION_RESPAWN_MAX_ATTEMPTS) {
|
|
281
|
+
await this.giveUp(donation.id, attempts, message);
|
|
282
|
+
}
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Stop trying: mark the donation `error` and stop whatever child the record
|
|
288
|
+
* still names. `error` is outside the store's live statuses, so the grant's
|
|
289
|
+
* quota frees and the borrower can provision a fresh node.
|
|
290
|
+
*
|
|
291
|
+
* The record is written BEFORE the stop for the same reason `terminate` does
|
|
292
|
+
* it in that order — the stop fires `onStateChange`, and a pass triggered by
|
|
293
|
+
* it must not see "node gone, record still `seeded`" and start respawning
|
|
294
|
+
* again.
|
|
295
|
+
*
|
|
296
|
+
* The child is stopped but NOT removed: `removeContainer` deletes the workdir,
|
|
297
|
+
* and the workdir holds the identity key the borrower's cadre approved. A
|
|
298
|
+
* later `terminate()` still works on an `error` record and reclaims it.
|
|
299
|
+
*
|
|
300
|
+
* NOTE: an `error`-after-give-up record therefore keeps its workdir forever
|
|
301
|
+
* unless someone calls `terminate`. If stale `error` workdirs ever pile up on
|
|
302
|
+
* real hosts, extend the reap sweep in `donation-service.ts` to cover them.
|
|
303
|
+
*/
|
|
304
|
+
async giveUp(id, attempts, lastError) {
|
|
305
|
+
const donation = this.store.get(id);
|
|
306
|
+
if (!donation)
|
|
307
|
+
return;
|
|
308
|
+
// The record may have gone terminal while the failing attempt was in flight
|
|
309
|
+
// — a `terminate` mid-pass is exactly why `respawn` threw. Overwriting that
|
|
310
|
+
// with `error` would rewrite the borrower's own ending as a host fault, and
|
|
311
|
+
// the stop below would fire against a child `terminate` already reclaimed.
|
|
312
|
+
if (!SUPERVISED_STATUSES.has(donation.status)) {
|
|
313
|
+
log('donation %s went %s before give-up — leaving it alone', id, donation.status);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
this.store.put({
|
|
317
|
+
...donation,
|
|
318
|
+
status: 'error',
|
|
319
|
+
error: `respawn gave up after ${attempts} attempts: ${lastError}`,
|
|
320
|
+
updatedAt: this.now().toISOString(),
|
|
321
|
+
});
|
|
322
|
+
log('gave up respawning donation %s after %d attempts: %s', id, attempts, lastError);
|
|
323
|
+
if (donation.dockerId) {
|
|
324
|
+
try {
|
|
325
|
+
await this.orchestrator.stopContainer(donation.dockerId);
|
|
326
|
+
}
|
|
327
|
+
catch (err) {
|
|
328
|
+
// The record names a live handle in the ordinary case — a failed respawn
|
|
329
|
+
// restores the handle it dropped, and a respawn that spawned but failed
|
|
330
|
+
// to record writes the new one onto the record. Still guarded: a handle
|
|
331
|
+
// can be lost to a host restart (state.json gone, or the child already
|
|
332
|
+
// reaped), and the child is dead either way, which is why we are here.
|
|
333
|
+
log('failed to stop container %s while giving up: %s', donation.dockerId, errorMessage(err));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function errorMessage(err) {
|
|
339
|
+
return err instanceof Error ? err.message : String(err);
|
|
340
|
+
}
|
|
341
|
+
//# sourceMappingURL=donation-supervisor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"donation-supervisor.js","sourceRoot":"","sources":["../../src/donation/donation-supervisor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { GrantStore } from './grant-store.js';
|
|
2
|
+
import type { Grant, GrantAdminHandlers, GrantValidation, GrantValidator } from './types.js';
|
|
3
|
+
/** Default node cap when the admin doesn't specify one. */
|
|
4
|
+
export declare const DEFAULT_MAX_NODES = 1;
|
|
5
|
+
/** Constructor options. */
|
|
6
|
+
export interface GrantServiceOptions {
|
|
7
|
+
/** Persistent grant store (`grants.json`). */
|
|
8
|
+
store: GrantStore;
|
|
9
|
+
/** Clock override for tests. */
|
|
10
|
+
now?: () => Date;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* GrantService — issues, validates, lists, and revokes donation grant tokens.
|
|
14
|
+
*
|
|
15
|
+
* It is the authority for a grant's *identity, expiry, and revocation*, and
|
|
16
|
+
* for the per-grantee *node cap* (`maxNodes`). It is deliberately **not** the
|
|
17
|
+
* authority for how many nodes are currently live under a grant — that tally
|
|
18
|
+
* belongs to the donation service (`2-donation-service`), which persists the
|
|
19
|
+
* grant→node records and can see terminations. `validateForProvision` takes
|
|
20
|
+
* the count as a parameter so the two never drift.
|
|
21
|
+
*
|
|
22
|
+
* All methods are synchronous: issuance/validation/revocation are pure local
|
|
23
|
+
* store operations with no node round-trip (unlike the trust-circle flow,
|
|
24
|
+
* which delegates to the owner node).
|
|
25
|
+
*/
|
|
26
|
+
export declare class GrantService implements GrantValidator {
|
|
27
|
+
private readonly store;
|
|
28
|
+
private readonly now;
|
|
29
|
+
constructor(opts: GrantServiceOptions);
|
|
30
|
+
/**
|
|
31
|
+
* Issue a new grant. Generates a high-entropy base64url token, persists it,
|
|
32
|
+
* and returns the whole `Grant` (the CLI/UI prints/QR-encodes the token).
|
|
33
|
+
*/
|
|
34
|
+
issue(opts: {
|
|
35
|
+
label: string;
|
|
36
|
+
maxNodes?: number;
|
|
37
|
+
ttlMs?: number;
|
|
38
|
+
}): Grant;
|
|
39
|
+
/**
|
|
40
|
+
* Validate a presented bearer token for a non-provisioning op: it must name a
|
|
41
|
+
* known grant that is neither revoked nor expired. No quota check. Never
|
|
42
|
+
* throws for a bad token — returns a denial.
|
|
43
|
+
*/
|
|
44
|
+
validate(token: string): GrantValidation;
|
|
45
|
+
/**
|
|
46
|
+
* Validate for a *new* provision request. Runs the identity/expiry/revocation
|
|
47
|
+
* checks of `validate`, then — only if those pass — consults the
|
|
48
|
+
* caller-supplied live-node tally against the grant's `maxNodes`.
|
|
49
|
+
*
|
|
50
|
+
* Concurrency boundary: this is a pure function of the passed count. Two
|
|
51
|
+
* provision requests racing at the quota edge (both seeing count = N-1)
|
|
52
|
+
* would both pass here; serialising them so only one wins is the donation
|
|
53
|
+
* service's responsibility (`2-donation-service` owns the grant→node lock).
|
|
54
|
+
*/
|
|
55
|
+
validateForProvision(token: string, liveNodeCount: (token: string) => number): GrantValidation;
|
|
56
|
+
/**
|
|
57
|
+
* Revoke a grant by token: mark it revoked (denies future requests). Existing
|
|
58
|
+
* live nodes are **not** torn down here — that is a separate admin action via
|
|
59
|
+
* the donation service's terminate (`2-donation-service`). Throws not_found
|
|
60
|
+
* when the token is unknown.
|
|
61
|
+
*/
|
|
62
|
+
revoke(token: string): void;
|
|
63
|
+
/** Enumerate all grants (for the CLI / UI). */
|
|
64
|
+
list(): Grant[];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Wrap a GrantService into the typed handler shape consumed by the loopback
|
|
68
|
+
* management server's `/grants-admin` routes. Errors propagate as GrantError;
|
|
69
|
+
* the server maps `.code` → HTTP status.
|
|
70
|
+
*/
|
|
71
|
+
export declare function createGrantAdminHandlers(service: GrantService): GrantAdminHandlers;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import debug from 'debug';
|
|
3
|
+
import { GrantError } from './types.js';
|
|
4
|
+
const log = debug('cadre:host:grant-service');
|
|
5
|
+
/** Token byte length (base64url-encoded; ~43 chars). Matches trust-circle. */
|
|
6
|
+
const TOKEN_BYTES = 32;
|
|
7
|
+
/** Default node cap when the admin doesn't specify one. */
|
|
8
|
+
export const DEFAULT_MAX_NODES = 1;
|
|
9
|
+
/**
|
|
10
|
+
* GrantService — issues, validates, lists, and revokes donation grant tokens.
|
|
11
|
+
*
|
|
12
|
+
* It is the authority for a grant's *identity, expiry, and revocation*, and
|
|
13
|
+
* for the per-grantee *node cap* (`maxNodes`). It is deliberately **not** the
|
|
14
|
+
* authority for how many nodes are currently live under a grant — that tally
|
|
15
|
+
* belongs to the donation service (`2-donation-service`), which persists the
|
|
16
|
+
* grant→node records and can see terminations. `validateForProvision` takes
|
|
17
|
+
* the count as a parameter so the two never drift.
|
|
18
|
+
*
|
|
19
|
+
* All methods are synchronous: issuance/validation/revocation are pure local
|
|
20
|
+
* store operations with no node round-trip (unlike the trust-circle flow,
|
|
21
|
+
* which delegates to the owner node).
|
|
22
|
+
*/
|
|
23
|
+
export class GrantService {
|
|
24
|
+
constructor(opts) {
|
|
25
|
+
this.store = opts.store;
|
|
26
|
+
this.now = opts.now ?? (() => new Date());
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Issue a new grant. Generates a high-entropy base64url token, persists it,
|
|
30
|
+
* and returns the whole `Grant` (the CLI/UI prints/QR-encodes the token).
|
|
31
|
+
*/
|
|
32
|
+
issue(opts) {
|
|
33
|
+
const label = normaliseLabel(opts.label);
|
|
34
|
+
const maxNodes = normaliseMaxNodes(opts.maxNodes);
|
|
35
|
+
const created = this.now();
|
|
36
|
+
const grant = {
|
|
37
|
+
token: generateToken(),
|
|
38
|
+
label,
|
|
39
|
+
maxNodes,
|
|
40
|
+
createdAt: created.toISOString(),
|
|
41
|
+
};
|
|
42
|
+
if (opts.ttlMs !== undefined) {
|
|
43
|
+
if (!Number.isFinite(opts.ttlMs) || opts.ttlMs <= 0) {
|
|
44
|
+
throw new GrantError('invalid_ttl', `Invalid ttlMs: ${opts.ttlMs}`);
|
|
45
|
+
}
|
|
46
|
+
grant.expiresAt = new Date(created.getTime() + opts.ttlMs).toISOString();
|
|
47
|
+
}
|
|
48
|
+
this.store.add(grant);
|
|
49
|
+
log('issued grant "%s" (token=%s, maxNodes=%d, expires=%s)', label, grant.token, maxNodes, grant.expiresAt ?? 'never');
|
|
50
|
+
return grant;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Validate a presented bearer token for a non-provisioning op: it must name a
|
|
54
|
+
* known grant that is neither revoked nor expired. No quota check. Never
|
|
55
|
+
* throws for a bad token — returns a denial.
|
|
56
|
+
*/
|
|
57
|
+
validate(token) {
|
|
58
|
+
if (!token)
|
|
59
|
+
return { ok: false, reason: 'unknown_token' };
|
|
60
|
+
const grant = this.store.get(token);
|
|
61
|
+
if (!grant)
|
|
62
|
+
return { ok: false, reason: 'unknown_token' };
|
|
63
|
+
// Revocation is an explicit admin action; check it before expiry so a
|
|
64
|
+
// grant that is both revoked and expired reports the stronger signal.
|
|
65
|
+
if (grant.revokedAt)
|
|
66
|
+
return { ok: false, grant, reason: 'revoked' };
|
|
67
|
+
if (grant.expiresAt) {
|
|
68
|
+
const expiresAt = new Date(grant.expiresAt);
|
|
69
|
+
if (Number.isFinite(expiresAt.getTime()) && this.now() > expiresAt) {
|
|
70
|
+
return { ok: false, grant, reason: 'expired' };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { ok: true, grant };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Validate for a *new* provision request. Runs the identity/expiry/revocation
|
|
77
|
+
* checks of `validate`, then — only if those pass — consults the
|
|
78
|
+
* caller-supplied live-node tally against the grant's `maxNodes`.
|
|
79
|
+
*
|
|
80
|
+
* Concurrency boundary: this is a pure function of the passed count. Two
|
|
81
|
+
* provision requests racing at the quota edge (both seeing count = N-1)
|
|
82
|
+
* would both pass here; serialising them so only one wins is the donation
|
|
83
|
+
* service's responsibility (`2-donation-service` owns the grant→node lock).
|
|
84
|
+
*/
|
|
85
|
+
validateForProvision(token, liveNodeCount) {
|
|
86
|
+
const base = this.validate(token);
|
|
87
|
+
if (!base.ok || !base.grant)
|
|
88
|
+
return base;
|
|
89
|
+
const live = liveNodeCount(token);
|
|
90
|
+
if (live >= base.grant.maxNodes) {
|
|
91
|
+
return { ok: false, grant: base.grant, reason: 'quota_exceeded' };
|
|
92
|
+
}
|
|
93
|
+
return { ok: true, grant: base.grant };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Revoke a grant by token: mark it revoked (denies future requests). Existing
|
|
97
|
+
* live nodes are **not** torn down here — that is a separate admin action via
|
|
98
|
+
* the donation service's terminate (`2-donation-service`). Throws not_found
|
|
99
|
+
* when the token is unknown.
|
|
100
|
+
*/
|
|
101
|
+
revoke(token) {
|
|
102
|
+
if (!this.store.markRevoked(token, this.now().toISOString())) {
|
|
103
|
+
throw new GrantError('not_found', `Unknown grant: ${token}`);
|
|
104
|
+
}
|
|
105
|
+
log('revoked grant (token=%s)', token);
|
|
106
|
+
}
|
|
107
|
+
/** Enumerate all grants (for the CLI / UI). */
|
|
108
|
+
list() {
|
|
109
|
+
return this.store.list();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Wrap a GrantService into the typed handler shape consumed by the loopback
|
|
114
|
+
* management server's `/grants-admin` routes. Errors propagate as GrantError;
|
|
115
|
+
* the server maps `.code` → HTTP status.
|
|
116
|
+
*/
|
|
117
|
+
export function createGrantAdminHandlers(service) {
|
|
118
|
+
return {
|
|
119
|
+
async postGrant(body) {
|
|
120
|
+
if (!body || typeof body.label !== 'string') {
|
|
121
|
+
throw new GrantError('invalid_label', 'label is required');
|
|
122
|
+
}
|
|
123
|
+
const grant = service.issue({
|
|
124
|
+
label: body.label,
|
|
125
|
+
...(body.maxNodes !== undefined ? { maxNodes: body.maxNodes } : {}),
|
|
126
|
+
...(body.ttlMs !== undefined ? { ttlMs: body.ttlMs } : {}),
|
|
127
|
+
});
|
|
128
|
+
return { grant };
|
|
129
|
+
},
|
|
130
|
+
async listGrants() {
|
|
131
|
+
return { grants: service.list() };
|
|
132
|
+
},
|
|
133
|
+
async deleteGrant(token) {
|
|
134
|
+
service.revoke(token);
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function generateToken() {
|
|
139
|
+
// base64url so it survives in URLs, QR codes, and shell arguments.
|
|
140
|
+
return randomBytes(TOKEN_BYTES).toString('base64url');
|
|
141
|
+
}
|
|
142
|
+
function normaliseLabel(label) {
|
|
143
|
+
if (typeof label !== 'string') {
|
|
144
|
+
throw new GrantError('invalid_label', 'label must be a string');
|
|
145
|
+
}
|
|
146
|
+
const trimmed = label.trim();
|
|
147
|
+
if (trimmed.length === 0) {
|
|
148
|
+
throw new GrantError('invalid_label', 'label must not be empty');
|
|
149
|
+
}
|
|
150
|
+
if (trimmed.length > 200) {
|
|
151
|
+
throw new GrantError('invalid_label', 'label must be 200 characters or fewer');
|
|
152
|
+
}
|
|
153
|
+
return trimmed;
|
|
154
|
+
}
|
|
155
|
+
function normaliseMaxNodes(maxNodes) {
|
|
156
|
+
if (maxNodes === undefined)
|
|
157
|
+
return DEFAULT_MAX_NODES;
|
|
158
|
+
if (typeof maxNodes !== 'number' || !Number.isInteger(maxNodes) || maxNodes < 1) {
|
|
159
|
+
throw new GrantError('invalid_max_nodes', `maxNodes must be a positive integer (got ${JSON.stringify(maxNodes)})`);
|
|
160
|
+
}
|
|
161
|
+
return maxNodes;
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=grant-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grant-service.js","sourceRoot":"","sources":["../../src/donation/grant-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Grant, GrantFile } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Atomic JSON store for `grants.json`. Modelled on `auth/trust-circle-store.ts`:
|
|
4
|
+
* a single file, written to `<path>.tmp` then renamed, with an in-memory cache
|
|
5
|
+
* keyed by token.
|
|
6
|
+
*
|
|
7
|
+
* Unlike the trust-circle pending rows, grants are **not** self-reaping: a
|
|
8
|
+
* grant may legitimately outlive many nodes, so expiry is evaluated at
|
|
9
|
+
* validate time (in `GrantService`) and the admin removes stale grants
|
|
10
|
+
* explicitly. This store keeps no clock.
|
|
11
|
+
*
|
|
12
|
+
* Concurrency assumption: only one cadre-host process owns a given rootDir
|
|
13
|
+
* (the orchestrator already enforces this). No file-locking primitives.
|
|
14
|
+
*/
|
|
15
|
+
export declare class GrantStore {
|
|
16
|
+
private readonly path;
|
|
17
|
+
private cache;
|
|
18
|
+
constructor(rootDir: string);
|
|
19
|
+
filePath(): string;
|
|
20
|
+
/**
|
|
21
|
+
* Load (with caching). A missing file returns an empty store (fresh
|
|
22
|
+
* install). A present-but-malformed file throws — silently wiping it would
|
|
23
|
+
* revoke every grantee at once.
|
|
24
|
+
*/
|
|
25
|
+
load(): GrantFile;
|
|
26
|
+
/** Persist the current state atomically (write then rename). */
|
|
27
|
+
save(state: GrantFile): void;
|
|
28
|
+
add(grant: Grant): void;
|
|
29
|
+
get(token: string): Grant | undefined;
|
|
30
|
+
list(): Grant[];
|
|
31
|
+
remove(token: string): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Mark a grant revoked at the given ISO instant. Idempotent: an
|
|
34
|
+
* already-revoked grant keeps its original `revokedAt`. Returns false when
|
|
35
|
+
* the token is unknown.
|
|
36
|
+
*/
|
|
37
|
+
markRevoked(token: string, at: string): boolean;
|
|
38
|
+
}
|