@shiprev/server-sdk-node 0.0.0-stage → 0.1.0-beta.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.
Files changed (43) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +253 -2
  3. package/dist/auth/token-verifier.d.ts +42 -0
  4. package/dist/auth/token-verifier.js +230 -0
  5. package/dist/client/connection-supervisor.d.ts +117 -0
  6. package/dist/client/connection-supervisor.js +547 -0
  7. package/dist/client/shiprev-client.d.ts +106 -0
  8. package/dist/client/shiprev-client.js +267 -0
  9. package/dist/index.d.ts +16 -0
  10. package/dist/index.js +47 -0
  11. package/dist/metering/backlog/host-adapter-backlog.d.ts +10 -0
  12. package/dist/metering/backlog/host-adapter-backlog.js +27 -0
  13. package/dist/metering/backlog/journal-backlog.d.ts +14 -0
  14. package/dist/metering/backlog/journal-backlog.js +135 -0
  15. package/dist/metering/backlog/memory-backlog.d.ts +9 -0
  16. package/dist/metering/backlog/memory-backlog.js +34 -0
  17. package/dist/metering/usage-buffer.d.ts +59 -0
  18. package/dist/metering/usage-buffer.js +175 -0
  19. package/dist/mirror/entitlement-mirror.d.ts +47 -0
  20. package/dist/mirror/entitlement-mirror.js +109 -0
  21. package/dist/nest/decorators.d.ts +15 -0
  22. package/dist/nest/decorators.js +21 -0
  23. package/dist/nest/index.d.ts +4 -0
  24. package/dist/nest/index.js +21 -0
  25. package/dist/nest/shiprev-auth.guard.d.ts +7 -0
  26. package/dist/nest/shiprev-auth.guard.js +52 -0
  27. package/dist/nest/shiprev-capability.guard.d.ts +9 -0
  28. package/dist/nest/shiprev-capability.guard.js +65 -0
  29. package/dist/nest/shiprev.module.d.ts +17 -0
  30. package/dist/nest/shiprev.module.js +57 -0
  31. package/dist/ruling/adjudicator.d.ts +15 -0
  32. package/dist/ruling/adjudicator.js +131 -0
  33. package/dist/transport/event-stream.d.ts +27 -0
  34. package/dist/transport/event-stream.js +92 -0
  35. package/dist/transport/http.transport.d.ts +64 -0
  36. package/dist/transport/http.transport.js +218 -0
  37. package/dist/transport/wire.d.ts +79 -0
  38. package/dist/transport/wire.js +8 -0
  39. package/dist/types.d.ts +191 -0
  40. package/dist/types.js +25 -0
  41. package/dist/util/decimal.d.ts +14 -0
  42. package/dist/util/decimal.js +57 -0
  43. package/package.json +98 -4
