s3db.js 13.4.0 → 13.6.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 (110) hide show
  1. package/README.md +25 -10
  2. package/dist/{s3db.cjs.js → s3db.cjs} +38801 -32446
  3. package/dist/s3db.cjs.map +1 -0
  4. package/dist/s3db.es.js +38653 -32291
  5. package/dist/s3db.es.js.map +1 -1
  6. package/package.json +218 -22
  7. package/src/concerns/id.js +90 -6
  8. package/src/concerns/index.js +2 -1
  9. package/src/concerns/password-hashing.js +150 -0
  10. package/src/database.class.js +6 -2
  11. package/src/plugins/api/auth/basic-auth.js +40 -10
  12. package/src/plugins/api/auth/index.js +49 -3
  13. package/src/plugins/api/auth/oauth2-auth.js +171 -0
  14. package/src/plugins/api/auth/oidc-auth.js +789 -0
  15. package/src/plugins/api/auth/oidc-client.js +462 -0
  16. package/src/plugins/api/auth/path-auth-matcher.js +284 -0
  17. package/src/plugins/api/concerns/event-emitter.js +134 -0
  18. package/src/plugins/api/concerns/failban-manager.js +651 -0
  19. package/src/plugins/api/concerns/guards-helpers.js +402 -0
  20. package/src/plugins/api/concerns/metrics-collector.js +346 -0
  21. package/src/plugins/api/index.js +510 -57
  22. package/src/plugins/api/middlewares/failban.js +305 -0
  23. package/src/plugins/api/middlewares/rate-limit.js +301 -0
  24. package/src/plugins/api/middlewares/request-id.js +74 -0
  25. package/src/plugins/api/middlewares/security-headers.js +120 -0
  26. package/src/plugins/api/middlewares/session-tracking.js +194 -0
  27. package/src/plugins/api/routes/auth-routes.js +119 -78
  28. package/src/plugins/api/routes/resource-routes.js +73 -30
  29. package/src/plugins/api/server.js +1139 -45
  30. package/src/plugins/api/utils/custom-routes.js +102 -0
  31. package/src/plugins/api/utils/guards.js +213 -0
  32. package/src/plugins/api/utils/mime-types.js +154 -0
  33. package/src/plugins/api/utils/openapi-generator.js +91 -12
  34. package/src/plugins/api/utils/path-matcher.js +173 -0
  35. package/src/plugins/api/utils/static-filesystem.js +262 -0
  36. package/src/plugins/api/utils/static-s3.js +231 -0
  37. package/src/plugins/api/utils/template-engine.js +188 -0
  38. package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +853 -0
  39. package/src/plugins/cloud-inventory/drivers/aws-driver.js +2554 -0
  40. package/src/plugins/cloud-inventory/drivers/azure-driver.js +637 -0
  41. package/src/plugins/cloud-inventory/drivers/base-driver.js +99 -0
  42. package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +620 -0
  43. package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +698 -0
  44. package/src/plugins/cloud-inventory/drivers/gcp-driver.js +645 -0
  45. package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +559 -0
  46. package/src/plugins/cloud-inventory/drivers/linode-driver.js +614 -0
  47. package/src/plugins/cloud-inventory/drivers/mock-drivers.js +449 -0
  48. package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +771 -0
  49. package/src/plugins/cloud-inventory/drivers/oracle-driver.js +768 -0
  50. package/src/plugins/cloud-inventory/drivers/vultr-driver.js +636 -0
  51. package/src/plugins/cloud-inventory/index.js +20 -0
  52. package/src/plugins/cloud-inventory/registry.js +146 -0
  53. package/src/plugins/cloud-inventory/terraform-exporter.js +362 -0
  54. package/src/plugins/cloud-inventory.plugin.js +1333 -0
  55. package/src/plugins/concerns/plugin-dependencies.js +62 -2
  56. package/src/plugins/eventual-consistency/analytics.js +1 -0
  57. package/src/plugins/eventual-consistency/consolidation.js +2 -2
  58. package/src/plugins/eventual-consistency/garbage-collection.js +2 -2
  59. package/src/plugins/eventual-consistency/install.js +2 -2
  60. package/src/plugins/identity/README.md +335 -0
  61. package/src/plugins/identity/concerns/mfa-manager.js +204 -0
  62. package/src/plugins/identity/concerns/password.js +138 -0
  63. package/src/plugins/identity/concerns/resource-schemas.js +273 -0
  64. package/src/plugins/identity/concerns/token-generator.js +172 -0
  65. package/src/plugins/identity/email-service.js +422 -0
  66. package/src/plugins/identity/index.js +1052 -0
  67. package/src/plugins/identity/oauth2-server.js +1033 -0
  68. package/src/plugins/identity/oidc-discovery.js +285 -0
  69. package/src/plugins/identity/rsa-keys.js +323 -0
  70. package/src/plugins/identity/server.js +500 -0
  71. package/src/plugins/identity/session-manager.js +453 -0
  72. package/src/plugins/identity/ui/layouts/base.js +251 -0
  73. package/src/plugins/identity/ui/middleware.js +135 -0
  74. package/src/plugins/identity/ui/pages/admin/client-form.js +247 -0
  75. package/src/plugins/identity/ui/pages/admin/clients.js +179 -0
  76. package/src/plugins/identity/ui/pages/admin/dashboard.js +181 -0
  77. package/src/plugins/identity/ui/pages/admin/user-form.js +283 -0
  78. package/src/plugins/identity/ui/pages/admin/users.js +263 -0
  79. package/src/plugins/identity/ui/pages/consent.js +262 -0
  80. package/src/plugins/identity/ui/pages/forgot-password.js +104 -0
  81. package/src/plugins/identity/ui/pages/login.js +144 -0
  82. package/src/plugins/identity/ui/pages/mfa-backup-codes.js +180 -0
  83. package/src/plugins/identity/ui/pages/mfa-enrollment.js +187 -0
  84. package/src/plugins/identity/ui/pages/mfa-verification.js +178 -0
  85. package/src/plugins/identity/ui/pages/oauth-error.js +225 -0
  86. package/src/plugins/identity/ui/pages/profile.js +361 -0
  87. package/src/plugins/identity/ui/pages/register.js +226 -0
  88. package/src/plugins/identity/ui/pages/reset-password.js +128 -0
  89. package/src/plugins/identity/ui/pages/verify-email.js +172 -0
  90. package/src/plugins/identity/ui/routes.js +2541 -0
  91. package/src/plugins/identity/ui/styles/main.css +465 -0
  92. package/src/plugins/index.js +4 -1
  93. package/src/plugins/ml/base-model.class.js +65 -16
  94. package/src/plugins/ml/classification-model.class.js +1 -1
  95. package/src/plugins/ml/timeseries-model.class.js +3 -1
  96. package/src/plugins/ml.plugin.js +584 -31
  97. package/src/plugins/shared/error-handler.js +147 -0
  98. package/src/plugins/shared/index.js +9 -0
  99. package/src/plugins/shared/middlewares/compression.js +117 -0
  100. package/src/plugins/shared/middlewares/cors.js +49 -0
  101. package/src/plugins/shared/middlewares/index.js +11 -0
  102. package/src/plugins/shared/middlewares/logging.js +54 -0
  103. package/src/plugins/shared/middlewares/rate-limit.js +73 -0
  104. package/src/plugins/shared/middlewares/security.js +158 -0
  105. package/src/plugins/shared/response-formatter.js +264 -0
  106. package/src/plugins/state-machine.plugin.js +57 -2
  107. package/src/resource.class.js +140 -12
  108. package/src/schema.class.js +30 -1
  109. package/src/validator.class.js +57 -6
  110. package/dist/s3db.cjs.js.map +0 -1
