@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.
- package/LICENSE +202 -0
- package/README.md +276 -2
- package/dist/auth/token-verifier.d.ts +56 -0
- package/dist/auth/token-verifier.js +257 -0
- package/dist/client/connection-supervisor.d.ts +148 -0
- package/dist/client/connection-supervisor.js +650 -0
- package/dist/client/shiprev-client.d.ts +139 -0
- package/dist/client/shiprev-client.js +400 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +47 -0
- package/dist/metering/backlog/host-adapter-backlog.d.ts +10 -0
- package/dist/metering/backlog/host-adapter-backlog.js +27 -0
- package/dist/metering/backlog/journal-backlog.d.ts +14 -0
- package/dist/metering/backlog/journal-backlog.js +135 -0
- package/dist/metering/backlog/memory-backlog.d.ts +9 -0
- package/dist/metering/backlog/memory-backlog.js +34 -0
- package/dist/metering/usage-buffer.d.ts +59 -0
- package/dist/metering/usage-buffer.js +175 -0
- package/dist/mirror/degradation.d.ts +22 -0
- package/dist/mirror/degradation.js +67 -0
- package/dist/mirror/entitlement-mirror.d.ts +61 -0
- package/dist/mirror/entitlement-mirror.js +143 -0
- package/dist/nest/decorators.d.ts +15 -0
- package/dist/nest/decorators.js +21 -0
- package/dist/nest/index.d.ts +4 -0
- package/dist/nest/index.js +21 -0
- package/dist/nest/shiprev-auth.guard.d.ts +7 -0
- package/dist/nest/shiprev-auth.guard.js +52 -0
- package/dist/nest/shiprev-capability.guard.d.ts +9 -0
- package/dist/nest/shiprev-capability.guard.js +65 -0
- package/dist/nest/shiprev.module.d.ts +17 -0
- package/dist/nest/shiprev.module.js +57 -0
- package/dist/ruling/adjudicator.d.ts +15 -0
- package/dist/ruling/adjudicator.js +123 -0
- package/dist/transport/event-stream.d.ts +27 -0
- package/dist/transport/event-stream.js +92 -0
- package/dist/transport/http.transport.d.ts +69 -0
- package/dist/transport/http.transport.js +238 -0
- package/dist/transport/wire.d.ts +79 -0
- package/dist/transport/wire.js +8 -0
- package/dist/types.d.ts +252 -0
- package/dist/types.js +37 -0
- package/dist/util/decimal.d.ts +14 -0
- package/dist/util/decimal.js +57 -0
- 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
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|