@shiprev/server-sdk-node 0.0.0-stage → 0.1.0-beta.1

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 (45) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +276 -2
  3. package/dist/auth/token-verifier.d.ts +56 -0
  4. package/dist/auth/token-verifier.js +257 -0
  5. package/dist/client/connection-supervisor.d.ts +148 -0
  6. package/dist/client/connection-supervisor.js +650 -0
  7. package/dist/client/shiprev-client.d.ts +139 -0
  8. package/dist/client/shiprev-client.js +400 -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/degradation.d.ts +22 -0
  20. package/dist/mirror/degradation.js +67 -0
  21. package/dist/mirror/entitlement-mirror.d.ts +61 -0
  22. package/dist/mirror/entitlement-mirror.js +143 -0
  23. package/dist/nest/decorators.d.ts +15 -0
  24. package/dist/nest/decorators.js +21 -0
  25. package/dist/nest/index.d.ts +4 -0
  26. package/dist/nest/index.js +21 -0
  27. package/dist/nest/shiprev-auth.guard.d.ts +7 -0
  28. package/dist/nest/shiprev-auth.guard.js +52 -0
  29. package/dist/nest/shiprev-capability.guard.d.ts +9 -0
  30. package/dist/nest/shiprev-capability.guard.js +65 -0
  31. package/dist/nest/shiprev.module.d.ts +17 -0
  32. package/dist/nest/shiprev.module.js +57 -0
  33. package/dist/ruling/adjudicator.d.ts +15 -0
  34. package/dist/ruling/adjudicator.js +123 -0
  35. package/dist/transport/event-stream.d.ts +27 -0
  36. package/dist/transport/event-stream.js +92 -0
  37. package/dist/transport/http.transport.d.ts +69 -0
  38. package/dist/transport/http.transport.js +238 -0
  39. package/dist/transport/wire.d.ts +79 -0
  40. package/dist/transport/wire.js +8 -0
  41. package/dist/types.d.ts +252 -0
  42. package/dist/types.js +37 -0
  43. package/dist/util/decimal.d.ts +14 -0
  44. package/dist/util/decimal.js +57 -0
  45. package/package.json +96 -3