@@ -0,0 +1,285 @@
1
+ /**
2
+ * OIDC Discovery - OpenID Connect Discovery Document Generator
3
+ *
4
+ * Generates .well-known/openid-configuration and JWKS endpoints
5
+ * Implements OpenID Connect Discovery 1.0 specification
6
+ */
7
+
8
+ /**
9
+ * Generate OpenID Connect Discovery Document
10
+ * @param {Object} options - Configuration options
11
+ * @param {string} options.issuer - Issuer URL (e.g., 'https://sso.example.com')
12
+ * @param {Array} options.grantTypes - Supported grant types
13
+ * @param {Array} options.responseTypes - Supported response types
14
+ * @param {Array} options.scopes - Supported scopes
15
+ * @returns {Object} OIDC Discovery document
16
+ */
17
+ export function generateDiscoveryDocument(options = {}) {
18
+ const {
19
+ issuer,
20
+ grantTypes = ['authorization_code', 'client_credentials', 'refresh_token'],
21
+ responseTypes = ['code', 'token', 'id_token', 'code id_token', 'code token', 'id_token token', 'code id_token token'],
22
+ scopes = ['openid', 'profile', 'email', 'offline_access']
23
+ } = options;
24
+
25
+ if (!issuer) {
26
+ throw new Error('Issuer URL is required for OIDC discovery');
27
+ }
28
+
29
+ // Remove trailing slash from issuer
30
+ const baseUrl = issuer.replace(/\/$/, '');
31
+
32
+ return {
33
+ issuer: baseUrl,
34
+ authorization_endpoint: `${baseUrl}/auth/authorize`,
35
+ token_endpoint: `${baseUrl}/auth/token`,
36
+ userinfo_endpoint: `${baseUrl}/auth/userinfo`,
37
+ jwks_uri: `${baseUrl}/.well-known/jwks.json`,
38
+ registration_endpoint: `${baseUrl}/auth/register`,
39
+ introspection_endpoint: `${baseUrl}/auth/introspect`,
40
+ revocation_endpoint: `${baseUrl}/auth/revoke`,
41
+ end_session_endpoint: `${baseUrl}/auth/logout`,
42
+
43
+ // Supported features
44
+ scopes_supported: scopes,
45
+ response_types_supported: responseTypes,
46
+ response_modes_supported: ['query', 'fragment', 'form_post'],
47
+ grant_types_supported: grantTypes,
48
+ subject_types_supported: ['public'],
49
+ id_token_signing_alg_values_supported: ['RS256'],
50
+ token_endpoint_auth_methods_supported: [
51
+ 'client_secret_basic',
52
+ 'client_secret_post',
53
+ 'none'
54
+ ],
55
+
56
+ // Claims
57
+ claims_supported: [
58
+ 'sub',
59
+ 'iss',
60
+ 'aud',
61
+ 'exp',
62
+ 'iat',
63
+ 'auth_time',
64
+ 'nonce',
65
+ 'email',
66
+ 'email_verified',
67
+ 'name',
68
+ 'given_name',
69
+ 'family_name',
70
+ 'picture',
71
+ 'locale'
72
+ ],
73
+
74
+ // Code challenge methods (PKCE)
75
+ code_challenge_methods_supported: ['plain', 'S256'],
76
+
77
+ // UI locales
78
+ ui_locales_supported: ['en', 'pt-BR'],
79
+
80
+ // Service documentation
81
+ service_documentation: `${baseUrl}/docs`,
82
+
83
+ // Additional metadata
84
+ claim_types_supported: ['normal'],
85
+ claims_parameter_supported: false,
86
+ request_parameter_supported: false,
87
+ request_uri_parameter_supported: false,
88
+ require_request_uri_registration: false,
89
+
90
+ // Discovery document version
91
+ version: '1.0'
92
+ };
93
+ }
94
+
95
+ /**
96
+ * Validate OAuth2/OIDC claims in JWT payload
97
+ * @param {Object} payload - JWT payload
98
+ * @param {Object} options - Validation options
99
+ * @param {string} options.issuer - Expected issuer
100
+ * @param {string} options.audience - Expected audience
101
+ * @param {number} options.clockTolerance - Clock skew tolerance in seconds (default: 60)
102
+ * @returns {Object} { valid: boolean, error: string|null }
103
+ */
104
+ export function validateClaims(payload, options = {}) {
105
+ const {
106
+ issuer,
107
+ audience,
108
+ clockTolerance = 60
109
+ } = options;
110
+
111
+ const now = Math.floor(Date.now() / 1000);
112
+
113
+ // Check required claims
114
+ if (!payload.sub) {
115
+ return { valid: false, error: 'Missing required claim: sub' };
116
+ }
117
+
118
+ if (!payload.iat) {
119
+ return { valid: false, error: 'Missing required claim: iat' };
120
+ }
121
+
122
+ if (!payload.exp) {
123
+ return { valid: false, error: 'Missing required claim: exp' };
124
+ }
125
+
126
+ // Validate issuer
127
+ if (issuer && payload.iss !== issuer) {
128
+ return {
129
+ valid: false,
130
+ error: `Invalid issuer. Expected: ${issuer}, Got: ${payload.iss}`
131
+ };
132
+ }
133
+
134
+ // Validate audience
135
+ if (audience) {
136
+ const audiences = Array.isArray(payload.aud) ? payload.aud : [payload.aud];
137
+
138
+ if (!audiences.includes(audience)) {
139
+ return {
140
+ valid: false,
141
+ error: `Invalid audience. Expected: ${audience}, Got: ${audiences.join(', ')}`
142
+ };
143
+ }
144
+ }
145
+
146
+ // Validate expiration with clock tolerance
147
+ if (payload.exp < (now - clockTolerance)) {
148
+ return { valid: false, error: 'Token has expired' };
149
+ }
150
+
151
+ // Validate not before (if present)
152
+ if (payload.nbf && payload.nbf > (now + clockTolerance)) {
153
+ return { valid: false, error: 'Token not yet valid (nbf)' };
154
+ }
155
+
156
+ // Validate issued at (basic sanity check - not in future)
157
+ if (payload.iat > (now + clockTolerance)) {
158
+ return { valid: false, error: 'Token issued in the future' };
159
+ }
160
+
161
+ return { valid: true, error: null };
162
+ }
163
+
164
+ /**
165
+ * Extract user claims from user object for ID token
166
+ * @param {Object} user - User object from database
167
+ * @param {Array} scopes - Requested scopes
168
+ * @returns {Object} User claims
169
+ */
170
+ export function extractUserClaims(user, scopes = []) {
171
+ const claims = {
172
+ sub: user.id // Subject - user ID
173
+ };
174
+
175
+ // Add email claims if 'email' scope requested
176
+ if (scopes.includes('email') && user.email) {
177
+ claims.email = user.email;
178
+ claims.email_verified = user.emailVerified || false;
179
+ }
180
+
181
+ // Add profile claims if 'profile' scope requested
182
+ if (scopes.includes('profile')) {
183
+ if (user.name) claims.name = user.name;
184
+ if (user.givenName) claims.given_name = user.givenName;
185
+ if (user.familyName) claims.family_name = user.familyName;
186
+ if (user.picture) claims.picture = user.picture;
187
+ if (user.locale) claims.locale = user.locale;
188
+ if (user.zoneinfo) claims.zoneinfo = user.zoneinfo;
189
+ if (user.birthdate) claims.birthdate = user.birthdate;
190
+ if (user.gender) claims.gender = user.gender;
191
+ }
192
+
193
+ return claims;
194
+ }
195
+
196
+ /**
197
+ * Parse scope string into array
198
+ * @param {string} scopeString - Space-separated scopes (e.g., 'openid profile email')
199
+ * @returns {Array} Array of scopes
200
+ */
201
+ export function parseScopes(scopeString) {
202
+ if (!scopeString || typeof scopeString !== 'string') {
203
+ return [];
204
+ }
205
+
206
+ return scopeString
207
+ .trim()
208
+ .split(/\s+/)
209
+ .filter(s => s.length > 0);
210
+ }
211
+
212
+ /**
213
+ * Validate requested scopes against supported scopes
214
+ * @param {Array} requestedScopes - Scopes requested by client
215
+ * @param {Array} supportedScopes - Scopes supported by server
216
+ * @returns {Object} { valid: boolean, error: string|null, scopes: Array }
217
+ */
218
+ export function validateScopes(requestedScopes, supportedScopes) {
219
+ if (!Array.isArray(requestedScopes)) {
220
+ requestedScopes = parseScopes(requestedScopes);
221
+ }
222
+
223
+ // Check if all requested scopes are supported
224
+ const invalidScopes = requestedScopes.filter(scope => !supportedScopes.includes(scope));
225
+
226
+ if (invalidScopes.length > 0) {
227
+ return {
228
+ valid: false,
229
+ error: `Unsupported scopes: ${invalidScopes.join(', ')}`,
230
+ scopes: []
231
+ };
232
+ }
233
+
234
+ return {
235
+ valid: true,
236
+ error: null,
237
+ scopes: requestedScopes
238
+ };
239
+ }
240
+
241
+ /**
242
+ * Generate authorization code (random string)
243
+ * @param {number} length - Code length (default: 32)
244
+ * @returns {string} Authorization code
245
+ */
246
+ export function generateAuthCode(length = 32) {
247
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
248
+ let code = '';
249
+
250
+ for (let i = 0; i < length; i++) {
251
+ code += chars.charAt(Math.floor(Math.random() * chars.length));
252
+ }
253
+
254
+ return code;
255
+ }
256
+
257
+ /**
258
+ * Generate client ID
259
+ * @returns {string} Client ID (UUID-like)
260
+ */
261
+ export function generateClientId() {
262
+ return crypto.randomUUID();
263
+ }
264
+
265
+ /**
266
+ * Generate client secret
267
+ * @param {number} length - Secret length (default: 64)
268
+ * @returns {string} Client secret
269
+ */
270
+ export function generateClientSecret(length = 64) {
271
+ return crypto.randomBytes(length / 2).toString('hex');
272
+ }
273
+
274
+ import crypto from 'crypto';
275
+
276
+ export default {
277
+ generateDiscoveryDocument,
278
+ validateClaims,
279
+ extractUserClaims,
280
+ parseScopes,
281
+ validateScopes,
282
+ generateAuthCode,
283
+ generateClientId,
284
+ generateClientSecret
285
+ };
@@ -0,0 +1,323 @@
1
+ /**
2
+ * RSA Key Management for OAuth2/OIDC
3
+ *
4
+ * Manages RS256 key pairs for signing and verifying JWTs
5
+ * Zero external dependencies - uses Node.js crypto only
6
+ */
7
+
8
+ import { generateKeyPairSync, createSign, createVerify, createHash } from 'crypto';
9
+
10
+ /**
11
+ * Generate RSA key pair for RS256
12
+ * @param {number} modulusLength - Key size in bits (default: 2048)
13
+ * @returns {Object} { publicKey, privateKey, kid }
14
+ */
15
+ export function generateKeyPair(modulusLength = 2048) {
16
+ const { publicKey, privateKey } = generateKeyPairSync('rsa', {
17
+ modulusLength,
18
+ publicKeyEncoding: {
19
+ type: 'spki',
20
+ format: 'pem'
21
+ },
22
+ privateKeyEncoding: {
23
+ type: 'pkcs8',
24
+ format: 'pem'
25
+ }
26
+ });
27
+
28
+ // Generate key ID (kid) from public key fingerprint
29
+ const kid = createHash('sha256')
30
+ .update(publicKey)
31
+ .digest('hex')
32
+ .substring(0, 16);
33
+
34
+ return {
35
+ publicKey,
36
+ privateKey,
37
+ kid,
38
+ algorithm: 'RS256',
39
+ use: 'sig',
40
+ createdAt: new Date().toISOString()
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Convert PEM public key to JWK format
46
+ * @param {string} publicKeyPem - PEM formatted public key
47
+ * @param {string} kid - Key ID
48
+ * @returns {Object} JWK (JSON Web Key)
49
+ */
50
+ export function pemToJwk(publicKeyPem, kid) {
51
+ // Extract key components using Node.js crypto
52
+ const keyObject = createPublicKey(publicKeyPem);
53
+ const exported = keyObject.export({ format: 'jwk' });
54
+
55
+ return {
56
+ kty: 'RSA',
57
+ use: 'sig',
58
+ alg: 'RS256',
59
+ kid,
60
+ n: exported.n, // modulus
61
+ e: exported.e // exponent
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Create RS256 JWT token
67
+ * @param {Object} payload - Token payload
68
+ * @param {string} privateKey - PEM formatted private key
69
+ * @param {string} kid - Key ID
70
+ * @param {string} expiresIn - Token expiration (e.g., '15m')
71
+ * @returns {string} JWT token
72
+ */
73
+ export function createRS256Token(payload, privateKey, kid, expiresIn = '15m') {
74
+ // Parse expiresIn
75
+ const match = expiresIn.match(/^(\d+)([smhd])$/);
76
+ if (!match) {
77
+ throw new Error('Invalid expiresIn format. Use: 60s, 30m, 24h, 7d');
78
+ }
79
+
80
+ const [, value, unit] = match;
81
+ const multipliers = { s: 1, m: 60, h: 3600, d: 86400 };
82
+ const expiresInSeconds = parseInt(value) * multipliers[unit];
83
+
84
+ const header = {
85
+ alg: 'RS256',
86
+ typ: 'JWT',
87
+ kid
88
+ };
89
+
90
+ const now = Math.floor(Date.now() / 1000);
91
+
92
+ const data = {
93
+ ...payload,
94
+ iat: now,
95
+ exp: now + expiresInSeconds
96
+ };
97
+
98
+ // Encode header and payload
99
+ const encodedHeader = Buffer.from(JSON.stringify(header)).toString('base64url');
100
+ const encodedPayload = Buffer.from(JSON.stringify(data)).toString('base64url');
101
+
102
+ // Sign with RSA private key
103
+ const sign = createSign('RSA-SHA256');
104
+ sign.update(`${encodedHeader}.${encodedPayload}`);
105
+ sign.end();
106
+
107
+ const signature = sign.sign(privateKey, 'base64url');
108
+
109
+ return `${encodedHeader}.${encodedPayload}.${signature}`;
110
+ }
111
+
112
+ /**
113
+ * Verify RS256 JWT token
114
+ * @param {string} token - JWT token
115
+ * @param {string} publicKey - PEM formatted public key
116
+ * @returns {Object|null} Decoded payload or null if invalid
117
+ */
118
+ export function verifyRS256Token(token, publicKey) {
119
+ try {
120
+ const parts = token.split('.');
121
+ if (parts.length !== 3) {
122
+ return null;
123
+ }
124
+
125
+ const [encodedHeader, encodedPayload, signature] = parts;
126
+
127
+ // Verify signature
128
+ const verify = createVerify('RSA-SHA256');
129
+ verify.update(`${encodedHeader}.${encodedPayload}`);
130
+ verify.end();
131
+
132
+ const isValid = verify.verify(publicKey, signature, 'base64url');
133
+
134
+ if (!isValid) {
135
+ return null;
136
+ }
137
+
138
+ // Decode header and payload
139
+ const header = JSON.parse(Buffer.from(encodedHeader, 'base64url').toString());
140
+ const payload = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString());
141
+
142
+ // Verify algorithm
143
+ if (header.alg !== 'RS256') {
144
+ return null;
145
+ }
146
+
147
+ // Check expiration
148
+ const now = Math.floor(Date.now() / 1000);
149
+ if (payload.exp && payload.exp < now) {
150
+ return null; // Expired
151
+ }
152
+
153
+ return {
154
+ header,
155
+ payload
156
+ };
157
+ } catch (err) {
158
+ return null;
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Get key ID (kid) from JWT token header
164
+ * @param {string} token - JWT token
165
+ * @returns {string|null} Key ID or null
166
+ */
167
+ export function getKidFromToken(token) {
168
+ try {
169
+ const [encodedHeader] = token.split('.');
170
+ const header = JSON.parse(Buffer.from(encodedHeader, 'base64url').toString());
171
+ return header.kid || null;
172
+ } catch (err) {
173
+ return null;
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Import createPublicKey for JWK conversion
179
+ */
180
+ import { createPublicKey } from 'crypto';
181
+
182
+ /**
183
+ * Key Manager class - manages key rotation and storage
184
+ */
185
+ export class KeyManager {
186
+ constructor(keyResource) {
187
+ this.keyResource = keyResource;
188
+ this.currentKey = null;
189
+ this.keys = new Map(); // kid → key
190
+ }
191
+
192
+ /**
193
+ * Initialize key manager - load or generate keys
194
+ */
195
+ async initialize() {
196
+ // Try to load existing keys
197
+ const existingKeys = await this.keyResource.list();
198
+
199
+ if (existingKeys.length > 0) {
200
+ // Load keys into memory
201
+ for (const keyRecord of existingKeys) {
202
+ this.keys.set(keyRecord.kid, {
203
+ publicKey: keyRecord.publicKey,
204
+ privateKey: keyRecord.privateKey,
205
+ kid: keyRecord.kid,
206
+ createdAt: keyRecord.createdAt,
207
+ active: keyRecord.active
208
+ });
209
+
210
+ if (keyRecord.active) {
211
+ this.currentKey = keyRecord;
212
+ }
213
+ }
214
+ }
215
+
216
+ // If no active key, generate one
217
+ if (!this.currentKey) {
218
+ await this.rotateKey();
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Rotate keys - generate new key pair
224
+ */
225
+ async rotateKey() {
226
+ const keyPair = generateKeyPair();
227
+
228
+ // Mark old keys as inactive
229
+ const oldKeys = await this.keyResource.query({ active: true });
230
+ for (const oldKey of oldKeys) {
231
+ await this.keyResource.update(oldKey.id, { active: false });
232
+ }
233
+
234
+ // Store new key
235
+ const keyRecord = await this.keyResource.insert({
236
+ kid: keyPair.kid,
237
+ publicKey: keyPair.publicKey,
238
+ privateKey: keyPair.privateKey,
239
+ algorithm: keyPair.algorithm,
240
+ use: keyPair.use,
241
+ active: true,
242
+ createdAt: keyPair.createdAt
243
+ });
244
+
245
+ this.currentKey = keyRecord;
246
+ this.keys.set(keyRecord.kid, keyRecord);
247
+
248
+ return keyRecord;
249
+ }
250
+
251
+ /**
252
+ * Get current active key
253
+ */
254
+ getCurrentKey() {
255
+ return this.currentKey;
256
+ }
257
+
258
+ /**
259
+ * Get key by kid
260
+ */
261
+ getKey(kid) {
262
+ return this.keys.get(kid);
263
+ }
264
+
265
+ /**
266
+ * Get all public keys in JWKS format
267
+ */
268
+ async getJWKS() {
269
+ const keys = Array.from(this.keys.values()).map(key => ({
270
+ kty: 'RSA',
271
+ use: 'sig',
272
+ alg: 'RS256',
273
+ kid: key.kid,
274
+ ...pemToJwk(key.publicKey, key.kid)
275
+ }));
276
+
277
+ return { keys };
278
+ }
279
+
280
+ /**
281
+ * Create JWT with current active key
282
+ */
283
+ createToken(payload, expiresIn = '15m') {
284
+ if (!this.currentKey) {
285
+ throw new Error('No active key available');
286
+ }
287
+
288
+ return createRS256Token(
289
+ payload,
290
+ this.currentKey.privateKey,
291
+ this.currentKey.kid,
292
+ expiresIn
293
+ );
294
+ }
295
+
296
+ /**
297
+ * Verify JWT token
298
+ */
299
+ async verifyToken(token) {
300
+ const kid = getKidFromToken(token);
301
+
302
+ if (!kid) {
303
+ return null;
304
+ }
305
+
306
+ const key = this.getKey(kid);
307
+
308
+ if (!key) {
309
+ return null;
310
+ }
311
+
312
+ return verifyRS256Token(token, key.publicKey);
313
+ }
314
+ }
315
+
316
+ export default {
317
+ generateKeyPair,
318
+ pemToJwk,
319
+ createRS256Token,
320
+ verifyRS256Token,
321
+ getKidFromToken,
322
+ KeyManager
323
+ };