@pipeline-builder/api-core 4.1.2 → 4.1.4
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/README.md +3 -3
- package/lib/errors/app-errors.d.ts +0 -4
- package/lib/errors/app-errors.js +1 -8
- package/lib/helpers/restore-helpers.d.ts +3 -3
- package/lib/helpers/restore-helpers.js +4 -4
- package/lib/helpers/sse-helpers.d.ts +8 -1
- package/lib/helpers/sse-helpers.js +11 -3
- package/lib/middleware/auth.d.ts +174 -43
- package/lib/middleware/auth.js +384 -179
- package/lib/middleware/index.d.ts +1 -0
- package/lib/middleware/index.js +2 -1
- package/lib/middleware/route-table.d.ts +104 -0
- package/lib/middleware/route-table.js +230 -0
- package/lib/middleware/step-up.d.ts +48 -16
- package/lib/middleware/step-up.js +74 -36
- package/lib/services/api-key-exchange.d.ts +25 -0
- package/lib/services/api-key-exchange.js +219 -0
- package/lib/services/audit-spool.d.ts +2 -3
- package/lib/services/audit-spool.js +3 -4
- package/lib/services/cache-service.d.ts +65 -21
- package/lib/services/cache-service.js +173 -78
- package/lib/services/compliance-event-subscriber.d.ts +5 -4
- package/lib/services/compliance-event-subscriber.js +8 -6
- package/lib/services/durable-event-bus.d.ts +22 -39
- package/lib/services/durable-event-bus.js +109 -47
- package/lib/services/env-redis.d.ts +114 -35
- package/lib/services/env-redis.js +215 -73
- package/lib/services/http-client.d.ts +10 -15
- package/lib/services/http-client.js +32 -27
- package/lib/services/index.d.ts +3 -0
- package/lib/services/index.js +4 -1
- package/lib/services/jwks-cache.d.ts +102 -0
- package/lib/services/jwks-cache.js +222 -0
- package/lib/services/leader-lock.d.ts +6 -3
- package/lib/services/leader-lock.js +20 -5
- package/lib/services/quota.d.ts +44 -10
- package/lib/services/quota.js +95 -51
- package/lib/services/remote-audit-client.d.ts +1 -1
- package/lib/services/remote-audit-client.js +32 -1
- package/lib/services/scheduler.d.ts +2 -3
- package/lib/services/scheduler.js +2 -2
- package/lib/services/service-boot.d.ts +4 -2
- package/lib/services/service-boot.js +7 -3
- package/lib/services/service-keys.d.ts +83 -0
- package/lib/services/service-keys.js +354 -0
- package/lib/services/sse-ticket-store.d.ts +55 -22
- package/lib/services/sse-ticket-store.js +122 -79
- package/lib/services/token-revocation.d.ts +1 -1
- package/lib/services/token-revocation.js +31 -19
- package/lib/testing/route-coverage.d.ts +64 -0
- package/lib/testing/route-coverage.js +183 -0
- package/lib/testing/service-tokens.d.ts +66 -0
- package/lib/testing/service-tokens.js +137 -0
- package/lib/testing/tier-mock.d.ts +2 -2
- package/lib/testing/tier-mock.js +3 -3
- package/lib/testing/user-tokens.d.ts +75 -0
- package/lib/testing/user-tokens.js +115 -0
- package/lib/types/catalog-metadata.d.ts +0 -2
- package/lib/types/catalog-metadata.js +1 -3
- package/lib/types/common.d.ts +103 -4
- package/lib/types/common.js +11 -1
- package/lib/types/error-codes.d.ts +15 -0
- package/lib/types/error-codes.js +22 -1
- package/lib/types/permissions.d.ts +35 -1
- package/lib/types/permissions.js +70 -2
- package/lib/utils/api-key.d.ts +34 -0
- package/lib/utils/api-key.js +75 -0
- package/lib/utils/env.d.ts +0 -2
- package/lib/utils/env.js +1 -6
- package/lib/utils/index.d.ts +3 -0
- package/lib/utils/index.js +4 -1
- package/lib/utils/jwk.d.ts +98 -0
- package/lib/utils/jwk.js +139 -0
- package/lib/utils/logger.d.ts +4 -1
- package/lib/utils/logger.js +1 -22
- package/lib/utils/secret-encryption.d.ts +7 -2
- package/lib/utils/secret-encryption.js +47 -4
- package/lib/utils/secret-rotation.d.ts +17 -0
- package/lib/utils/secret-rotation.js +54 -0
- package/lib/validation/common-schemas.js +5 -4
- package/package.json +20 -1
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
// Copyright 2026 Pipeline Builder Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Per-service signing keys for INTERNAL service-to-service tokens (#14).
|
|
5
|
+
*
|
|
6
|
+
* Before this, every service held one shared `JWT_SECRET`, so any service could
|
|
7
|
+
* mint a token naming any OTHER service — "billing said so" was unfalsifiable,
|
|
8
|
+
* and a single compromised workload could speak for the whole fleet. Now each
|
|
9
|
+
* service signs with its OWN ES256 (EC P-256) key and consumers verify against
|
|
10
|
+
* the public halves. The `kid` is bound to the signer's NAME, so a token whose
|
|
11
|
+
* `sub` says `service:billing` but was signed with compliance's key is a
|
|
12
|
+
* forgery, not a valid token with a confusing subject.
|
|
13
|
+
*
|
|
14
|
+
* ## Why public keys are distributed by CONFIG, not by per-service JWKS endpoints
|
|
15
|
+
*
|
|
16
|
+
* The user chain (#5) fetches platform's JWKS over HTTP because there is exactly
|
|
17
|
+
* one signer and it is the one service that must be up for anybody to sign in.
|
|
18
|
+
* The service chain is the opposite shape, and three properties decided it:
|
|
19
|
+
*
|
|
20
|
+
* 1. **`verifyServicePrincipal` must stay synchronous.** The global rate
|
|
21
|
+
* limiter's `skip` and app-factory's `/warmup` guard verify a service token
|
|
22
|
+
* BEFORE `requireAuth` runs, in synchronous Express callbacks. An HTTP JWKS
|
|
23
|
+
* fetch would force those async — a change that reaches into every
|
|
24
|
+
* service's limiter wiring.
|
|
25
|
+
* 2. **No availability coupling.** With per-service JWKS endpoints, verifying a
|
|
26
|
+
* token minted by `billing` needs `billing` to be reachable — so billing
|
|
27
|
+
* being down would stop platform from accepting billing's in-flight calls,
|
|
28
|
+
* turning one outage into several. A mounted file has no such edge.
|
|
29
|
+
* 3. **No N×N address config.** 10 services would each need every peer's
|
|
30
|
+
* host/port (and compose, which has no mesh, would need them too).
|
|
31
|
+
*
|
|
32
|
+
* So the deploy generates one keypair per service, mounts each private key ONLY
|
|
33
|
+
* into its own service, and mounts ONE public bundle (read-only) everywhere:
|
|
34
|
+
*
|
|
35
|
+
* `SERVICE_SIGNING_KEY_FILE` this service's EC P-256 private key (PKCS#8 PEM)
|
|
36
|
+
* `SERVICE_KEY_BUNDLE_FILE` `{ "services": { "<name>": { "keys": [<jwk>…] } } }`
|
|
37
|
+
*
|
|
38
|
+
* Rotation is by `kid`, in the same shape as the user chain: add the new public
|
|
39
|
+
* key to the bundle (the service now publishes two), roll the bundle out, swap
|
|
40
|
+
* that service's private key, then drop the retired public key. The bundle is
|
|
41
|
+
* re-read on an interval AND on an mtime change, so a rotation needs no restart.
|
|
42
|
+
* `deploy/bin/service-signing-keys.sh` generates and rotates both halves.
|
|
43
|
+
*
|
|
44
|
+
* ## Ephemeral mode
|
|
45
|
+
*
|
|
46
|
+
* With no `SERVICE_SIGNING_KEY_FILE` configured, a keypair is generated
|
|
47
|
+
* IN-PROCESS per requested service name and registered in the in-memory bundle.
|
|
48
|
+
* That keeps single-process suites and local one-off scripts working with no key
|
|
49
|
+
* material, and it can never widen trust: the only verifier that accepts such a
|
|
50
|
+
* token is the process that minted it. It is logged once at warn level, and
|
|
51
|
+
* counted, because in a real deployment it means this service's peers will
|
|
52
|
+
* reject everything it sends.
|
|
53
|
+
*/
|
|
54
|
+
import crypto, {} from 'crypto';
|
|
55
|
+
import { readFileSync, statSync } from 'fs';
|
|
56
|
+
import jwt from 'jsonwebtoken';
|
|
57
|
+
import { publicJwkFrom, publicKeyFromJwk } from '../utils/jwk.js';
|
|
58
|
+
import { createLogger } from '../utils/logger.js';
|
|
59
|
+
import { emitCounter } from '../utils/metric-emitter.js';
|
|
60
|
+
const logger = createLogger('service-keys');
|
|
61
|
+
/** The ONLY algorithm an internal service token may be signed with. */
|
|
62
|
+
export const SERVICE_TOKEN_ALGORITHM = 'ES256';
|
|
63
|
+
/** Subject prefix naming the calling service on a service token (`service:<name>`). */
|
|
64
|
+
export const SERVICE_SUBJECT_PREFIX = 'service:';
|
|
65
|
+
/** How long a loaded bundle is served before it is re-read (rotation pickup). */
|
|
66
|
+
const BUNDLE_REFRESH_INTERVAL_MS = 300_000; // 5 minutes
|
|
67
|
+
/** The bundle could not be loaded or does not know the key a token names. */
|
|
68
|
+
export class ServiceKeyError extends Error {
|
|
69
|
+
constructor(message) {
|
|
70
|
+
super(message);
|
|
71
|
+
this.name = 'ServiceKeyError';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* This process's service identity — the `<name>` in `service:<name>` and the
|
|
76
|
+
* name its key is published under. `SERVICE_NAME` is already set per service in
|
|
77
|
+
* every deploy target (compose, minikube, ec2, eks), and is the same value the
|
|
78
|
+
* audit and metric labels use, so there is one source of truth for "who am I".
|
|
79
|
+
*/
|
|
80
|
+
export function serviceIdentity() {
|
|
81
|
+
return process.env.SERVICE_NAME || 'api';
|
|
82
|
+
}
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Local signing key
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
let localKey;
|
|
87
|
+
let ephemeralKeys;
|
|
88
|
+
let ephemeralWarned = false;
|
|
89
|
+
function loadConfiguredSigningKey(file) {
|
|
90
|
+
const serviceName = serviceIdentity();
|
|
91
|
+
let privateKey;
|
|
92
|
+
try {
|
|
93
|
+
privateKey = crypto.createPrivateKey(readFileSync(file, 'utf8'));
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
throw new ServiceKeyError(`Could not read the service signing key at ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
97
|
+
}
|
|
98
|
+
// publicJwkFrom rejects anything but an EC P-256 key, so a service configured
|
|
99
|
+
// with (say) an RSA key fails HERE rather than minting tokens nobody verifies.
|
|
100
|
+
const publicJwk = publicJwkFrom(crypto.createPublicKey(privateKey));
|
|
101
|
+
return { serviceName, kid: publicJwk.kid, privateKey, publicJwk };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The in-process key for `serviceName` in ephemeral mode, generated on first
|
|
105
|
+
* use and registered in the in-memory bundle so this process can verify its own
|
|
106
|
+
* tokens. Keyed by NAME (not just one key) so a single-process suite can mint
|
|
107
|
+
* tokens for several services, as the real fleet does across processes.
|
|
108
|
+
*/
|
|
109
|
+
function ephemeralSigningKey(serviceName) {
|
|
110
|
+
ephemeralKeys ??= new Map();
|
|
111
|
+
const existing = ephemeralKeys.get(serviceName);
|
|
112
|
+
if (existing)
|
|
113
|
+
return existing;
|
|
114
|
+
if (!ephemeralWarned) {
|
|
115
|
+
ephemeralWarned = true;
|
|
116
|
+
emitCounter('service_signing_ephemeral_key_total', { service: serviceIdentity() });
|
|
117
|
+
logger.warn('SERVICE_SIGNING_KEY_FILE is not set — minting internal service tokens with an EPHEMERAL key. Only this process will accept them; every peer will reject them. Set SERVICE_SIGNING_KEY_FILE + SERVICE_KEY_BUNDLE_FILE (deploy/bin/service-signing-keys.sh).');
|
|
118
|
+
}
|
|
119
|
+
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
|
|
120
|
+
const publicJwk = publicJwkFrom(publicKey);
|
|
121
|
+
const generated = { serviceName, kid: publicJwk.kid, privateKey, publicJwk };
|
|
122
|
+
ephemeralKeys.set(serviceName, generated);
|
|
123
|
+
return generated;
|
|
124
|
+
}
|
|
125
|
+
/** Whether this process signs with a mounted key (`configured`) or an in-process one. */
|
|
126
|
+
export function serviceKeyMode() {
|
|
127
|
+
return process.env.SERVICE_SIGNING_KEY_FILE ? 'configured' : 'ephemeral';
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The key `serviceName` must be signed with.
|
|
131
|
+
*
|
|
132
|
+
* In `configured` mode there is exactly ONE key — this service's — so a request
|
|
133
|
+
* to sign as some OTHER service is a bug and throws. That matters: before #14 a
|
|
134
|
+
* service could legitimately mint `service:platform` with the shared secret
|
|
135
|
+
* (api/plugin did, for its registry pushes), and under per-service keys such a
|
|
136
|
+
* token would be silently unverifiable everywhere. Failing at the mint turns
|
|
137
|
+
* that into an immediate, obvious error instead of a runtime 401.
|
|
138
|
+
*/
|
|
139
|
+
function signingKeyFor(serviceName) {
|
|
140
|
+
const file = process.env.SERVICE_SIGNING_KEY_FILE;
|
|
141
|
+
if (!file)
|
|
142
|
+
return ephemeralSigningKey(serviceName);
|
|
143
|
+
if (!localKey || localKey.serviceName !== serviceIdentity()) {
|
|
144
|
+
localKey = loadConfiguredSigningKey(file);
|
|
145
|
+
}
|
|
146
|
+
if (serviceName !== localKey.serviceName) {
|
|
147
|
+
throw new ServiceKeyError(`This process is '${localKey.serviceName}' and holds only its own signing key, so it cannot mint a token for '${serviceName}'. `
|
|
148
|
+
+ 'Name the calling service correctly, or give the caller its own key.');
|
|
149
|
+
}
|
|
150
|
+
return localKey;
|
|
151
|
+
}
|
|
152
|
+
let bundle;
|
|
153
|
+
function parseBundle(raw, file) {
|
|
154
|
+
let document;
|
|
155
|
+
try {
|
|
156
|
+
document = JSON.parse(raw);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
throw new ServiceKeyError(`${file} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
160
|
+
}
|
|
161
|
+
const services = document?.services;
|
|
162
|
+
if (!services || typeof services !== 'object') {
|
|
163
|
+
throw new ServiceKeyError(`${file} has no "services" object`);
|
|
164
|
+
}
|
|
165
|
+
const byKid = new Map();
|
|
166
|
+
for (const [serviceName, entry] of Object.entries(services)) {
|
|
167
|
+
for (const jwk of entry?.keys ?? []) {
|
|
168
|
+
if (!jwk?.kid)
|
|
169
|
+
continue;
|
|
170
|
+
try {
|
|
171
|
+
byKid.set(jwk.kid, { serviceName, key: publicKeyFromJwk(jwk) });
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
// One unusable entry must not discard the rest of the bundle — the same
|
|
175
|
+
// rule the JWKS cache applies to platform's published set.
|
|
176
|
+
logger.warn('Skipping unusable service key', { serviceName, kid: jwk.kid, error: error instanceof Error ? error.message : String(error) });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (byKid.size === 0)
|
|
181
|
+
throw new ServiceKeyError(`${file} contained no usable service keys`);
|
|
182
|
+
return byKid;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* The `kid` → service map, re-read when the refresh interval has passed or the
|
|
186
|
+
* file's mtime changed (so a rotation is picked up without a restart).
|
|
187
|
+
*
|
|
188
|
+
* In ephemeral mode the "bundle" is whatever this process generated for itself.
|
|
189
|
+
* Never throws for a missing file in that mode — it just holds those keys.
|
|
190
|
+
*/
|
|
191
|
+
function verificationKeys() {
|
|
192
|
+
const file = process.env.SERVICE_KEY_BUNDLE_FILE;
|
|
193
|
+
if (!file) {
|
|
194
|
+
const local = new Map();
|
|
195
|
+
for (const key of ephemeralKeys?.values() ?? []) {
|
|
196
|
+
local.set(key.kid, { serviceName: key.serviceName, key: crypto.createPublicKey(key.privateKey) });
|
|
197
|
+
}
|
|
198
|
+
return local;
|
|
199
|
+
}
|
|
200
|
+
const now = Date.now();
|
|
201
|
+
let mtimeMs = 0;
|
|
202
|
+
try {
|
|
203
|
+
mtimeMs = statSync(file).mtimeMs;
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// Unreadable now: keep serving a bundle already in hand (public keys don't
|
|
207
|
+
// expire, and refusing every internal call because a mount blipped would be
|
|
208
|
+
// a self-inflicted outage). With nothing in hand, fail closed below.
|
|
209
|
+
if (bundle)
|
|
210
|
+
return bundle.byKid;
|
|
211
|
+
}
|
|
212
|
+
if (bundle && bundle.mtimeMs === mtimeMs && now - bundle.loadedAt < BUNDLE_REFRESH_INTERVAL_MS) {
|
|
213
|
+
return bundle.byKid;
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
const byKid = parseBundle(readFileSync(file, 'utf8'), file);
|
|
217
|
+
bundle = { byKid, loadedAt: now, mtimeMs };
|
|
218
|
+
return byKid;
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
if (bundle) {
|
|
222
|
+
logger.warn('Service key bundle reload failed; continuing on the loaded copy', { file, error: error instanceof Error ? error.message : String(error) });
|
|
223
|
+
return bundle.byKid;
|
|
224
|
+
}
|
|
225
|
+
throw error instanceof ServiceKeyError ? error : new ServiceKeyError(String(error));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Whether `kid` belongs to an internal SERVICE key. This is how a bearer token
|
|
230
|
+
* is routed between the two ES256 chains: both user and service tokens are
|
|
231
|
+
* ES256 with a `kid`, and `kid` is an RFC 7638 thumbprint, so ownership of the
|
|
232
|
+
* id — not a claim inside the token — decides which key set may verify it.
|
|
233
|
+
* Returns `false` (never throws) when no bundle can be read; the token then
|
|
234
|
+
* takes the user chain and is rejected there.
|
|
235
|
+
*/
|
|
236
|
+
export function isServiceKid(kid) {
|
|
237
|
+
try {
|
|
238
|
+
return verificationKeys().has(kid);
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/** Every service name the bundle publishes a key for. Diagnostics and tests. */
|
|
245
|
+
export function knownServiceNames() {
|
|
246
|
+
try {
|
|
247
|
+
return [...new Set([...verificationKeys().values()].map((v) => v.serviceName))].sort();
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return [];
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// Sign / verify
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
/** base64url of a JSON value, the JWS way (no padding, URL alphabet). */
|
|
257
|
+
function b64uJson(value) {
|
|
258
|
+
return Buffer.from(JSON.stringify(value), 'utf-8').toString('base64url');
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Sign `claims` as this service's internal token: ES256, `kid` = the signer's
|
|
262
|
+
* thumbprint, `sub` = `service:<serviceName>`. `iat`/`exp` and the optional
|
|
263
|
+
* issuer/audience are stamped here so there is one place that decides what a
|
|
264
|
+
* service token says.
|
|
265
|
+
*
|
|
266
|
+
* Hand-rolled (rather than `jwt.sign`) only so the header carries the `kid` in
|
|
267
|
+
* the same shape platform's signer produces — `crypto.sign` is synchronous, so
|
|
268
|
+
* unlike the KMS path this costs nothing.
|
|
269
|
+
*/
|
|
270
|
+
export function signServiceJwt(claims, options) {
|
|
271
|
+
const key = signingKeyFor(options.serviceName);
|
|
272
|
+
const now = Math.floor(Date.now() / 1000);
|
|
273
|
+
const body = {
|
|
274
|
+
...claims,
|
|
275
|
+
sub: `${SERVICE_SUBJECT_PREFIX}${options.serviceName}`,
|
|
276
|
+
iat: now,
|
|
277
|
+
exp: now + options.expiresInSeconds,
|
|
278
|
+
...(options.issuer ? { iss: options.issuer } : {}),
|
|
279
|
+
...(options.audience ? { aud: options.audience } : {}),
|
|
280
|
+
};
|
|
281
|
+
const signingInput = `${b64uJson({ alg: SERVICE_TOKEN_ALGORITHM, typ: 'JWT', kid: key.kid })}.${b64uJson(body)}`;
|
|
282
|
+
const signature = crypto.sign('sha256', Buffer.from(signingInput, 'utf-8'), {
|
|
283
|
+
key: key.privateKey,
|
|
284
|
+
dsaEncoding: 'ieee-p1363',
|
|
285
|
+
});
|
|
286
|
+
return `${signingInput}.${signature.toString('base64url')}`;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Verify an internal service token, SYNCHRONOUSLY, and return its claims.
|
|
290
|
+
*
|
|
291
|
+
* The security property this function exists for: the `kid` selects the key
|
|
292
|
+
* AND names the signer, and the token's `sub` must agree. So `billing` cannot
|
|
293
|
+
* be impersonated by any other service — not even one that holds a valid key of
|
|
294
|
+
* its own — which is what the shared secret could never guarantee.
|
|
295
|
+
*
|
|
296
|
+
* @throws {jwt.JsonWebTokenError} unknown `kid`, wrong algorithm, bad
|
|
297
|
+
* signature, expired, or a `sub` that disagrees with the signing key.
|
|
298
|
+
* @throws {ServiceKeyError} no verification bundle could be read at all — the
|
|
299
|
+
* token's validity is UNKNOWN and the caller must not treat it as valid.
|
|
300
|
+
*/
|
|
301
|
+
export function verifyServiceJwt(token, options) {
|
|
302
|
+
const entry = verificationKeys().get(options.kid);
|
|
303
|
+
if (!entry) {
|
|
304
|
+
emitCounter('service_token_verify_total', { result: 'unknown_kid' });
|
|
305
|
+
throw new jwt.JsonWebTokenError(`No published service key for kid ${options.kid}`);
|
|
306
|
+
}
|
|
307
|
+
let claims;
|
|
308
|
+
try {
|
|
309
|
+
claims = jwt.verify(token, entry.key, {
|
|
310
|
+
algorithms: [SERVICE_TOKEN_ALGORITHM],
|
|
311
|
+
...(options.issuer ? { issuer: options.issuer } : {}),
|
|
312
|
+
...(options.audience ? { audience: options.audience } : {}),
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
emitCounter('service_token_verify_total', { result: 'invalid' });
|
|
317
|
+
throw error;
|
|
318
|
+
}
|
|
319
|
+
if (claims.sub !== `${SERVICE_SUBJECT_PREFIX}${entry.serviceName}`) {
|
|
320
|
+
// A valid signature by the WRONG service: the key belongs to `entry.serviceName`
|
|
321
|
+
// but the token speaks for someone else. This is the cross-service forgery
|
|
322
|
+
// the shared secret made undetectable.
|
|
323
|
+
emitCounter('service_token_verify_total', { result: 'subject_mismatch', service: entry.serviceName });
|
|
324
|
+
throw new jwt.JsonWebTokenError(`Service token subject ${String(claims.sub)} was signed by ${entry.serviceName}`);
|
|
325
|
+
}
|
|
326
|
+
emitCounter('service_token_verify_total', { result: 'ok', service: entry.serviceName });
|
|
327
|
+
return claims;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* True while this service still publishes more than one verification key — i.e.
|
|
331
|
+
* a signing-key rotation is open and the retiring key is still trusted. Feeds
|
|
332
|
+
* the `secret_rotation_previous_set{secret="SERVICE_SIGNING_KEY"}` gauge.
|
|
333
|
+
*/
|
|
334
|
+
export function isRetiringServiceKeyPublished() {
|
|
335
|
+
try {
|
|
336
|
+
const me = serviceIdentity();
|
|
337
|
+
return [...verificationKeys().values()].filter((v) => v.serviceName === me).length > 1;
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Test seam: drop the loaded signing key, the bundle and any ephemeral keys so
|
|
345
|
+
* the next sign/verify re-reads the environment. Don't call from production code
|
|
346
|
+
* — rotation is picked up by the refresh interval and the mtime check.
|
|
347
|
+
*/
|
|
348
|
+
export function _resetServiceKeysForTests() {
|
|
349
|
+
localKey = undefined;
|
|
350
|
+
bundle = undefined;
|
|
351
|
+
ephemeralKeys = undefined;
|
|
352
|
+
ephemeralWarned = false;
|
|
353
|
+
}
|
|
354
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VydmljZS1rZXlzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3NlcnZpY2VzL3NlcnZpY2Uta2V5cy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwrQ0FBK0M7QUFDL0Msc0NBQXNDO0FBRXRDOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztHQWtERztBQUVILE9BQU8sTUFBTSxFQUFFLEVBQWtCLE1BQU0sUUFBUSxDQUFDO0FBQ2hELE9BQU8sRUFBRSxZQUFZLEVBQUUsUUFBUSxFQUFFLE1BQU0sSUFBSSxDQUFDO0FBQzVDLE9BQU8sR0FBRyxNQUFNLGNBQWMsQ0FBQztBQUMvQixPQUFPLEVBQUUsYUFBYSxFQUFFLGdCQUFnQixFQUFrQixNQUFNLGlCQUFpQixDQUFDO0FBQ2xGLE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQztBQUNsRCxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0sNEJBQTRCLENBQUM7QUFFekQsTUFBTSxNQUFNLEdBQUcsWUFBWSxDQUFDLGNBQWMsQ0FBQyxDQUFDO0FBRTVDLHVFQUF1RTtBQUN2RSxNQUFNLENBQUMsTUFBTSx1QkFBdUIsR0FBRyxPQUFPLENBQUM7QUFFL0MsdUZBQXVGO0FBQ3ZGLE1BQU0sQ0FBQyxNQUFNLHNCQUFzQixHQUFHLFVBQVUsQ0FBQztBQUVqRCxpRkFBaUY7QUFDakYsTUFBTSwwQkFBMEIsR0FBRyxPQUFPLENBQUMsQ0FBQyxZQUFZO0FBb0J4RCw2RUFBNkU7QUFDN0UsTUFBTSxPQUFPLGVBQWdCLFNBQVEsS0FBSztJQUN4QyxZQUFZLE9BQWU7UUFDekIsS0FBSyxDQUFDLE9BQU8sQ0FBQyxDQUFDO1FBQ2YsSUFBSSxDQUFDLElBQUksR0FBRyxpQkFBaUIsQ0FBQztJQUNoQyxDQUFDO0NBQ0Y7QUFFRDs7Ozs7R0FLRztBQUNILE1BQU0sVUFBVSxlQUFlO0lBQzdCLE9BQU8sT0FBTyxDQUFDLEdBQUcsQ0FB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V0FBVyxFQUFFLENBQUMsQ0FBQztRQUN0RyxNQUFNLElBQUksR0FBRyxDQUFDLGlCQUFpQixDQUFDLHlCQUF5QixNQUFNLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxrQkFBa0IsS0FBSyxDQUFDLFdBQVcsRUFBRSxDQUFDLENBQUM7SUFDcEgsQ0FBQztJQUNELFdBQVcsQ0FBQyw0QkFBNEIsRUFBRSxFQUFFLE1BQU0sRUFBRSxJQUFJLEVBQUUsT0FBTyxFQUFFLEtBQUssQ0FBQyxXQUFXLEVBQUUsQ0FBQyxDQUFDO0lBQ3hGLE9BQU8sTUFBTSxDQUFDO0FBQ2hCLENBQUM7QUFFRDs7OztHQUlHO0FBQ0gsTUFBTSxVQUFVLDZCQUE2QjtJQUMzQyxJQUFJLENBQUM7UUFDSCxNQUFNLEVBQUUsR0FBRyxlQUFlLEVBQUUsQ0FBQztRQUM3QixPQUFPLENBQUMsR0FBRyxnQkFBZ0IsRUFBRSxDQUFDLE1BQU0sRUFBRSxDQUFDLENBQUMsTUFBTSxDQUFDLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFDLENBQUMsV0FBVyxLQUFLLEVBQUUsQ0FBQyxDQUFDLE1BQU0sR0FBRyxDQUFDLENBQUM7SUFDekYsQ0FBQztJQUFDLE1BQU0sQ0FBQztRQUNQLE9BQU8sS0FBSyxDQUFDO0lBQ2YsQ0FBQztBQUNILENBQUM7QUFFRDs7OztHQUlHO0FBQ0gsTUFBTSxVQUFVLHlCQUF5QjtJQUN2QyxRQUFRLEdBQUcsU0FBUyxDQUFDO0lBQ3JCLE1BQU0sR0FBRyxTQUFTLENBQUM7SUFDbkIsYUFBYSxHQUFHLFNBQVMsQ0FBQztJQUMxQixlQUFlLEdBQUcsS0FBSyxDQUFDO0FBQzFCLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBDb3B5cmlnaHQgMjAyNiBQaXBlbGluZSBCdWlsZGVyIENvbnRyaWJ1dG9yc1xuLy8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEFwYWNoZS0yLjBcblxuLyoqXG4gKiBQZXItc2VydmljZSBzaWduaW5nIGtleXMgZm9yIElOVEVSTkFMIHNlcnZpY2UtdG8tc2VydmljZSB0b2tlbnMgKCMxNCkuXG4gKlxuICogQmVmb3JlIHRoaXMsIGV2ZXJ5IHNlcnZpY2UgaGVsZCBvbmUgc2hhcmVkIGBKV1RfU0VDUkVUYCwgc28gYW55IHNlcnZpY2UgY291bGRcbiAqIG1pbnQgYSB0b2tlbiBuYW1pbmcgYW55IE9USEVSIHNlcnZpY2Ug4oCUIFwiYmlsbGluZyBzYWlkIHNvXCIgd2FzIHVuZmFsc2lmaWFibGUsXG4gKiBhbmQgYSBzaW5nbGUgY29tcHJvbWlzZWQgd29ya2xvYWQgY291bGQgc3BlYWsgZm9yIHRoZSB3aG9sZSBmbGVldC4gTm93IGVhY2hcbiAqIHNlcnZpY2Ugc2lnbnMgd2l0aCBpdHMgT1dOIEVTMjU2IChFQyBQLTI1Nikga2V5IGFuZCBjb25zdW1lcnMgdmVyaWZ5IGFnYWluc3RcbiAqIHRoZSBwdWJsaWMgaGFsdmVzLiBUaGUgYGtpZGAgaXMgYm91bmQgdG8gdGhlIHNpZ25lcidzIE5BTUUsIHNvIGEgdG9rZW4gd2hvc2VcbiAqIGBzdWJgIHNheXMgYHNlcnZpY2U6YmlsbGluZ2AgYnV0IHdhcyBzaWduZWQgd2l0aCBjb21wbGlhbmNlJ3Mga2V5IGlzIGFcbiAqIGZvcmdlcnksIG5vdCBhIHZhbGlkIHRva2VuIHdpdGggYSBjb25mdXNpbmcgc3ViamVjdC5cbiAqXG4gKiAjIyBXaHkgcHVibGljIGtleXMgYXJlIGRpc3RyaWJ1dGVkIGJ5IENPTkZJRywgbm90IGJ5IHBlci1zZXJ2aWNlIEpXS1MgZW5kcG9pbnRzXG4gKlxuICogVGhlIHVzZXIgY2hhaW4gKCM1KSBmZXRjaGVzIHBsYXRmb3JtJ3MgSldLUyBvdmVyIEhUVFAgYmVjYXVzZSB0aGVyZSBpcyBleGFjdGx5XG4gKiBvbmUgc2lnbmVyIGFuZCBpdCBpcyB0aGUgb25lIHNlcnZpY2UgdGhhdCBtdXN0IGJlIHVwIGZvciBhbnlib2R5IHRvIHNpZ24gaW4uXG4gKiBUaGUgc2VydmljZSBjaGFpbiBpcyB0aGUgb3Bwb3NpdGUgc2hhcGUsIGFuZCB0aHJlZSBwcm9wZXJ0aWVzIGRlY2lkZWQgaXQ6XG4gKlxuICogIDEuICoqYHZlcmlmeVNlcnZpY2VQcmluY2lwYWxgIG11c3Qgc3RheSBzeW5jaHJvbm91cy4qKiBUaGUgZ2xvYmFsIHJhdGVcbiAqICAgICBsaW1pdGVyJ3MgYHNraXBgIGFuZCBhcHAtZmFjdG9yeSdzIGAvd2FybXVwYCBndWFyZCB2ZXJpZnkgYSBzZXJ2aWNlIHRva2VuXG4gKiAgICAgQkVGT1JFIGByZXF1aXJlQXV0aGAgcnVucywgaW4gc3luY2hyb25vdXMgRXhwcmVzcyBjYWxsYmFja3MuIEFuIEhUVFAgSldLU1xuICogICAgIGZldGNoIHdvdWxkIGZvcmNlIHRob3NlIGFzeW5jIOKAlCBhIGNoYW5nZSB0aGF0IHJlYWNoZXMgaW50byBldmVyeVxuICogICAgIHNlcnZpY2UncyBsaW1pdGVyIHdpcmluZy5cbiAqICAyLiAqKk5vIGF2YWlsYWJpbGl0eSBjb3VwbGluZy4qKiBXaXRoIHBlci1zZXJ2aWNlIEpXS1MgZW5kcG9pbnRzLCB2ZXJpZnlpbmcgYVxuICogICAgIHRva2VuIG1pbnRlZCBieSBgYmlsbGluZ2AgbmVlZHMgYGJpbGxpbmdgIHRvIGJlIHJlYWNoYWJsZSDigJQgc28gYmlsbGluZ1xuICogICAgIGJlaW5nIGRvd24gd291bGQgc3RvcCBwbGF0Zm9ybSBmcm9tIGFjY2VwdGluZyBiaWxsaW5nJ3MgaW4tZmxpZ2h0IGNhbGxzLFxuICogICAgIHR1cm5pbmcgb25lIG91dGFnZSBpbnRvIHNldmVyYWwuIEEgbW91bnRlZCBmaWxlIGhhcyBubyBzdWNoIGVkZ2UuXG4gKiAgMy4gKipObyBOw5dOIGFkZHJlc3MgY29uZmlnLioqIDEwIHNlcnZpY2VzIHdvdWxkIGVhY2ggbmVlZCBldmVyeSBwZWVyJ3NcbiAqICAgICBob3N0L3BvcnQgKGFuZCBjb21wb3NlLCB3aGljaCBoYXMgbm8gbWVzaCwgd291bGQgbmVlZCB0aGVtIHRvbykuXG4gKlxuICogU28gdGhlIGRlcGxveSBnZW5lcmF0ZXMgb25lIGtleXBhaXIgcGVyIHNlcnZpY2UsIG1vdW50cyBlYWNoIHByaXZhdGUga2V5IE9OTFlcbiAqIGludG8gaXRzIG93biBzZXJ2aWNlLCBhbmQgbW91bnRzIE9ORSBwdWJsaWMgYnVuZGxlIChyZWFkLW9ubHkpIGV2ZXJ5d2hlcmU6XG4gKlxuICogICBgU0VSVklDRV9TSUdOSU5HX0tFWV9GSUxFYCAgdGhpcyBzZXJ2aWNlJ3MgRUMgUC0yNTYgcHJpdmF0ZSBrZXkgKFBLQ1MjOCBQRU0pXG4gKiAgIGBTRVJWSUNFX0tFWV9CVU5ETEVfRklMRWAgICBgeyBcInNlcnZpY2VzXCI6IHsgXCI8bmFtZT5cIjogeyBcImtleXNcIjogWzxqd2s+4oCmXSB9IH0gfWBcbiAqXG4gKiBSb3RhdGlvbiBpcyBieSBga2lkYCwgaW4gdGhlIHNhbWUgc2hhcGUgYXMgdGhlIHVzZXIgY2hhaW46IGFkZCB0aGUgbmV3IHB1YmxpY1xuICoga2V5IHRvIHRoZSBidW5kbGUgKHRoZSBzZXJ2aWNlIG5vdyBwdWJsaXNoZXMgdHdvKSwgcm9sbCB0aGUgYnVuZGxlIG91dCwgc3dhcFxuICogdGhhdCBzZXJ2aWNlJ3MgcHJpdmF0ZSBrZXksIHRoZW4gZHJvcCB0aGUgcmV0aXJlZCBwdWJsaWMga2V5LiBUaGUgYnVuZGxlIGlzXG4gKiByZS1yZWFkIG9uIGFuIGludGVydmFsIEFORCBvbiBhbiBtdGltZSBjaGFuZ2UsIHNvIGEgcm90YXRpb24gbmVlZHMgbm8gcmVzdGFydC5cbiAqIGBkZXBsb3kvYmluL3NlcnZpY2Utc2lnbmluZy1rZXlzLnNoYCBnZW5lcmF0ZXMgYW5kIHJvdGF0ZXMgYm90aCBoYWx2ZXMuXG4gKlxuICogIyMgRXBoZW1lcmFsIG1vZGVcbiAqXG4gKiBXaXRoIG5vIGBTRVJWSUNFX1NJR05JTkdfS0VZX0ZJTEVgIGNvbmZpZ3VyZWQsIGEga2V5cGFpciBpcyBnZW5lcmF0ZWRcbiAqIElOLVBST0NFU1MgcGVyIHJlcXVlc3RlZCBzZXJ2aWNlIG5hbWUgYW5kIHJlZ2lzdGVyZWQgaW4gdGhlIGluLW1lbW9yeSBidW5kbGUuXG4gKiBUaGF0IGtlZXBzIHNpbmdsZS1wcm9jZXNzIHN1aXRlcyBhbmQgbG9jYWwgb25lLW9mZiBzY3JpcHRzIHdvcmtpbmcgd2l0aCBubyBrZXlcbiAqIG1hdGVyaWFsLCBhbmQgaXQgY2FuIG5ldmVyIHdpZGVuIHRydXN0OiB0aGUgb25seSB2ZXJpZmllciB0aGF0IGFjY2VwdHMgc3VjaCBhXG4gKiB0b2tlbiBpcyB0aGUgcHJvY2VzcyB0aGF0IG1pbnRlZCBpdC4gSXQgaXMgbG9nZ2VkIG9uY2UgYXQgd2FybiBsZXZlbCwgYW5kXG4gKiBjb3VudGVkLCBiZWNhdXNlIGluIGEgcmVhbCBkZXBsb3ltZW50IGl0IG1lYW5zIHRoaXMgc2VydmljZSdzIHBlZXJzIHdpbGxcbiAqIHJlamVjdCBldmVyeXRoaW5nIGl0IHNlbmRzLlxuICovXG5cbmltcG9ydCBjcnlwdG8sIHsgdHlwZSBLZXlPYmplY3QgfSBmcm9tICdjcnlwdG8nO1xuaW1wb3J0IHsgcmVhZEZpbGVTeW5jLCBzdGF0U3luYyB9IGZyb20gJ2ZzJztcbmltcG9ydCBqd3QgZnJvbSAnanNvbndlYnRva2VuJztcbmltcG9ydCB7IHB1YmxpY0p3a0Zyb20sIHB1YmxpY0tleUZyb21Kd2ssIHR5cGUgUHVibGljSndrIH0gZnJvbSAnLi4vdXRpbHMvandrLmpzJztcbmltcG9ydCB7IGNyZWF0ZUxvZ2dlciB9IGZyb20gJy4uL3V0aWxzL2xvZ2dlci5qcyc7XG5pbXBvcnQgeyBlbWl0Q291bnRlciB9IGZyb20gJy4uL3V0aWxzL21ldHJpYy1lbWl0dGVyLmpzJztcblxuY29uc3QgbG9nZ2VyID0gY3JlYXRlTG9nZ2VyKCdzZXJ2aWNlLWtleXMnKTtcblxuLyoqIFRoZSBPTkxZIGFsZ29yaXRobSBhbiBpbnRlcm5hbCBzZXJ2aWNlIHRva2VuIG1heSBiZSBzaWduZWQgd2l0aC4gKi9cbmV4cG9ydCBjb25zdCBTRVJWSUNFX1RPS0VOX0FMR09SSVRITSA9ICdFUzI1Nic7XG5cbi8qKiBTdWJqZWN0IHByZWZpeCBuYW1pbmcgdGhlIGNhbGxpbmcgc2VydmljZSBvbiBhIHNlcnZpY2UgdG9rZW4gKGBzZXJ2aWNlOjxuYW1lPmApLiAqL1xuZXhwb3J0IGNvbnN0IFNFUlZJQ0VfU1VCSkVDVF9QUkVGSVggPSAnc2VydmljZTonO1xuXG4vKiogSG93IGxvbmcgYSBsb2FkZWQgYnVuZGxlIGlzIHNlcnZlZCBiZWZvcmUgaXQgaXMgcmUtcmVhZCAocm90YXRpb24gcGlja3VwKS4gKi9cbmNvbnN0IEJVTkRMRV9SRUZSRVNIX0lOVEVSVkFMX01TID0gMzAwXzAwMDsgLy8gNSBtaW51dGVzXG5cbi8qKiBPbmUgc2VydmljZSdzIHB1Ymxpc2hlZCB2ZXJpZmljYXRpb24ga2V5cywgYXMgdGhleSBhcHBlYXIgaW4gdGhlIGJ1bmRsZSBmaWxlLiAqL1xuZXhwb3J0IGludGVyZmFjZSBTZXJ2aWNlS2V5RW50cnkge1xuICBrZXlzOiBQdWJsaWNKd2tbXTtcbn1cblxuLyoqIFRoZSBkb2N1bWVudCBgU0VSVklDRV9LRVlfQlVORExFX0ZJTEVgIGhvbGRzOiBldmVyeSBzZXJ2aWNlJ3MgcHVibGljIGtleXMuICovXG5leHBvcnQgaW50ZXJmYWNlIFNlcnZpY2VLZXlCdW5kbGUge1xuICBzZXJ2aWNlczogUmVjb3JkPHN0cmluZywgU2VydmljZUtleUVudHJ5Pjtcbn1cblxuLyoqIEEgc2lnbmluZyBrZXkgdGhpcyBwcm9jZXNzIGhvbGRzLCBpbiB0aGUgZm9ybXMgc2lnbmluZyBuZWVkcy4gKi9cbmludGVyZmFjZSBMb2NhbFNpZ25pbmdLZXkge1xuICBzZXJ2aWNlTmFtZTogc3RyaW5nO1xuICBraWQ6IHN0cmluZztcbiAgcHJpdmF0ZUtleTogS2V5T2JqZWN0O1xuICBwdWJsaWNKd2s6IFB1YmxpY0p3aztcbn1cblxuLyoqIFRoZSBidW5kbGUgY291bGQgbm90IGJlIGxvYWRlZCBvciBkb2VzIG5vdCBrbm93IHRoZSBrZXkgYSB0b2tlbiBuYW1lcy4gKi9cbmV4cG9ydCBjbGFzcyBTZXJ2aWNlS2V5RXJyb3IgZXh0ZW5kcyBFcnJvciB7XG4gIGNvbnN0cnVjdG9yKG1lc3NhZ2U6IHN0cmluZykge1xuICAgIHN1cGVyKG1lc3NhZ2UpO1xuICAgIHRoaXMubmFtZSA9ICdTZXJ2aWNlS2V5RXJyb3InO1xuICB9XG59XG5cbi8qKlxuICogVGhpcyBwcm9jZXNzJ3Mgc2VydmljZSBpZGVudGl0eSDigJQgdGhlIGA8bmFtZT5gIGluIGBzZXJ2aWNlOjxuYW1lPmAgYW5kIHRoZVxuICogbmFtZSBpdHMga2V5IGlzIHB1Ymxpc2hlZCB1bmRlci4gYFNFUlZJQ0VfTkFNRWAgaXMgYWxyZWFkeSBzZXQgcGVyIHNlcnZpY2UgaW5cbiAqIGV2ZXJ5IGRlcGxveSB0YXJnZXQgKGNvbXBvc2UsIG1pbmlrdWJlLCBlYzIsIGVrcyksIGFuZCBpcyB0aGUgc2FtZSB2YWx1ZSB0aGVcbiAqIGF1ZGl0IGFuZCBtZXRyaWMgbGFiZWxzIHVzZSwgc28gdGhlcmUgaXMgb25lIHNvdXJjZSBvZiB0cnV0aCBmb3IgXCJ3aG8gYW0gSVwiLlxuICovXG5leHBvcnQgZnVuY3Rpb24gc2VydmljZUlkZW50aXR5KCk6IHN0cmluZyB7XG4gIHJldHVybiBwcm9jZXNzLmVudi5TRVJWSUNFX05BTUUgfHwgJ2FwaSc7XG59XG5cbi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLVxuLy8gTG9jYWwgc2lnbmluZyBrZXlcbi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLVxuXG5sZXQgbG9jYWxLZXk6IExvY2FsU2lnbmluZ0tleSB8IHVuZGVmaW5lZDtcbmxldCBlcGhlbWVyYWxLZXlzOiBNYXA8c3RyaW5nLCBMb2NhbFNpZ25pbmdLZXk+IHwgdW5kZWZpbmVkO1xubGV0IGVwaGVtZXJhbFdhcm5lZCA9IGZhbHNlO1xuXG5mdW5jdGlvbiBsb2FkQ29uZmlndXJlZFNpZ25pbmdLZXkoZmlsZTogc3RyaW5nKTogTG9jYWxTaWduaW5nS2V5IHtcbiAgY29uc3Qgc2VydmljZU5hbWUgPSBzZXJ2aWNlSWRlbnRpdHkoKTtcbiAgbGV0IHByaXZhdGVLZXk6IEtleU9iamVjdDtcbiAgdHJ5IHtcbiAgICBwcml2YXRlS2V5ID0gY3J5cHRvLmNyZWF0ZVByaXZhdGVLZXkocmVhZEZpbGVTeW5jKGZpbGUsICd1dGY4JykpO1xuICB9IGNhdGNoIChlcnJvcikge1xuICAgIHRocm93IG5ldyBTZXJ2aWNlS2V5RXJyb3IoYENvdWxkIG5vdCByZWFkIHRoZSBzZXJ2aWNlIHNpZ25pbmcga2V5IGF0ICR7ZmlsZX06ICR7ZXJyb3IgaW5zdGFuY2VvZiBFcnJvciA/IGVycm9yLm1lc3NhZ2UgOiBTdHJpbmcoZXJyb3IpfWApO1xuICB9XG4gIC8vIHB1YmxpY0p3a0Zyb20gcmVqZWN0cyBhbnl0aGluZyBidXQgYW4gRUMgUC0yNTYga2V5LCBzbyBhIHNlcnZpY2UgY29uZmlndXJlZFxuICAvLyB3aXRoIChzYXkpIGFuIFJTQSBrZXkgZmFpbHMgSEVSRSByYXRoZXIgdGhhbiBtaW50aW5nIHRva2VucyBub2JvZHkgdmVyaWZpZXMuXG4gIGNvbnN0IHB1YmxpY0p3ayA9IHB1YmxpY0p3a0Zyb20oY3J5cHRvLmNyZWF0ZVB1YmxpY0tleShwcml2YXRlS2V5KSk7XG4gIHJldHVybiB7IHNlcnZpY2VOYW1lLCBraWQ6IHB1YmxpY0p3ay5raWQsIHByaXZhdGVLZXksIHB1YmxpY0p3ayB9O1xufVxuXG4vKipcbiAqIFRoZSBpbi1wcm9jZXNzIGtleSBmb3IgYHNlcnZpY2VOYW1lYCBpbiBlcGhlbWVyYWwgbW9kZSwgZ2VuZXJhdGVkIG9uIGZpcnN0XG4gKiB1c2UgYW5kIHJlZ2lzdGVyZWQgaW4gdGhlIGluLW1lbW9yeSBidW5kbGUgc28gdGhpcyBwcm9jZXNzIGNhbiB2ZXJpZnkgaXRzIG93blxuICogdG9rZW5zLiBLZXllZCBieSBOQU1FIChub3QganVzdCBvbmUga2V5KSBzbyBhIHNpbmdsZS1wcm9jZXNzIHN1aXRlIGNhbiBtaW50XG4gKiB0b2tlbnMgZm9yIHNldmVyYWwgc2VydmljZXMsIGFzIHRoZSByZWFsIGZsZWV0IGRvZXMgYWNyb3NzIHByb2Nlc3Nlcy5cbiAqL1xuZnVuY3Rpb24gZXBoZW1lcmFsU2lnbmluZ0tleShzZXJ2aWNlTmFtZTogc3RyaW5nKTogTG9jYWxTaWduaW5nS2V5IHtcbiAgZXBoZW1lcmFsS2V5cyA/Pz0gbmV3IE1hcCgpO1xuICBjb25zdCBleGlzdGluZyA9IGVwaGVtZXJhbEtleXMuZ2V0KHNlcnZpY2VOYW1lKTtcbiAgaWYgKGV4aXN0aW5nKSByZXR1cm4gZXhpc3Rpbmc7XG5cbiAgaWYgKCFlcGhlbWVyYWxXYXJuZWQpIHtcbiAgICBlcGhlbWVyYWxXYXJuZWQgPSB0cnVlO1xuICAgIGVtaXRDb3VudGVyKCdzZXJ2aWNlX3NpZ25pbmdfZXBoZW1lcmFsX2tleV90b3RhbCcsIHsgc2VydmljZTogc2VydmljZUlkZW50aXR5KCkgfSk7XG4gICAgbG9nZ2VyLndhcm4oJ1NFUlZJQ0VfU0lHTklOR19LRVlfRklMRSBpcyBub3Qgc2V0IOKAlCBtaW50aW5nIGludGVybmFsIHNlcnZpY2UgdG9rZW5zIHdpdGggYW4gRVBIRU1FUkFMIGtleS4gT25seSB0aGlzIHByb2Nlc3Mgd2lsbCBhY2NlcHQgdGhlbTsgZXZlcnkgcGVlciB3aWxsIHJlamVjdCB0aGVtLiBTZXQgU0VSVklDRV9TSUdOSU5HX0tFWV9GSUxFICsgU0VSVklDRV9LRVlfQlVORExFX0ZJTEUgKGRlcGxveS9iaW4vc2VydmljZS1zaWduaW5nLWtleXMuc2gpLicpO1xuICB9XG4gIGNvbnN0IHsgcHJpdmF0ZUtleSwgcHVibGljS2V5IH0gPSBjcnlwdG8uZ2VuZXJhdGVLZXlQYWlyU3luYygnZWMnLCB7IG5hbWVkQ3VydmU6ICdwcmltZTI1NnYxJyB9KTtcbiAgY29uc3QgcHVibGljSndrID0gcHVibGljSndrRnJvbShwdWJsaWNLZXkpO1xuICBjb25zdCBnZW5lcmF0ZWQ6IExvY2FsU2lnbmluZ0tleSA9IHsgc2VydmljZU5hbWUsIGtpZDogcHVibGljSndrLmtpZCwgcHJpdmF0ZUtleSwgcHVibGljSndrIH07XG4gIGVwaGVtZXJhbEtleXMuc2V0KHNlcnZpY2VOYW1lLCBnZW5lcmF0ZWQpO1xuICByZXR1cm4gZ2VuZXJhdGVkO1xufVxuXG4vKiogV2hldGhlciB0aGlzIHByb2Nlc3Mgc2lnbnMgd2l0aCBhIG1vdW50ZWQga2V5IChgY29uZmlndXJlZGApIG9yIGFuIGluLXByb2Nlc3Mgb25lLiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHNlcnZpY2VLZXlNb2RlKCk6ICdjb25maWd1cmVkJyB8ICdlcGhlbWVyYWwnIHtcbiAgcmV0dXJuIHByb2Nlc3MuZW52LlNFUlZJQ0VfU0lHTklOR19LRVlfRklMRSA/ICdjb25maWd1cmVkJyA6ICdlcGhlbWVyYWwnO1xufVxuXG4vKipcbiAqIFRoZSBrZXkgYHNlcnZpY2VOYW1lYCBtdXN0IGJlIHNpZ25lZCB3aXRoLlxuICpcbiAqIEluIGBjb25maWd1cmVkYCBtb2RlIHRoZXJlIGlzIGV4YWN0bHkgT05FIGtleSDigJQgdGhpcyBzZXJ2aWNlJ3Mg4oCUIHNvIGEgcmVxdWVzdFxuICogdG8gc2lnbiBhcyBzb21lIE9USEVSIHNlcnZpY2UgaXMgYSBidWcgYW5kIHRocm93cy4gVGhhdCBtYXR0ZXJzOiBiZWZvcmUgIzE0IGFcbiAqIHNlcnZpY2UgY291bGQgbGVnaXRpbWF0ZWx5IG1pbnQgYHNlcnZpY2U6cGxhdGZvcm1gIHdpdGggdGhlIHNoYXJlZCBzZWNyZXRcbiAqIChhcGkvcGx1Z2luIGRpZCwgZm9yIGl0cyByZWdpc3RyeSBwdXNoZXMpLCBhbmQgdW5kZXIgcGVyLXNlcnZpY2Uga2V5cyBzdWNoIGFcbiAqIHRva2VuIHdvdWxkIGJlIHNpbGVudGx5IHVudmVyaWZpYWJsZSBldmVyeXdoZXJlLiBGYWlsaW5nIGF0IHRoZSBtaW50IHR1cm5zXG4gKiB0aGF0IGludG8gYW4gaW1tZWRpYXRlLCBvYnZpb3VzIGVycm9yIGluc3RlYWQgb2YgYSBydW50aW1lIDQwMS5cbiAqL1xuZnVuY3Rpb24gc2lnbmluZ0tleUZvcihzZXJ2aWNlTmFtZTogc3RyaW5nKTogTG9jYWxTaWduaW5nS2V5IHtcbiAgY29uc3QgZmlsZSA9IHByb2Nlc3MuZW52LlNFUlZJQ0VfU0lHTklOR19LRVlfRklMRTtcbiAgaWYgKCFmaWxlKSByZXR1cm4gZXBoZW1lcmFsU2lnbmluZ0tleShzZXJ2aWNlTmFtZSk7XG5cbiAgaWYgKCFsb2NhbEtleSB8fCBsb2NhbEtleS5zZXJ2aWNlTmFtZSAhPT0gc2VydmljZUlkZW50aXR5KCkpIHtcbiAgICBsb2NhbEtleSA9IGxvYWRDb25maWd1cmVkU2lnbmluZ0tleShmaWxlKTtcbiAgfVxuICBpZiAoc2VydmljZU5hbWUgIT09IGxvY2FsS2V5LnNlcnZpY2VOYW1lKSB7XG4gICAgdGhyb3cgbmV3IFNlcnZpY2VLZXlFcnJvcihcbiAgICAgIGBUaGlzIHByb2Nlc3MgaXMgJyR7bG9jYWxLZXkuc2VydmljZU5hbWV9JyBhbmQgaG9sZHMgb25seSBpdHMgb3duIHNpZ25pbmcga2V5LCBzbyBpdCBjYW5ub3QgbWludCBhIHRva2VuIGZvciAnJHtzZXJ2aWNlTmFtZX0nLiBgXG4gICAgICArICdOYW1lIHRoZSBjYWxsaW5nIHNlcnZpY2UgY29ycmVjdGx5LCBvciBnaXZlIHRoZSBjYWxsZXIgaXRzIG93biBrZXkuJyxcbiAgICApO1xuICB9XG4gIHJldHVybiBsb2NhbEtleTtcbn1cblxuLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG4vLyBWZXJpZmljYXRpb24gYnVuZGxlXG4vLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS1cblxuaW50ZXJmYWNlIExvYWRlZEJ1bmRsZSB7XG4gIC8qKiBga2lkYCDihpIgdGhlIHNlcnZpY2UgdGhhdCBwdWJsaXNoZWQgaXQsIHBsdXMgdGhlIHZlcmlmaWNhdGlvbiBrZXkuICovXG4gIGJ5S2lkOiBNYXA8c3RyaW5nLCB7IHNlcnZpY2VOYW1lOiBzdHJpbmc7IGtleTogS2V5T2JqZWN0IH0+O1xuICBsb2FkZWRBdDogbnVtYmVyO1xuICBtdGltZU1zOiBudW1iZXI7XG59XG5cbmxldCBidW5kbGU6IExvYWRlZEJ1bmRsZSB8IHVuZGVmaW5lZDtcblxuZnVuY3Rpb24gcGFyc2VCdW5kbGUocmF3OiBzdHJpbmcsIGZpbGU6IHN0cmluZyk6IE1hcDxzdHJpbmcsIHsgc2VydmljZU5hbWU6IHN0cmluZzsga2V5OiBLZXlPYmplY3QgfT4ge1xuICBsZXQgZG9jdW1lbnQ6IHVua25vd247XG4gIHRyeSB7XG4gICAgZG9jdW1lbnQgPSBKU09OLnBhcnNlKHJhdyk7XG4gIH0gY2F0Y2ggKGVycm9yKSB7XG4gICAgdGhyb3cgbmV3IFNlcnZpY2VLZXlFcnJvcihgJHtmaWxlfSBpcyBub3QgdmFsaWQgSlNPTjogJHtlcnJvciBpbnN0YW5jZW9mIEVycm9yID8gZXJyb3IubWVzc2FnZSA6IFN0cmluZyhlcnJvcil9YCk7XG4gIH1cbiAgY29uc3Qgc2VydmljZXMgPSAoZG9jdW1lbnQgYXMgUGFydGlhbDxTZXJ2aWNlS2V5QnVuZGxlPik/LnNlcnZpY2VzO1xuICBpZiAoIXNlcnZpY2VzIHx8IHR5cGVvZiBzZXJ2aWNlcyAhPT0gJ29iamVjdCcpIHtcbiAgICB0aHJvdyBuZXcgU2VydmljZUtleUVycm9yKGAke2ZpbGV9IGhhcyBubyBcInNlcnZpY2VzXCIgb2JqZWN0YCk7XG4gIH1cbiAgY29uc3QgYnlLaWQgPSBuZXcgTWFwPHN0cmluZywgeyBzZXJ2aWNlTmFtZTogc3RyaW5nOyBrZXk6IEtleU9iamVjdCB9PigpO1xuICBmb3IgKGNvbnN0IFtzZXJ2aWNlTmFtZSwgZW50cnldIG9mIE9iamVjdC5lbnRyaWVzKHNlcnZpY2VzKSkge1xuICAgIGZvciAoY29uc3QgandrIG9mIGVudHJ5Py5rZXlzID8/IFtdKSB7XG4gICAgICBpZiAoIWp3az8ua2lkKSBjb250aW51ZTtcbiAgICAgIHRyeSB7XG4gICAgICAgIGJ5S2lkLnNldChqd2sua2lkLCB7IHNlcnZpY2VOYW1lLCBrZXk6IHB1YmxpY0tleUZyb21Kd2soandrKSB9KTtcbiAgICAgIH0gY2F0Y2ggKGVycm9yKSB7XG4gICAgICAgIC8vIE9uZSB1bnVzYWJsZSBlbnRyeSBtdXN0IG5vdCBkaXNjYXJkIHRoZSByZXN0IG9mIHRoZSBidW5kbGUg4oCUIHRoZSBzYW1lXG4gICAgICAgIC8vIHJ1bGUgdGhlIEpXS1MgY2FjaGUgYXBwbGllcyB0byBwbGF0Zm9ybSdzIHB1Ymxpc2hlZCBzZXQuXG4gICAgICAgIGxvZ2dlci53YXJuKCdTa2lwcGluZyB1bnVzYWJsZSBzZXJ2aWNlIGtleScsIHsgc2VydmljZU5hbWUsIGtpZDogandrLmtpZCwgZXJyb3I6IGVycm9yIGluc3RhbmNlb2YgRXJyb3IgPyBlcnJvci5tZXNzYWdlIDogU3RyaW5nKGVycm9yKSB9KTtcbiAgICAgIH1cbiAgICB9XG4gIH1cbiAgaWYgKGJ5S2lkLnNpemUgPT09IDApIHRocm93IG5ldyBTZXJ2aWNlS2V5RXJyb3IoYCR7ZmlsZX0gY29udGFpbmVkIG5vIHVzYWJsZSBzZXJ2aWNlIGtleXNgKTtcbiAgcmV0dXJuIGJ5S2lkO1xufVxuXG4vKipcbiAqIFRoZSBga2lkYCDihpIgc2VydmljZSBtYXAsIHJlLXJlYWQgd2hlbiB0aGUgcmVmcmVzaCBpbnRlcnZhbCBoYXMgcGFzc2VkIG9yIHRoZVxuICogZmlsZSdzIG10aW1lIGNoYW5nZWQgKHNvIGEgcm90YXRpb24gaXMgcGlja2VkIHVwIHdpdGhvdXQgYSByZXN0YXJ0KS5cbiAqXG4gKiBJbiBlcGhlbWVyYWwgbW9kZSB0aGUgXCJidW5kbGVcIiBpcyB3aGF0ZXZlciB0aGlzIHByb2Nlc3MgZ2VuZXJhdGVkIGZvciBpdHNlbGYuXG4gKiBOZXZlciB0aHJvd3MgZm9yIGEgbWlzc2luZyBmaWxlIGluIHRoYXQgbW9kZSDigJQgaXQganVzdCBob2xkcyB0aG9zZSBrZXlzLlxuICovXG5mdW5jdGlvbiB2ZXJpZmljYXRpb25LZXlzKCk6IE1hcDxzdHJpbmcsIHsgc2VydmljZU5hbWU6IHN0cmluZzsga2V5OiBLZXlPYmplY3QgfT4ge1xuICBjb25zdCBmaWxlID0gcHJvY2Vzcy5lbnYuU0VSVklDRV9LRVlfQlVORExFX0ZJTEU7XG4gIGlmICghZmlsZSkge1xuICAgIGNvbnN0IGxvY2FsID0gbmV3IE1hcDxzdHJpbmcsIHsgc2VydmljZU5hbWU6IHN0cmluZzsga2V5OiBLZXlPYmplY3QgfT4oKTtcbiAgICBmb3IgKGNvbnN0IGtleSBvZiBlcGhlbWVyYWxLZXlzPy52YWx1ZXMoKSA/PyBbXSkge1xuICAgICAgbG9jYWwuc2V0KGtleS5raWQsIHsgc2VydmljZU5hbWU6IGtleS5zZXJ2aWNlTmFtZSwga2V5OiBjcnlwdG8uY3JlYXRlUHVibGljS2V5KGtleS5wcml2YXRlS2V5KSB9KTtcbiAgICB9XG4gICAgcmV0dXJuIGxvY2FsO1xuICB9XG5cbiAgY29uc3Qgbm93ID0gRGF0ZS5ub3coKTtcbiAgbGV0IG10aW1lTXMgPSAwO1xuICB0cnkge1xuICAgIG10aW1lTXMgPSBzdGF0U3luYyhmaWxlKS5tdGltZU1zO1xuICB9IGNhdGNoIHtcbiAgICAvLyBVbnJlYWRhYmxlIG5vdzoga2VlcCBzZXJ2aW5nIGEgYnVuZGxlIGFscmVhZHkgaW4gaGFuZCAocHVibGljIGtleXMgZG9uJ3RcbiAgICAvLyBleHBpcmUsIGFuZCByZWZ1c2luZyBldmVyeSBpbnRlcm5hbCBjYWxsIGJlY2F1c2UgYSBtb3VudCBibGlwcGVkIHdvdWxkIGJlXG4gICAgLy8gYSBzZWxmLWluZmxpY3RlZCBvdXRhZ2UpLiBXaXRoIG5vdGhpbmcgaW4gaGFuZCwgZmFpbCBjbG9zZWQgYmVsb3cuXG4gICAgaWYgKGJ1bmRsZSkgcmV0dXJuIGJ1bmRsZS5ieUtpZDtcbiAgfVxuICBpZiAoYnVuZGxlICYmIGJ1bmRsZS5tdGltZU1zID09PSBtdGltZU1zICYmIG5vdyAtIGJ1bmRsZS5sb2FkZWRBdCA8IEJVTkRMRV9SRUZSRVNIX0lOVEVSVkFMX01TKSB7XG4gICAgcmV0dXJuIGJ1bmRsZS5ieUtpZDtcbiAgfVxuXG4gIHRyeSB7XG4gICAgY29uc3QgYnlLaWQgPSBwYXJzZUJ1bmRsZShyZWFkRmlsZVN5bmMoZmlsZSwgJ3V0ZjgnKSwgZmlsZSk7XG4gICAgYnVuZGxlID0geyBieUtpZCwgbG9hZGVkQXQ6IG5vdywgbXRpbWVNcyB9O1xuICAgIHJldHVybiBieUtpZDtcbiAgfSBjYXRjaCAoZXJyb3IpIHtcbiAgICBpZiAoYnVuZGxlKSB7XG4gICAgICBsb2dnZXIud2FybignU2VydmljZSBrZXkgYnVuZGxlIHJlbG9hZCBmYWlsZWQ7IGNvbnRpbnVpbmcgb24gdGhlIGxvYWRlZCBjb3B5JywgeyBmaWxlLCBlcnJvcjogZXJyb3IgaW5zdGFuY2VvZiBFcnJvciA/IGVycm9yLm1lc3NhZ2UgOiBTdHJpbmcoZXJyb3IpIH0pO1xuICAgICAgcmV0dXJuIGJ1bmRsZS5ieUtpZDtcbiAgICB9XG4gICAgdGhyb3cgZXJyb3IgaW5zdGFuY2VvZiBTZXJ2aWNlS2V5RXJyb3IgPyBlcnJvciA6IG5ldyBTZXJ2aWNlS2V5RXJyb3IoU3RyaW5nKGVycm9yKSk7XG4gIH1cbn1cblxuLyoqXG4gKiBXaGV0aGVyIGBraWRgIGJlbG9uZ3MgdG8gYW4gaW50ZXJuYWwgU0VSVklDRSBrZXkuIFRoaXMgaXMgaG93IGEgYmVhcmVyIHRva2VuXG4gKiBpcyByb3V0ZWQgYmV0d2VlbiB0aGUgdHdvIEVTMjU2IGNoYWluczogYm90aCB1c2VyIGFuZCBzZXJ2aWNlIHRva2VucyBhcmVcbiAqIEVTMjU2IHdpdGggYSBga2lkYCwgYW5kIGBraWRgIGlzIGFuIFJGQyA3NjM4IHRodW1icHJpbnQsIHNvIG93bmVyc2hpcCBvZiB0aGVcbiAqIGlkIOKAlCBub3QgYSBjbGFpbSBpbnNpZGUgdGhlIHRva2VuIOKAlCBkZWNpZGVzIHdoaWNoIGtleSBzZXQgbWF5IHZlcmlmeSBpdC5cbiAqIFJldHVybnMgYGZhbHNlYCAobmV2ZXIgdGhyb3dzKSB3aGVuIG5vIGJ1bmRsZSBjYW4gYmUgcmVhZDsgdGhlIHRva2VuIHRoZW5cbiAqIHRha2VzIHRoZSB1c2VyIGNoYWluIGFuZCBpcyByZWplY3RlZCB0aGVyZS5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGlzU2VydmljZUtpZChraWQ6IHN0cmluZyk6IGJvb2xlYW4ge1xuICB0cnkge1xuICAgIHJldHVybiB2ZXJpZmljYXRpb25LZXlzKCkuaGFzKGtpZCk7XG4gIH0gY2F0Y2gge1xuICAgIHJldHVybiBmYWxzZTtcbiAgfVxufVxuXG4vKiogRXZlcnkgc2VydmljZSBuYW1lIHRoZSBidW5kbGUgcHVibGlzaGVzIGEga2V5IGZvci4gRGlhZ25vc3RpY3MgYW5kIHRlc3RzLiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGtub3duU2VydmljZU5hbWVzKCk6IHN0cmluZ1tdIHtcbiAgdHJ5IHtcbiAgICByZXR1cm4gWy4uLm5ldyBTZXQoWy4uLnZlcmlmaWNhdGlvbktleXMoKS52YWx1ZXMoKV0ubWFwKCh2KSA9PiB2LnNlcnZpY2VOYW1lKSldLnNvcnQoKTtcbiAgfSBjYXRjaCB7XG4gICAgcmV0dXJuIFtdO1xuICB9XG59XG5cbi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLVxuLy8gU2lnbiAvIHZlcmlmeVxuLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG5cbi8qKiBiYXNlNjR1cmwgb2YgYSBKU09OIHZhbHVlLCB0aGUgSldTIHdheSAobm8gcGFkZGluZywgVVJMIGFscGhhYmV0KS4gKi9cbmZ1bmN0aW9uIGI2NHVKc29uKHZhbHVlOiB1bmtub3duKTogc3RyaW5nIHtcbiAgcmV0dXJuIEJ1ZmZlci5mcm9tKEpTT04uc3RyaW5naWZ5KHZhbHVlKSwgJ3V0Zi04JykudG9TdHJpbmcoJ2Jhc2U2NHVybCcpO1xufVxuXG4vKipcbiAqIFNpZ24gYGNsYWltc2AgYXMgdGhpcyBzZXJ2aWNlJ3MgaW50ZXJuYWwgdG9rZW46IEVTMjU2LCBga2lkYCA9IHRoZSBzaWduZXInc1xuICogdGh1bWJwcmludCwgYHN1YmAgPSBgc2VydmljZTo8c2VydmljZU5hbWU+YC4gYGlhdGAvYGV4cGAgYW5kIHRoZSBvcHRpb25hbFxuICogaXNzdWVyL2F1ZGllbmNlIGFyZSBzdGFtcGVkIGhlcmUgc28gdGhlcmUgaXMgb25lIHBsYWNlIHRoYXQgZGVjaWRlcyB3aGF0IGFcbiAqIHNlcnZpY2UgdG9rZW4gc2F5cy5cbiAqXG4gKiBIYW5kLXJvbGxlZCAocmF0aGVyIHRoYW4gYGp3dC5zaWduYCkgb25seSBzbyB0aGUgaGVhZGVyIGNhcnJpZXMgdGhlIGBraWRgIGluXG4gKiB0aGUgc2FtZSBzaGFwZSBwbGF0Zm9ybSdzIHNpZ25lciBwcm9kdWNlcyDigJQgYGNyeXB0by5zaWduYCBpcyBzeW5jaHJvbm91cywgc29cbiAqIHVubGlrZSB0aGUgS01TIHBhdGggdGhpcyBjb3N0cyBub3RoaW5nLlxuICovXG5leHBvcnQgZnVuY3Rpb24gc2lnblNlcnZpY2VKd3QoXG4gIGNsYWltczogUmVjb3JkPHN0cmluZywgdW5rbm93bj4sXG4gIG9wdGlvbnM6IHsgc2VydmljZU5hbWU6IHN0cmluZzsgZXhwaXJlc0luU2Vjb25kczogbnVtYmVyOyBpc3N1ZXI/OiBzdHJpbmc7IGF1ZGllbmNlPzogc3RyaW5nIH0sXG4pOiBzdHJpbmcge1xuICBjb25zdCBrZXkgPSBzaWduaW5nS2V5Rm9yKG9wdGlvbnMuc2VydmljZU5hbWUpO1xuICBjb25zdCBub3cgPSBNYXRoLmZsb29yKERhdGUubm93KCkgLyAxMDAwKTtcbiAgY29uc3QgYm9keSA9IHtcbiAgICAuLi5jbGFpbXMsXG4gICAgc3ViOiBgJHtTRVJWSUNFX1NVQkpFQ1RfUFJFRklYfSR7b3B0aW9ucy5zZXJ2aWNlTmFtZX1gLFxuICAgIGlhdDogbm93LFxuICAgIGV4cDogbm93ICsgb3B0aW9ucy5leHBpcmVzSW5TZWNvbmRzLFxuICAgIC4uLihvcHRpb25zLmlzc3VlciA/IHsgaXNzOiBvcHRpb25zLmlzc3VlciB9IDoge30pLFxuICAgIC4uLihvcHRpb25zLmF1ZGllbmNlID8geyBhdWQ6IG9wdGlvbnMuYXVkaWVuY2UgfSA6IHt9KSxcbiAgfTtcbiAgY29uc3Qgc2lnbmluZ0lucHV0ID0gYCR7YjY0dUpzb24oeyBhbGc6IFNFUlZJQ0VfVE9LRU5fQUxHT1JJVEhNLCB0eXA6ICdKV1QnLCBraWQ6IGtleS5raWQgfSl9LiR7YjY0dUpzb24oYm9keSl9YDtcbiAgY29uc3Qgc2lnbmF0dXJlID0gY3J5cHRvLnNpZ24oJ3NoYTI1NicsIEJ1ZmZlci5mcm9tKHNpZ25pbmdJbnB1dCwgJ3V0Zi04JyksIHtcbiAgICBrZXk6IGtleS5wcml2YXRlS2V5LFxuICAgIGRzYUVuY29kaW5nOiAnaWVlZS1wMTM2MycsXG4gIH0pO1xuICByZXR1cm4gYCR7c2lnbmluZ0lucHV0fS4ke3NpZ25hdHVyZS50b1N0cmluZygnYmFzZTY0dXJsJyl9YDtcbn1cblxuLyoqXG4gKiBWZXJpZnkgYW4gaW50ZXJuYWwgc2VydmljZSB0b2tlbiwgU1lOQ0hST05PVVNMWSwgYW5kIHJldHVybiBpdHMgY2xhaW1zLlxuICpcbiAqIFRoZSBzZWN1cml0eSBwcm9wZXJ0eSB0aGlzIGZ1bmN0aW9uIGV4aXN0cyBmb3I6IHRoZSBga2lkYCBzZWxlY3RzIHRoZSBrZXlcbiAqIEFORCBuYW1lcyB0aGUgc2lnbmVyLCBhbmQgdGhlIHRva2VuJ3MgYHN1YmAgbXVzdCBhZ3JlZS4gU28gYGJpbGxpbmdgIGNhbm5vdFxuICogYmUgaW1wZXJzb25hdGVkIGJ5IGFueSBvdGhlciBzZXJ2aWNlIOKAlCBub3QgZXZlbiBvbmUgdGhhdCBob2xkcyBhIHZhbGlkIGtleSBvZlxuICogaXRzIG93biDigJQgd2hpY2ggaXMgd2hhdCB0aGUgc2hhcmVkIHNlY3JldCBjb3VsZCBuZXZlciBndWFyYW50ZWUuXG4gKlxuICogQHRocm93cyB7and0Lkpzb25XZWJUb2tlbkVycm9yfSB1bmtub3duIGBraWRgLCB3cm9uZyBhbGdvcml0aG0sIGJhZFxuICogICAgICAgICBzaWduYXR1cmUsIGV4cGlyZWQsIG9yIGEgYHN1YmAgdGhhdCBkaXNhZ3JlZXMgd2l0aCB0aGUgc2lnbmluZyBrZXkuXG4gKiBAdGhyb3dzIHtTZXJ2aWNlS2V5RXJyb3J9IG5vIHZlcmlmaWNhdGlvbiBidW5kbGUgY291bGQgYmUgcmVhZCBhdCBhbGwg4oCUIHRoZVxuICogICAgICAgICB0b2tlbidzIHZhbGlkaXR5IGlzIFVOS05PV04gYW5kIHRoZSBjYWxsZXIgbXVzdCBub3QgdHJlYXQgaXQgYXMgdmFsaWQuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiB2ZXJpZnlTZXJ2aWNlSnd0PFQgPSBSZWNvcmQ8c3RyaW5nLCB1bmtub3duPj4oXG4gIHRva2VuOiBzdHJpbmcsXG4gIG9wdGlvbnM6IHsga2lkOiBzdHJpbmc7IGlzc3Vlcj86IHN0cmluZzsgYXVkaWVuY2U/OiBzdHJpbmcgfSxcbik6IFQge1xuICBjb25zdCBlbnRyeSA9IHZlcmlmaWNhdGlvbktleXMoKS5nZXQob3B0aW9ucy5raWQpO1xuICBpZiAoIWVudHJ5KSB7XG4gICAgZW1pdENvdW50ZXIoJ3NlcnZpY2VfdG9rZW5fdmVyaWZ5X3RvdGFsJywgeyByZXN1bHQ6ICd1bmtub3duX2tpZCcgfSk7XG4gICAgdGhyb3cgbmV3IGp3dC5Kc29uV2ViVG9rZW5FcnJvcihgTm8gcHVibGlzaGVkIHNlcnZpY2Uga2V5IGZvciBraWQgJHtvcHRpb25zLmtpZH1gKTtcbiAgfVxuICBsZXQgY2xhaW1zOiBUICYgeyBzdWI/OiB1bmtub3duIH07XG4gIHRyeSB7XG4gICAgY2xhaW1zID0gand0LnZlcmlmeSh0b2tlbiwgZW50cnkua2V5LCB7XG4gICAgICBhbGdvcml0aG1zOiBbU0VSVklDRV9UT0tFTl9BTEdPUklUSE1dLFxuICAgICAgLi4uKG9wdGlvbnMuaXNzdWVyID8geyBpc3N1ZXI6IG9wdGlvbnMuaXNzdWVyIH0gOiB7fSksXG4gICAgICAuLi4ob3B0aW9ucy5hdWRpZW5jZSA/IHsgYXVkaWVuY2U6IG9wdGlvbnMuYXVkaWVuY2UgfSA6IHt9KSxcbiAgICB9KSBhcyBUICYgeyBzdWI/OiB1bmtub3duIH07XG4gIH0gY2F0Y2ggKGVycm9yKSB7XG4gICAgZW1pdENvdW50ZXIoJ3NlcnZpY2VfdG9rZW5fdmVyaWZ5X3RvdGFsJywgeyByZXN1bHQ6ICdpbnZhbGlkJyB9KTtcbiAgICB0aHJvdyBlcnJvcjtcbiAgfVxuICBpZiAoY2xhaW1zLnN1YiAhPT0gYCR7U0VSVklDRV9TVUJKRUNUX1BSRUZJWH0ke2VudHJ5LnNlcnZpY2VOYW1lfWApIHtcbiAgICAvLyBBIHZhbGlkIHNpZ25hdHVyZSBieSB0aGUgV1JPTkcgc2VydmljZTogdGhlIGtleSBiZWxvbmdzIHRvIGBlbnRyeS5zZXJ2aWNlTmFtZWBcbiAgICAvLyBidXQgdGhlIHRva2VuIHNwZWFrcyBmb3Igc29tZW9uZSBlbHNlLiBUaGlzIGlzIHRoZSBjcm9zcy1zZXJ2aWNlIGZvcmdlcnlcbiAgICAvLyB0aGUgc2hhcmVkIHNlY3JldCBtYWRlIHVuZGV0ZWN0YWJsZS5cbiAgICBlbWl0Q291bnRlcignc2VydmljZV90b2tlbl92ZXJpZnlfdG90YWwnLCB7IHJlc3VsdDogJ3N1YmplY3RfbWlzbWF0Y2gnLCBzZXJ2aWNlOiBlbnRyeS5zZXJ2aWNlTmFtZSB9KTtcbiAgICB0aHJvdyBuZXcgand0Lkpzb25XZWJUb2tlbkVycm9yKGBTZXJ2aWNlIHRva2VuIHN1YmplY3QgJHtTdHJpbmcoY2xhaW1zLnN1Yil9IHdhcyBzaWduZWQgYnkgJHtlbnRyeS5zZXJ2aWNlTmFtZX1gKTtcbiAgfVxuICBlbWl0Q291bnRlcignc2VydmljZV90b2tlbl92ZXJpZnlfdG90YWwnLCB7IHJlc3VsdDogJ29rJywgc2VydmljZTogZW50cnkuc2VydmljZU5hbWUgfSk7XG4gIHJldHVybiBjbGFpbXM7XG59XG5cbi8qKlxuICogVHJ1ZSB3aGlsZSB0aGlzIHNlcnZpY2Ugc3RpbGwgcHVibGlzaGVzIG1vcmUgdGhhbiBvbmUgdmVyaWZpY2F0aW9uIGtleSDigJQgaS5lLlxuICogYSBzaWduaW5nLWtleSByb3RhdGlvbiBpcyBvcGVuIGFuZCB0aGUgcmV0aXJpbmcga2V5IGlzIHN0aWxsIHRydXN0ZWQuIEZlZWRzXG4gKiB0aGUgYHNlY3JldF9yb3RhdGlvbl9wcmV2aW91c19zZXR7c2VjcmV0PVwiU0VSVklDRV9TSUdOSU5HX0tFWVwifWAgZ2F1Z2UuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBpc1JldGlyaW5nU2VydmljZUtleVB1Ymxpc2hlZCgpOiBib29sZWFuIHtcbiAgdHJ5IHtcbiAgICBjb25zdCBtZSA9IHNlcnZpY2VJZGVudGl0eSgpO1xuICAgIHJldHVybiBbLi4udmVyaWZpY2F0aW9uS2V5cygpLnZhbHVlcygpXS5maWx0ZXIoKHYpID0+IHYuc2VydmljZU5hbWUgPT09IG1lKS5sZW5ndGggPiAxO1xuICB9IGNhdGNoIHtcbiAgICByZXR1cm4gZmFsc2U7XG4gIH1cbn1cblxuLyoqXG4gKiBUZXN0IHNlYW06IGRyb3AgdGhlIGxvYWRlZCBzaWduaW5nIGtleSwgdGhlIGJ1bmRsZSBhbmQgYW55IGVwaGVtZXJhbCBrZXlzIHNvXG4gKiB0aGUgbmV4dCBzaWduL3ZlcmlmeSByZS1yZWFkcyB0aGUgZW52aXJvbm1lbnQuIERvbid0IGNhbGwgZnJvbSBwcm9kdWN0aW9uIGNvZGVcbiAqIOKAlCByb3RhdGlvbiBpcyBwaWNrZWQgdXAgYnkgdGhlIHJlZnJlc2ggaW50ZXJ2YWwgYW5kIHRoZSBtdGltZSBjaGVjay5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIF9yZXNldFNlcnZpY2VLZXlzRm9yVGVzdHMoKTogdm9pZCB7XG4gIGxvY2FsS2V5ID0gdW5kZWZpbmVkO1xuICBidW5kbGUgPSB1bmRlZmluZWQ7XG4gIGVwaGVtZXJhbEtleXMgPSB1bmRlZmluZWQ7XG4gIGVwaGVtZXJhbFdhcm5lZCA9IGZhbHNlO1xufVxuIl19
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import { type ReadyAwareRedis } from './env-redis.js';
|
|
2
|
+
/** What a ticket authorizes: the owning org and, for subject-bound channels, the stream subject. */
|
|
3
|
+
export interface SseTicketRecord {
|
|
4
|
+
orgId: string;
|
|
5
|
+
/** Stream subject the ticket is bound to (e.g. a build-log requestId). Absent for org-keyed channels. */
|
|
6
|
+
subject?: string;
|
|
7
|
+
}
|
|
1
8
|
/** Result of {@link SseTicketStore.issue}. */
|
|
2
9
|
export type SseTicketIssueResult = {
|
|
3
10
|
ok: true;
|
|
@@ -7,21 +14,31 @@ export type SseTicketIssueResult = {
|
|
|
7
14
|
reason: 'total' | 'org';
|
|
8
15
|
};
|
|
9
16
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
17
|
+
* The ONE store for short-lived, single-use SSE auth tickets, plus the
|
|
18
|
+
* stream-ownership bindings subject-bound channels need.
|
|
19
|
+
*
|
|
20
|
+
* A client exchanges its JWT for a ticket (so the token never lands in an
|
|
21
|
+
* EventSource query string / access log), then redeems it on the SSE `GET`.
|
|
13
22
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
23
|
+
* - **Tickets** are single-use across pods: the Redis backend redeems with
|
|
24
|
+
* `GETDEL`, so two pods can never both spend one ticket.
|
|
25
|
+
* - **Caps count LIVE tickets** (issued, not yet redeemed, not expired) — per org
|
|
26
|
+
* and in total. A redeemed or expired ticket frees its slot immediately.
|
|
27
|
+
* - **Ownership** records which org owns a stream subject, so a subject-bound
|
|
28
|
+
* channel can refuse to mint a ticket for another org's stream.
|
|
29
|
+
*
|
|
30
|
+
* Both backends share these semantics; the in-memory one is correct for a
|
|
31
|
+
* single process only.
|
|
17
32
|
*/
|
|
18
33
|
export interface SseTicketStore {
|
|
19
|
-
/** Mint a ticket for `orgId
|
|
20
|
-
issue(orgId: string): Promise<SseTicketIssueResult>;
|
|
21
|
-
/**
|
|
22
|
-
consume(ticketId: string): Promise<
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
/** Mint a ticket for `orgId` (optionally bound to `subject`), or reject when a live-ticket cap is reached. */
|
|
35
|
+
issue(orgId: string, subject?: string): Promise<SseTicketIssueResult>;
|
|
36
|
+
/** Redeem a ticket exactly once; null when unknown, expired or already spent. */
|
|
37
|
+
consume(ticketId: string): Promise<SseTicketRecord | null>;
|
|
38
|
+
/** Record the org that owns `subject` for `ttlMs`. */
|
|
39
|
+
bindOwner(subject: string, orgId: string, ttlMs: number): Promise<void>;
|
|
40
|
+
/** The org that owns `subject`, or null when none is bound. Throws when the backend can't answer. */
|
|
41
|
+
getOwner(subject: string): Promise<string | null>;
|
|
25
42
|
/** Release timers/resources. In-memory clears its sweep interval; Redis is a no-op. */
|
|
26
43
|
stop(): void;
|
|
27
44
|
}
|
|
@@ -29,23 +46,39 @@ export interface SseTicketStore {
|
|
|
29
46
|
export interface SseTicketStoreConfig {
|
|
30
47
|
/** Ticket lifetime — long enough for the client to open the EventSource. */
|
|
31
48
|
ttlMs: number;
|
|
32
|
-
/**
|
|
49
|
+
/** Cap on LIVE tickets across all orgs (abuse / memory bound). */
|
|
33
50
|
maxTotal: number;
|
|
34
|
-
/**
|
|
51
|
+
/** Cap on LIVE tickets per org (single-tenant fairness bound). */
|
|
35
52
|
maxPerOrg: number;
|
|
36
53
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* another. Omit for the default
|
|
54
|
+
* Key namespace so independent channels (message notifications, reporting
|
|
55
|
+
* execution-status, a service's build-log stream) never share tickets, caps
|
|
56
|
+
* or ownership bindings — a ticket minted for one channel must not be
|
|
57
|
+
* redeemable on another. Omit for the default namespace.
|
|
41
58
|
*/
|
|
42
59
|
keyPrefix?: string;
|
|
43
60
|
}
|
|
61
|
+
/** In-memory backend — single-process fallback when Redis isn't configured. */
|
|
62
|
+
export declare function createMemorySseTicketStore(config: SseTicketStoreConfig): SseTicketStore;
|
|
63
|
+
/** Minimal ioredis surface the Redis backend needs (GETDEL requires Redis ≥ 6.2). */
|
|
64
|
+
export interface SseTicketRedis extends ReadyAwareRedis {
|
|
65
|
+
eval(script: string, numKeys: number, ...args: (string | number)[]): Promise<unknown>;
|
|
66
|
+
getdel(key: string): Promise<string | null>;
|
|
67
|
+
zrem(key: string, ...members: string[]): Promise<number>;
|
|
68
|
+
set(key: string, value: string, ...args: (string | number)[]): Promise<unknown>;
|
|
69
|
+
get(key: string): Promise<string | null>;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Redis backend — multi-replica safe.
|
|
73
|
+
*
|
|
74
|
+
* Fail-CLOSED: a Redis error makes `issue` reject (`total`), `consume` return
|
|
75
|
+
* null and `getOwner` throw, so an outage degrades to "no live stream" rather
|
|
76
|
+
* than handing out or accepting unvalidated tickets. `bindOwner` is best-effort.
|
|
77
|
+
*/
|
|
78
|
+
export declare function createRedisSseTicketStore(redis: SseTicketRedis, config: SseTicketStoreConfig): SseTicketStore;
|
|
44
79
|
/**
|
|
45
80
|
* Build an {@link SseTicketStore} from the standard Redis env (`REDIS_URL` or
|
|
46
|
-
* `
|
|
47
|
-
*
|
|
48
|
-
* backend. A service opts in with one boot line:
|
|
49
|
-
* `const tickets = createEnvSseTicketStore({ ttlMs, maxTotal, maxPerOrg });`
|
|
81
|
+
* `REDIS_SENTINELS`): the Redis backend when configured (multi-replica safe),
|
|
82
|
+
* otherwise the in-memory single-process backend.
|
|
50
83
|
*/
|
|
51
84
|
export declare function createEnvSseTicketStore(config: SseTicketStoreConfig): SseTicketStore;
|