@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
package/README.md CHANGED
@@ -22,8 +22,10 @@ details remain inside adapters.
22
22
 
23
23
  `src/database` contains the persistence contracts, dataset provisioner, and concrete MongoDB
24
24
  and MySQL adapters. `src/records` contains the base record model and handler used by services
25
- to build tenant-aware record policies. `src/utils` contains the execution-context helpers that
26
- bind service work to the current request or job.
25
+ to build tenant-aware record policies. `src/runtime` holds the runtime execution context that
26
+ binds service work to the current request or job. `src/keys` holds the key contracts, the
27
+ Jose, OpenBao, and JWKS providers, and their factory. `src/tokens` holds the access-token
28
+ verifier and claims schema.
27
29
 
28
30
  Services such as `@salamandradevs/auth` use Core to keep persistence details out of transport and
29
31
  application code while still enforcing tenant boundaries close to the record layer.
package/dist/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  export * from './storage/index.js';
2
2
  export * from './infra/database/index.js';
3
- export * from './utils/index.js';
4
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsY0FBYyxvQkFBb0IsQ0FBQztBQUNuQyxjQUFjLDJCQUEyQixDQUFDO0FBQzFDLGNBQWMsa0JBQWtCLENBQUMifQ==
3
+ export * from './keys/index.js';
4
+ export * from './tokens/index.js';
5
+ export * from './runtime/index.js';
6
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsY0FBYyxvQkFBb0IsQ0FBQztBQUNuQyxjQUFjLDJCQUEyQixDQUFDO0FBQzFDLGNBQWMsaUJBQWlCLENBQUM7QUFDaEMsY0FBYyxtQkFBbUIsQ0FBQztBQUNsQyxjQUFjLG9CQUFvQixDQUFDIn0=
@@ -0,0 +1,3 @@
1
+ export * from './schema.js';
2
+ export * from './types.js';
3
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMva2V5cy9jb25maWcvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxZQUFZLENBQUMifQ==
@@ -0,0 +1,51 @@
1
+ import { z } from 'zod';
2
+ import { openBaoSigningKeysConfigSchema } from '../openbao/schema.js';
3
+ import { joseSigningKeysConfigSchema } from '../jose/schema.js';
4
+ /**
5
+ * Supported signing-key provider identifiers; grows as new providers are added.
6
+ *
7
+ * @group Keys
8
+ * @category Key Providers
9
+ */
10
+ export const signingKeyProviderSchema = z.enum(['jose', 'openbao']);
11
+ /**
12
+ * Signing-key provider selector resolved from the environment.
13
+ *
14
+ * - Provider: Active signing-key provider; read from `SIGNING_KEY_PROVIDER`.
15
+ *
16
+ * @remarks
17
+ * `provider` is marked `.meta({ env })`, so `loadEnvVariables` resolves it from
18
+ * `SIGNING_KEY_PROVIDER`; the active provider is chosen at runtime, never baked into the
19
+ * configuration file.
20
+ * @group Keys
21
+ * @category Key Providers
22
+ */
23
+ export const signingKeyProviderSelectorSchema = z
24
+ .object({
25
+ provider: signingKeyProviderSchema.meta({ env: 'SIGNING_KEY_PROVIDER' })
26
+ })
27
+ .strict();
28
+ /**
29
+ * Provider-neutral signing lifecycle schema.
30
+ *
31
+ * - Algorithm: JOSE algorithm supported by this key set.
32
+ * - Jose: Jose provider configuration, present when the deployment may select `jose`.
33
+ * - Openbao: OpenBao provider configuration, present when the deployment may select
34
+ * `openbao`.
35
+ *
36
+ * @remarks
37
+ * Each provider block is optional so a deployment configures only the providers it uses;
38
+ * the active one is chosen at runtime by `SIGNING_KEY_PROVIDER` and must have its block
39
+ * present. Composed into each service's own `serviceConfigSchema` and validated there; core
40
+ * never reads a configuration file.
41
+ * @group Keys
42
+ * @category Key Providers
43
+ */
44
+ export const signingKeysConfigSchema = z
45
+ .object({
46
+ algorithm: z.literal('ES256'),
47
+ jose: joseSigningKeysConfigSchema.optional(),
48
+ openbao: openBaoSigningKeysConfigSchema.optional()
49
+ })
50
+ .strict();
51
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2NoZW1hLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2tleXMvY29uZmlnL3NjaGVtYS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsQ0FBQyxFQUFFLE1BQU0sS0FBSyxDQUFDO0FBRXhCLE9BQU8sRUFBRSw4QkFBOEIsRUFBRSxNQUFNLHNCQUFzQixDQUFDO0FBQ3RFLE9BQU8sRUFBRSwyQkFBMkIsRUFBRSxNQUFNLG1CQUFtQixDQUFDO0FBRWhFOzs7OztHQUtHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sd0JBQXdCLEdBQUcsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLE1BQU0sRUFBRSxTQUFTLENBQUMsQ0FBQyxDQUFDO0FBRXBFOzs7Ozs7Ozs7OztHQVdHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sZ0NBQWdDLEdBQUcsQ0FBQztLQUM5QyxNQUFNLENBQUM7SUFDTixRQUFRLEVBQUUsd0JBQXdCLENBQUMsSUFBSSxDQUFDLEVBQUUsR0FBRyxFQUFFLHNCQUFzQixFQUFFLENBQUM7Q0FDekUsQ0FBQztLQUNELE1BQU0sRUFBRSxDQUFDO0FBRVo7Ozs7Ozs7Ozs7Ozs7OztHQWVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sdUJBQXVCLEdBQUcsQ0FBQztLQUNyQyxNQUFNLENBQUM7SUFDTixTQUFTLEVBQUUsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxPQUFPLENBQUM7SUFDN0IsSUFBSSxFQUFFLDJCQUEyQixDQUFDLFFBQVEsRUFBRTtJQUM1QyxPQUFPLEVBQUUsOEJBQThCLENBQUMsUUFBUSxFQUFFO0NBQ25ELENBQUM7S0FDRCxNQUFNLEVBQUUsQ0FBQyJ9
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHlwZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMva2V5cy9jb25maWcvdHlwZXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiJ9
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29udHJhY3RzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2tleXMvY29udHJhY3RzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiIifQ==
@@ -0,0 +1,61 @@
1
+ import { CustomError } from '@salamandradevs/tools/shared';
2
+ import { loadEnvVariables } from '@salamandradevs/tools/node';
3
+ import { OpenBaoKeysProvider } from './openbao/index.js';
4
+ import { JoseKeysProvider } from './jose/index.js';
5
+ import { signingKeyProviderSchema } from './config/schema.js';
6
+ import { signingKeyProviderSelectorSchema } from './config/schema.js';
7
+ /**
8
+ * Selects and constructs the keys provider named by `SIGNING_KEY_PROVIDER`.
9
+ *
10
+ * @remarks
11
+ * Construction only: each provider resolves its own environment as it is constructed but
12
+ * performs no input or output, so all network and file work is deferred to `initialize()`.
13
+ * The `switch` is the single extension point; a new provider is a new enum member, a new
14
+ * `case`, and its own configuration block, with no change to the bootstrap step or the
15
+ * services that consume the provider. Returns the owner of the key set as
16
+ * {@link IKeysProvider}; the composition root hands each consumer only the role it uses.
17
+ * @group Keys
18
+ * @category Key Providers
19
+ * @param config - Signing lifecycle and per-provider configuration.
20
+ * @returns The keys provider named by `SIGNING_KEY_PROVIDER`, ready for `initialize()`.
21
+ * @throws {CustomError} ENV_VAR_ERROR when `SIGNING_KEY_PROVIDER` is missing or not a
22
+ * recognized provider, or (for OpenBao) when the provider's own variables are missing.
23
+ * @throws {CustomError} CONFIG_ERROR when the selected provider has no configuration block.
24
+ */
25
+ export function createKeysProvider(config) {
26
+ const selector = loadEnvVariables(signingKeyProviderSelectorSchema);
27
+ // loadEnvVariables validates the value but widens it to `string`; re-read it through the
28
+ // enum to recover the literal type the exhaustive `switch` below relies on.
29
+ const provider = signingKeyProviderSchema.parse(selector.provider);
30
+ switch (provider) {
31
+ case 'openbao': {
32
+ if (!config.openbao) {
33
+ throw new CustomError({
34
+ name: 'CONFIG_ERROR',
35
+ message: 'SIGNING_KEY_PROVIDER is openbao but signingKeys.openbao is not configured'
36
+ });
37
+ }
38
+ // The provider reads its own environment from the variables its config names, so this
39
+ // factory composes it from configuration alone and never handles the origin or token.
40
+ return new OpenBaoKeysProvider(config.openbao, config.algorithm, config.openbao.refreshCooldownSecs);
41
+ }
42
+ case 'jose': {
43
+ if (!config.jose) {
44
+ throw new CustomError({
45
+ name: 'CONFIG_ERROR',
46
+ message: 'SIGNING_KEY_PROVIDER is jose but signingKeys.jose is not configured'
47
+ });
48
+ }
49
+ return new JoseKeysProvider(config.jose, config.algorithm);
50
+ }
51
+ default: {
52
+ // Exhaustiveness: adding a provider to the enum without a case here fails to compile.
53
+ const unknownProvider = provider;
54
+ throw new CustomError({
55
+ name: 'CONFIG_ERROR',
56
+ message: `Unhandled signing-key provider: ${String(unknownProvider)}`
57
+ });
58
+ }
59
+ }
60
+ }
61
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZmFjdG9yeS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9rZXlzL2ZhY3RvcnkudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFdBQVcsRUFBRSxNQUFNLDhCQUE4QixDQUFDO0FBQzNELE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLDRCQUE0QixDQUFDO0FBRTlELE9BQU8sRUFBRSxtQkFBbUIsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBQ3pELE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBQ25ELE9BQU8sRUFBRSx3QkFBd0IsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBQzlELE9BQU8sRUFBRSxnQ0FBZ0MsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBS3RFOzs7Ozs7Ozs7Ozs7Ozs7OztHQWlCRztBQUNILE1BQU0sVUFBVSxrQkFBa0IsQ0FBQyxNQUF5QjtJQUMxRCxNQUFNLFFBQVEsR0FBMkIsZ0JBQWdCLENBQ3ZELGdDQUFnQyxDQUNqQyxDQUFDO0lBRUYseUZBQXlGO0lBQ3pGLDRFQUE0RTtJQUM1RSxNQUFNLFFBQVEsR0FBdUIsd0JBQXdCLENBQUMsS0FBSyxDQUFDLFFBQVEsQ0FBQyxRQUFRLENBQUMsQ0FBQztJQUV2RixRQUFRLFFBQVEsRUFBRSxDQUFDO1FBQ2pCLEtBQUssU0FBUyxDQUFDLENBQUMsQ0FBQztZQUNmLElBQUksQ0FBQyxNQUFNLENBQUMsT0FBTyxFQUFFLENBQUM7Z0JBQ3BCLE1BQU0sSUFBSSxXQUFXLENBQUM7b0JBQ3BCLElBQUksRUFBRSxjQUFjO29CQUNwQixPQUFPLEVBQ0wsMkVBQTJFO2lCQUM5RSxDQUFDLENBQUM7WUFDTCxDQUFDO1lBRUQsc0ZBQXNGO1lBQ3RGLHNGQUFzRjtZQUN0RixPQUFPLElBQUksbUJBQW1CLENBQzVCLE1BQU0sQ0FBQyxPQUFPLEVBQ2QsTUFBTSxDQUFDLFNBQVMsRUFDaEIsTUFBTSxDQUFDLE9BQU8sQ0FBQyxtQkFBbUIsQ0FDbkMsQ0FBQztRQUNKLENBQUM7UUFFRCxLQUFLLE1BQU0sQ0FBQyxDQUFDLENBQUM7WUFDWixJQUFJLENBQUMsTUFBTSxDQUFDLElBQUksRUFBRSxDQUFDO2dCQUNqQixNQUFNLElBQUksV0FBVyxDQUFDO29CQUNwQixJQUFJLEVBQUUsY0FBYztvQkFDcEIsT0FBTyxFQUFFLHFFQUFxRTtpQkFDL0UsQ0FBQyxDQUFDO1lBQ0wsQ0FBQztZQUVELE9BQU8sSUFBSSxnQkFBZ0IsQ0FBQyxNQUFNLENBQUMsSUFBSSxFQUFFLE1BQU0sQ0FBQyxTQUFTLENBQUMsQ0FBQztRQUM3RCxDQUFDO1FBRUQsT0FBTyxDQUFDLENBQUMsQ0FBQztZQUNSLHNGQUFzRjtZQUN0RixNQUFNLGVBQWUsR0FBVSxRQUFRLENBQUM7WUFFeEMsTUFBTSxJQUFJLFdBQVcsQ0FBQztnQkFDcEIsSUFBSSxFQUFFLGNBQWM7Z0JBQ3BCLE9BQU8sRUFBRSxtQ0FBbUMsTUFBTSxDQUFDLGVBQWUsQ0FBQyxFQUFFO2FBQ3RFLENBQUMsQ0FBQztRQUNMLENBQUM7SUFDSCxDQUFDO0FBQ0gsQ0FBQyJ9
@@ -0,0 +1,10 @@
1
+ export * from './config/index.js';
2
+ export * from './contracts.js';
3
+ export * from './factory.js';
4
+ export * from './jose/index.js';
5
+ export * from './jwks/index.js';
6
+ export * from './openbao/index.js';
7
+ export * from './schema.js';
8
+ export * from './types.js';
9
+ export * from './validation.js';
10
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMva2V5cy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxjQUFjLG1CQUFtQixDQUFDO0FBQ2xDLGNBQWMsZ0JBQWdCLENBQUM7QUFDL0IsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyxpQkFBaUIsQ0FBQztBQUNoQyxjQUFjLGlCQUFpQixDQUFDO0FBQ2hDLGNBQWMsb0JBQW9CLENBQUM7QUFDbkMsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxZQUFZLENBQUM7QUFDM0IsY0FBYyxpQkFBaUIsQ0FBQyJ9
@@ -0,0 +1,3 @@
1
+ export * from './jose-keys-provider.js';
2
+ export * from './schema.js';
3
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMva2V5cy9qb3NlL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLGNBQWMseUJBQXlCLENBQUM7QUFDeEMsY0FBYyxhQUFhLENBQUMifQ==
@@ -0,0 +1,97 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { dirname } from 'node:path';
3
+ import { CompactEncrypt, compactDecrypt } from 'jose';
4
+ import { CustomError } from '@salamandradevs/tools/shared';
5
+ import { keyFilePayloadSchema } from './schema.js';
6
+ const encoder = new TextEncoder();
7
+ const decoder = new TextDecoder();
8
+ /** JWE key-management and content-encryption algorithms used for the key file. */
9
+ const KEY_MANAGEMENT_ALG = 'PBES2-HS256+A128KW';
10
+ const CONTENT_ENCRYPTION_ALG = 'A256GCM';
11
+ /**
12
+ * Reads and decrypts the encrypted key file, or reports its absence.
13
+ *
14
+ * @remarks
15
+ * A decrypt or parse failure is reported, never swallowed: a wrong passphrase must fail
16
+ * loudly rather than be mistaken for an absent file, so a typo cannot discard the keys.
17
+ * @group Keys
18
+ * @category Key Providers
19
+ * @param filePath - Path to the encrypted key file.
20
+ * @param secret - Passphrase that decrypts the compact JWE.
21
+ * @returns Validated key-file payload, or null when no file exists.
22
+ * @throws {CustomError} KEYS_PROVIDER_ERROR when the file exists but cannot be read,
23
+ * decrypted, or parsed into a valid key-file payload.
24
+ */
25
+ export async function readKeyFile(filePath, secret) {
26
+ let jwe;
27
+ try {
28
+ jwe = await readFile(filePath, 'utf8');
29
+ }
30
+ catch (error) {
31
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
32
+ return null;
33
+ }
34
+ throw new CustomError({
35
+ name: 'KEYS_PROVIDER_ERROR',
36
+ message: `Signing-key file at '${filePath}' could not be read`,
37
+ cause: error
38
+ });
39
+ }
40
+ try {
41
+ // `jose` requires PBES2 key management to be opted into on decrypt (an attacker-chosen
42
+ // iteration count is a DoS vector), so allow exactly the algorithms this module writes and
43
+ // reject anything else.
44
+ const { plaintext } = await compactDecrypt(jwe, encoder.encode(secret), {
45
+ keyManagementAlgorithms: [KEY_MANAGEMENT_ALG],
46
+ contentEncryptionAlgorithms: [CONTENT_ENCRYPTION_ALG]
47
+ });
48
+ const payload = JSON.parse(decoder.decode(plaintext));
49
+ return keyFilePayloadSchema.parse(payload);
50
+ }
51
+ catch (error) {
52
+ throw new CustomError({
53
+ name: 'KEYS_PROVIDER_ERROR',
54
+ message: `Signing-key file at '${filePath}' could not be decrypted or is invalid; ` +
55
+ `the passphrase may be wrong`,
56
+ cause: error
57
+ });
58
+ }
59
+ }
60
+ /**
61
+ * Encrypts and writes the key file, creating it only when none already exists.
62
+ *
63
+ * @remarks
64
+ * The `wx` flag makes the create exclusive and `0o600` restricts it to the owner, so two
65
+ * processes racing on one host resolve to one winner; the loser sees the file already
66
+ * exists, is told so by the false return, and reads the winner's key instead of minting a
67
+ * second.
68
+ * @group Keys
69
+ * @category Key Providers
70
+ * @param filePath - Path to the encrypted key file.
71
+ * @param secret - Passphrase that encrypts the compact JWE.
72
+ * @param payload - Key-file payload to persist.
73
+ * @returns True when this call created the file, false when one already existed.
74
+ * @throws {CustomError} KEYS_PROVIDER_ERROR when encryption or the write fails for any
75
+ * reason other than the file already existing.
76
+ */
77
+ export async function writeKeyFile(filePath, secret, payload) {
78
+ try {
79
+ const jwe = await new CompactEncrypt(encoder.encode(JSON.stringify(payload)))
80
+ .setProtectedHeader({ alg: KEY_MANAGEMENT_ALG, enc: CONTENT_ENCRYPTION_ALG })
81
+ .encrypt(encoder.encode(secret));
82
+ await mkdir(dirname(filePath), { recursive: true });
83
+ await writeFile(filePath, jwe, { mode: 0o600, flag: 'wx' });
84
+ return true;
85
+ }
86
+ catch (error) {
87
+ if (error instanceof Error && 'code' in error && error.code === 'EEXIST') {
88
+ return false;
89
+ }
90
+ throw new CustomError({
91
+ name: 'KEYS_PROVIDER_ERROR',
92
+ message: `Signing-key file at '${filePath}' could not be written`,
93
+ cause: error
94
+ });
95
+ }
96
+ }
97
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiam9zZS1rZXlmaWxlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2tleXMvam9zZS9qb3NlLWtleWZpbGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLEtBQUssRUFBRSxRQUFRLEVBQUUsU0FBUyxFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFDOUQsT0FBTyxFQUFFLE9BQU8sRUFBRSxNQUFNLFdBQVcsQ0FBQztBQUVwQyxPQUFPLEVBQUUsY0FBYyxFQUFFLGNBQWMsRUFBRSxNQUFNLE1BQU0sQ0FBQztBQUN0RCxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0sOEJBQThCLENBQUM7QUFFM0QsT0FBTyxFQUFFLG9CQUFvQixFQUFFLE1BQU0sYUFBYSxDQUFDO0FBR25ELE1BQU0sT0FBTyxHQUFnQixJQUFJLFdBQVcsRUFBRSxDQUFDO0FBQy9DLE1BQU0sT0FBTyxHQUFnQixJQUFJLFdBQVcsRUFBRSxDQUFDO0FBRS9DLGtGQUFrRjtBQUNsRixNQUFNLGtCQUFrQixHQUFXLG9CQUFvQixDQUFDO0FBQ3hELE1BQU0sc0JBQXNCLEdBQVcsU0FBUyxDQUFDO0FBRWpEOzs7Ozs7Ozs7Ozs7O0dBYUc7QUFDSCxNQUFNLENBQUMsS0FBSyxVQUFVLFdBQVcsQ0FDL0IsUUFBZ0IsRUFDaEIsTUFBYztJQUVkLElBQUksR0FBVyxDQUFDO0lBRWhCLElBQUksQ0FBQztRQUNILEdBQUcsR0FBRyxNQUFNLFFBQVEsQ0FBQyxRQUFRLEVBQUUsTUFBTSxDQUFDLENBQUM7SUFDekMsQ0FBQztJQUFDLE9BQU8sS0FBYyxFQUFFLENBQUM7UUFDeEIsSUFBSSxLQUFLLFlBQVksS0FBSyxJQUFJLE1BQU0sSUFBSSxLQUFLLElBQUksS0FBSyxDQUFDLElBQUksS0FBSyxRQUFRLEVBQUUsQ0FBQztZQUN6RSxPQUFPLElBQUksQ0FBQztRQUNkLENBQUM7UUFFRCxNQUFNLElBQUksV0FBVyxDQUFDO1lBQ3BCLElBQUksRUFBRSxxQkFBcUI7WUFDM0IsT0FBTyxFQUFFLHdCQUF3QixRQUFRLHFCQUFxQjtZQUM5RCxLQUFLLEVBQUUsS0FBSztTQUNiLENBQUMsQ0FBQztJQUNMLENBQUM7SUFFRCxJQUFJLENBQUM7UUFDSCx1RkFBdUY7UUFDdkYsMkZBQTJGO1FBQzNGLHdCQUF3QjtRQUN4QixNQUFNLEVBQUUsU0FBUyxFQUFFLEdBQUcsTUFBTSxjQUFjLENBQUMsR0FBRyxFQUFFLE9BQU8sQ0FBQyxNQUFNLENBQUMsTUFBTSxDQUFDLEVBQUU7WUFDdEUsdUJBQXVCLEVBQUUsQ0FBQyxrQkFBa0IsQ0FBQztZQUM3QywyQkFBMkIsRUFBRSxDQUFDLHNCQUFzQixDQUFDO1NBQ3RELENBQUMsQ0FBQztRQUNILE1BQU0sT0FBTyxHQUFZLElBQUksQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLE1BQU0sQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDO1FBRS9ELE9BQU8sb0JBQW9CLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxDQUFDO0lBQzdDLENBQUM7SUFBQyxPQUFPLEtBQWMsRUFBRSxDQUFDO1FBQ3hCLE1BQU0sSUFBSSxXQUFXLENBQUM7WUFDcEIsSUFBSSxFQUFFLHFCQUFxQjtZQUMzQixPQUFPLEVBQ0wsd0JBQXdCLFFBQVEsMENBQTBDO2dCQUMxRSw2QkFBNkI7WUFDL0IsS0FBSyxFQUFFLEtBQUs7U0FDYixDQUFDLENBQUM7SUFDTCxDQUFDO0FBQ0gsQ0FBQztBQUVEOzs7Ozs7Ozs7Ozs7Ozs7O0dBZ0JHO0FBQ0gsTUFBTSxDQUFDLEtBQUssVUFBVSxZQUFZLENBQ2hDLFFBQWdCLEVBQ2hCLE1BQWMsRUFDZCxPQUF1QjtJQUV2QixJQUFJLENBQUM7UUFDSCxNQUFNLEdBQUcsR0FBVyxNQUFNLElBQUksY0FBYyxDQUFDLE9BQU8sQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDO2FBQ2xGLGtCQUFrQixDQUFDLEVBQUUsR0FBRyxFQUFFLGtCQUFrQixFQUFFLEdBQUcsRUFBRSxzQkFBc0IsRUFBRSxDQUFDO2FBQzVFLE9BQU8sQ0FBQyxPQUFPLENBQUMsTUFBTSxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUM7UUFFbkMsTUFBTSxLQUFLLENBQUMsT0FBTyxDQUFDLFFBQVEsQ0FBQyxFQUFFLEVBQUUsU0FBUyxFQUFFLElBQUksRUFBRSxDQUFDLENBQUM7UUFDcEQsTUFBTSxTQUFTLENBQUMsUUFBUSxFQUFFLEdBQUcsRUFBRSxFQUFFLElBQUksRUFBRSxLQUFLLEVBQUUsSUFBSSxFQUFFLElBQUksRUFBRSxDQUFDLENBQUM7UUFFNUQsT0FBTyxJQUFJLENBQUM7SUFDZCxDQUFDO0lBQUMsT0FBTyxLQUFjLEVBQUUsQ0FBQztRQUN4QixJQUFJLEtBQUssWUFBWSxLQUFLLElBQUksTUFBTSxJQUFJLEtBQUssSUFBSSxLQUFLLENBQUMsSUFBSSxLQUFLLFFBQVEsRUFBRSxDQUFDO1lBQ3pFLE9BQU8sS0FBSyxDQUFDO1FBQ2YsQ0FBQztRQUVELE1BQU0sSUFBSSxXQUFXLENBQUM7WUFDcEIsSUFBSSxFQUFFLHFCQUFxQjtZQUMzQixPQUFPLEVBQUUsd0JBQXdCLFFBQVEsd0JBQXdCO1lBQ2pFLEtBQUssRUFBRSxLQUFLO1NBQ2IsQ0FBQyxDQUFDO0lBQ0wsQ0FBQztBQUNILENBQUMifQ==
@@ -0,0 +1,256 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { exportJWK, generateKeyPair, importJWK } from 'jose';
3
+ import { CustomError } from '@salamandradevs/tools/shared';
4
+ import { loadEnvVariables } from '@salamandradevs/tools/node';
5
+ import { readKeyFile, writeKeyFile } from './jose-keyfile.js';
6
+ import { joseStorageConfigSchema } from './schema.js';
7
+ import { validateSigningKeyState } from '../validation.js';
8
+ /**
9
+ * Node.js-native keys provider that keeps its key set in an encrypted local file.
10
+ *
11
+ * @remarks
12
+ * Generates its own ES256 key on first run and keeps the private material in an encrypted
13
+ * file keyed from the environment, then signs and verifies against that one key set. It
14
+ * fits a small deployment: a single host, or replicas sharing one key file, with the file
15
+ * on a persistent volume and the file and `JOSE_KEYFILE_PASSPHRASE` backed up separately.
16
+ * Compared with {@link OpenBaoKeysProvider}, it accepts that the private key lives in
17
+ * process memory, that replacing the key file reissues every credential it signed, and that
18
+ * no policy layer limits key use; OpenBao is the choice when those costs are not
19
+ * acceptable. It serves one active key and never rotates.
20
+ * @group Keys
21
+ * @category Key Providers
22
+ */
23
+ export class JoseKeysProvider {
24
+ config;
25
+ algorithm;
26
+ /** Built once in initialize(); no rotation means no refresh machinery. */
27
+ snapshot = null;
28
+ /**
29
+ * @param config - Validated Jose provider configuration.
30
+ * @param algorithm - JOSE algorithm enforced for the generated key.
31
+ */
32
+ constructor(config, algorithm) {
33
+ this.config = config;
34
+ this.algorithm = algorithm;
35
+ }
36
+ /**
37
+ * Loads the encrypted key file and publishes the first snapshot, generating a key on
38
+ * first run when the file is absent.
39
+ *
40
+ * @remarks
41
+ * Hard reset: delete the key file and restart. An absent file re-enters the
42
+ * generate-on-empty path and mints a fresh key, so there is no separate reset operation.
43
+ * The file is read once here, so deleting it while the process runs takes effect only on
44
+ * the next initialize(); the running provider keeps serving the key already in memory.
45
+ * This is a hard reset, not a rotation: the previous kid leaves the published key set at
46
+ * once and every credential it signed stops verifying. A decrypt failure fails loudly and
47
+ * is never treated as an empty file, so a wrong passphrase cannot discard the keys.
48
+ * @group Keys
49
+ * @category Key Providers
50
+ * @returns Completed provider initialization.
51
+ * @throws {CustomError} ENV_VAR_ERROR when the passphrase env variable is unset or blank.
52
+ * @throws {CustomError} KEYS_PROVIDER_ERROR when the file cannot be decrypted or violates
53
+ * a signing-key invariant.
54
+ */
55
+ async initialize() {
56
+ // The passphrase is sensitive, so it is read and validated from the environment; the config
57
+ // holds only a placeholder.
58
+ const secret = loadEnvVariables(joseStorageConfigSchema).passphrase;
59
+ const existing = await readKeyFile(this.config.storage.filePath, secret);
60
+ const payload = existing ?? (await this.createKeyFile(secret));
61
+ await this.publishSnapshot(payload);
62
+ }
63
+ /**
64
+ * Returns a signer bound to the active key.
65
+ *
66
+ * @group Keys
67
+ * @category Key Providers
68
+ * @returns Provider-neutral signing capability.
69
+ * @throws {CustomError} KEYS_PROVIDER_ERROR when initialize() has not completed.
70
+ */
71
+ getCurrentSigner() {
72
+ const { activeSigner } = this.currentSnapshot();
73
+ return Promise.resolve({
74
+ kid: activeSigner.kid,
75
+ algorithm: this.algorithm,
76
+ // Web Crypto ECDSA P-256 with SHA-256 returns the raw 64-byte r||s pair, identical to
77
+ // the OpenBao Transit path and to what the JWS signing input expects.
78
+ sign: async (input) => {
79
+ // Copy into a fresh ArrayBuffer-backed view so the Web Crypto BufferSource type is
80
+ // satisfied whatever buffer backs the caller's input.
81
+ const signature = await crypto.subtle.sign({ name: 'ECDSA', hash: 'SHA-256' }, activeSigner.privateKey, new Uint8Array(input));
82
+ return new Uint8Array(signature);
83
+ }
84
+ });
85
+ }
86
+ /**
87
+ * Resolves one eligible public key by exact key identifier.
88
+ *
89
+ * @group Keys
90
+ * @category Key Providers
91
+ * @param kid - Key identifier carried with the signature.
92
+ * @returns Public verification key, or null for an unknown key.
93
+ * @throws {CustomError} KEYS_PROVIDER_ERROR when initialize() has not completed.
94
+ */
95
+ getPublicKey(kid) {
96
+ return Promise.resolve(this.currentSnapshot().byKid.get(kid) ?? null);
97
+ }
98
+ /**
99
+ * Lists the public keys eligible for JWKS publication.
100
+ *
101
+ * @group Keys
102
+ * @category Key Providers
103
+ * @returns Retained public JSON Web Keys.
104
+ * @throws {CustomError} KEYS_PROVIDER_ERROR when initialize() has not completed.
105
+ */
106
+ listPublicKeys() {
107
+ return Promise.resolve(this.currentSnapshot().keys);
108
+ }
109
+ /**
110
+ * Generates the first key and writes it, adopting an existing file when a concurrent
111
+ * start won the race.
112
+ *
113
+ * @param secret - Passphrase that encrypts the key file.
114
+ * @returns The persisted key-file payload.
115
+ * @throws {CustomError} KEYS_PROVIDER_ERROR when the file cannot be written or re-read.
116
+ */
117
+ async createKeyFile(secret) {
118
+ const payload = await this.generatePayload();
119
+ const created = await writeKeyFile(this.config.storage.filePath, secret, payload);
120
+ if (created) {
121
+ return payload;
122
+ }
123
+ // Another process created the file first; adopt its key rather than minting a second.
124
+ const existing = await readKeyFile(this.config.storage.filePath, secret);
125
+ if (!existing) {
126
+ throw new CustomError({
127
+ name: 'KEYS_PROVIDER_ERROR',
128
+ message: `Signing-key file at '${this.config.storage.filePath}' vanished after creation`
129
+ });
130
+ }
131
+ return existing;
132
+ }
133
+ /**
134
+ * Generates one active ES256 key and the key-file payload that persists it.
135
+ *
136
+ * @returns Key-file payload with a single active key and its private material.
137
+ * @throws {CustomError} KEYS_PROVIDER_ERROR when the generated key is missing JWK
138
+ * coordinates.
139
+ */
140
+ async generatePayload() {
141
+ const { publicKey, privateKey } = await generateKeyPair(this.algorithm, {
142
+ extractable: true
143
+ });
144
+ const publicJwkRaw = await exportJWK(publicKey);
145
+ const privateJwkRaw = await exportJWK(privateKey);
146
+ if (!publicJwkRaw.x || !publicJwkRaw.y || !privateJwkRaw.d) {
147
+ throw new CustomError({
148
+ name: 'KEYS_PROVIDER_ERROR',
149
+ message: 'Generated EC key is missing required JWK coordinates'
150
+ });
151
+ }
152
+ const kid = `${this.config.keyNamePrefix}-${randomUUID()}`;
153
+ const now = new Date().toISOString();
154
+ const publicJwk = {
155
+ kty: 'EC',
156
+ kid,
157
+ use: 'sig',
158
+ alg: this.algorithm,
159
+ crv: 'P-256',
160
+ x: publicJwkRaw.x,
161
+ y: publicJwkRaw.y
162
+ };
163
+ const privateJwk = {
164
+ kty: 'EC',
165
+ crv: 'P-256',
166
+ x: publicJwkRaw.x,
167
+ y: publicJwkRaw.y,
168
+ d: privateJwkRaw.d
169
+ };
170
+ // Build against KeyFilePayload directly: the lifecycle types are read-only projections,
171
+ // while the persisted shape is the mutable schema-inferred one.
172
+ return {
173
+ state: {
174
+ currentKid: kid,
175
+ keys: [
176
+ {
177
+ kid,
178
+ providerKeyId: kid,
179
+ status: 'active',
180
+ publicJwk,
181
+ activateAfter: now,
182
+ activatedAt: now,
183
+ retiredAt: null,
184
+ verifyUntil: null
185
+ }
186
+ ]
187
+ },
188
+ privateJwks: { [kid]: privateJwk }
189
+ };
190
+ }
191
+ /**
192
+ * Validates the payload and publishes the in-process snapshot.
193
+ *
194
+ * @param payload - Key-file payload to project.
195
+ * @returns Completed snapshot publication.
196
+ * @throws {CustomError} KEYS_PROVIDER_ERROR when the state is invalid or a JWK cannot be
197
+ * imported as an asymmetric key.
198
+ */
199
+ async publishSnapshot(payload) {
200
+ const state = validateSigningKeyState(payload.state, this.algorithm);
201
+ const keys = [];
202
+ const byKid = new Map();
203
+ for (const entry of state.keys) {
204
+ const imported = await importJWK(entry.publicJwk, this.algorithm);
205
+ // `importJWK` returns bytes for symmetric keys; a published JWK must be asymmetric.
206
+ if (imported instanceof Uint8Array) {
207
+ throw new CustomError({
208
+ name: 'KEYS_PROVIDER_ERROR',
209
+ message: 'Signing-key state contains a non-public JWK'
210
+ });
211
+ }
212
+ keys.push(entry.publicJwk);
213
+ byKid.set(entry.kid, imported);
214
+ }
215
+ const activeKid = state.currentKid;
216
+ if (activeKid === null || !(activeKid in payload.privateJwks)) {
217
+ throw new CustomError({
218
+ name: 'KEYS_PROVIDER_ERROR',
219
+ message: 'Signing-key file has no private key for the active key'
220
+ });
221
+ }
222
+ const privateJwk = payload.privateJwks[activeKid];
223
+ const privateKey = await importJWK(privateJwk, this.algorithm);
224
+ if (privateKey instanceof Uint8Array) {
225
+ throw new CustomError({
226
+ name: 'KEYS_PROVIDER_ERROR',
227
+ message: 'Signing-key file contains a non-EC private JWK'
228
+ });
229
+ }
230
+ // Single assignment, so readers move from no snapshot to one complete snapshot.
231
+ this.snapshot = {
232
+ state,
233
+ keys,
234
+ byKid,
235
+ activeSigner: { kid: activeKid, privateKey }
236
+ };
237
+ }
238
+ /**
239
+ * Returns the snapshot published by initialize().
240
+ *
241
+ * @returns The in-process projection of the signing-key state.
242
+ * @throws {CustomError} KEYS_PROVIDER_ERROR when initialize() has not completed.
243
+ */
244
+ currentSnapshot() {
245
+ // Built once in initialize() and never refreshed, so a missing snapshot means the provider
246
+ // was used before initialize() completed.
247
+ if (!this.snapshot) {
248
+ throw new CustomError({
249
+ name: 'KEYS_PROVIDER_ERROR',
250
+ message: 'Signing-key snapshot is unavailable; initialize() has not completed'
251
+ });
252
+ }
253
+ return this.snapshot;
254
+ }
255
+ }
256
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiam9zZS1rZXlzLXByb3ZpZGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2tleXMvam9zZS9qb3NlLWtleXMtcHJvdmlkZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLGFBQWEsQ0FBQztBQUV6QyxPQUFPLEVBQUUsU0FBUyxFQUFFLGVBQWUsRUFBRSxTQUFTLEVBQUUsTUFBTSxNQUFNLENBQUM7QUFDN0QsT0FBTyxFQUFFLFdBQVcsRUFBRSxNQUFNLDhCQUE4QixDQUFDO0FBQzNELE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLDRCQUE0QixDQUFDO0FBRzlELE9BQU8sRUFBRSxXQUFXLEVBQUUsWUFBWSxFQUFFLE1BQU0sbUJBQW1CLENBQUM7QUFDOUQsT0FBTyxFQUFFLHVCQUF1QixFQUFFLE1BQU0sYUFBYSxDQUFDO0FBQ3RELE9BQU8sRUFBRSx1QkFBdUIsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBUTNEOzs7Ozs7Ozs7Ozs7OztHQWNHO0FBQ0gsTUFBTSxPQUFPLGdCQUFnQjtJQVNSO0lBQ0E7SUFUbkIsMEVBQTBFO0lBQ2xFLFFBQVEsR0FBOEIsSUFBSSxDQUFDO0lBRW5EOzs7T0FHRztJQUNILFlBQ21CLE1BQTZCLEVBQzdCLFNBQWtCO1FBRGxCLFdBQU0sR0FBTixNQUFNLENBQXVCO1FBQzdCLGNBQVMsR0FBVCxTQUFTLENBQVM7SUFDbEMsQ0FBQztJQUVKOzs7Ozs7Ozs7Ozs7Ozs7Ozs7T0FrQkc7SUFDSCxLQUFLLENBQUMsVUFBVTtRQUNkLDRGQUE0RjtRQUM1Riw0QkFBNEI7UUFDNUIsTUFBTSxNQUFNLEdBQVcsZ0JBQWdCLENBQUMsdUJBQXVCLENBQUMsQ0FBQyxVQUFVLENBQUM7UUFFNUUsTUFBTSxRQUFRLEdBQTBCLE1BQU0sV0FBVyxDQUN2RCxJQUFJLENBQUMsTUFBTSxDQUFDLE9BQU8sQ0FBQyxRQUFRLEVBQzVCLE1BQU0sQ0FDUCxDQUFD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
@@ -0,0 +1,68 @@
1
+ import { z } from 'zod';
2
+ import { signingKeyStateSchema } from '../schema.js';
3
+ /**
4
+ * Jose key-file storage schema.
5
+ *
6
+ * - FilePath: Path to the encrypted key file; must be listed in `.gitignore`.
7
+ * - Passphrase: Encrypted key-file passphrase; a secret, read from `JOSE_KEYFILE_PASSPHRASE`.
8
+ *
9
+ * @remarks
10
+ * `passphrase` is marked `.meta({ env })`, so `loadEnvVariables` resolves it from
11
+ * `JOSE_KEYFILE_PASSPHRASE`; the configuration file holds only a placeholder.
12
+ */
13
+ export const joseStorageConfigSchema = z
14
+ .object({
15
+ filePath: z.string().min(1),
16
+ passphrase: z
17
+ .string({ error: 'is required' })
18
+ .min(1, 'must not be empty')
19
+ .meta({ env: 'JOSE_KEYFILE_PASSPHRASE' })
20
+ })
21
+ .strict();
22
+ /**
23
+ * Jose provider configuration and local-storage schema.
24
+ *
25
+ * - KeyNamePrefix: Prefix for the generated `kid` and provider key identifier.
26
+ * - Storage: Encrypted key-file location and passphrase source.
27
+ *
28
+ * @remarks
29
+ * Self-contained so the provider directory owns its whole storage story without the factory
30
+ * ever handling a file path or a secret.
31
+ */
32
+ export const joseSigningKeysConfigSchema = z
33
+ .object({
34
+ keyNamePrefix: z.string().min(1),
35
+ storage: joseStorageConfigSchema
36
+ })
37
+ .strict();
38
+ /**
39
+ * EC P-256 private JWK persisted in the encrypted key file.
40
+ *
41
+ * - Kty: Key type, always `EC`.
42
+ * - Crv: Curve, always `P-256`.
43
+ * - X: Public x coordinate.
44
+ * - Y: Public y coordinate.
45
+ * - D: Private scalar; the secret half, which is why the file is encrypted at rest.
46
+ */
47
+ export const privateJwkSchema = z
48
+ .object({
49
+ kty: z.literal('EC'),
50
+ crv: z.literal('P-256'),
51
+ x: z.string().min(1),
52
+ y: z.string().min(1),
53
+ d: z.string().min(1)
54
+ })
55
+ .strict();
56
+ /**
57
+ * Decrypted key-file payload.
58
+ *
59
+ * - State: Signing-key lifecycle document shared with every provider.
60
+ * - PrivateJwks: Private signing material indexed by `kid`.
61
+ */
62
+ export const keyFilePayloadSchema = z
63
+ .object({
64
+ state: signingKeyStateSchema,
65
+ privateJwks: z.record(z.string().min(1), privateJwkSchema)
66
+ })
67
+ .strict();
68
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2NoZW1hLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2tleXMvam9zZS9zY2hlbWEudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLENBQUMsRUFBRSxNQUFNLEtBQUssQ0FBQztBQUV4QixPQUFPLEVBQUUscUJBQXFCLEVBQUUsTUFBTSxjQUFjLENBQUM7QUFFckQ7Ozs7Ozs7OztHQVNHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sdUJBQXVCLEdBQUcsQ0FBQztLQUNyQyxNQUFNLENBQUM7SUFDTixRQUFRLEVBQUUsQ0FBQyxDQUFDLE1BQU0sRUFBRSxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUM7SUFDM0IsVUFBVSxFQUFFLENBQUM7U0FDVixNQUFNLENBQUMsRUFBRSxLQUFLLEVBQUUsYUFBYSxFQUFFLENBQUM7U0FDaEMsR0FBRyxDQUFDLENBQUMsRUFBRSxtQkFBbUIsQ0FBQztTQUMzQixJQUFJLENBQUMsRUFBRSxHQUFHLEVBQUUseUJBQXlCLEVBQUUsQ0FBQztDQUM1QyxDQUFDO0tBQ0QsTUFBTSxFQUFFLENBQUM7QUFFWjs7Ozs7Ozs7O0dBU0c7QUFDSCxNQUFNLENBQUMsTUFBTSwyQkFBMkIsR0FBRyxDQUFDO0tBQ3pDLE1BQU0sQ0FBQztJQUNOLGFBQWEsRUFBRSxDQUFDLENBQUMsTUFBTSxFQUFFLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQztJQUNoQyxPQUFPLEVBQUUsdUJBQXVCO0NBQ2pDLENBQUM7S0FDRCxNQUFNLEVBQUUsQ0FBQztBQUVaOzs7Ozs7OztHQVFHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sZ0JBQWdCLEdBQUcsQ0FBQztLQUM5QixNQUFNLENBQUM7SUFDTixHQUFHLEVBQUUsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUM7SUFDcEIsR0FBRyxFQUFFLENBQUMsQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDO0lBQ3ZCLENBQUMsRUFBRSxDQUFDLENBQUMsTUFBTSxFQUFFLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQztJQUNwQixDQUFDLEVBQUUsQ0FBQyxDQUFDLE1BQU0sRUFBRSxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUM7SUFDcEIsQ0FBQyxFQUFFLENBQUMsQ0FBQyxNQUFNLEVBQUUsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDO0NBQ3JCLENBQUM7S0FDRCxNQUFNLEVBQUUsQ0FBQztBQUVaOzs7OztHQUtHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sb0JBQW9CLEdBQUcsQ0FBQztLQUNsQyxNQUFNLENBQUM7SUFDTixLQUFLLEVBQUUscUJBQXFCO0lBQzVCLFdBQVcsRUFBRSxDQUFDLENBQUMsTUFBTSxDQUFDLENBQUMsQ0FBQyxNQUFNLEVBQUUsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLEVBQUUsZ0JBQWdCLENBQUM7Q0FDM0QsQ0FBQztLQUNELE1BQU0sRUFBRSxDQUFDIn0=
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHlwZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMva2V5cy9qb3NlL3R5cGVzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiIifQ==
@@ -0,0 +1,2 @@
1
+ export * from './jwks-verification-key-provider.js';
2
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMva2V5cy9qd2tzL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLGNBQWMscUNBQXFDLENBQUMifQ==