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