@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.
- package/LICENSE +202 -0
- package/README.md +253 -2
- package/dist/auth/token-verifier.d.ts +42 -0
- package/dist/auth/token-verifier.js +230 -0
- package/dist/client/connection-supervisor.d.ts +117 -0
- package/dist/client/connection-supervisor.js +547 -0
- package/dist/client/shiprev-client.d.ts +106 -0
- package/dist/client/shiprev-client.js +267 -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/entitlement-mirror.d.ts +47 -0
- package/dist/mirror/entitlement-mirror.js +109 -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 +131 -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 +64 -0
- package/dist/transport/http.transport.js +218 -0
- package/dist/transport/wire.d.ts +79 -0
- package/dist/transport/wire.js +8 -0
- package/dist/types.d.ts +191 -0
- package/dist/types.js +25 -0
- package/dist/util/decimal.d.ts +14 -0
- package/dist/util/decimal.js +57 -0
- package/package.json +98 -4
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
export type RulingSource = 'realtime' | 'authoritative' | 'degraded' | 'client_default';
|
|
2
|
+
export interface Ruling {
|
|
3
|
+
allowed: boolean;
|
|
4
|
+
remaining?: number;
|
|
5
|
+
limit?: number;
|
|
6
|
+
source: RulingSource;
|
|
7
|
+
reason?: 'disconnected_write' | 'invalid_request' | string;
|
|
8
|
+
adjudicated_at: number;
|
|
9
|
+
}
|
|
10
|
+
export interface AdjudicateOptions {
|
|
11
|
+
organization_id: string;
|
|
12
|
+
capability_key: string;
|
|
13
|
+
quantity?: number;
|
|
14
|
+
operation: 'read' | 'write';
|
|
15
|
+
}
|
|
16
|
+
export interface TokenClaims {
|
|
17
|
+
account_id: string;
|
|
18
|
+
organization_id: string;
|
|
19
|
+
project_id: string;
|
|
20
|
+
tenant_id: string;
|
|
21
|
+
key_version: number;
|
|
22
|
+
scope: string;
|
|
23
|
+
exp: number;
|
|
24
|
+
iat?: number;
|
|
25
|
+
iss?: string;
|
|
26
|
+
sub?: string;
|
|
27
|
+
aud?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Overage policy of a capability, as the service reports it. Unknown future values are kept as
|
|
31
|
+
* strings and treated like BLOCK by local rulings.
|
|
32
|
+
*/
|
|
33
|
+
export type OveragePolicy = 'BLOCK' | 'ALLOW_AND_BILL' | 'PROMPT_UPGRADE' | (string & {});
|
|
34
|
+
/**
|
|
35
|
+
* One capability of an organization's snapshot, as served by `GET /sdk/v1/entitlements/snapshot`.
|
|
36
|
+
* Quantities are fixed-point decimal strings (e.g. "10.000000"), never floating point.
|
|
37
|
+
*/
|
|
38
|
+
export interface SnapshotItem {
|
|
39
|
+
capability_key: string;
|
|
40
|
+
allowance: string;
|
|
41
|
+
used: string;
|
|
42
|
+
remaining: string;
|
|
43
|
+
overage_policy: OveragePolicy;
|
|
44
|
+
starts_at?: string;
|
|
45
|
+
ends_at?: string;
|
|
46
|
+
reset_interval?: string;
|
|
47
|
+
billing_interval?: string;
|
|
48
|
+
}
|
|
49
|
+
/** One buyer organization inside the application snapshot. */
|
|
50
|
+
export interface OrganizationSnapshot {
|
|
51
|
+
revision: number;
|
|
52
|
+
wallet_balance: string;
|
|
53
|
+
items: Record<string, SnapshotItem>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Application-level entitlement snapshot envelope served by `GET /sdk/v1/entitlements/snapshot`:
|
|
57
|
+
* every buyer organization of the application, keyed by organization ID.
|
|
58
|
+
*/
|
|
59
|
+
export interface Snapshot {
|
|
60
|
+
project_id: string;
|
|
61
|
+
schema_version: number;
|
|
62
|
+
generated_at: string;
|
|
63
|
+
organizations: Record<string, OrganizationSnapshot>;
|
|
64
|
+
}
|
|
65
|
+
export interface ClientPresets {
|
|
66
|
+
cold_start?: 'allow' | 'deny';
|
|
67
|
+
connect_timeout_ms?: number;
|
|
68
|
+
reservation_ttl_s?: number;
|
|
69
|
+
}
|
|
70
|
+
export interface BacklogItem {
|
|
71
|
+
id: string;
|
|
72
|
+
event: UsageEventItem;
|
|
73
|
+
enqueued_at: number;
|
|
74
|
+
attempts: number;
|
|
75
|
+
}
|
|
76
|
+
export interface BacklogAdapter {
|
|
77
|
+
enqueue(event: UsageEventItem): Promise<void>;
|
|
78
|
+
peek(limit: number): Promise<BacklogItem[]>;
|
|
79
|
+
ack(ids: string[]): Promise<void>;
|
|
80
|
+
nack(ids: string[]): Promise<void>;
|
|
81
|
+
size(): Promise<number>;
|
|
82
|
+
}
|
|
83
|
+
export interface BacklogConfig {
|
|
84
|
+
mode?: 'memory' | 'host_adapter' | 'journal';
|
|
85
|
+
journal_path?: string;
|
|
86
|
+
adapter?: BacklogAdapter;
|
|
87
|
+
max_attempts?: number;
|
|
88
|
+
}
|
|
89
|
+
export interface UsageBufferConfig {
|
|
90
|
+
batch_size?: number;
|
|
91
|
+
flush_interval_ms?: number;
|
|
92
|
+
}
|
|
93
|
+
export interface UsageEventItem {
|
|
94
|
+
usage_event_id: string;
|
|
95
|
+
organization_id: string;
|
|
96
|
+
capability_key: string;
|
|
97
|
+
quantity: number;
|
|
98
|
+
occurred_at: string;
|
|
99
|
+
idempotency_key?: string;
|
|
100
|
+
properties?: Record<string, unknown>;
|
|
101
|
+
}
|
|
102
|
+
export interface RejectedEvent {
|
|
103
|
+
usage_event_id: string;
|
|
104
|
+
reason: string;
|
|
105
|
+
}
|
|
106
|
+
export interface UsageEventsResponse {
|
|
107
|
+
accepted: number;
|
|
108
|
+
duplicates_dropped: number;
|
|
109
|
+
rejected: RejectedEvent[];
|
|
110
|
+
}
|
|
111
|
+
export interface ReserveOptions {
|
|
112
|
+
organization_id: string;
|
|
113
|
+
capability_key: string;
|
|
114
|
+
amount: number;
|
|
115
|
+
idempotency_key?: string;
|
|
116
|
+
ttl_seconds?: number;
|
|
117
|
+
}
|
|
118
|
+
export interface ReservationResult {
|
|
119
|
+
reservation_id: string;
|
|
120
|
+
status: 'PENDING' | 'CONFIRMED' | 'RELEASED' | 'RECLAIMED';
|
|
121
|
+
expires_at: string;
|
|
122
|
+
}
|
|
123
|
+
export interface ConfirmReservationResult {
|
|
124
|
+
reservation_id: string;
|
|
125
|
+
status: 'CONFIRMED';
|
|
126
|
+
confirmed_amount: number;
|
|
127
|
+
}
|
|
128
|
+
export interface ReleaseReservationResult {
|
|
129
|
+
reservation_id: string;
|
|
130
|
+
status: 'RELEASED';
|
|
131
|
+
}
|
|
132
|
+
export interface AdjudicationLookupRequest {
|
|
133
|
+
organization_id: string;
|
|
134
|
+
capability_key: string;
|
|
135
|
+
quantity?: number;
|
|
136
|
+
operation: 'read' | 'write';
|
|
137
|
+
}
|
|
138
|
+
export interface AdjudicationLookupResponse {
|
|
139
|
+
allowed: boolean;
|
|
140
|
+
remaining?: number;
|
|
141
|
+
limit?: number;
|
|
142
|
+
source: 'authoritative';
|
|
143
|
+
reason?: string;
|
|
144
|
+
}
|
|
145
|
+
export interface ShipRevClientOptions {
|
|
146
|
+
api_key: string;
|
|
147
|
+
base_url: string;
|
|
148
|
+
presets?: ClientPresets;
|
|
149
|
+
buffer?: UsageBufferConfig;
|
|
150
|
+
backlog?: BacklogConfig;
|
|
151
|
+
on_flush_failure?: (events: UsageEventItem[], error: Error) => void;
|
|
152
|
+
on_rejected?: (rejected: RejectedEvent[]) => void;
|
|
153
|
+
}
|
|
154
|
+
export declare class DisconnectedWriteError extends Error {
|
|
155
|
+
readonly reason = "disconnected_write";
|
|
156
|
+
constructor(message?: string);
|
|
157
|
+
}
|
|
158
|
+
/** Why `verifyToken()` refused a correctly signed, unexpired token. */
|
|
159
|
+
export type TokenRejectionReason =
|
|
160
|
+
/** The client has not obtained a machine token yet, so the expected issuer and application are unknown. */
|
|
161
|
+
'not_ready'
|
|
162
|
+
/** `aud` or `scope` is not `'public'`: not a buyer session token. */
|
|
163
|
+
| 'audience_mismatch'
|
|
164
|
+
/** `iss` differs from the issuer of this client's machine token. */
|
|
165
|
+
| 'issuer_mismatch'
|
|
166
|
+
/** `pid` differs from the application of this client's machine token. */
|
|
167
|
+
| 'project_mismatch'
|
|
168
|
+
/** The token carries a `purpose` claim: a one-time token such as an email verification link. */
|
|
169
|
+
| 'not_a_session_token'
|
|
170
|
+
/** The token has no numeric `exp`. */
|
|
171
|
+
| 'missing_expiry';
|
|
172
|
+
/**
|
|
173
|
+
* Thrown by `verifyToken()` when a token is correctly signed but is not a session token this client
|
|
174
|
+
* accepts. The message names the reason and never contains the token.
|
|
175
|
+
*/
|
|
176
|
+
export declare class TokenRejectedError extends Error {
|
|
177
|
+
readonly reason: TokenRejectionReason;
|
|
178
|
+
constructor(reason: TokenRejectionReason, message?: string);
|
|
179
|
+
}
|
|
180
|
+
export type ClientState = 'COLD' | 'CLAIMED' | 'LIVE' | 'COLD_OFFLINE' | 'DISCONNECTED';
|
|
181
|
+
export interface JwksKey {
|
|
182
|
+
kid: string;
|
|
183
|
+
kty: string;
|
|
184
|
+
alg?: string;
|
|
185
|
+
use?: string;
|
|
186
|
+
n: string;
|
|
187
|
+
e: string;
|
|
188
|
+
}
|
|
189
|
+
export interface JwksResponse {
|
|
190
|
+
keys: JwksKey[];
|
|
191
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TokenRejectedError = exports.DisconnectedWriteError = void 0;
|
|
4
|
+
class DisconnectedWriteError extends Error {
|
|
5
|
+
reason = 'disconnected_write';
|
|
6
|
+
constructor(message = 'Write operation denied while client is disconnected') {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'DisconnectedWriteError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
exports.DisconnectedWriteError = DisconnectedWriteError;
|
|
12
|
+
/**
|
|
13
|
+
* Thrown by `verifyToken()` when a token is correctly signed but is not a session token this client
|
|
14
|
+
* accepts. The message names the reason and never contains the token.
|
|
15
|
+
*/
|
|
16
|
+
class TokenRejectedError extends Error {
|
|
17
|
+
reason;
|
|
18
|
+
constructor(reason, message = `Token rejected: ${reason}`) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = 'TokenRejectedError';
|
|
21
|
+
this.reason = reason;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
exports.TokenRejectedError = TokenRejectedError;
|
|
25
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exact helpers for the fixed-point decimal strings the service uses for quantities and amounts.
|
|
3
|
+
* Public SDK types carry numbers; these helpers convert at the wire boundary and compare
|
|
4
|
+
* decimal strings without going through floating point.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Converts a finite number to a plain decimal string (no exponent), e.g. 3 -> "3", 1e-7 -> "0.0000001".
|
|
8
|
+
* Throws RangeError for NaN or infinite values.
|
|
9
|
+
*/
|
|
10
|
+
export declare function toDecimalString(value: number): string;
|
|
11
|
+
/** Compares two decimal strings exactly: negative when a < b, zero when equal, positive when a > b. */
|
|
12
|
+
export declare function compareDecimal(a: string, b: string): number;
|
|
13
|
+
/** Reads a decimal string from the wire into the number used by public SDK types. */
|
|
14
|
+
export declare function decimalToNumber(value: string | null | undefined): number | undefined;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Exact helpers for the fixed-point decimal strings the service uses for quantities and amounts.
|
|
4
|
+
* Public SDK types carry numbers; these helpers convert at the wire boundary and compare
|
|
5
|
+
* decimal strings without going through floating point.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.toDecimalString = toDecimalString;
|
|
9
|
+
exports.compareDecimal = compareDecimal;
|
|
10
|
+
exports.decimalToNumber = decimalToNumber;
|
|
11
|
+
const DECIMAL_PATTERN = /^(-)?(\d+)(?:\.(\d+))?$/;
|
|
12
|
+
/**
|
|
13
|
+
* Converts a finite number to a plain decimal string (no exponent), e.g. 3 -> "3", 1e-7 -> "0.0000001".
|
|
14
|
+
* Throws RangeError for NaN or infinite values.
|
|
15
|
+
*/
|
|
16
|
+
function toDecimalString(value) {
|
|
17
|
+
if (!Number.isFinite(value)) {
|
|
18
|
+
throw new RangeError(`Quantity must be a finite number, got ${value}`);
|
|
19
|
+
}
|
|
20
|
+
const text = String(value);
|
|
21
|
+
if (!/e/i.test(text)) {
|
|
22
|
+
return text;
|
|
23
|
+
}
|
|
24
|
+
// Exponent notation: expand through toFixed and drop trailing zeros.
|
|
25
|
+
const fixed = Math.abs(value) >= 1 ? BigInt(Math.trunc(value)).toString() : value.toFixed(20);
|
|
26
|
+
return fixed.includes('.') ? fixed.replace(/0+$/, '').replace(/\.$/, '') : fixed;
|
|
27
|
+
}
|
|
28
|
+
function parse(value) {
|
|
29
|
+
const match = DECIMAL_PATTERN.exec(value.trim());
|
|
30
|
+
if (!match) {
|
|
31
|
+
throw new RangeError(`Not a decimal string: ${value}`);
|
|
32
|
+
}
|
|
33
|
+
const fraction = match[3] ?? '';
|
|
34
|
+
return {
|
|
35
|
+
negative: match[1] === '-',
|
|
36
|
+
digits: BigInt(`${match[2]}${fraction}`),
|
|
37
|
+
scale: fraction.length,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** Compares two decimal strings exactly: negative when a < b, zero when equal, positive when a > b. */
|
|
41
|
+
function compareDecimal(a, b) {
|
|
42
|
+
const pa = parse(a);
|
|
43
|
+
const pb = parse(b);
|
|
44
|
+
const scale = Math.max(pa.scale, pb.scale);
|
|
45
|
+
const va = (pa.negative ? -1n : 1n) * pa.digits * 10n ** BigInt(scale - pa.scale);
|
|
46
|
+
const vb = (pb.negative ? -1n : 1n) * pb.digits * 10n ** BigInt(scale - pb.scale);
|
|
47
|
+
return va < vb ? -1 : va > vb ? 1 : 0;
|
|
48
|
+
}
|
|
49
|
+
/** Reads a decimal string from the wire into the number used by public SDK types. */
|
|
50
|
+
function decimalToNumber(value) {
|
|
51
|
+
if (value === null || value === undefined) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
const parsed = Number(value);
|
|
55
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=decimal.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,100 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shiprev/server-sdk-node",
|
|
3
|
-
"version": "0.0.0
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0-beta.0",
|
|
4
|
+
"description": "Server SDK for Node.js and NestJS services: token verification, local entitlement rulings, usage reporting, and two-phase reservations against a ShipRev instance.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"shiprev",
|
|
7
|
+
"entitlements",
|
|
8
|
+
"metering",
|
|
9
|
+
"usage-based-billing",
|
|
10
|
+
"nestjs",
|
|
11
|
+
"sdk"
|
|
12
|
+
],
|
|
13
|
+
"license": "Apache-2.0",
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=20.0.0"
|
|
16
|
+
},
|
|
17
|
+
"main": "dist/index.js",
|
|
18
|
+
"types": "dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./nest": {
|
|
25
|
+
"types": "./dist/nest/index.d.ts",
|
|
26
|
+
"default": "./dist/nest/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
"typesVersions": {
|
|
31
|
+
"*": {
|
|
32
|
+
"nest": [
|
|
33
|
+
"./dist/nest/index.d.ts"
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist",
|
|
39
|
+
"!dist/**/*.map"
|
|
40
|
+
],
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public",
|
|
43
|
+
"tag": "beta"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
47
|
+
"build": "tsc -p tsconfig.build.json",
|
|
48
|
+
"prepack": "pnpm run clean && pnpm run build",
|
|
49
|
+
"test": "jest",
|
|
50
|
+
"test:nest9": "node scripts/test-nest9.mjs",
|
|
51
|
+
"lint": "eslint \"src/**/*.ts\"",
|
|
52
|
+
"format": "prettier --write \"src/**/*.ts\""
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"@nestjs/common": "^9.0.0 || ^10.0.0 || ^11.0.0",
|
|
56
|
+
"@nestjs/core": "^9.0.0 || ^10.0.0 || ^11.0.0",
|
|
57
|
+
"reflect-metadata": "^0.1.13 || ^0.2.0"
|
|
58
|
+
},
|
|
59
|
+
"peerDependenciesMeta": {
|
|
60
|
+
"@nestjs/common": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"@nestjs/core": {
|
|
64
|
+
"optional": true
|
|
65
|
+
},
|
|
66
|
+
"reflect-metadata": {
|
|
67
|
+
"optional": true
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"devDependencies": {
|
|
71
|
+
"@nestjs/common": "^11.0.1",
|
|
72
|
+
"@nestjs/core": "^11.1.12",
|
|
73
|
+
"@types/jest": "^29.5.14",
|
|
74
|
+
"@types/node": "^22.10.7",
|
|
75
|
+
"eslint": "^9.18.0",
|
|
76
|
+
"eslint-plugin-prettier": "^5.2.2",
|
|
77
|
+
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
78
|
+
"globals": "^16.0.0",
|
|
79
|
+
"jest": "^29.7.0",
|
|
80
|
+
"prettier": "^3.0.0",
|
|
81
|
+
"reflect-metadata": "^0.2.2",
|
|
82
|
+
"rxjs": "^7.8.1",
|
|
83
|
+
"ts-jest": "^29.2.5",
|
|
84
|
+
"typescript": "^5.7.3",
|
|
85
|
+
"typescript-eslint": "^8.20.0"
|
|
86
|
+
},
|
|
87
|
+
"jest": {
|
|
88
|
+
"moduleFileExtensions": [
|
|
89
|
+
"js",
|
|
90
|
+
"json",
|
|
91
|
+
"ts"
|
|
92
|
+
],
|
|
93
|
+
"rootDir": "src",
|
|
94
|
+
"testRegex": ".*\\.spec\\.ts$",
|
|
95
|
+
"transform": {
|
|
96
|
+
"^.+\\.ts$": "ts-jest"
|
|
97
|
+
},
|
|
98
|
+
"testEnvironment": "node"
|
|
99
|
+
}
|
|
100
|
+
}
|