@@ -0,0 +1,547 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConnectionSupervisor = void 0;
4
+ const token_verifier_1 = require("../auth/token-verifier");
5
+ const event_stream_1 = require("../transport/event-stream");
6
+ const http_transport_1 = require("../transport/http.transport");
7
+ /** Retry interval while the instance refuses the API key (401 or 403 on the token exchange). */
8
+ const REFUSED_RETRY_MS = 30_000;
9
+ const BACKOFF_STEPS_MS = [1_000, 2_000, 4_000, 8_000, 16_000, 30_000];
10
+ const RENEW_AT = 0.8;
11
+ const HEARTBEAT_TIMEOUT_FACTOR = 3;
12
+ function backoffDelay(step) {
13
+ const base = BACKOFF_STEPS_MS[Math.min(step, BACKOFF_STEPS_MS.length - 1)];
14
+ return Math.round(base * (0.8 + Math.random() * 0.4));
15
+ }
16
+ function unrefTimer(callback, ms) {
17
+ const timer = setTimeout(callback, Math.max(0, ms));
18
+ timer.unref();
19
+ return timer;
20
+ }
21
+ class ConnectionSupervisor {
22
+ host;
23
+ token = null;
24
+ tokenExpiresAt = 0;
25
+ projectId = null;
26
+ credentialRefused = false;
27
+ exchangeInFlight = null;
28
+ keysLoaded = false;
29
+ stream = null;
30
+ streams = new Set();
31
+ live = false;
32
+ running = false;
33
+ paused = false;
34
+ /** Bumped by stop, pause and start; work begun under an older generation drops its result. */
35
+ generation = 0;
36
+ connecting = null;
37
+ rotating = false;
38
+ backoffStep = 0;
39
+ renewalBackoffStep = 0;
40
+ /** When the next renewal is due (local clock), kept across a pause. */
41
+ renewDueAt = null;
42
+ renewTimer = null;
43
+ reconnectTimer = null;
44
+ watchdogTimer = null;
45
+ refillTargets = new Map();
46
+ refillsInFlight = new Set();
47
+ constructor(host) {
48
+ this.host = host;
49
+ }
50
+ /** The machine token in use, or `null`. */
51
+ currentToken() {
52
+ return this.token;
53
+ }
54
+ /**
55
+ * Keys, token, stream (waiting for `ready`), then a full pull. Never throws: whatever fails is
56
+ * retried in the background.
57
+ */
58
+ async start() {
59
+ this.halt();
60
+ this.running = true;
61
+ this.paused = false;
62
+ this.keysLoaded = false;
63
+ const attempt = this.attempt(true);
64
+ this.connecting = attempt;
65
+ try {
66
+ await attempt;
67
+ }
68
+ finally {
69
+ if (this.connecting === attempt)
70
+ this.connecting = null;
71
+ }
72
+ }
73
+ /** Closes the stream and stops every timer. */
74
+ stop() {
75
+ this.running = false;
76
+ this.paused = false;
77
+ this.halt();
78
+ }
79
+ /** `setOnline(false)`: closes the stream and suspends supervision until `resume()`. */
80
+ pause() {
81
+ if (!this.running)
82
+ return;
83
+ this.paused = true;
84
+ this.halt();
85
+ }
86
+ /** `setOnline(true)`: resumes supervision; the next attempt corrects the state. */
87
+ resume() {
88
+ if (!this.running)
89
+ return;
90
+ if (this.paused) {
91
+ this.paused = false;
92
+ if (this.renewDueAt !== null) {
93
+ this.scheduleRenewal(this.renewDueAt - Date.now());
94
+ }
95
+ }
96
+ if (!this.live) {
97
+ void this.reconnect();
98
+ }
99
+ }
100
+ /** A token set by hand (`setM2MToken`): used as is, never renewed on a timer. */
101
+ injectToken(token) {
102
+ this.token = token;
103
+ this.tokenExpiresAt = (0, token_verifier_1.expiryOfToken)(token) ?? Number.POSITIVE_INFINITY;
104
+ const expectations = (0, token_verifier_1.expectationsFromMachineToken)(token);
105
+ if (expectations)
106
+ this.projectId = expectations.projectId;
107
+ }
108
+ /**
109
+ * Runs an authenticated request. A 401 triggers one renewal, shared by every request that failed
110
+ * with the same token, and one retry with the new token; a second failure is the request's own.
111
+ */
112
+ async authorized(call) {
113
+ const token = this.token;
114
+ if (!token) {
115
+ throw new Error('Client is not authenticated: M2M token missing');
116
+ }
117
+ try {
118
+ return await call(token);
119
+ }
120
+ catch (error) {
121
+ if (!(0, http_transport_1.isHttpStatus)(error, 401))
122
+ throw error;
123
+ let fresh;
124
+ try {
125
+ fresh = await this.renewAfterUnauthorized(token);
126
+ }
127
+ catch {
128
+ throw error;
129
+ }
130
+ return call(fresh);
131
+ }
132
+ }
133
+ // ─── Connection attempts ──────────────────────────────────────────────────
134
+ /**
135
+ * One attempt to get online. `initial` (start) pulls even when the stream cannot be opened, so the
136
+ * mirror is loaded; later attempts pull only once a stream is ready, except while the mirror is
137
+ * still empty.
138
+ */
139
+ async attempt(initial) {
140
+ const generation = this.generation;
141
+ const current = () => generation === this.generation;
142
+ let opened = null;
143
+ try {
144
+ if (initial || !this.keysLoaded) {
145
+ await this.host.loadKeys();
146
+ this.keysLoaded = true;
147
+ }
148
+ if (!current())
149
+ return;
150
+ const token = await this.validToken();
151
+ if (!current())
152
+ return;
153
+ let streamError;
154
+ try {
155
+ opened = await this.openStream(token);
156
+ }
157
+ catch (error) {
158
+ streamError = error;
159
+ if ((0, http_transport_1.isHttpStatus)(error, 401) && this.token === token) {
160
+ // The instance no longer accepts this token: exchange a new one on the next attempt.
161
+ this.tokenExpiresAt = 0;
162
+ }
163
+ }
164
+ if (!current())
165
+ return;
166
+ if (streamError !== undefined && !initial && !this.host.mirror.isEmpty())
167
+ throw streamError;
168
+ await this.pull();
169
+ if (!current())
170
+ return;
171
+ if (streamError !== undefined)
172
+ throw streamError;
173
+ if (opened.closed)
174
+ throw new Error('The event stream ended');
175
+ this.adopt(opened);
176
+ opened = null;
177
+ this.goLive(!initial);
178
+ }
179
+ catch {
180
+ if (!current())
181
+ return;
182
+ this.goOffline();
183
+ this.scheduleReconnect();
184
+ }
185
+ finally {
186
+ if (opened)
187
+ this.retire(opened);
188
+ }
189
+ }
190
+ reconnect() {
191
+ if (!this.running || this.paused)
192
+ return Promise.resolve();
193
+ if (this.connecting)
194
+ return this.connecting;
195
+ this.clearTimer('reconnectTimer');
196
+ const attempt = this.attempt(false);
197
+ this.connecting = attempt;
198
+ void attempt.finally(() => {
199
+ if (this.connecting === attempt)
200
+ this.connecting = null;
201
+ });
202
+ return attempt;
203
+ }
204
+ scheduleReconnect() {
205
+ if (!this.running || this.paused)
206
+ return;
207
+ // While the API key is refused the 30-second renewal drives recovery (§5.2 rule 4).
208
+ if (this.credentialRefused && !(this.token && this.tokenExpiresAt > Date.now()))
209
+ return;
210
+ this.clearTimer('reconnectTimer');
211
+ this.reconnectTimer = unrefTimer(() => {
212
+ this.reconnectTimer = null;
213
+ void this.reconnect();
214
+ }, backoffDelay(this.backoffStep++));
215
+ }
216
+ goLive(flush) {
217
+ this.live = true;
218
+ this.backoffStep = 0;
219
+ this.host.setOnline(true);
220
+ this.armWatchdog();
221
+ if (flush) {
222
+ void this.host.flush().catch(() => undefined);
223
+ }
224
+ }
225
+ goOffline() {
226
+ this.live = false;
227
+ if (this.stream) {
228
+ this.retire(this.stream);
229
+ this.stream = null;
230
+ }
231
+ this.clearTimer('watchdogTimer');
232
+ this.host.setOnline(false);
233
+ }
234
+ /** An unplanned break of the active stream: one immediate reconnect, still LIVE meanwhile. */
235
+ onBreak(stream) {
236
+ if (stream !== this.stream || !this.running || this.paused)
237
+ return;
238
+ this.retire(stream);
239
+ this.stream = null;
240
+ this.clearTimer('watchdogTimer');
241
+ void this.reconnect();
242
+ }
243
+ // ─── Token ────────────────────────────────────────────────────────────────
244
+ async validToken() {
245
+ if (this.token && this.tokenExpiresAt > Date.now()) {
246
+ return this.token;
247
+ }
248
+ if (this.credentialRefused) {
249
+ throw new Error('The API key was refused; waiting for the next renewal attempt');
250
+ }
251
+ return this.exchange();
252
+ }
253
+ exchange() {
254
+ if (!this.exchangeInFlight) {
255
+ const exchange = (async () => {
256
+ try {
257
+ const issuedAt = Date.now();
258
+ const auth = await this.host.transport.exchangeM2MToken(this.host.apiKey);
259
+ const expectations = (0, token_verifier_1.expectationsFromMachineToken)(auth.access_token);
260
+ if (!expectations) {
261
+ throw new Error('The service token names no application (pid)');
262
+ }
263
+ this.credentialRefused = false;
264
+ this.token = auth.access_token;
265
+ this.projectId = expectations.projectId;
266
+ const lifetimeMs = auth.expires_in * 1000;
267
+ this.tokenExpiresAt = Math.min(issuedAt + lifetimeMs, (0, token_verifier_1.expiryOfToken)(auth.access_token) ?? Infinity);
268
+ this.host.onToken(auth.access_token);
269
+ this.scheduleRenewal(lifetimeMs * RENEW_AT);
270
+ return auth.access_token;
271
+ }
272
+ catch (error) {
273
+ if ((0, http_transport_1.isHttpStatus)(error, 401, 403)) {
274
+ this.credentialRefused = true;
275
+ this.scheduleRenewal(REFUSED_RETRY_MS);
276
+ }
277
+ throw error;
278
+ }
279
+ })();
280
+ this.exchangeInFlight = exchange;
281
+ void exchange.then(() => this.clearExchange(exchange), () => this.clearExchange(exchange));
282
+ }
283
+ return this.exchangeInFlight;
284
+ }
285
+ clearExchange(exchange) {
286
+ if (this.exchangeInFlight === exchange)
287
+ this.exchangeInFlight = null;
288
+ }
289
+ async renewAfterUnauthorized(staleToken) {
290
+ if (this.token && this.token !== staleToken) {
291
+ return this.token;
292
+ }
293
+ if (this.credentialRefused && !this.exchangeInFlight) {
294
+ throw new Error('The API key was refused');
295
+ }
296
+ const token = await this.exchange();
297
+ this.afterRenewal(token);
298
+ return token;
299
+ }
300
+ scheduleRenewal(delayMs) {
301
+ this.renewDueAt = Date.now() + delayMs;
302
+ if (!this.running || this.paused)
303
+ return;
304
+ this.clearTimer('renewTimer');
305
+ this.renewTimer = unrefTimer(() => {
306
+ this.renewTimer = null;
307
+ void this.renew();
308
+ }, delayMs);
309
+ }
310
+ async renew() {
311
+ if (!this.running || this.paused)
312
+ return;
313
+ try {
314
+ const token = await this.exchange();
315
+ this.renewalBackoffStep = 0;
316
+ this.afterRenewal(token);
317
+ }
318
+ catch (error) {
319
+ // A refusal already scheduled the 30-second retry.
320
+ if ((0, http_transport_1.isHttpStatus)(error, 401, 403))
321
+ return;
322
+ const delay = backoffDelay(this.renewalBackoffStep++);
323
+ // Past the old token's expiry the reconnection exchanges a new one itself.
324
+ if (Date.now() + delay < this.tokenExpiresAt) {
325
+ this.scheduleRenewal(delay);
326
+ }
327
+ }
328
+ }
329
+ /** A new token: rotate a live stream onto it, or reconnect if the client is not live. */
330
+ afterRenewal(token) {
331
+ if (!this.running || this.paused)
332
+ return;
333
+ if (this.live && this.stream) {
334
+ void this.rotate(token);
335
+ }
336
+ else if (!this.live) {
337
+ void this.reconnect();
338
+ }
339
+ }
340
+ /** Planned rotation: the new stream must be ready before the old one is closed; no pull. */
341
+ async rotate(token) {
342
+ if (this.rotating)
343
+ return;
344
+ this.rotating = true;
345
+ const generation = this.generation;
346
+ try {
347
+ const fresh = await this.openStream(token);
348
+ if (generation !== this.generation || !this.live || this.token !== token || fresh.closed) {
349
+ this.retire(fresh);
350
+ return;
351
+ }
352
+ const old = this.stream;
353
+ this.adopt(fresh);
354
+ if (old)
355
+ this.retire(old);
356
+ }
357
+ catch {
358
+ // Keep the old stream; the instance ends it at the old token's expiry, which reconnects.
359
+ }
360
+ finally {
361
+ this.rotating = false;
362
+ }
363
+ }
364
+ // ─── Stream ───────────────────────────────────────────────────────────────
365
+ /** Opens a stream and resolves once its `ready` frame arrived (bounded by the connect timeout). */
366
+ openStream(token) {
367
+ const stream = {
368
+ controller: new AbortController(),
369
+ heartbeatTimeoutMs: 0,
370
+ lastFrameAt: 0,
371
+ ready: false,
372
+ retired: false,
373
+ closed: false,
374
+ };
375
+ this.streams.add(stream);
376
+ return new Promise((resolve, reject) => {
377
+ let settled = false;
378
+ const fail = (error) => {
379
+ if (settled)
380
+ return;
381
+ settled = true;
382
+ clearTimeout(timer);
383
+ this.retire(stream);
384
+ reject(error);
385
+ };
386
+ const timer = unrefTimer(() => fail(new Error(`No ready frame within ${this.host.connectTimeoutMs}ms`)), this.host.connectTimeoutMs);
387
+ const onFrame = (event, data) => {
388
+ if (stream.retired)
389
+ return;
390
+ stream.lastFrameAt = Date.now();
391
+ if (stream.ready) {
392
+ this.onFrame(event, data);
393
+ return;
394
+ }
395
+ if (event !== 'ready')
396
+ return;
397
+ const ready = (data ?? {});
398
+ if (typeof ready.project_id !== 'string' || ready.project_id !== this.projectId) {
399
+ fail(new Error('The event stream belongs to another application'));
400
+ return;
401
+ }
402
+ const interval = typeof ready.heartbeat_interval_s === 'number' && ready.heartbeat_interval_s > 0
403
+ ? ready.heartbeat_interval_s
404
+ : 15;
405
+ stream.heartbeatTimeoutMs = interval * 1000 * HEARTBEAT_TIMEOUT_FACTOR;
406
+ stream.ready = true;
407
+ settled = true;
408
+ clearTimeout(timer);
409
+ resolve(stream);
410
+ };
411
+ const ended = (error) => {
412
+ stream.closed = true;
413
+ if (!settled) {
414
+ fail(error instanceof Error ? error : new Error('The event stream ended before ready'));
415
+ return;
416
+ }
417
+ this.streams.delete(stream);
418
+ this.onBreak(stream);
419
+ };
420
+ this.host.transport
421
+ .openEventStream(token, stream.controller.signal)
422
+ .then((response) => (0, event_stream_1.readEventStream)(response.body, (frame) => {
423
+ let data;
424
+ try {
425
+ data = JSON.parse(frame.data);
426
+ }
427
+ catch {
428
+ // Not JSON: only an unknown event could carry it, and unknown events are ignored.
429
+ return;
430
+ }
431
+ onFrame(frame.event, data);
432
+ }))
433
+ .then(() => ended(), (error) => ended(error));
434
+ });
435
+ }
436
+ adopt(stream) {
437
+ this.stream = stream;
438
+ this.armWatchdog();
439
+ }
440
+ retire(stream) {
441
+ stream.retired = true;
442
+ this.streams.delete(stream);
443
+ stream.controller.abort();
444
+ }
445
+ armWatchdog() {
446
+ this.clearTimer('watchdogTimer');
447
+ const stream = this.stream;
448
+ if (!stream || !stream.ready)
449
+ return;
450
+ const due = stream.lastFrameAt + stream.heartbeatTimeoutMs - Date.now();
451
+ this.watchdogTimer = unrefTimer(() => {
452
+ this.watchdogTimer = null;
453
+ if (stream !== this.stream)
454
+ return;
455
+ if (Date.now() - stream.lastFrameAt >= stream.heartbeatTimeoutMs) {
456
+ this.onBreak(stream);
457
+ }
458
+ else {
459
+ this.armWatchdog();
460
+ }
461
+ }, due);
462
+ }
463
+ // ─── Push consumption ─────────────────────────────────────────────────────
464
+ onFrame(event, data) {
465
+ if (event === 'snapshot.changed') {
466
+ const changed = (data ?? {});
467
+ if (typeof changed.organization_id === 'string' && typeof changed.revision === 'number') {
468
+ this.onChanged(changed.organization_id, changed.revision);
469
+ }
470
+ }
471
+ else if (event === 'snapshot.resync') {
472
+ void this.pull().catch(() => undefined);
473
+ }
474
+ // `heartbeat` only refreshes lastFrameAt; unknown events are ignored.
475
+ }
476
+ onChanged(organizationId, revision) {
477
+ const known = this.host.mirror.getOrganizationRevision(organizationId) ?? 0;
478
+ if (known >= revision)
479
+ return;
480
+ this.refillTargets.set(organizationId, Math.max(this.refillTargets.get(organizationId) ?? 0, revision));
481
+ if (this.refillsInFlight.has(organizationId))
482
+ return;
483
+ void this.refill(organizationId);
484
+ }
485
+ /** Refills one organization until the mirror reaches the highest announced revision. */
486
+ async refill(organizationId) {
487
+ this.refillsInFlight.add(organizationId);
488
+ try {
489
+ for (;;) {
490
+ const envelope = await this.authorized((token) => this.host.transport.fetchSnapshot(token, organizationId));
491
+ if (!this.running)
492
+ return;
493
+ if (envelope.organizations && organizationId in envelope.organizations) {
494
+ this.host.mirror.mergeSnapshot(envelope);
495
+ }
496
+ else {
497
+ this.host.mirror.removeOrganization(organizationId);
498
+ this.refillTargets.delete(organizationId);
499
+ return;
500
+ }
501
+ const target = this.refillTargets.get(organizationId) ?? 0;
502
+ if ((this.host.mirror.getOrganizationRevision(organizationId) ?? 0) >= target) {
503
+ this.refillTargets.delete(organizationId);
504
+ return;
505
+ }
506
+ }
507
+ }
508
+ catch {
509
+ // The state does not change; the target stays for the next notification or full pull.
510
+ }
511
+ finally {
512
+ this.refillsInFlight.delete(organizationId);
513
+ }
514
+ }
515
+ /** Full pull: replaces the whole mirror. */
516
+ async pull() {
517
+ const snapshot = await this.authorized((token) => this.host.transport.fetchSnapshot(token));
518
+ this.host.mirror.loadSnapshot(snapshot);
519
+ for (const [organizationId, target] of this.refillTargets) {
520
+ if ((this.host.mirror.getOrganizationRevision(organizationId) ?? 0) >= target) {
521
+ this.refillTargets.delete(organizationId);
522
+ }
523
+ }
524
+ }
525
+ // ─── Housekeeping ─────────────────────────────────────────────────────────
526
+ /** Drops the stream, every timer and any attempt in flight. */
527
+ halt() {
528
+ this.generation++;
529
+ this.live = false;
530
+ this.connecting = null;
531
+ this.stream = null;
532
+ for (const stream of [...this.streams]) {
533
+ this.retire(stream);
534
+ }
535
+ this.clearTimer('renewTimer');
536
+ this.clearTimer('reconnectTimer');
537
+ this.clearTimer('watchdogTimer');
538
+ }
539
+ clearTimer(name) {
540
+ const timer = this[name];
541
+ if (timer)
542
+ clearTimeout(timer);
543
+ this[name] = null;
544
+ }
545
+ }
546
+ exports.ConnectionSupervisor = ConnectionSupervisor;
547
+ //# sourceMappingURL=connection-supervisor.js.map
@@ -0,0 +1,106 @@
1
+ import { UsageBuffer } from '../metering/usage-buffer';
2
+ import { HttpTransport } from '../transport/http.transport';
3
+ import { AdjudicateOptions, ClientState, ConfirmReservationResult, ReleaseReservationResult, ReservationResult, ReserveOptions, Ruling, ShipRevClientOptions, Snapshot, TokenClaims, UsageEventItem } from '../types';
4
+ export declare class ShipRevClient {
5
+ private readonly options;
6
+ private readonly transport;
7
+ private readonly tokenVerifier;
8
+ private readonly mirror;
9
+ private readonly adjudicator;
10
+ private readonly usageBuffer;
11
+ private readonly supervisor;
12
+ /** COLD until start() or setOnline(); otherwise the state follows the online signal and the mirror. */
13
+ private active;
14
+ private isOnline;
15
+ /** Issuer and application expected of session tokens, learned from the machine token. */
16
+ private expectations;
17
+ /** Amounts reserved by this client, so `confirm()` without an amount confirms what was reserved. */
18
+ private readonly reservedAmounts;
19
+ constructor(options: ShipRevClientOptions);
20
+ /**
21
+ * Connects the client: fetches the signing keys, exchanges the API key for a service token, opens
22
+ * the push stream and waits for its `ready` frame, then loads the full entitlement snapshot. Never
23
+ * throws: LIVE when all four succeed, DISCONNECTED when only the stream failed, COLD_OFFLINE when
24
+ * nothing could be loaded. From here until `stop()` the client renews its token, follows the push
25
+ * stream and reconnects by itself.
26
+ */
27
+ start(): Promise<{
28
+ project_id: string;
29
+ snapshot_revision: number;
30
+ state: ClientState;
31
+ }>;
32
+ /**
33
+ * Closes the push stream, stops every timer and flushes buffered usage events.
34
+ */
35
+ stop(): Promise<void>;
36
+ /**
37
+ * Enqueues a metered usage event into the memory buffer for batched delivery.
38
+ */
39
+ reportUsage(event: UsageEventItem): Promise<void>;
40
+ /**
41
+ * Manually flushes currently buffered usage events to the core service.
42
+ */
43
+ flush(): Promise<void>;
44
+ /**
45
+ * Verifies an RS256 JWT token locally and extracts subject and isolation keys.
46
+ */
47
+ verifyToken(jwt: string): Promise<TokenClaims>;
48
+ /**
49
+ * Executes a four-tier local adjudication (synchronous, zero-IO, P95 <= 5ms).
50
+ */
51
+ adjudicate(options: AdjudicateOptions): Ruling;
52
+ /**
53
+ * Asynchronous adjudication with authoritative fallback.
54
+ * If local mirror misses the organization or rule and client is online, queries core service and backfills mirror.
55
+ */
56
+ adjudicateAsync(options: AdjudicateOptions): Promise<Ruling>;
57
+ /**
58
+ * Guard for write operations (fail-closed).
59
+ * Intercepts action if write operation is denied (e.g. disconnected).
60
+ */
61
+ guardWrite<T>(options: Omit<AdjudicateOptions, 'operation'>, action: () => Promise<T> | T): Promise<T>;
62
+ /**
63
+ * Two-phase entitlement reservation: reserve
64
+ */
65
+ reserve(options: ReserveOptions): Promise<ReservationResult>;
66
+ /**
67
+ * Two-phase entitlement reservation: confirm. Without `actualAmount` the reserved amount is confirmed.
68
+ */
69
+ confirm(reservationId: string, actualAmount?: number): Promise<ConfirmReservationResult>;
70
+ /**
71
+ * Two-phase entitlement reservation: release
72
+ */
73
+ release(reservationId: string): Promise<ReleaseReservationResult>;
74
+ /**
75
+ * Directly loads a snapshot into the local mirror (e.g. for offline seed testing).
76
+ */
77
+ loadSnapshot(snapshot: Snapshot): boolean;
78
+ /**
79
+ * Returns current internal state and mirror revision.
80
+ */
81
+ state(): {
82
+ state: ClientState;
83
+ snapshot_revision: number;
84
+ last_sync_at?: number;
85
+ };
86
+ /**
87
+ * Switches the connection state by hand, for fault injection and tests. `false` closes the push
88
+ * stream and suspends reconnection until `setOnline(true)`, which sets LIVE at once and resumes
89
+ * supervision; on a started client the supervisor then corrects the state.
90
+ */
91
+ setOnline(online: boolean): void;
92
+ /**
93
+ * Sets the machine token by hand (for tests without `start()`); a decodable JWT also sets the
94
+ * issuer and application `verifyToken()` expects.
95
+ */
96
+ setM2MToken(token: string): void;
97
+ /**
98
+ * Directly preloads a public key into the token verifier cache (for testing without network).
99
+ */
100
+ setPublicKey(kid: string, pem: string): void;
101
+ getUsageBuffer(): UsageBuffer;
102
+ getTransport(): HttpTransport;
103
+ private applyOnline;
104
+ private learnExpectations;
105
+ private refillOrganization;
106
+ }