@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
|
@@ -0,0 +1,230 @@
|
|
|
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 __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.TokenVerifier = void 0;
|
|
37
|
+
exports.jwkToPem = jwkToPem;
|
|
38
|
+
exports.expectationsFromMachineToken = expectationsFromMachineToken;
|
|
39
|
+
exports.expiryOfToken = expiryOfToken;
|
|
40
|
+
const crypto = __importStar(require("crypto"));
|
|
41
|
+
const types_1 = require("../types");
|
|
42
|
+
/** The audience and scope of the session tokens the portal issues to buyer end users. */
|
|
43
|
+
const SESSION_AUDIENCE = 'public';
|
|
44
|
+
function base64UrlDecode(str) {
|
|
45
|
+
let base64 = str.replace(/-/g, '+').replace(/_/g, '/');
|
|
46
|
+
while (base64.length % 4 !== 0) {
|
|
47
|
+
base64 += '=';
|
|
48
|
+
}
|
|
49
|
+
return Buffer.from(base64, 'base64');
|
|
50
|
+
}
|
|
51
|
+
function jwkToPem(jwk) {
|
|
52
|
+
const keyObj = crypto.createPublicKey({
|
|
53
|
+
key: {
|
|
54
|
+
kty: jwk.kty,
|
|
55
|
+
n: jwk.n,
|
|
56
|
+
e: jwk.e,
|
|
57
|
+
},
|
|
58
|
+
format: 'jwk',
|
|
59
|
+
});
|
|
60
|
+
return keyObj.export({ type: 'spki', format: 'pem' });
|
|
61
|
+
}
|
|
62
|
+
class TokenVerifier {
|
|
63
|
+
transport;
|
|
64
|
+
publicKeyCache = new Map();
|
|
65
|
+
constructor(transport) {
|
|
66
|
+
this.transport = transport;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Refreshes the local in-memory public key cache from the JWKS endpoint
|
|
70
|
+
*/
|
|
71
|
+
async refreshKeys() {
|
|
72
|
+
const jwks = await this.transport.fetchJwks();
|
|
73
|
+
for (const key of jwks.keys) {
|
|
74
|
+
const pem = jwkToPem(key);
|
|
75
|
+
this.publicKeyCache.set(key.kid, pem);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Directly caches a key in memory (e.g. for testing or preloading)
|
|
80
|
+
*/
|
|
81
|
+
setPublicKey(kid, pem) {
|
|
82
|
+
this.publicKeyCache.set(kid, pem);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Verifies an RS256 JWT token locally, extracting the subject and isolation keys.
|
|
86
|
+
* If the kid is unknown in cache, it triggers a single JWKS refresh.
|
|
87
|
+
*
|
|
88
|
+
* With `expectations` (or `null`, meaning they are not known yet), the token must also be a buyer
|
|
89
|
+
* session token of that issuer and application; the checks run after the signature, in the order
|
|
90
|
+
* exp · purpose · aud/scope · iss · pid, and refusals are `TokenRejectedError`. Without the
|
|
91
|
+
* argument only the signature and, when present, the expiry are checked.
|
|
92
|
+
*/
|
|
93
|
+
async verifyToken(token, expectations) {
|
|
94
|
+
if (!token || typeof token !== 'string') {
|
|
95
|
+
throw new Error('Token must be a non-empty string');
|
|
96
|
+
}
|
|
97
|
+
const parts = token.split('.');
|
|
98
|
+
if (parts.length !== 3) {
|
|
99
|
+
throw new Error('Invalid JWT format');
|
|
100
|
+
}
|
|
101
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
102
|
+
// Decode header to retrieve kid and alg
|
|
103
|
+
let header;
|
|
104
|
+
try {
|
|
105
|
+
header = JSON.parse(base64UrlDecode(headerB64).toString('utf-8'));
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
throw new Error('Invalid JWT header format');
|
|
109
|
+
}
|
|
110
|
+
if (header.alg !== 'RS256') {
|
|
111
|
+
throw new Error(`Unsupported JWT algorithm: ${header.alg ?? 'unknown'}, expected RS256`);
|
|
112
|
+
}
|
|
113
|
+
const kid = header.kid;
|
|
114
|
+
if (!kid) {
|
|
115
|
+
throw new Error('JWT header is missing kid');
|
|
116
|
+
}
|
|
117
|
+
// Lookup public key from local cache
|
|
118
|
+
let publicKeyPem = this.publicKeyCache.get(kid);
|
|
119
|
+
// Cache miss / key rotation: refresh keys once
|
|
120
|
+
if (!publicKeyPem) {
|
|
121
|
+
await this.refreshKeys();
|
|
122
|
+
publicKeyPem = this.publicKeyCache.get(kid);
|
|
123
|
+
if (!publicKeyPem) {
|
|
124
|
+
throw new Error(`Unknown key ID: kid=${kid}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Verify RS256 signature
|
|
128
|
+
const verifier = crypto.createVerify('RSA-SHA256');
|
|
129
|
+
verifier.update(`${headerB64}.${payloadB64}`);
|
|
130
|
+
verifier.end();
|
|
131
|
+
const signature = base64UrlDecode(signatureB64);
|
|
132
|
+
const isValid = verifier.verify(publicKeyPem, signature);
|
|
133
|
+
if (!isValid) {
|
|
134
|
+
throw new Error('Invalid JWT signature');
|
|
135
|
+
}
|
|
136
|
+
// Decode and validate payload claims
|
|
137
|
+
let payload;
|
|
138
|
+
try {
|
|
139
|
+
payload = JSON.parse(base64UrlDecode(payloadB64).toString('utf-8'));
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
throw new Error('Invalid JWT payload format');
|
|
143
|
+
}
|
|
144
|
+
// Expiration check
|
|
145
|
+
const exp = typeof payload.exp === 'number' ? payload.exp : undefined;
|
|
146
|
+
if (exp !== undefined && exp * 1000 <= Date.now()) {
|
|
147
|
+
throw new Error('JWT token has expired');
|
|
148
|
+
}
|
|
149
|
+
if (expectations !== undefined) {
|
|
150
|
+
checkExpectations(payload, expectations);
|
|
151
|
+
}
|
|
152
|
+
const accountId = String(payload.sub ?? '');
|
|
153
|
+
const organizationId = String(payload.oid ?? '');
|
|
154
|
+
const projectId = String(payload.pid ?? '');
|
|
155
|
+
const tenantId = String(payload.tid ?? '');
|
|
156
|
+
const scope = String(payload.scope ?? '');
|
|
157
|
+
const keyVersion = typeof payload.ver === 'number' ? payload.ver : 1;
|
|
158
|
+
return {
|
|
159
|
+
account_id: accountId,
|
|
160
|
+
organization_id: organizationId,
|
|
161
|
+
project_id: projectId,
|
|
162
|
+
tenant_id: tenantId,
|
|
163
|
+
key_version: keyVersion,
|
|
164
|
+
scope,
|
|
165
|
+
exp: exp ?? 0,
|
|
166
|
+
iat: typeof payload.iat === 'number' ? payload.iat : undefined,
|
|
167
|
+
iss: typeof payload.iss === 'string' ? payload.iss : undefined,
|
|
168
|
+
sub: accountId,
|
|
169
|
+
aud: typeof payload.aud === 'string' ? payload.aud : undefined,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
exports.TokenVerifier = TokenVerifier;
|
|
174
|
+
/** Session-token checks of slice §5.3, in their fixed order. */
|
|
175
|
+
function checkExpectations(payload, expectations) {
|
|
176
|
+
if (typeof payload.exp !== 'number') {
|
|
177
|
+
throw new types_1.TokenRejectedError('missing_expiry', 'Token rejected: it carries no expiry');
|
|
178
|
+
}
|
|
179
|
+
if (payload.purpose !== undefined) {
|
|
180
|
+
throw new types_1.TokenRejectedError('not_a_session_token', 'Token rejected: it is a one-time token, not a session token');
|
|
181
|
+
}
|
|
182
|
+
if (!expectations) {
|
|
183
|
+
throw new types_1.TokenRejectedError('not_ready', 'Token rejected: the client has no service token yet, so the expected issuer and application are unknown');
|
|
184
|
+
}
|
|
185
|
+
if (payload.aud !== SESSION_AUDIENCE || payload.scope !== SESSION_AUDIENCE) {
|
|
186
|
+
throw new types_1.TokenRejectedError('audience_mismatch', 'Token rejected: it is not a buyer session token');
|
|
187
|
+
}
|
|
188
|
+
if (payload.iss !== expectations.issuer) {
|
|
189
|
+
throw new types_1.TokenRejectedError('issuer_mismatch', 'Token rejected: it was issued by another instance');
|
|
190
|
+
}
|
|
191
|
+
if (payload.pid !== expectations.projectId) {
|
|
192
|
+
throw new types_1.TokenRejectedError('project_mismatch', 'Token rejected: it belongs to another application');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Reads the expectations from a machine token's payload without verifying it (the client obtained
|
|
197
|
+
* it from the instance itself). Returns `null` when the value is not a decodable JWT or lacks `iss`
|
|
198
|
+
* or `pid`.
|
|
199
|
+
*/
|
|
200
|
+
function expectationsFromMachineToken(token) {
|
|
201
|
+
const parts = typeof token === 'string' ? token.split('.') : [];
|
|
202
|
+
if (parts.length !== 3) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const payload = JSON.parse(base64UrlDecode(parts[1]).toString('utf-8'));
|
|
207
|
+
if (typeof payload.iss === 'string' && payload.iss && typeof payload.pid === 'string' && payload.pid) {
|
|
208
|
+
return { issuer: payload.iss, projectId: payload.pid };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// Not a JWT: no expectations.
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
/** The `exp` of a JWT in milliseconds, or `undefined` when it cannot be read. */
|
|
217
|
+
function expiryOfToken(token) {
|
|
218
|
+
const parts = typeof token === 'string' ? token.split('.') : [];
|
|
219
|
+
if (parts.length !== 3) {
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
const payload = JSON.parse(base64UrlDecode(parts[1]).toString('utf-8'));
|
|
224
|
+
return typeof payload.exp === 'number' ? payload.exp * 1000 : undefined;
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
//# sourceMappingURL=token-verifier.js.map
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { EntitlementMirror } from '../mirror/entitlement-mirror';
|
|
2
|
+
import { HttpTransport } from '../transport/http.transport';
|
|
3
|
+
/**
|
|
4
|
+
* Keeps a started client connected to its instance, from `start()` to `stop()`:
|
|
5
|
+
*
|
|
6
|
+
* - holds the machine token and renews it at 80 % of its lifetime; a 401 on any authenticated
|
|
7
|
+
* request triggers one shared renewal and a single retry;
|
|
8
|
+
* - holds the push stream `GET /sdk/v1/events`, which is the only online signal: the client is
|
|
9
|
+
* online while the stream delivered `ready`, has not been silent for three heartbeat intervals,
|
|
10
|
+
* and the mirror was pulled after the stream opened;
|
|
11
|
+
* - on a planned renewal opens a second stream with the new token and closes the old one once the
|
|
12
|
+
* new one is ready, without pulling; on an unplanned break reconnects once at once, then backs
|
|
13
|
+
* off (1, 2, 4, 8, 16, 30 s, ±20 %);
|
|
14
|
+
* - turns `snapshot.changed` into per-organization refills (at most one in flight per organization)
|
|
15
|
+
* and `snapshot.resync` into a full pull. It never pulls on a timer.
|
|
16
|
+
*
|
|
17
|
+
* Every timer is unref'd, so the supervisor never keeps the host process alive.
|
|
18
|
+
*/
|
|
19
|
+
/** What the supervisor needs from the client it serves. */
|
|
20
|
+
export interface SupervisorHost {
|
|
21
|
+
transport: HttpTransport;
|
|
22
|
+
mirror: EntitlementMirror;
|
|
23
|
+
apiKey: string;
|
|
24
|
+
connectTimeoutMs: number;
|
|
25
|
+
/** Fetches the signing keys. */
|
|
26
|
+
loadKeys(): Promise<void>;
|
|
27
|
+
/** A new machine token is in use. */
|
|
28
|
+
onToken(token: string): void;
|
|
29
|
+
/** The online signal changed. */
|
|
30
|
+
setOnline(online: boolean): void;
|
|
31
|
+
/** Delivers buffered and backlogged usage. */
|
|
32
|
+
flush(): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
export declare class ConnectionSupervisor {
|
|
35
|
+
private readonly host;
|
|
36
|
+
private token;
|
|
37
|
+
private tokenExpiresAt;
|
|
38
|
+
private projectId;
|
|
39
|
+
private credentialRefused;
|
|
40
|
+
private exchangeInFlight;
|
|
41
|
+
private keysLoaded;
|
|
42
|
+
private stream;
|
|
43
|
+
private readonly streams;
|
|
44
|
+
private live;
|
|
45
|
+
private running;
|
|
46
|
+
private paused;
|
|
47
|
+
/** Bumped by stop, pause and start; work begun under an older generation drops its result. */
|
|
48
|
+
private generation;
|
|
49
|
+
private connecting;
|
|
50
|
+
private rotating;
|
|
51
|
+
private backoffStep;
|
|
52
|
+
private renewalBackoffStep;
|
|
53
|
+
/** When the next renewal is due (local clock), kept across a pause. */
|
|
54
|
+
private renewDueAt;
|
|
55
|
+
private renewTimer;
|
|
56
|
+
private reconnectTimer;
|
|
57
|
+
private watchdogTimer;
|
|
58
|
+
private readonly refillTargets;
|
|
59
|
+
private readonly refillsInFlight;
|
|
60
|
+
constructor(host: SupervisorHost);
|
|
61
|
+
/** The machine token in use, or `null`. */
|
|
62
|
+
currentToken(): string | null;
|
|
63
|
+
/**
|
|
64
|
+
* Keys, token, stream (waiting for `ready`), then a full pull. Never throws: whatever fails is
|
|
65
|
+
* retried in the background.
|
|
66
|
+
*/
|
|
67
|
+
start(): Promise<void>;
|
|
68
|
+
/** Closes the stream and stops every timer. */
|
|
69
|
+
stop(): void;
|
|
70
|
+
/** `setOnline(false)`: closes the stream and suspends supervision until `resume()`. */
|
|
71
|
+
pause(): void;
|
|
72
|
+
/** `setOnline(true)`: resumes supervision; the next attempt corrects the state. */
|
|
73
|
+
resume(): void;
|
|
74
|
+
/** A token set by hand (`setM2MToken`): used as is, never renewed on a timer. */
|
|
75
|
+
injectToken(token: string): void;
|
|
76
|
+
/**
|
|
77
|
+
* Runs an authenticated request. A 401 triggers one renewal, shared by every request that failed
|
|
78
|
+
* with the same token, and one retry with the new token; a second failure is the request's own.
|
|
79
|
+
*/
|
|
80
|
+
authorized<T>(call: (token: string) => Promise<T>): Promise<T>;
|
|
81
|
+
/**
|
|
82
|
+
* One attempt to get online. `initial` (start) pulls even when the stream cannot be opened, so the
|
|
83
|
+
* mirror is loaded; later attempts pull only once a stream is ready, except while the mirror is
|
|
84
|
+
* still empty.
|
|
85
|
+
*/
|
|
86
|
+
private attempt;
|
|
87
|
+
private reconnect;
|
|
88
|
+
private scheduleReconnect;
|
|
89
|
+
private goLive;
|
|
90
|
+
private goOffline;
|
|
91
|
+
/** An unplanned break of the active stream: one immediate reconnect, still LIVE meanwhile. */
|
|
92
|
+
private onBreak;
|
|
93
|
+
private validToken;
|
|
94
|
+
private exchange;
|
|
95
|
+
private clearExchange;
|
|
96
|
+
private renewAfterUnauthorized;
|
|
97
|
+
private scheduleRenewal;
|
|
98
|
+
private renew;
|
|
99
|
+
/** A new token: rotate a live stream onto it, or reconnect if the client is not live. */
|
|
100
|
+
private afterRenewal;
|
|
101
|
+
/** Planned rotation: the new stream must be ready before the old one is closed; no pull. */
|
|
102
|
+
private rotate;
|
|
103
|
+
/** Opens a stream and resolves once its `ready` frame arrived (bounded by the connect timeout). */
|
|
104
|
+
private openStream;
|
|
105
|
+
private adopt;
|
|
106
|
+
private retire;
|
|
107
|
+
private armWatchdog;
|
|
108
|
+
private onFrame;
|
|
109
|
+
private onChanged;
|
|
110
|
+
/** Refills one organization until the mirror reaches the highest announced revision. */
|
|
111
|
+
private refill;
|
|
112
|
+
/** Full pull: replaces the whole mirror. */
|
|
113
|
+
private pull;
|
|
114
|
+
/** Drops the stream, every timer and any attempt in flight. */
|
|
115
|
+
private halt;
|
|
116
|
+
private clearTimer;
|
|
117
|
+
}
|