@salamandradevs/core 0.1.4 → 0.2.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.
Files changed (87) hide show
  1. package/README.md +4 -2
  2. package/dist/index.js +4 -2
  3. package/dist/keys/config/index.js +3 -0
  4. package/dist/keys/config/schema.js +51 -0
  5. package/dist/keys/config/types.js +2 -0
  6. package/dist/keys/contracts.js +2 -0
  7. package/dist/keys/factory.js +61 -0
  8. package/dist/keys/index.js +10 -0
  9. package/dist/keys/jose/index.js +3 -0
  10. package/dist/keys/jose/jose-keyfile.js +97 -0
  11. package/dist/keys/jose/jose-keys-provider.js +256 -0
  12. package/dist/keys/jose/schema.js +68 -0
  13. package/dist/keys/jose/types.js +2 -0
  14. package/dist/keys/jwks/index.js +2 -0
  15. package/dist/keys/jwks/jwks-verification-key-provider.js +43 -0
  16. package/dist/keys/openbao/index.js +3 -0
  17. package/dist/keys/openbao/openbao-keys-provider.js +461 -0
  18. package/dist/keys/openbao/schema.js +67 -0
  19. package/dist/keys/openbao/types.js +2 -0
  20. package/dist/keys/schema.js +65 -0
  21. package/dist/keys/types.js +2 -0
  22. package/dist/keys/validation.js +100 -0
  23. package/dist/runtime/context.js +42 -0
  24. package/dist/runtime/index.js +2 -0
  25. package/dist/storage/records-handler.js +2 -2
  26. package/dist/tokens/access-token-verifier.js +75 -0
  27. package/dist/tokens/contracts.js +2 -0
  28. package/dist/tokens/index.js +5 -0
  29. package/dist/tokens/schema.js +36 -0
  30. package/dist/tokens/types.js +2 -0
  31. package/package.json +2 -1
  32. package/types/index.d.ts +3 -1
  33. package/types/index.d.ts.map +1 -1
  34. package/types/keys/config/index.d.ts +3 -0
  35. package/types/keys/config/index.d.ts.map +1 -0
  36. package/types/keys/config/schema.d.ts +66 -0
  37. package/types/keys/config/schema.d.ts.map +1 -0
  38. package/types/keys/config/types.d.ts +8 -0
  39. package/types/keys/config/types.d.ts.map +1 -0
  40. package/types/keys/contracts.d.ts +52 -0
  41. package/types/keys/contracts.d.ts.map +1 -0
  42. package/types/keys/factory.d.ts +22 -0
  43. package/types/keys/factory.d.ts.map +1 -0
  44. package/types/keys/index.d.ts +10 -0
  45. package/types/keys/index.d.ts.map +1 -0
  46. package/types/keys/jose/index.d.ts +3 -0
  47. package/types/keys/jose/index.d.ts.map +1 -0
  48. package/types/keys/jose/jose-keyfile.d.ts +35 -0
  49. package/types/keys/jose/jose-keyfile.d.ts.map +1 -0
  50. package/types/keys/jose/jose-keys-provider.d.ts +112 -0
  51. package/types/keys/jose/jose-keys-provider.d.ts.map +1 -0
  52. package/types/keys/jose/schema.d.ts +91 -0
  53. package/types/keys/jose/schema.d.ts.map +1 -0
  54. package/types/keys/jose/types.d.ts +35 -0
  55. package/types/keys/jose/types.d.ts.map +1 -0
  56. package/types/keys/jwks/index.d.ts +2 -0
  57. package/types/keys/jwks/index.d.ts.map +1 -0
  58. package/types/keys/jwks/jwks-verification-key-provider.d.ts +27 -0
  59. package/types/keys/jwks/jwks-verification-key-provider.d.ts.map +1 -0
  60. package/types/keys/openbao/index.d.ts +3 -0
  61. package/types/keys/openbao/index.d.ts.map +1 -0
  62. package/types/keys/openbao/openbao-keys-provider.d.ts +175 -0
  63. package/types/keys/openbao/openbao-keys-provider.d.ts.map +1 -0
  64. package/types/keys/openbao/schema.d.ts +68 -0
  65. package/types/keys/openbao/schema.d.ts.map +1 -0
  66. package/types/keys/openbao/types.d.ts +52 -0
  67. package/types/keys/openbao/types.d.ts.map +1 -0
  68. package/types/keys/schema.d.ts +73 -0
  69. package/types/keys/schema.d.ts.map +1 -0
  70. package/types/keys/types.d.ts +53 -0
  71. package/types/keys/types.d.ts.map +1 -0
  72. package/types/keys/validation.d.ts +19 -0
  73. package/types/keys/validation.d.ts.map +1 -0
  74. package/types/runtime/context.d.ts +67 -0
  75. package/types/runtime/context.d.ts.map +1 -0
  76. package/types/runtime/index.d.ts +2 -0
  77. package/types/runtime/index.d.ts.map +1 -0
  78. package/types/tokens/access-token-verifier.d.ts +36 -0
  79. package/types/tokens/access-token-verifier.d.ts.map +1 -0
  80. package/types/tokens/contracts.d.ts +21 -0
  81. package/types/tokens/contracts.d.ts.map +1 -0
  82. package/types/tokens/index.d.ts +5 -0
  83. package/types/tokens/index.d.ts.map +1 -0
  84. package/types/tokens/schema.d.ts +34 -0
  85. package/types/tokens/schema.d.ts.map +1 -0
  86. package/types/tokens/types.d.ts +20 -0
  87. package/types/tokens/types.d.ts.map +1 -0