@@ -0,0 +1,139 @@
1
+ import { UsageBuffer } from '../metering/usage-buffer';
2
+ import { HttpTransport } from '../transport/http.transport';
3
+ import { AdjudicateOptions, ClientState, ConfirmReservationResult, EffectiveLevel, ReleaseReservationResult, ReservationResult, ReserveOptions, Ruling, ShipRevClientOptions, Snapshot, TokenClaims, UsageEventItem, UsageSendEvent, UsageSendResult } 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
+ /** Effective levels last reported through `on_level_change`. */
20
+ private readonly reportedLevels;
21
+ /** Fires when the earliest `valid_until` passes while disconnected, to report the expiry. */
22
+ private expiryTimer;
23
+ constructor(options: ShipRevClientOptions);
24
+ /**
25
+ * Connects the client: fetches the signing keys, exchanges the API key for a service token, opens
26
+ * the push stream and waits for its `ready` frame, then loads the full entitlement snapshot. Never
27
+ * throws: LIVE when all four succeed, DISCONNECTED when only the stream failed, COLD_OFFLINE when
28
+ * nothing could be loaded. From here until `stop()` the client renews its token, follows the push
29
+ * stream and reconnects by itself.
30
+ */
31
+ start(): Promise<{
32
+ project_id: string;
33
+ snapshot_revision: number;
34
+ state: ClientState;
35
+ }>;
36
+ /**
37
+ * Closes the push stream, stops every timer and flushes buffered usage events.
38
+ */
39
+ stop(): Promise<void>;
40
+ /**
41
+ * Enqueues a metered usage event into the memory buffer for batched delivery.
42
+ */
43
+ reportUsage(event: UsageEventItem): Promise<void>;
44
+ /**
45
+ * Manually flushes currently buffered usage events to the core service.
46
+ */
47
+ flush(): Promise<void>;
48
+ /**
49
+ * Verifies an RS256 JWT token locally and extracts subject and isolation keys.
50
+ */
51
+ verifyToken(jwt: string): Promise<TokenClaims>;
52
+ /**
53
+ * Executes a four-tier local adjudication (synchronous, zero-IO, P95 <= 5ms).
54
+ */
55
+ adjudicate(options: AdjudicateOptions): Ruling;
56
+ /**
57
+ * Asynchronous adjudication with authoritative fallback.
58
+ * If local mirror misses the organization or rule and client is online, queries core service and backfills mirror.
59
+ */
60
+ adjudicateAsync(options: AdjudicateOptions): Promise<Ruling>;
61
+ /**
62
+ * The effective degradation level of an organization: the level of the mirror entry while the instance
63
+ * answers; without an answer, the entry's level until its `valid_until`, then at least `RESTRICT_USAGE`
64
+ * (marked `expired`). `undefined` when the mirror does not hold the organization. Synchronous, no I/O.
65
+ *
66
+ * It does not change `adjudicate()` or `guardWrite()`: a disconnected write is refused whatever the level.
67
+ */
68
+ effectiveLevel(organizationId: string): EffectiveLevel | undefined;
69
+ /**
70
+ * Pulls one organization now (sharing a pull already in flight for it) and resolves with the level of the
71
+ * answer, `source: 'answer'`. When the pull fails or does not finish within `timeout_ms` (default: the
72
+ * connect timeout), resolves with `effectiveLevel()` as it stands. `undefined` when the instance no longer
73
+ * holds the organization, or the mirror never did.
74
+ */
75
+ refresh(organizationId: string, options?: {
76
+ timeout_ms?: number;
77
+ }): Promise<EffectiveLevel | undefined>;
78
+ /**
79
+ * Sends a batch of usage events at once, outside the buffer and the backlog, and resolves with what the
80
+ * instance did with each. A 401 renews the token once, as every request does. Throws `UsageDeliveryError`
81
+ * when the batch did not reach the instance or it failed to process it (network error, timeout, 5xx); any
82
+ * other refusal throws as it came. Sending the same batch again is safe: events are deduplicated by key.
83
+ */
84
+ sendUsage(events: UsageSendEvent[]): Promise<UsageSendResult>;
85
+ /**
86
+ * Guard for write operations (fail-closed).
87
+ * Intercepts action if write operation is denied (e.g. disconnected).
88
+ */
89
+ guardWrite<T>(options: Omit<AdjudicateOptions, 'operation'>, action: () => Promise<T> | T): Promise<T>;
90
+ /**
91
+ * Two-phase entitlement reservation: reserve
92
+ */
93
+ reserve(options: ReserveOptions): Promise<ReservationResult>;
94
+ /**
95
+ * Two-phase entitlement reservation: confirm. Without `actualAmount` the reserved amount is confirmed.
96
+ */
97
+ confirm(reservationId: string, actualAmount?: number): Promise<ConfirmReservationResult>;
98
+ /**
99
+ * Two-phase entitlement reservation: release
100
+ */
101
+ release(reservationId: string): Promise<ReleaseReservationResult>;
102
+ /**
103
+ * Directly loads a snapshot into the local mirror (e.g. for offline seed testing).
104
+ */
105
+ loadSnapshot(snapshot: Snapshot): boolean;
106
+ /**
107
+ * Returns current internal state and mirror revision.
108
+ */
109
+ state(): {
110
+ state: ClientState;
111
+ snapshot_revision: number;
112
+ last_sync_at?: number;
113
+ };
114
+ /**
115
+ * Switches the connection state by hand, for fault injection and tests. `false` closes the push
116
+ * stream and suspends reconnection until `setOnline(true)`, which sets LIVE at once and resumes
117
+ * supervision; on a started client the supervisor then corrects the state.
118
+ */
119
+ setOnline(online: boolean): void;
120
+ /**
121
+ * Sets the machine token by hand (for tests without `start()`); a decodable JWT also sets the
122
+ * issuer and application `verifyToken()` expects.
123
+ */
124
+ setM2MToken(token: string): void;
125
+ /**
126
+ * Directly preloads a public key into the token verifier cache (for testing without network).
127
+ */
128
+ setPublicKey(kid: string, pem: string): void;
129
+ getUsageBuffer(): UsageBuffer;
130
+ getTransport(): HttpTransport;
131
+ private applyOnline;
132
+ private onMirrorChange;
133
+ /** Reports every organization whose effective level differs from what was last reported. */
134
+ private notifyLevels;
135
+ /** While disconnected, wakes up when the earliest entry still valid expires, to report it. */
136
+ private armExpiryTimer;
137
+ private learnExpectations;
138
+ private refillOrganization;
139
+ }
@@ -0,0 +1,400 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ShipRevClient = void 0;
4
+ const token_verifier_1 = require("../auth/token-verifier");
5
+ const usage_buffer_1 = require("../metering/usage-buffer");
6
+ const degradation_1 = require("../mirror/degradation");
7
+ const entitlement_mirror_1 = require("../mirror/entitlement-mirror");
8
+ const adjudicator_1 = require("../ruling/adjudicator");
9
+ const http_transport_1 = require("../transport/http.transport");
10
+ const types_1 = require("../types");
11
+ const connection_supervisor_1 = require("./connection-supervisor");
12
+ class ShipRevClient {
13
+ options;
14
+ transport;
15
+ tokenVerifier;
16
+ mirror;
17
+ adjudicator;
18
+ usageBuffer;
19
+ supervisor;
20
+ /** COLD until start() or setOnline(); otherwise the state follows the online signal and the mirror. */
21
+ active = false;
22
+ isOnline = false;
23
+ /** Issuer and application expected of session tokens, learned from the machine token. */
24
+ expectations = null;
25
+ /** Amounts reserved by this client, so `confirm()` without an amount confirms what was reserved. */
26
+ reservedAmounts = new Map();
27
+ /** Effective levels last reported through `on_level_change`. */
28
+ reportedLevels = new Map();
29
+ /** Fires when the earliest `valid_until` passes while disconnected, to report the expiry. */
30
+ expiryTimer = null;
31
+ constructor(options) {
32
+ this.options = options;
33
+ this.transport = new http_transport_1.HttpTransport({
34
+ baseUrl: options.base_url,
35
+ connectTimeoutMs: options.presets?.connect_timeout_ms,
36
+ });
37
+ this.tokenVerifier = new token_verifier_1.TokenVerifier(this.transport);
38
+ this.mirror = new entitlement_mirror_1.EntitlementMirror((organizationIds) => this.onMirrorChange(organizationIds));
39
+ this.adjudicator = new adjudicator_1.Adjudicator(this.mirror, options.presets);
40
+ this.supervisor = new connection_supervisor_1.ConnectionSupervisor({
41
+ transport: this.transport,
42
+ mirror: this.mirror,
43
+ apiKey: options.api_key,
44
+ connectTimeoutMs: options.presets?.connect_timeout_ms ?? 5000,
45
+ loadKeys: () => this.tokenVerifier.refreshKeys(),
46
+ onToken: (token) => this.learnExpectations(token),
47
+ setOnline: (online) => this.applyOnline(online),
48
+ flush: () => this.usageBuffer.flush(),
49
+ });
50
+ this.usageBuffer = new usage_buffer_1.UsageBuffer({
51
+ transport: this.transport,
52
+ getToken: () => this.supervisor.currentToken(),
53
+ isOnline: () => this.isOnline,
54
+ send: (events) => this.supervisor.authorized((token) => this.transport.reportUsageEvents(events, token)),
55
+ bufferConfig: options.buffer,
56
+ backlogConfig: options.backlog,
57
+ onFlushFailure: options.on_flush_failure,
58
+ onRejected: options.on_rejected,
59
+ });
60
+ }
61
+ /**
62
+ * Connects the client: fetches the signing keys, exchanges the API key for a service token, opens
63
+ * the push stream and waits for its `ready` frame, then loads the full entitlement snapshot. Never
64
+ * throws: LIVE when all four succeed, DISCONNECTED when only the stream failed, COLD_OFFLINE when
65
+ * nothing could be loaded. From here until `stop()` the client renews its token, follows the push
66
+ * stream and reconnects by itself.
67
+ */
68
+ async start() {
69
+ this.active = true;
70
+ await this.supervisor.start();
71
+ return {
72
+ project_id: this.mirror.getProjectId(),
73
+ snapshot_revision: this.mirror.getRevision(),
74
+ state: this.state().state,
75
+ };
76
+ }
77
+ /**
78
+ * Closes the push stream, stops every timer and flushes buffered usage events.
79
+ */
80
+ async stop() {
81
+ this.supervisor.stop();
82
+ if (this.expiryTimer) {
83
+ clearTimeout(this.expiryTimer);
84
+ this.expiryTimer = null;
85
+ }
86
+ await this.usageBuffer.stop();
87
+ this.active = false;
88
+ this.isOnline = false;
89
+ }
90
+ /**
91
+ * Enqueues a metered usage event into the memory buffer for batched delivery.
92
+ */
93
+ async reportUsage(event) {
94
+ if (!Number.isFinite(event.quantity)) {
95
+ throw new RangeError(`Usage quantity must be a finite number, got ${event.quantity}`);
96
+ }
97
+ await this.usageBuffer.reportUsage(event);
98
+ }
99
+ /**
100
+ * Manually flushes currently buffered usage events to the core service.
101
+ */
102
+ async flush() {
103
+ await this.usageBuffer.flush();
104
+ }
105
+ /**
106
+ * Verifies an RS256 JWT token locally and extracts subject and isolation keys.
107
+ */
108
+ async verifyToken(jwt) {
109
+ return this.tokenVerifier.verifyToken(jwt, this.expectations);
110
+ }
111
+ /**
112
+ * Executes a four-tier local adjudication (synchronous, zero-IO, P95 <= 5ms).
113
+ */
114
+ adjudicate(options) {
115
+ return this.adjudicator.adjudicate(options, this.isOnline);
116
+ }
117
+ /**
118
+ * Asynchronous adjudication with authoritative fallback.
119
+ * If local mirror misses the organization or rule and client is online, queries core service and backfills mirror.
120
+ */
121
+ async adjudicateAsync(options) {
122
+ const ruling = this.adjudicate(options);
123
+ // If local ruling is already allowed (realtime or degraded read) or degraded write rejection, return directly
124
+ if (ruling.allowed || ruling.source === 'degraded' || !this.isOnline) {
125
+ return ruling;
126
+ }
127
+ // Two cases are not decided by the mirror, and the service is asked for an authoritative ruling:
128
+ // the mirror misses the organization or capability, or the write goes beyond the allowance the mirror
129
+ // counts (credits and overdraft are the service's state). The mirror's refusal stands when no ruling comes.
130
+ const beyondAllowance = ruling.reason === 'quota_exhausted' && ruling.source === 'realtime';
131
+ if (this.supervisor.currentToken() && (ruling.reason === 'capability_not_granted' || beyondAllowance)) {
132
+ try {
133
+ const remote = await this.supervisor.authorized((token) => this.transport.lookupAdjudication({
134
+ organization_id: options.organization_id,
135
+ capability_key: options.capability_key,
136
+ quantity: options.quantity,
137
+ operation: options.operation,
138
+ }, token));
139
+ if (remote.allowed) {
140
+ await this.refillOrganization(options.organization_id);
141
+ }
142
+ return {
143
+ allowed: remote.allowed,
144
+ remaining: remote.remaining,
145
+ limit: remote.limit,
146
+ source: 'authoritative',
147
+ reason: remote.reason,
148
+ adjudicated_at: Date.now(),
149
+ };
150
+ }
151
+ catch {
152
+ // Fall back to original ruling if remote call fails
153
+ return ruling;
154
+ }
155
+ }
156
+ return ruling;
157
+ }
158
+ /**
159
+ * The effective degradation level of an organization: the level of the mirror entry while the instance
160
+ * answers; without an answer, the entry's level until its `valid_until`, then at least `RESTRICT_USAGE`
161
+ * (marked `expired`). `undefined` when the mirror does not hold the organization. Synchronous, no I/O.
162
+ *
163
+ * It does not change `adjudicate()` or `guardWrite()`: a disconnected write is refused whatever the level.
164
+ */
165
+ effectiveLevel(organizationId) {
166
+ const entry = this.mirror.getOrganization(organizationId);
167
+ return entry ? (0, degradation_1.combineLevel)(entry, this.isOnline, Date.now()) : undefined;
168
+ }
169
+ /**
170
+ * Pulls one organization now (sharing a pull already in flight for it) and resolves with the level of the
171
+ * answer, `source: 'answer'`. When the pull fails or does not finish within `timeout_ms` (default: the
172
+ * connect timeout), resolves with `effectiveLevel()` as it stands. `undefined` when the instance no longer
173
+ * holds the organization, or the mirror never did.
174
+ */
175
+ async refresh(organizationId, options) {
176
+ const timeoutMs = options?.timeout_ms ?? this.options.presets?.connect_timeout_ms ?? 5000;
177
+ let timer;
178
+ try {
179
+ const pulled = await Promise.race([
180
+ this.supervisor.pullOrganization(organizationId),
181
+ new Promise((_resolve, reject) => {
182
+ timer = setTimeout(() => reject(new Error('refresh timed out')), timeoutMs);
183
+ timer.unref();
184
+ }),
185
+ ]);
186
+ // Only an answer the mirror took is an answer; a stale or unsupported one leaves the level as it stands.
187
+ if (pulled.outcome === 'applied' && pulled.entry)
188
+ return (0, degradation_1.combineLevel)(pulled.entry, true, Date.now());
189
+ if (pulled.outcome === 'removed')
190
+ return undefined;
191
+ return this.effectiveLevel(organizationId);
192
+ }
193
+ catch {
194
+ return this.effectiveLevel(organizationId);
195
+ }
196
+ finally {
197
+ if (timer)
198
+ clearTimeout(timer);
199
+ }
200
+ }
201
+ /**
202
+ * Sends a batch of usage events at once, outside the buffer and the backlog, and resolves with what the
203
+ * instance did with each. A 401 renews the token once, as every request does. Throws `UsageDeliveryError`
204
+ * when the batch did not reach the instance or it failed to process it (network error, timeout, 5xx); any
205
+ * other refusal throws as it came. Sending the same batch again is safe: events are deduplicated by key.
206
+ */
207
+ async sendUsage(events) {
208
+ try {
209
+ return await this.supervisor.authorized((token) => this.transport.sendUsageEvents(events, token));
210
+ }
211
+ catch (error) {
212
+ if (error instanceof http_transport_1.HttpError && error.status < 500)
213
+ throw error;
214
+ throw new types_1.UsageDeliveryError(error instanceof Error ? error.message : 'Usage events could not be delivered');
215
+ }
216
+ }
217
+ /**
218
+ * Guard for write operations (fail-closed).
219
+ * Intercepts action if write operation is denied (e.g. disconnected).
220
+ */
221
+ async guardWrite(options, action) {
222
+ const ruling = this.adjudicate({ ...options, operation: 'write' });
223
+ if (!ruling.allowed) {
224
+ throw new types_1.DisconnectedWriteError(ruling.reason === 'disconnected_write'
225
+ ? 'Write operation denied while client is disconnected'
226
+ : `Write operation denied: ${ruling.reason ?? 'not allowed'}`);
227
+ }
228
+ return action();
229
+ }
230
+ /**
231
+ * Two-phase entitlement reservation: reserve
232
+ */
233
+ async reserve(options) {
234
+ const ttlSeconds = options.ttl_seconds ?? this.options.presets?.reservation_ttl_s;
235
+ const result = await this.supervisor.authorized((token) => this.transport.reserve(ttlSeconds !== undefined ? { ...options, ttl_seconds: ttlSeconds } : options, token));
236
+ this.reservedAmounts.set(result.reservation_id, options.amount);
237
+ return result;
238
+ }
239
+ /**
240
+ * Two-phase entitlement reservation: confirm. Without `actualAmount` the reserved amount is confirmed.
241
+ */
242
+ async confirm(reservationId, actualAmount) {
243
+ const amount = actualAmount ?? this.reservedAmounts.get(reservationId);
244
+ if (amount === undefined) {
245
+ throw new Error(`Reservation '${reservationId}' was not made by this client: pass the actual amount to confirm it`);
246
+ }
247
+ const result = await this.supervisor.authorized((token) => this.transport.confirmReservation(reservationId, amount, token));
248
+ this.reservedAmounts.delete(reservationId);
249
+ return result;
250
+ }
251
+ /**
252
+ * Two-phase entitlement reservation: release
253
+ */
254
+ async release(reservationId) {
255
+ const result = await this.supervisor.authorized((token) => this.transport.releaseReservation(reservationId, token));
256
+ this.reservedAmounts.delete(reservationId);
257
+ return result;
258
+ }
259
+ /**
260
+ * Directly loads a snapshot into the local mirror (e.g. for offline seed testing).
261
+ */
262
+ loadSnapshot(snapshot) {
263
+ return this.mirror.loadSnapshot(snapshot);
264
+ }
265
+ /**
266
+ * Returns current internal state and mirror revision.
267
+ */
268
+ state() {
269
+ const state = !this.active
270
+ ? 'COLD'
271
+ : this.isOnline
272
+ ? 'LIVE'
273
+ : this.mirror.isEmpty()
274
+ ? 'COLD_OFFLINE'
275
+ : 'DISCONNECTED';
276
+ return {
277
+ state,
278
+ snapshot_revision: this.mirror.getRevision(),
279
+ last_sync_at: this.mirror.getLastUpdatedAt() || undefined,
280
+ };
281
+ }
282
+ /**
283
+ * Switches the connection state by hand, for fault injection and tests. `false` closes the push
284
+ * stream and suspends reconnection until `setOnline(true)`, which sets LIVE at once and resumes
285
+ * supervision; on a started client the supervisor then corrects the state.
286
+ */
287
+ setOnline(online) {
288
+ this.active = true;
289
+ this.applyOnline(online);
290
+ if (online) {
291
+ this.supervisor.resume();
292
+ }
293
+ else {
294
+ this.supervisor.pause();
295
+ }
296
+ }
297
+ /**
298
+ * Sets the machine token by hand (for tests without `start()`); a decodable JWT also sets the
299
+ * issuer and application `verifyToken()` expects.
300
+ */
301
+ setM2MToken(token) {
302
+ this.supervisor.injectToken(token);
303
+ this.learnExpectations(token);
304
+ }
305
+ /**
306
+ * Directly preloads a public key into the token verifier cache (for testing without network).
307
+ */
308
+ setPublicKey(kid, pem) {
309
+ this.tokenVerifier.setPublicKey(kid, pem);
310
+ }
311
+ getUsageBuffer() {
312
+ return this.usageBuffer;
313
+ }
314
+ getTransport() {
315
+ return this.transport;
316
+ }
317
+ applyOnline(online) {
318
+ const changed = this.isOnline !== online;
319
+ this.isOnline = online;
320
+ if (changed) {
321
+ this.notifyLevels(this.mirror.organizationIds());
322
+ this.armExpiryTimer();
323
+ }
324
+ }
325
+ onMirrorChange(organizationIds) {
326
+ if (organizationIds === null) {
327
+ // The whole mirror was replaced: organizations that left it are forgotten.
328
+ for (const organizationId of [...this.reportedLevels.keys()]) {
329
+ if (!this.mirror.hasOrganization(organizationId))
330
+ this.reportedLevels.delete(organizationId);
331
+ }
332
+ }
333
+ this.notifyLevels(organizationIds ?? this.mirror.organizationIds());
334
+ this.armExpiryTimer();
335
+ this.supervisor?.scheduleRenewals();
336
+ }
337
+ /** Reports every organization whose effective level differs from what was last reported. */
338
+ notifyLevels(organizationIds) {
339
+ const listener = this.options.on_level_change;
340
+ if (!listener)
341
+ return;
342
+ for (const organizationId of organizationIds) {
343
+ const level = this.effectiveLevel(organizationId);
344
+ if (!level) {
345
+ this.reportedLevels.delete(organizationId);
346
+ continue;
347
+ }
348
+ if ((0, degradation_1.sameLevel)(level, this.reportedLevels.get(organizationId)))
349
+ continue;
350
+ this.reportedLevels.set(organizationId, level);
351
+ try {
352
+ listener(organizationId, level);
353
+ }
354
+ catch {
355
+ // A listener failure never reaches the connection.
356
+ }
357
+ }
358
+ }
359
+ /** While disconnected, wakes up when the earliest entry still valid expires, to report it. */
360
+ armExpiryTimer() {
361
+ if (this.expiryTimer) {
362
+ clearTimeout(this.expiryTimer);
363
+ this.expiryTimer = null;
364
+ }
365
+ if (!this.options.on_level_change || this.isOnline || !this.active)
366
+ return;
367
+ const now = Date.now();
368
+ let next = Number.POSITIVE_INFINITY;
369
+ for (const organizationId of this.mirror.organizationIds()) {
370
+ const validUntil = (0, degradation_1.validUntilOf)(this.mirror.getOrganization(organizationId));
371
+ if (validUntil !== undefined && validUntil > now)
372
+ next = Math.min(next, validUntil);
373
+ }
374
+ if (next === Number.POSITIVE_INFINITY)
375
+ return;
376
+ this.expiryTimer = setTimeout(() => {
377
+ this.expiryTimer = null;
378
+ this.notifyLevels(this.mirror.organizationIds());
379
+ this.armExpiryTimer();
380
+ }, next - now);
381
+ this.expiryTimer.unref();
382
+ }
383
+ learnExpectations(token) {
384
+ const expectations = (0, token_verifier_1.expectationsFromMachineToken)(token);
385
+ if (expectations) {
386
+ this.expectations = expectations;
387
+ }
388
+ }
389
+ async refillOrganization(organizationId) {
390
+ try {
391
+ const snapshot = await this.supervisor.authorized((token) => this.transport.fetchSnapshot(token, organizationId));
392
+ this.mirror.mergeSnapshot(snapshot);
393
+ }
394
+ catch {
395
+ // The authoritative ruling stands; the next full sync fills the mirror.
396
+ }
397
+ }
398
+ }
399
+ exports.ShipRevClient = ShipRevClient;
400
+ //# sourceMappingURL=shiprev-client.js.map
@@ -0,0 +1,16 @@
1
+ import { ShipRevClient } from './client/shiprev-client';
2
+ import { ShipRevClientOptions } from './types';
3
+ export { jwkToPem, TokenVerifier } from './auth/token-verifier';
4
+ export { ShipRevClient } from './client/shiprev-client';
5
+ export { HostAdapterBacklog } from './metering/backlog/host-adapter-backlog';
6
+ export { JournalBacklog } from './metering/backlog/journal-backlog';
7
+ export { MemoryBacklog } from './metering/backlog/memory-backlog';
8
+ export { UsageBuffer } from './metering/usage-buffer';
9
+ export { EntitlementMirror, MAX_SUPPORTED_SCHEMA_VERSION } from './mirror/entitlement-mirror';
10
+ export { Adjudicator } from './ruling/adjudicator';
11
+ export { HttpTransport } from './transport/http.transport';
12
+ export * from './types';
13
+ /**
14
+ * Creates and initializes a ShipRev Server SDK client instance for Node.js services.
15
+ */
16
+ export declare function createClient(options: ShipRevClientOptions): ShipRevClient;
package/dist/index.js ADDED
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.HttpTransport = exports.Adjudicator = exports.MAX_SUPPORTED_SCHEMA_VERSION = exports.EntitlementMirror = exports.UsageBuffer = exports.MemoryBacklog = exports.JournalBacklog = exports.HostAdapterBacklog = exports.ShipRevClient = exports.TokenVerifier = exports.jwkToPem = void 0;
18
+ exports.createClient = createClient;
19
+ const shiprev_client_1 = require("./client/shiprev-client");
20
+ var token_verifier_1 = require("./auth/token-verifier");
21
+ Object.defineProperty(exports, "jwkToPem", { enumerable: true, get: function () { return token_verifier_1.jwkToPem; } });
22
+ Object.defineProperty(exports, "TokenVerifier", { enumerable: true, get: function () { return token_verifier_1.TokenVerifier; } });
23
+ var shiprev_client_2 = require("./client/shiprev-client");
24
+ Object.defineProperty(exports, "ShipRevClient", { enumerable: true, get: function () { return shiprev_client_2.ShipRevClient; } });
25
+ var host_adapter_backlog_1 = require("./metering/backlog/host-adapter-backlog");
26
+ Object.defineProperty(exports, "HostAdapterBacklog", { enumerable: true, get: function () { return host_adapter_backlog_1.HostAdapterBacklog; } });
27
+ var journal_backlog_1 = require("./metering/backlog/journal-backlog");
28
+ Object.defineProperty(exports, "JournalBacklog", { enumerable: true, get: function () { return journal_backlog_1.JournalBacklog; } });
29
+ var memory_backlog_1 = require("./metering/backlog/memory-backlog");
30
+ Object.defineProperty(exports, "MemoryBacklog", { enumerable: true, get: function () { return memory_backlog_1.MemoryBacklog; } });
31
+ var usage_buffer_1 = require("./metering/usage-buffer");
32
+ Object.defineProperty(exports, "UsageBuffer", { enumerable: true, get: function () { return usage_buffer_1.UsageBuffer; } });
33
+ var entitlement_mirror_1 = require("./mirror/entitlement-mirror");
34
+ Object.defineProperty(exports, "EntitlementMirror", { enumerable: true, get: function () { return entitlement_mirror_1.EntitlementMirror; } });
35
+ Object.defineProperty(exports, "MAX_SUPPORTED_SCHEMA_VERSION", { enumerable: true, get: function () { return entitlement_mirror_1.MAX_SUPPORTED_SCHEMA_VERSION; } });
36
+ var adjudicator_1 = require("./ruling/adjudicator");
37
+ Object.defineProperty(exports, "Adjudicator", { enumerable: true, get: function () { return adjudicator_1.Adjudicator; } });
38
+ var http_transport_1 = require("./transport/http.transport");
39
+ Object.defineProperty(exports, "HttpTransport", { enumerable: true, get: function () { return http_transport_1.HttpTransport; } });
40
+ __exportStar(require("./types"), exports);
41
+ /**
42
+ * Creates and initializes a ShipRev Server SDK client instance for Node.js services.
43
+ */
44
+ function createClient(options) {
45
+ return new shiprev_client_1.ShipRevClient(options);
46
+ }
47
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,10 @@
1
+ import { BacklogAdapter, BacklogItem, UsageEventItem } from '../../types';
2
+ export declare class HostAdapterBacklog implements BacklogAdapter {
3
+ private readonly adapter;
4
+ constructor(customAdapter?: BacklogAdapter);
5
+ enqueue(event: UsageEventItem): Promise<void>;
6
+ peek(limit: number): Promise<BacklogItem[]>;
7
+ ack(ids: string[]): Promise<void>;
8
+ nack(ids: string[]): Promise<void>;
9
+ size(): Promise<number>;
10
+ }
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HostAdapterBacklog = void 0;
4
+ const memory_backlog_1 = require("./memory-backlog");
5
+ class HostAdapterBacklog {
6
+ adapter;
7
+ constructor(customAdapter) {
8
+ this.adapter = customAdapter ?? new memory_backlog_1.MemoryBacklog();
9
+ }
10
+ async enqueue(event) {
11
+ return this.adapter.enqueue(event);
12
+ }
13
+ async peek(limit) {
14
+ return this.adapter.peek(limit);
15
+ }
16
+ async ack(ids) {
17
+ return this.adapter.ack(ids);
18
+ }
19
+ async nack(ids) {
20
+ return this.adapter.nack(ids);
21
+ }
22
+ async size() {
23
+ return this.adapter.size();
24
+ }
25
+ }
26
+ exports.HostAdapterBacklog = HostAdapterBacklog;
27
+ //# sourceMappingURL=host-adapter-backlog.js.map
@@ -0,0 +1,14 @@
1
+ import { BacklogAdapter, BacklogItem, UsageEventItem } from '../../types';
2
+ export declare class JournalBacklog implements BacklogAdapter {
3
+ private readonly filePath;
4
+ private queue;
5
+ private isCompacting;
6
+ constructor(filePath?: string);
7
+ private recoverSync;
8
+ enqueue(event: UsageEventItem): Promise<void>;
9
+ peek(limit: number): Promise<BacklogItem[]>;
10
+ ack(ids: string[]): Promise<void>;
11
+ nack(ids: string[]): Promise<void>;
12
+ size(): Promise<number>;
13
+ private compact;
14
+ }