@salamandradevs/core 0.1.3 → 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.
- package/README.md +4 -2
- package/dist/index.js +4 -2
- package/dist/infra/database/database-provider.js +3 -1
- package/dist/infra/database/mongo/mongo-db-connection.js +14 -2
- package/dist/infra/database/mysql/mysql-db-connection.js +16 -2
- package/dist/keys/config/index.js +3 -0
- package/dist/keys/config/schema.js +51 -0
- package/dist/keys/config/types.js +2 -0
- package/dist/keys/contracts.js +2 -0
- package/dist/keys/factory.js +61 -0
- package/dist/keys/index.js +10 -0
- package/dist/keys/jose/index.js +3 -0
- package/dist/keys/jose/jose-keyfile.js +97 -0
- package/dist/keys/jose/jose-keys-provider.js +256 -0
- package/dist/keys/jose/schema.js +68 -0
- package/dist/keys/jose/types.js +2 -0
- package/dist/keys/jwks/index.js +2 -0
- package/dist/keys/jwks/jwks-verification-key-provider.js +43 -0
- package/dist/keys/openbao/index.js +3 -0
- package/dist/keys/openbao/openbao-keys-provider.js +461 -0
- package/dist/keys/openbao/schema.js +67 -0
- package/dist/keys/openbao/types.js +2 -0
- package/dist/keys/schema.js +65 -0
- package/dist/keys/types.js +2 -0
- package/dist/keys/validation.js +100 -0
- package/dist/runtime/context.js +42 -0
- package/dist/runtime/index.js +2 -0
- package/dist/storage/records-handler.js +2 -2
- package/dist/tokens/access-token-verifier.js +75 -0
- package/dist/tokens/contracts.js +2 -0
- package/dist/tokens/index.js +5 -0
- package/dist/tokens/schema.js +36 -0
- package/dist/tokens/types.js +2 -0
- package/package.json +2 -1
- package/types/index.d.ts +3 -1
- package/types/index.d.ts.map +1 -1
- package/types/infra/database/contracts.d.ts +9 -1
- package/types/infra/database/contracts.d.ts.map +1 -1
- package/types/infra/database/database-provider.d.ts +2 -0
- package/types/infra/database/database-provider.d.ts.map +1 -1
- package/types/infra/database/mongo/mongo-db-connection.d.ts +2 -0
- package/types/infra/database/mongo/mongo-db-connection.d.ts.map +1 -1
- package/types/infra/database/mysql/mysql-db-connection.d.ts +2 -0
- package/types/infra/database/mysql/mysql-db-connection.d.ts.map +1 -1
- package/types/keys/config/index.d.ts +3 -0
- package/types/keys/config/index.d.ts.map +1 -0
- package/types/keys/config/schema.d.ts +66 -0
- package/types/keys/config/schema.d.ts.map +1 -0
- package/types/keys/config/types.d.ts +8 -0
- package/types/keys/config/types.d.ts.map +1 -0
- package/types/keys/contracts.d.ts +52 -0
- package/types/keys/contracts.d.ts.map +1 -0
- package/types/keys/factory.d.ts +22 -0
- package/types/keys/factory.d.ts.map +1 -0
- package/types/keys/index.d.ts +10 -0
- package/types/keys/index.d.ts.map +1 -0
- package/types/keys/jose/index.d.ts +3 -0
- package/types/keys/jose/index.d.ts.map +1 -0
- package/types/keys/jose/jose-keyfile.d.ts +35 -0
- package/types/keys/jose/jose-keyfile.d.ts.map +1 -0
- package/types/keys/jose/jose-keys-provider.d.ts +112 -0
- package/types/keys/jose/jose-keys-provider.d.ts.map +1 -0
- package/types/keys/jose/schema.d.ts +91 -0
- package/types/keys/jose/schema.d.ts.map +1 -0
- package/types/keys/jose/types.d.ts +35 -0
- package/types/keys/jose/types.d.ts.map +1 -0
- package/types/keys/jwks/index.d.ts +2 -0
- package/types/keys/jwks/index.d.ts.map +1 -0
- package/types/keys/jwks/jwks-verification-key-provider.d.ts +27 -0
- package/types/keys/jwks/jwks-verification-key-provider.d.ts.map +1 -0
- package/types/keys/openbao/index.d.ts +3 -0
- package/types/keys/openbao/index.d.ts.map +1 -0
- package/types/keys/openbao/openbao-keys-provider.d.ts +175 -0
- package/types/keys/openbao/openbao-keys-provider.d.ts.map +1 -0
- package/types/keys/openbao/schema.d.ts +68 -0
- package/types/keys/openbao/schema.d.ts.map +1 -0
- package/types/keys/openbao/types.d.ts +52 -0
- package/types/keys/openbao/types.d.ts.map +1 -0
- package/types/keys/schema.d.ts +73 -0
- package/types/keys/schema.d.ts.map +1 -0
- package/types/keys/types.d.ts +53 -0
- package/types/keys/types.d.ts.map +1 -0
- package/types/keys/validation.d.ts +19 -0
- package/types/keys/validation.d.ts.map +1 -0
- package/types/runtime/context.d.ts +67 -0
- package/types/runtime/context.d.ts.map +1 -0
- package/types/runtime/index.d.ts +2 -0
- package/types/runtime/index.d.ts.map +1 -0
- package/types/tokens/access-token-verifier.d.ts +36 -0
- package/types/tokens/access-token-verifier.d.ts.map +1 -0
- package/types/tokens/contracts.d.ts +21 -0
- package/types/tokens/contracts.d.ts.map +1 -0
- package/types/tokens/index.d.ts +5 -0
- package/types/tokens/index.d.ts.map +1 -0
- package/types/tokens/schema.d.ts +34 -0
- package/types/tokens/schema.d.ts.map +1 -0
- package/types/tokens/types.d.ts +20 -0
- 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/
|
|
26
|
-
|
|
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 './
|
|
4
|
-
|
|
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=
|
|
@@ -55,6 +55,8 @@ export class DatabaseProvider {
|
|
|
55
55
|
* without a matching `case` fails the `never` exhaustiveness check. Calling it again
|
|
56
56
|
* reconnects and replaces the cached handle.
|
|
57
57
|
* @returns The connected database handle.
|
|
58
|
+
* @throws {CustomError} DB_BACKEND_ERROR when the server is unreachable or rejects the
|
|
59
|
+
* connection; propagated from the backend connection.
|
|
58
60
|
*/
|
|
59
61
|
async connect() {
|
|
60
62
|
switch (this.connection.backend) {
|
|
@@ -101,4 +103,4 @@ export class DatabaseProvider {
|
|
|
101
103
|
await this.connection.close();
|
|
102
104
|
}
|
|
103
105
|
}
|
|
104
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
106
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGF0YWJhc2UtcHJvdmlkZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvaW5mcmEvZGF0YWJhc2UvZGF0YWJhc2UtcHJvdmlkZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFdBQVcsRUFBRSxNQUFNLDhCQUE4QixDQUFDO0FBRTNELE9BQU8sRUFBRSxpQkFBaUIsRUFBRSxNQUFNLGdDQUFnQyxDQUFDO0FBQ25FLE9BQU8sRUFBRSxpQkFBaUIsRUFBRSxNQUFNLGdDQUFnQyxDQUFDO0FBSW5FOzs7Ozs7Ozs7O0dBVUc7QUFDSCxNQUFNLE9BQU8sZ0JBQWdCO0lBYVQ7SUFaRCxVQUFVLENBQXdDO0lBQzNELE1BQU0sR0FBMEIsSUFBSSxDQUFDO0lBRTdDOzs7Ozs7O09BT0c7SUFDSCxZQUNrQixPQUF3QixFQUN4QyxHQUFXLEVBQ1gsWUFBb0I7UUFGSixZQUFPLEdBQVAsT0FBTyxDQUFpQjtRQUl4QyxtRkFBbUY7UUFDbkYsOEVBQThFO1FBQzlFLFFBQVEsT0FBTyxFQUFFLENBQUM7WUFDaEIsS0FBSyxPQUFPLENBQUMsQ0FBQyxDQUFDO2dCQUNiLElBQUksQ0FBQyxVQUFVLEdBQUcsSUFBSSxpQkFBaUIsQ0FBQyxHQUFHLEVBQUUsWUFBWSxDQUFDLENBQUM7Z0JBRTNELE1BQU07WUFDUixDQUFDO1lBRUQsS0FBSyxTQUFTLENBQUMsQ0FBQyxDQUFDO2dCQUNmLElBQUksQ0FBQyxVQUFVLEdBQUcsSUFBSSxpQkFBaUIsQ0FBQyxHQUFHLEVBQUUsWUFBWSxDQUFDLENBQUM7Z0JBRTNELE1BQU07WUFDUixDQUFDO1lBRUQsT0FBTyxDQUFDLENBQUMsQ0FBQztnQkFDUixNQUFNLFdBQVcsR0FBVSxPQUFPLENBQUM7Z0JBRW5DLE1BQU0sSUFBSSxXQUFXLENBQUM7b0JBQ3BCLElBQUksRUFBRSxrQkFBa0I7b0JBQ3hCLE9BQU8sRUFBRSxpQ0FBaUMsTUFBTSxDQUFDLFdBQVcsQ0FBQyxFQUFFO2lCQUNoRSxDQUFDLENBQUM7WUFDTCxDQUFDO1FBQ0gsQ0FBQztJQUNILENBQUM7SUFFRDs7Ozs7Ozs7Ozs7T0FXRztJQUNILEtBQUssQ0FBQyxPQUFPO1FBQ1gsUUFBUSxJQUFJLENBQUMsVUFBVSxDQUFDLE9BQU8sRUFBRSxDQUFDO1lBQ2hDLEtBQUssT0FBTyxDQUFDLENBQUMsQ0FBQztnQkFDYixJQUFJLENBQUMsTUFBTSxHQUFHLEVBQUUsT0FBTyxFQUFFLE9BQU8sRUFBRSxJQUFJLEVBQUUsTUFBTSxJQUFJLENBQUMsVUFBVSxDQUFDLE9BQU8sRUFBRSxFQUFFLENBQUM7Z0JBRTFFLE1BQU07WUFDUixDQUFDO1lBRUQsS0FBSyxTQUFTLENBQUMsQ0FBQyxDQUFDO2dCQUNmLElBQUksQ0FBQyxNQUFNLEdBQUcsRUFBRSxPQUFPLEVBQUUsU0FBUyxFQUFFLEVBQUUsRUFBRSxNQUFNLElBQUksQ0FBQyxVQUFVLENBQUMsT0FBTyxFQUFFLEVBQUUsQ0FBQztnQkFFMUUsTUFBTTtZQUNSLENBQUM7WUFFRCxPQUFPLENBQUMsQ0FBQyxDQUFDO2dCQUNSLE1BQU0sV0FBVyxHQUFVLElBQUksQ0FBQyxVQUFVLENBQUM7Z0JBRTNDLE1BQU0sSUFBSSxXQUFXLENBQUM7b0JBQ3BCLElBQUksRUFBRSxrQkFBa0I7b0JBQ3hCLE9BQU8sRUFBRSxpQ0FBaUMsTUFBTSxDQUFDLFdBQVcsQ0FBQyxFQUFFO2lCQUNoRSxDQUFDLENBQUM7WUFDTCxDQUFDO1FBQ0gsQ0FBQztRQUVELE9BQU8sSUFBSSxDQUFDLE1BQU0sQ0FBQztJQUNyQixDQUFDO0lBRUQ7Ozs7OztPQU1HO0lBQ0gsU0FBUztRQUNQLElBQUksQ0FBQyxJQUFJLENBQUMsTUFBTSxFQUFFLENBQUM7WUFDakIsTUFBTSxJQUFJLFdBQVcsQ0FBQztnQkFDcEIsSUFBSSxFQUFFLGtCQUFrQjtnQkFDeEIsT0FBTyxFQUFFLGtEQUFrRDthQUM1RCxDQUFDLENBQUM7UUFDTCxDQUFDO1FBRUQsT0FBTyxJQUFJLENBQUMsTUFBTSxDQUFDO0lBQ3JCLENBQUM7SUFFRDs7OztPQUlHO0lBQ0gsS0FBSyxDQUFDLEtBQUs7UUFDVCxNQUFNLElBQUksQ0FBQyxVQUFVLENBQUMsS0FBSyxFQUFFLENBQUM7SUFDaEMsQ0FBQztDQUNGIn0=
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MongoClient } from 'mongodb';
|
|
2
|
+
import { CustomError } from '@salamandradevs/tools/shared';
|
|
2
3
|
/**
|
|
3
4
|
* MongoDB connection adapter for application bootstrap.
|
|
4
5
|
*
|
|
@@ -30,9 +31,20 @@ export class MongoDbConnection {
|
|
|
30
31
|
* Opens the Mongo connection and returns the database handle.
|
|
31
32
|
*
|
|
32
33
|
* @returns Connected Mongo database handle.
|
|
34
|
+
* @throws {CustomError} DB_BACKEND_ERROR when the server is unreachable or rejects the
|
|
35
|
+
* connection; the driver error is kept as the cause.
|
|
33
36
|
*/
|
|
34
37
|
async connect() {
|
|
35
|
-
|
|
38
|
+
try {
|
|
39
|
+
await this.client.connect();
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
throw new CustomError({
|
|
43
|
+
name: 'DB_BACKEND_ERROR',
|
|
44
|
+
message: 'MongoDB connection failed',
|
|
45
|
+
cause: error
|
|
46
|
+
});
|
|
47
|
+
}
|
|
36
48
|
return this.client.db(this.dbName);
|
|
37
49
|
}
|
|
38
50
|
/**
|
|
@@ -44,4 +56,4 @@ export class MongoDbConnection {
|
|
|
44
56
|
await this.client.close();
|
|
45
57
|
}
|
|
46
58
|
}
|
|
47
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
59
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibW9uZ28tZGItY29ubmVjdGlvbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3NyYy9pbmZyYS9kYXRhYmFzZS9tb25nby9tb25nby1kYi1jb25uZWN0aW9uLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxXQUFXLEVBQVcsTUFBTSxTQUFTLENBQUM7QUFDL0MsT0FBTyxFQUFFLFdBQVcsRUFBRSxNQUFNLDhCQUE4QixDQUFDO0FBSTNEOzs7Ozs7Ozs7R0FTRztBQUNILE1BQU0sT0FBTyxpQkFBaUI7SUFhVDtJQUNBO0lBYm5CLDRFQUE0RTtJQUNuRSxPQUFPLEdBQUcsU0FBa0IsQ0FBQztJQUVyQixNQUFNLENBQWM7SUFFckM7Ozs7O09BS0c7SUFDSCxZQUNtQixHQUFXLEVBQ1gsTUFBYztRQURkLFFBQUcsR0FBSCxHQUFHLENBQVE7UUFDWCxXQUFNLEdBQU4sTUFBTSxDQUFRO1FBRS9CLElBQUksQ0FBQyxNQUFNLEdBQUcsSUFBSSxXQUFXLENBQUMsR0FBRyxDQUFDLENBQUM7SUFDckMsQ0FBQztJQUVEOzs7Ozs7T0FNRztJQUNILEtBQUssQ0FBQyxPQUFPO1FBQ1gsSUFBSSxDQUFDO1lBQ0gsTUFBTSxJQUFJLENBQUMsTUFBTSxDQUFDLE9BQU8sRUFBRSxDQUFDO1FBQzlCLENBQUM7UUFBQyxPQUFPLEtBQWMsRUFBRSxDQUFDO1lBQ3hCLE1BQU0sSUFBSSxXQUFXLENBQUM7Z0JBQ3BCLElBQUksRUFBRSxrQkFBa0I7Z0JBQ3hCLE9BQU8sRUFBRSwyQkFBMkI7Z0JBQ3BDLEtBQUssRUFBRSxLQUFLO2FBQ2IsQ0FBQyxDQUFDO1FBQ0wsQ0FBQztRQUVELE9BQU8sSUFBSSxDQUFDLE1BQU0sQ0FBQyxFQUFFLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxDQUFDO0lBQ3JDLENBQUM7SUFFRDs7OztPQUlHO0lBQ0gsS0FBSyxDQUFDLEtBQUs7UUFDVCxNQUFNLElBQUksQ0FBQyxNQUFNLENBQUMsS0FBSyxFQUFFLENBQUM7SUFDNUIsQ0FBQztDQUNGIn0=
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createPool } from 'mysql2/promise';
|
|
2
|
+
import { CustomError } from '@salamandradevs/tools/shared';
|
|
2
3
|
/**
|
|
3
4
|
* MySQL connection adapter for application bootstrap.
|
|
4
5
|
*
|
|
@@ -29,13 +30,26 @@ export class MySqlDbConnection {
|
|
|
29
30
|
* Opens the MySQL connection and returns the backend handle.
|
|
30
31
|
*
|
|
31
32
|
* @returns Connected MySQL backend handle.
|
|
33
|
+
* @throws {CustomError} DB_BACKEND_ERROR when the server is unreachable or rejects the
|
|
34
|
+
* connection; the driver error is kept as the cause.
|
|
32
35
|
*/
|
|
33
36
|
async connect() {
|
|
34
37
|
this.pool = createPool({
|
|
35
38
|
uri: this.uri,
|
|
36
39
|
database: this.databaseName
|
|
37
40
|
});
|
|
38
|
-
|
|
41
|
+
// `createPool` opens nothing; the probe query is the first network call, so it is the
|
|
42
|
+
// point where an unreachable server or a rejected login surfaces.
|
|
43
|
+
try {
|
|
44
|
+
await this.pool.query('SELECT 1');
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
throw new CustomError({
|
|
48
|
+
name: 'DB_BACKEND_ERROR',
|
|
49
|
+
message: 'MySQL connection failed',
|
|
50
|
+
cause: error
|
|
51
|
+
});
|
|
52
|
+
}
|
|
39
53
|
return this.pool;
|
|
40
54
|
}
|
|
41
55
|
/**
|
|
@@ -51,4 +65,4 @@ export class MySqlDbConnection {
|
|
|
51
65
|
this.pool = null;
|
|
52
66
|
}
|
|
53
67
|
}
|
|
54
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
68
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibXlzcWwtZGItY29ubmVjdGlvbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3NyYy9pbmZyYS9kYXRhYmFzZS9teXNxbC9teXNxbC1kYi1jb25uZWN0aW9uLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUM1QyxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0sOEJBQThCLENBQUM7QUFLM0Q7Ozs7Ozs7OztHQVNHO0FBQ0gsTUFBTSxPQUFPLGlCQUFpQjtJQWFUO0lBQ0E7SUFibkIsMEVBQTBFO0lBQ2pFLE9BQU8sR0FBRyxPQUFnQixDQUFDO0lBRTVCLElBQUksR0FBZ0IsSUFBSSxDQUFDO0lBRWpDOzs7OztPQUtHO0lBQ0gsWUFDbUIsR0FBVyxFQUNYLFlBQW9CO1FBRHBCLFFBQUcsR0FBSCxHQUFHLENBQVE7UUFDWCxpQkFBWSxHQUFaLFlBQVksQ0FBUTtJQUNwQyxDQUFDO0lBRUo7Ozs7OztPQU1HO0lBQ0gsS0FBSyxDQUFDLE9BQU87UUFDWCxJQUFJLENBQUMsSUFBSSxHQUFHLFVBQVUsQ0FBQztZQUNyQixHQUFHLEVBQUUsSUFBSSxDQUFDLEdBQUc7WUFDYixRQUFRLEVBQUUsSUFBSSxDQUFDLFlBQVk7U0FDNUIsQ0FBQyxDQUFDO1FBRUgsc0ZBQXNGO1FBQ3RGLGtFQUFrRTtRQUNsRSxJQUFJLENBQUM7WUFDSCxNQUFNLElBQUksQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLFVBQVUsQ0FBQyxDQUFDO1FBQ3BDLENBQUM7UUFBQyxPQUFPLEtBQWMsRUFBRSxDQUFDO1lBQ3hCLE1BQU0sSUFBSSxXQUFXLENBQUM7Z0JBQ3BCLElBQUksRUFBRSxrQkFBa0I7Z0JBQ3hCLE9BQU8sRUFBRSx5QkFBeUI7Z0JBQ2xDLEtBQUssRUFBRSxLQUFLO2FBQ2IsQ0FBQyxDQUFDO1FBQ0wsQ0FBQztRQUVELE9BQU8sSUFBSSxDQUFDLElBQUksQ0FBQztJQUNuQixDQUFDO0lBRUQ7Ozs7T0FJRztJQUNILEtBQUssQ0FBQyxLQUFLO1FBQ1QsSUFBSSxDQUFDLElBQUksQ0FBQyxJQUFJLEVBQUUsQ0FBQztZQUNmLE9BQU87UUFDVCxDQUFDO1FBRUQsTUFBTSxJQUFJLENBQUMsSUFBSSxDQUFDLEdBQUcsRUFBRSxDQUFDO1FBQ3RCLElBQUksQ0FBQyxJQUFJLEdBQUcsSUFBSSxDQUFDO0lBQ25CLENBQUM7Q0FDRiJ9
|
|
@@ -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,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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
|