@@ -0,0 +1,43 @@
1
+ import { createRemoteJWKSet, errors } from 'jose';
2
+ import { CustomError } from '@salamandradevs/tools/shared';
3
+ /**
4
+ * Verification keys read from a remote JWKS, such as auth's published key set.
5
+ *
6
+ * @remarks
7
+ * Delegates fetching and caching to `jose`'s remote key set, which refetches on an unknown
8
+ * `kid` after its cooldown. A `kid` the set does not hold resolves to null; a key set that
9
+ * cannot be fetched or parsed is an infrastructure fault.
10
+ * @group Keys
11
+ * @category Key Providers
12
+ */
13
+ export class JwksVerificationKeyProvider {
14
+ jwks;
15
+ /** @param jwksUrl - Absolute URL of the JWKS document. */
16
+ constructor(jwksUrl) {
17
+ this.jwks = createRemoteJWKSet(new URL(jwksUrl));
18
+ }
19
+ /**
20
+ * Resolves an ES256 public key from the remote key set.
21
+ *
22
+ * @param kid - Key identifier carried with the signature.
23
+ * @returns The public key, or null when the key set has no key with that identifier.
24
+ * @throws {CustomError} EXTERNAL_API_ERROR when the key set cannot be fetched or is
25
+ * invalid.
26
+ */
27
+ async getPublicKey(kid) {
28
+ try {
29
+ return await this.jwks({ alg: 'ES256', kid });
30
+ }
31
+ catch (error) {
32
+ if (error instanceof errors.JWKSNoMatchingKey) {
33
+ return null;
34
+ }
35
+ throw new CustomError({
36
+ name: 'EXTERNAL_API_ERROR',
37
+ message: 'Verification key set could not be loaded',
38
+ cause: error
39
+ });
40
+ }
41
+ }
42
+ }
43
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiandrcy12ZXJpZmljYXRpb24ta2V5LXByb3ZpZGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2tleXMvandrcy9qd2tzLXZlcmlmaWNhdGlvbi1rZXktcHJvdmlkZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLGtCQUFrQixFQUFFLE1BQU0sRUFBRSxNQUFNLE1BQU0sQ0FBQztBQUNsRCxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0sOEJBQThCLENBQUM7QUFLM0Q7Ozs7Ozs7OztHQVNHO0FBQ0gsTUFBTSxPQUFPLDJCQUEyQjtJQUNyQixJQUFJLENBQXdDO0lBRTdELDBEQUEwRDtJQUMxRCxZQUFZLE9BQWU7UUFDekIsSUFBSSxDQUFDLElBQUksR0FBRyxrQkFBa0IsQ0FBQyxJQUFJLEdBQUcsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDO0lBQ25ELENBQUM7SUFFRDs7Ozs7OztPQU9HO0lBQ0gsS0FBSyxDQUFDLFlBQVksQ0FBQyxHQUFXO1FBQzVCLElBQUksQ0FBQztZQUNILE9BQU8sTUFBTSxJQUFJLENBQUMsSUFBSSxDQUFDLEVBQUUsR0FBRyxFQUFFLE9BQU8sRUFBRSxHQUFHLEVBQUUsQ0FBQyxDQUFDO1FBQ2hELENBQUM7UUFBQyxPQUFPLEtBQWMsRUFBRSxDQUFDO1lBQ3hCLElBQUksS0FBSyxZQUFZLE1BQU0sQ0FBQyxpQkFBaUIsRUFBRSxDQUFDO2dCQUM5QyxPQUFPLElBQUksQ0FBQztZQUNkLENBQUM7WUFFRCxNQUFNLElBQUksV0FBVyxDQUFDO2dCQUNwQixJQUFJLEVBQUUsb0JBQW9CO2dCQUMxQixPQUFPLEVBQUUsMENBQTBDO2dCQUNuRCxLQUFLLEVBQUUsS0FBSzthQUNiLENBQUMsQ0FBQztRQUNMLENBQUM7SUFDSCxDQUFDO0NBQ0YifQ==
@@ -0,0 +1,3 @@
1
+ export * from './openbao-keys-provider.js';
2
+ export * from './schema.js';
3
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMva2V5cy9vcGVuYmFvL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLGNBQWMsNEJBQTRCLENBQUM7QUFDM0MsY0FBYyxhQUFhLENBQUMifQ==
@@ -0,0 +1,461 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { importJWK } from 'jose';
3
+ import { CustomError, singleFlight } from '@salamandradevs/tools/shared';
4
+ import { loadEnvVariables } from '@salamandradevs/tools/node';
5
+ import { openBaoSigningKeysConfigSchema } from './schema.js';
6
+ import { openBaoErrorResponseSchema } from './schema.js';
7
+ import { openBaoKvReadResponseSchema } from './schema.js';
8
+ import { openBaoTransitSignResponseSchema } from './schema.js';
9
+ import { validateSigningKeyState } from '../validation.js';
10
+ /** Number of ES256 signature bytes, as the concatenated `r||s` pair JWT requires. */
11
+ const ES256_SIGNATURE_BYTES = 64;
12
+ /**
13
+ * Durable OpenBao-backed keys provider that signs with Transit and verifies locally.
14
+ *
15
+ * @remarks
16
+ * Private signing keys never enter this process. OpenBao holds the private material and
17
+ * performs signatures; the signing keys document records which key is active and which
18
+ * retired keys are still verifiable. An operator owns that document and the signing keys it
19
+ * names.
20
+ *
21
+ * It hands a consumer a signer bound to the active key, resolves a public key by `kid` for
22
+ * verification, and lists the public keys eligible for JWKS publication. Its token needs
23
+ * only `read` on the document path and `update` on the signing path; see
24
+ * `docs/en/howto/openbao/README.md` for how keys are provisioned and rotated.
25
+ *
26
+ * Operationally: every signature costs one OpenBao call, so an unreachable or sealed
27
+ * OpenBao stops all issuance. Verification of already-signed credentials is served from the
28
+ * in-process snapshot with no network call, so they survive an outage that blocks new
29
+ * ones.
30
+ * @group Keys
31
+ * @category Key Providers
32
+ */
33
+ export class OpenBaoKeysProvider {
34
+ config;
35
+ algorithm;
36
+ refreshCooldownSecs;
37
+ /** Last validated signing key state, projected for verification and JWKS reads. */
38
+ snapshot = null;
39
+ /** Single-flight wrapper over refreshSnapshot; concurrent callers share one document read. */
40
+ synchronize = singleFlight(() => this.refreshSnapshot());
41
+ /** OpenBao origin read from `BAO_ADDR`. */
42
+ baseUrl;
43
+ /** Runtime OpenBao token read from `BAO_TOKEN`. */
44
+ token;
45
+ /**
46
+ * @param config - Validated OpenBao key namespace and request policy configuration.
47
+ * @param algorithm - JOSE algorithm enforced for all signing keys.
48
+ * @param refreshCooldownSecs - Minimum time between two re-reads of the lifecycle
49
+ * document triggered by an unknown `kid`; it bounds how often a verification miss may
50
+ * synchronize the snapshot from OpenBao.
51
+ * @throws {CustomError} ENV_VAR_ERROR when `BAO_ADDR` or `BAO_TOKEN` is missing, blank,
52
+ * or malformed.
53
+ */
54
+ constructor(config, algorithm, refreshCooldownSecs) {
55
+ this.config = config;
56
+ this.algorithm = algorithm;
57
+ this.refreshCooldownSecs = refreshCooldownSecs;
58
+ // loadEnvVariables reads and validates connection variables from
59
+ // the config schema's `meta.env` fields name.
60
+ const { baoAddr, baoToken } = loadEnvVariables(openBaoSigningKeysConfigSchema);
61
+ this.baseUrl = baoAddr;
62
+ this.token = baoToken;
63
+ }
64
+ /**
65
+ * Loads the signing keys document and publishes the first snapshot.
66
+ *
67
+ * @group Keys
68
+ * @category Key Providers
69
+ * @returns Completed provider initialization.
70
+ * @throws {CustomError} KEYS_PROVIDER_ERROR when no signing keys document exists or it
71
+ * violates its invariants.
72
+ * @throws {CustomError} EXTERNAL_API_ERROR when OpenBao is unreachable or answers
73
+ * unusably.
74
+ */
75
+ async initialize() {
76
+ // Bootstrap depends on this method by name through the ISigningKeyProvider contract. It runs
77
+ // the first synchronize, which loads the signing keys document and publishes the snapshot;
78
+ // an absent document is a provisioning failure, not a first run.
79
+ await this.synchronize();
80
+ }
81
+ /**
82
+ * Selects the active key and returns a signer bound to that key.
83
+ *
84
+ * @group Keys
85
+ * @category Key Providers
86
+ * @returns Provider-neutral signing capability.
87
+ * @throws {CustomError} KEYS_PROVIDER_ERROR when durable state has no usable active key.
88
+ */
89
+ async getCurrentSigner() {
90
+ // Runs once per access token issued. The synchronize lets a replica pick up a key the
91
+ // operator promoted without waiting for a restart.
92
+ await this.synchronize();
93
+ const snapshot = await this.currentSnapshot();
94
+ const activeKey = snapshot.state.keys.find((entry) => entry.status === 'active');
95
+ if (!activeKey) {
96
+ throw new CustomError({
97
+ name: 'KEYS_PROVIDER_ERROR',
98
+ message: 'No active signing key is available'
99
+ });
100
+ }
101
+ // Bind the handle to this one `activeKey`: the `kid` returned and the `providerKeyId` the
102
+ // sign closure captures come from the same entry, so they are a matched pair. A later
103
+ // synchronize may promote a different key, but this handle still signs with the key named by
104
+ // the `kid` the caller stamps into the JWT header, so the two can never disagree.
105
+ return {
106
+ kid: activeKey.kid,
107
+ algorithm: this.algorithm,
108
+ sign: async (input) => this.signWithTransit(activeKey.providerKeyId, input)
109
+ };
110
+ }
111
+ /**
112
+ * Resolves one eligible public key by exact key identifier.
113
+ *
114
+ * @group Keys
115
+ * @category Key Providers
116
+ * @param kid - Key identifier carried with the signature.
117
+ * @returns Public verification key, or null for an unknown or expired key.
118
+ * @throws {CustomError} EXTERNAL_API_ERROR when a synchronize is due but OpenBao prevents
119
+ * it, so an outage is never reported as an unknown key.
120
+ */
121
+ async getPublicKey(kid) {
122
+ // Runs once per bearer token verified, and normally answered entirely from the in-process
123
+ // snapshot. That is why verification survives an OpenBao outage while issuance does not.
124
+ let snapshot = await this.currentSnapshot();
125
+ let cachedKey = snapshot.byKid.get(kid);
126
+ // An unknown `kid` usually means the operator rotated and this replica has not caught up, so
127
+ // synchronizing (re-reading the signing keys document) could pick up the new key. The
128
+ // synchronize runs only once the snapshot is older than its cache age; while the snapshot is
129
+ // still fresh, a miss returns null without calling OpenBao. That caps synchronizes at one
130
+ // per cache-age window, so a flood of unknown `kid`s (forged or stale) cannot turn into one
131
+ // OpenBao request per request.
132
+ if (!cachedKey && Date.now() - snapshot.loadedAt >= this.refreshCooldownSecs * 1000) {
133
+ await this.synchronize();
134
+ snapshot = await this.currentSnapshot();
135
+ cachedKey = snapshot.byKid.get(kid);
136
+ }
137
+ if (!cachedKey) {
138
+ return null;
139
+ }
140
+ // Only retired entries carry `verifyUntil`. Refuse an expired one even when the operator
141
+ // has not yet removed it, so the overlap window is honoured to the second.
142
+ if (cachedKey.verifyUntil && Date.parse(cachedKey.verifyUntil) <= Date.now()) {
143
+ return null;
144
+ }
145
+ return cachedKey.key;
146
+ }
147
+ /**
148
+ * Synchronizes, then lists the keys currently eligible for JWKS publication.
149
+ *
150
+ * @group Keys
151
+ * @category Key Providers
152
+ * @returns Retained public JSON Web Keys.
153
+ * @throws {CustomError} EXTERNAL_API_ERROR when OpenBao prevents the synchronize, so a
154
+ * stale snapshot is never served as though it had succeeded.
155
+ */
156
+ async listPublicKeys() {
157
+ // Backs `GET /.well-known/jwks.json`. Synchronizing here is what lets every replica publish
158
+ // a key the operator added, which is a precondition for that key ever being promoted safely.
159
+ await this.synchronize();
160
+ const snapshot = await this.currentSnapshot();
161
+ return snapshot.keys;
162
+ }
163
+ /**
164
+ * Returns the published snapshot, synchronizing once if none has been built yet.
165
+ *
166
+ * @returns The most recently published projection of durable signing key state.
167
+ * @throws {CustomError} KEYS_PROVIDER_ERROR when a synchronize completed without
168
+ * publishing.
169
+ */
170
+ async currentSnapshot() {
171
+ if (!this.snapshot) {
172
+ await this.synchronize();
173
+ }
174
+ // Reaching here without a snapshot would mean a synchronize returned without publishing,
175
+ // which no path does. Failing loudly beats handing callers an empty key set.
176
+ if (!this.snapshot) {
177
+ throw new CustomError({
178
+ name: 'KEYS_PROVIDER_ERROR',
179
+ message: 'Signing-key snapshot is unavailable'
180
+ });
181
+ }
182
+ return this.snapshot;
183
+ }
184
+ /**
185
+ * Reads the signing keys document and rebuilds the published snapshot; the work each
186
+ * synchronize performs.
187
+ *
188
+ * @returns Completed synchronize with a freshly published snapshot.
189
+ * @throws {CustomError} KEYS_PROVIDER_ERROR when no signing keys document exists.
190
+ */
191
+ async refreshSnapshot() {
192
+ const state = await this.readSigningKeyState();
193
+ // The operator provisions the first key and this document. Creating one here would mint a
194
+ // signing key nobody asked for and invalidate every token already in circulation.
195
+ if (!state) {
196
+ throw new CustomError({
197
+ name: 'KEYS_PROVIDER_ERROR',
198
+ message: `No lifecycle document at '${this.config.kvMount}/${this.config.statePath}'. ` +
199
+ `Provision the first signing key and its document before starting the service; ` +
200
+ `see docs/en/howto/openbao/README.md`
201
+ });
202
+ }
203
+ await this.publishSnapshot(state);
204
+ }
205
+ /**
206
+ * Signs one exact JWS signing input with a captured signing key.
207
+ *
208
+ * @param providerKeyId - Signing key name captured when the signer was selected.
209
+ * @param input - Exact JWS signing input bytes to sign.
210
+ * @returns Raw 64-byte ES256 signature.
211
+ * @throws {CustomError} EXTERNAL_API_ERROR when OpenBao signs with an unpublished version
212
+ * or returns a signature of the wrong length.
213
+ */
214
+ async signWithTransit(providerKeyId, input) {
215
+ const response = await this.request('sign', `/v1/${this.config.transitMount}/sign/${providerKeyId}`, {
216
+ method: 'POST',
217
+ body: JSON.stringify({
218
+ // `prehashed: false` leaves the SHA-256 to OpenBao, so this process never handles even
219
+ // a digest. `marshaling_algorithm: 'jws'` asks for the concatenated `r||s` form JWT
220
+ // requires, URL-safe base64 encoded, instead of the ASN.1 form OpenSSL uses.
221
+ input: Buffer.from(input).toString('base64'),
222
+ hash_algorithm: 'sha2-256',
223
+ prehashed: false,
224
+ marshaling_algorithm: 'jws'
225
+ })
226
+ }, openBaoTransitSignResponseSchema);
227
+ return this.parseTransitSignature(response.data.signature, providerKeyId);
228
+ }
229
+ /**
230
+ * Reads and validates the signing keys document, or reports its absence.
231
+ *
232
+ * @returns Validated state, or null when no document exists.
233
+ * @throws {CustomError} KEYS_PROVIDER_ERROR when the document exists but is not a signing
234
+ * keys document, or violates a signing key invariant.
235
+ */
236
+ async readSigningKeyState() {
237
+ try {
238
+ const response = await this.request('read-state', `/v1/${this.config.kvMount}/data/${this.config.statePath}`, { method: 'GET' }, openBaoKvReadResponseSchema);
239
+ const state = response.data.data.state;
240
+ // The document exists but holds something else, so something other than this service owns
241
+ // that KV path. Say what was found rather than reporting a generic validation failure.
242
+ if (!state || typeof state !== 'object') {
243
+ const present = Object.keys(response.data.data).join(', ') || 'none';
244
+ throw new CustomError({
245
+ name: 'KEYS_PROVIDER_ERROR',
246
+ message: `KV document '${this.config.kvMount}/${this.config.statePath}' (version ` +
247
+ `${String(response.data.metadata.version)}) holds no 'state' key; present keys: ` +
248
+ `${present}. See docs/en/howto/openbao/README.md`
249
+ });
250
+ }
251
+ let validated;
252
+ try {
253
+ validated = validateSigningKeyState(state, this.algorithm);
254
+ }
255
+ catch (error) {
256
+ // Keep a classified failure as-is; only a raw parse error needs wrapping.
257
+ if (error instanceof CustomError) {
258
+ throw error;
259
+ }
260
+ throw new CustomError({
261
+ name: 'KEYS_PROVIDER_ERROR',
262
+ message: 'OpenBao lifecycle state is invalid',
263
+ cause: error
264
+ });
265
+ }
266
+ return validated;
267
+ }
268
+ catch (error) {
269
+ // Absence is reported to the caller, which turns it into a provisioning error. Note this
270
+ // is 404 only for a path the policy permits; a path outside the policy answers 403 and
271
+ // correctly propagates as a transport failure.
272
+ if (this.failureCause(error)?.status === 404) {
273
+ return null;
274
+ }
275
+ throw error;
276
+ }
277
+ }
278
+ /**
279
+ * Projects validated state into the in-memory snapshot that serves verification and JWKS.
280
+ *
281
+ * @param state - Validated signing key state to project.
282
+ * @returns Completed snapshot publication.
283
+ * @throws {CustomError} KEYS_PROVIDER_ERROR when an entry holds a non-public JWK.
284
+ */
285
+ async publishSnapshot(state) {
286
+ const keys = [];
287
+ const byKid = new Map();
288
+ // Build into locals, not into `this.snapshot`. A failed import part-way through then leaves
289
+ // the previous snapshot serving traffic instead of exposing a half-built key set to
290
+ // concurrent verification calls.
291
+ for (const entry of state.keys) {
292
+ // Past its verification deadline: drop it from JWKS now, even though the operator has not
293
+ // yet removed the entry.
294
+ if (entry.verifyUntil && Date.parse(entry.verifyUntil) <= Date.now()) {
295
+ continue;
296
+ }
297
+ const imported = await importJWK(entry.publicJwk, this.algorithm);
298
+ // `importJWK` returns bytes for symmetric keys. Reaching that here would mean secret
299
+ // material sat in a document that is published to the world.
300
+ if (imported instanceof Uint8Array) {
301
+ throw new CustomError({
302
+ name: 'KEYS_PROVIDER_ERROR',
303
+ message: 'OpenBao lifecycle state contains a non-public JWK'
304
+ });
305
+ }
306
+ keys.push(entry.publicJwk);
307
+ byKid.set(entry.kid, {
308
+ key: imported,
309
+ status: entry.status,
310
+ verifyUntil: entry.verifyUntil
311
+ });
312
+ }
313
+ // Single assignment, so readers move from one complete snapshot to the next.
314
+ this.snapshot = { loadedAt: Date.now(), state, keys, byKid };
315
+ }
316
+ /**
317
+ * Parses a Transit sign response into the raw ES256 signature bytes.
318
+ *
319
+ * @param marshaledSignature - Raw `signature` string returned by Transit's sign endpoint.
320
+ * @param providerKeyId - Transit key name, named in the error when the version is
321
+ * unexpected.
322
+ * @returns Raw 64-byte ES256 signature.
323
+ * @throws {CustomError} EXTERNAL_API_ERROR when Transit signed with an unpublished key
324
+ * version or returned a signature of the wrong length.
325
+ */
326
+ parseTransitSignature(marshaledSignature, providerKeyId) {
327
+ // Transit returns the signature as `vault:v<version>:<base64url r||s>`. Parse that shape to
328
+ // separate the two parts: group 1 is the key version that produced the signature, group 2
329
+ // is the marshaled signature payload. The version is checked next; group 2 is decoded below.
330
+ //
331
+ // Only version 1 is expected: the provisioning procedure creates one key per rotation and
332
+ // never rotates versions inside a key, so only version 1 has a public half in JWKS. Any
333
+ // other version means someone rotated the key in place, and the resulting token would be
334
+ // unverifiable.
335
+ const marshaled = /^vault:v(\d+):(.+)$/.exec(marshaledSignature);
336
+ if (!marshaled || marshaled[1] !== '1') {
337
+ throw new CustomError({
338
+ name: 'EXTERNAL_API_ERROR',
339
+ message: `OpenBao signed with an unpublished key version for '${providerKeyId}'`
340
+ });
341
+ }
342
+ const signature = Buffer.from(marshaled[2], 'base64url');
343
+ // A correct ES256 signature is exactly two 32-byte integers. A different length means the
344
+ // marshaling was not what was asked for, and jose would reject the token later anyway.
345
+ if (signature.byteLength !== ES256_SIGNATURE_BYTES) {
346
+ throw new CustomError({
347
+ name: 'EXTERNAL_API_ERROR',
348
+ message: 'OpenBao returned an invalid ES256 signature'
349
+ });
350
+ }
351
+ return signature;
352
+ }
353
+ /**
354
+ * Performs one OpenBao HTTP call and converts every failure into a classified
355
+ * `CustomError`.
356
+ *
357
+ * @param operation - Short label naming the step, retained in the error cause.
358
+ * @param path - Absolute OpenBao API path, including the `/v1` prefix.
359
+ * @param init - Method and body for the request.
360
+ * @param schema - Optional response schema; the body is validated when one is supplied.
361
+ * @param schema.parse - Validator applied to the parsed response body.
362
+ * @returns The validated response body, or the raw body when no schema is supplied.
363
+ * @throws {CustomError} EXTERNAL_API_ERROR on any transport failure, timeout, or
364
+ * non-success status.
365
+ */
366
+ async request(operation, path, init, schema) {
367
+ // The single place OpenBao transport lives; only two call sites reach it, a state read and
368
+ // a signature.
369
+ const controller = new AbortController();
370
+ const timeout = setTimeout(() => {
371
+ controller.abort();
372
+ }, this.config.requestTimeoutMs);
373
+ try {
374
+ const response = await fetch(`${this.baseUrl}${path}`, {
375
+ ...init,
376
+ signal: controller.signal,
377
+ headers: {
378
+ 'content-type': 'application/json',
379
+ 'x-vault-token': this.token
380
+ }
381
+ });
382
+ // A failure body may not be JSON at all; that must not throw before the status has been
383
+ // classified below.
384
+ const raw = await response.json().catch(() => ({}));
385
+ if (!response.ok) {
386
+ throw this.failure(operation, path, response.status, raw);
387
+ }
388
+ // Validate against the schema when one is supplied; OpenBao shapes differ between
389
+ // endpoints in ways easy to assume wrongly. A schemaless call returns the raw body as `T`.
390
+ return schema ? schema.parse(raw) : raw;
391
+ }
392
+ catch (error) {
393
+ // Already classified by `failure`; keep its status.
394
+ if (error instanceof CustomError) {
395
+ throw error;
396
+ }
397
+ // The request never completed: a timeout, DNS failure, refused connection, or a schema
398
+ // rejection. There is no status to classify, so the cause carries the original error.
399
+ throw new CustomError({
400
+ name: 'EXTERNAL_API_ERROR',
401
+ message: `OpenBao ${operation} request to ${path} failed`,
402
+ cause: { operation, path, error }
403
+ });
404
+ }
405
+ finally {
406
+ clearTimeout(timeout);
407
+ }
408
+ }
409
+ /**
410
+ * Classifies one non-success OpenBao status into an actionable error.
411
+ *
412
+ * @param operation - Short label naming the step.
413
+ * @param path - Absolute OpenBao API path that failed.
414
+ * @param status - HTTP status OpenBao returned.
415
+ * @param raw - Parsed response body, consulted only for its sanitized error list.
416
+ * @returns The classified error, for the caller to throw.
417
+ */
418
+ failure(operation, path, status, raw) {
419
+ const parsed = openBaoErrorResponseSchema.safeParse(raw);
420
+ const errors = parsed.success ? parsed.data.errors : [];
421
+ let detail = `status ${String(status)}`;
422
+ if (status === 503) {
423
+ // 503 covers both a sealed instance and a node that is not active. The remedies differ,
424
+ // so consult the message rather than reporting one for the other.
425
+ detail = errors.some((message) => message.toLowerCase().includes('sealed'))
426
+ ? 'the instance is sealed; unseal it to restore signing'
427
+ : 'the instance is sealed or has no active node';
428
+ }
429
+ else if (status === 403) {
430
+ // 403 is returned for an expired or revoked token and for a policy missing a capability
431
+ // alike. The service needs `read` on the state path and `update` on `transit/sign/*`.
432
+ detail =
433
+ 'the token was rejected; it has expired, been revoked, or its policy lacks a capability on this path';
434
+ }
435
+ else if (status === 429) {
436
+ detail = 'the request was rate limited by a quota';
437
+ }
438
+ // The response body can name mounts, policies and paths, so only the status category and
439
+ // the path this service already knows are allowed into the message.
440
+ return new CustomError({
441
+ name: 'EXTERNAL_API_ERROR',
442
+ message: `OpenBao ${operation} request to ${path} failed: ${detail}`,
443
+ cause: { operation, path, status }
444
+ });
445
+ }
446
+ /**
447
+ * Narrows an unknown error to the structured cause this class attaches to its failures.
448
+ *
449
+ * @param error - Error to inspect.
450
+ * @returns The cause when the error came from this provider's transport, else undefined.
451
+ */
452
+ failureCause(error) {
453
+ if (!(error instanceof CustomError) ||
454
+ typeof error.cause !== 'object' ||
455
+ error.cause === null) {
456
+ return undefined;
457
+ }
458
+ return error.cause;
459
+ }
460
+ }
461
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3BlbmJhby1rZXlzLXByb3ZpZGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2tleXMvb3BlbmJhby9vcGVuYmFvLWtleXMtcHJvdmlkZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLGFBQWEsQ0FBQztBQUVyQyxPQUFPLEVBQUUsU0FBUyxFQUFFLE1BQU0sTUFBTSxDQUFDO0FBQ2pDLE9BQU8sRUFBRSxXQUFXLEVBQUUsWUFBWSxFQUFFLE1BQU0sOEJBQThCLENBQUM7QUFDekUsT0FBTyxFQUFFLGdCQUFnQixFQUFFLE1BQU0sNEJBQTRCLENBQUM7QUFHOUQsT0FBTyxFQUFFLDhCQUE4QixFQUFFLE1BQU0sYUFBYSxDQUFDO0FBQzdELE9BQU8sRUFBRSwwQkFBMEIsRUFBRSxNQUFNLGFBQWEsQ0FBQztBQUN6RCxPQUFPLEVBQUUsMkJBQTJCLEVBQUUsTUFBTSxhQUFhLENBQUM7QUFDMUQsT0FBTyxFQUFFLGdDQUFnQyxFQUFFLE1BQU0sYUFBYSxDQUFDO0FBQy9ELE9BQU8sRUFBRSx1QkFBdUIsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBVzNELHFGQUFxRjtBQUNyRixNQUFNLHFCQUFxQixHQUFXLEVBQUUsQ0FBQztBQUV6Qzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7R0FvQkc7QUFDSCxNQUFNLE9BQU8sbUJBQW1CO0lBdUJYO0lBQ0E7SUFDQTtJQXhCbkIsbUZBQW1GO0lBQzNFLFFBQVEsR0FBOEIsSUFBSSxDQUFDO0lBRW5ELDhGQUE4RjtJQUM3RSxXQUFXLEdBQUcsWUFBWSxDQUFDLEdBQUcsRUFBRSxDQUFDLElBQUksQ0FBQyxlQUFlLEVBQUUsQ0FBQyxDQUFDO0lBRTFFLDJDQUEyQztJQUMxQixPQUFPLENBQVM7SUFFakMsbURBQW1EO0lBQ2xDLEtBQUssQ0FBUztJQUUvQjs7Ozs7Ozs7T0FRRztJQUNILFlBQ21CLE1BQWdDLEVBQ2hDLFNBQWtCLEVBQ2xCLG1C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
@@ -0,0 +1,67 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * OpenBao key namespace, request policy, and connection schema.
4
+ *
5
+ * - TransitMount: Transit secrets-engine mount.
6
+ * - KvMount: KV v2 secrets-engine mount.
7
+ * - StatePath: KV path containing lifecycle state.
8
+ * - KeyNamePrefix: Prefix for provider-managed Transit keys.
9
+ * - RequestTimeoutMs: Deadline applied to every OpenBao HTTP request.
10
+ * - RefreshCooldownSecs: Minimum time between two re-reads of the lifecycle document
11
+ * triggered by an unknown `kid`.
12
+ * - BaoAddr: OpenBao origin without embedded credentials; read from `BAO_ADDR`.
13
+ * - BaoToken: Runtime OpenBao token; a secret, read from `BAO_TOKEN`.
14
+ *
15
+ * @remarks
16
+ * `baoAddr` and `baoToken` are marked `.meta({ env })`, so `loadEnvVariables` resolves them
17
+ * from `BAO_ADDR` and `BAO_TOKEN` and hands the values to the provider; the configuration
18
+ * file holds only placeholders, so the token never appears in the config object.
19
+ */
20
+ export const openBaoSigningKeysConfigSchema = z
21
+ .object({
22
+ transitMount: z.string().min(1),
23
+ kvMount: z.string().min(1),
24
+ statePath: z.string().min(1),
25
+ keyNamePrefix: z.string().min(1),
26
+ requestTimeoutMs: z.number().int().positive().default(10_000),
27
+ refreshCooldownSecs: z.number().int().positive(),
28
+ baoAddr: z.url({ error: 'is required and must be a URL' }).meta({ env: 'BAO_ADDR' }),
29
+ baoToken: z
30
+ .string({ error: 'is required' })
31
+ .min(1, 'must not be empty')
32
+ .meta({ env: 'BAO_TOKEN' })
33
+ })
34
+ .strict();
35
+ /**
36
+ * KV v2 lifecycle-state read response schema.
37
+ *
38
+ * - Data: KV envelope carrying the stored document and its version metadata.
39
+ *
40
+ * @remarks
41
+ * OpenBao wraps every logical response in an envelope carrying `request_id`, `lease_id`,
42
+ * `lease_duration`, `renewable`, `warnings`, `wrap_info`, `auth` and `mount_type`, and the
43
+ * version metadata adds `created_time`, `custom_metadata`, `deletion_time` and `destroyed`.
44
+ * None of these response schemas may be `strict`, because those unmodelled fields are
45
+ * always present. Verified against OpenBao 2.7.0.
46
+ */
47
+ export const openBaoKvReadResponseSchema = z.object({
48
+ data: z.object({
49
+ data: z.record(z.string(), z.unknown()),
50
+ metadata: z.object({ version: z.number().int().positive() })
51
+ })
52
+ });
53
+ /**
54
+ * Transit signing response schema.
55
+ *
56
+ * - Data: JWS-formatted signature returned by Transit.
57
+ */
58
+ export const openBaoTransitSignResponseSchema = z.object({
59
+ data: z.object({ signature: z.string().min(1) })
60
+ });
61
+ /**
62
+ * OpenBao error response schema.
63
+ *
64
+ * - Errors: Sanitized error messages returned by OpenBao.
65
+ */
66
+ export const openBaoErrorResponseSchema = z.object({ errors: z.array(z.string()) });
67
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2NoZW1hLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2tleXMvb3BlbmJhby9zY2hlbWEudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLENBQUMsRUFBRSxNQUFNLEtBQUssQ0FBQztBQUV4Qjs7Ozs7Ozs7Ozs7Ozs7Ozs7R0FpQkc7QUFDSCxNQUFNLENBQUMsTUFBTSw4QkFBOEIsR0FBRyxDQUFDO0tBQzVDLE1BQU0sQ0FBQztJQUNOLFlBQVksRUFBRSxDQUFDLENBQUMsTUFBTSxFQUFFLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQztJQUMvQixPQUFPLEVBQUUsQ0FBQyxDQUFDLE1BQU0sRUFBRSxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUM7SUFDMUIsU0FBUyxFQUFFLENBQUMsQ0FBQyxNQUFNLEVBQUUsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDO0lBQzVCLGFBQWEsRUFBRSxDQUFDLENBQUMsTUFBTSxFQUFFLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQztJQUNoQyxnQkFBZ0IsRUFBRSxDQUFDLENBQUMsTUFBTSxFQUFFLENBQUMsR0FBRyxFQUFFLENBQUMsUUFBUSxFQUFFLENBQUMsT0FBTyxDQUFDLE1BQU0sQ0FBQztJQUM3RCxtQkFBbUIsRUFBRSxDQUFDLENBQUMsTUFBTSxFQUFFLENBQUMsR0FBRyxFQUFFLENBQUMsUUFBUSxFQUFFO0lBQ2hELE9BQU8sRUFBRSxDQUFDLENBQUMsR0FBRyxDQUFDLEVBQUUsS0FBSyxFQUFFLCtCQUErQixFQUFFLENBQUMsQ0FBQyxJQUFJLENBQUMsRUFBRSxHQUFHLEVBQUUsVUFBVSxFQUFFLENBQUM7SUFDcEYsUUFBUSxFQUFFLENBQUM7U0FDUixNQUFNLENBQUMsRUFBRSxLQUFLLEVBQUUsYUFBYSxFQUFFLENBQUM7U0FDaEMsR0FBRyxDQUFDLENBQUMsRUFBRSxtQkFBbUIsQ0FBQztTQUMzQixJQUFJLENBQUMsRUFBRSxHQUFHLEVBQUUsV0FBVyxFQUFFLENBQUM7Q0FDOUIsQ0FBQztLQUNELE1BQU0sRUFBRSxDQUFDO0FBRVo7Ozs7Ozs7Ozs7O0dBV0c7QUFDSCxNQUFNLENBQUMsTUFBTSwyQkFBMkIsR0FBRyxDQUFDLENBQUMsTUFBTSxDQUFDO0lBQ2xELElBQUksRUFBRSxDQUFDLENBQUMsTUFBTSxDQUFDO1FBQ2IsSUFBSSxFQUFFLENBQUMsQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLE1BQU0sRUFBRSxFQUFFLENBQUMsQ0FBQyxPQUFPLEVBQUUsQ0FBQztRQUN2QyxRQUFRLEVBQUUsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxFQUFFLE9BQU8sRUFBRSxDQUFDLENBQUMsTUFBTSxFQUFFLENBQUMsR0FBRyxFQUFFLENBQUMsUUFBUSxFQUFFLEVBQUUsQ0FBQztLQUM3RCxDQUFDO0NBQ0gsQ0FBQyxDQUFDO0FBRUg7Ozs7R0FJRztBQUNILE1BQU0sQ0FBQyxNQUFNLGdDQUFnQyxHQUFHLENBQUMsQ0FBQyxNQUFNLENBQUM7SUFDdkQsSUFBSSxFQUFFLENBQUMsQ0FBQyxNQUFNLENBQUMsRUFBRSxTQUFTLEVBQUUsQ0FBQyxDQUFDLE1BQU0sRUFBRSxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO0NBQ2pELENBQUMsQ0FBQztBQUVIOzs7O0dBSUc7QUFDSCxNQUFNLENBQUMsTUFBTSwwQkFBMEIsR0FBRyxDQUFDLENBQUMsTUFBTSxDQUFDLEVBQUUsTUFBTSxFQUFFLENBQUMsQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLE1BQU0sRUFBRSxDQUFDLEVBQUUsQ0FBQyxDQUFDIn0=
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHlwZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMva2V5cy9vcGVuYmFvL3R5cGVzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiIifQ==