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,402 @@
1
+ /**
2
+ * Guards Helpers - Framework-agnostic context creation
3
+ *
4
+ * Creates GuardContext from different web frameworks (Express, Hono, Fastify)
5
+ */
6
+
7
+ /**
8
+ * Create framework-agnostic GuardContext from Express request
9
+ * @param {Object} req - Express request
10
+ * @returns {Object} GuardContext
11
+ */
12
+ export function createExpressContext(req) {
13
+ const context = {
14
+ user: req.user || {},
15
+ params: req.params || {},
16
+ body: req.body || {},
17
+ query: req.query || {},
18
+ headers: req.headers || {},
19
+
20
+ // Internal state
21
+ partitionName: null,
22
+ partitionValues: {},
23
+ tenantId: null,
24
+ userId: null,
25
+
26
+ // Helper to set partition
27
+ setPartition(name, values) {
28
+ this.partitionName = name;
29
+ this.partitionValues = values;
30
+ },
31
+
32
+ // Framework raw (for advanced use)
33
+ raw: { req }
34
+ };
35
+
36
+ return context;
37
+ }
38
+
39
+ /**
40
+ * Create framework-agnostic GuardContext from Hono context
41
+ * @param {Object} c - Hono context
42
+ * @returns {Promise<Object>} GuardContext
43
+ */
44
+ export async function createHonoContext(c) {
45
+ const context = {
46
+ user: c.get('user') || {},
47
+ params: c.req.param(),
48
+ body: await c.req.json().catch(() => ({})),
49
+ query: c.req.query(),
50
+ headers: Object.fromEntries(c.req.raw.headers.entries()),
51
+
52
+ // Internal state
53
+ partitionName: null,
54
+ partitionValues: {},
55
+ tenantId: null,
56
+ userId: null,
57
+
58
+ // Helper to set partition
59
+ setPartition(name, values) {
60
+ this.partitionName = name;
61
+ this.partitionValues = values;
62
+ },
63
+
64
+ // Framework raw
65
+ raw: { c }
66
+ };
67
+
68
+ return context;
69
+ }
70
+
71
+ /**
72
+ * Create framework-agnostic GuardContext from Fastify request
73
+ * @param {Object} request - Fastify request
74
+ * @returns {Object} GuardContext
75
+ */
76
+ export function createFastifyContext(request) {
77
+ const context = {
78
+ user: request.user || {},
79
+ params: request.params || {},
80
+ body: request.body || {},
81
+ query: request.query || {},
82
+ headers: request.headers || {},
83
+
84
+ // Internal state
85
+ partitionName: null,
86
+ partitionValues: {},
87
+ tenantId: null,
88
+ userId: null,
89
+
90
+ // Helper to set partition
91
+ setPartition(name, values) {
92
+ this.partitionName = name;
93
+ this.partitionValues = values;
94
+ },
95
+
96
+ // Framework raw
97
+ raw: { request }
98
+ };
99
+
100
+ return context;
101
+ }
102
+
103
+ /**
104
+ * Execute guards and apply results to list options
105
+ * @param {Resource} resource - Resource instance
106
+ * @param {Object} context - GuardContext
107
+ * @param {Object} options - List options
108
+ * @returns {Promise<Object>} Modified options
109
+ */
110
+ export async function applyGuardsToList(resource, context, options = {}) {
111
+ // Execute list guard
112
+ const allowed = await resource.executeGuard('list', context);
113
+
114
+ if (!allowed) {
115
+ throw new Error('Forbidden: Guard denied access to list');
116
+ }
117
+
118
+ // Apply partition from guard if set
119
+ if (context.partitionName) {
120
+ options.partition = context.partitionName;
121
+ options.partitionValues = context.partitionValues || {};
122
+ }
123
+
124
+ return options;
125
+ }
126
+
127
+ /**
128
+ * Execute guards for get operation
129
+ * @param {Resource} resource - Resource instance
130
+ * @param {Object} context - GuardContext
131
+ * @param {Object} record - Record to check
132
+ * @returns {Promise<Object|null>} Record if allowed, null if denied
133
+ */
134
+ export async function applyGuardsToGet(resource, context, record) {
135
+ if (!record) return null;
136
+
137
+ // Execute get guard
138
+ const allowed = await resource.executeGuard('get', context, record);
139
+
140
+ if (!allowed) {
141
+ // Return null instead of error (404 instead of 403)
142
+ return null;
143
+ }
144
+
145
+ return record;
146
+ }
147
+
148
+ /**
149
+ * Execute guards for insert operation
150
+ * @param {Resource} resource - Resource instance
151
+ * @param {Object} context - GuardContext
152
+ * @param {Object} data - Data to insert
153
+ * @returns {Promise<Object>} Modified data
154
+ */
155
+ export async function applyGuardsToInsert(resource, context, data) {
156
+ // Execute insert guard
157
+ const allowed = await resource.executeGuard('insert', context);
158
+
159
+ if (!allowed) {
160
+ throw new Error('Forbidden: Guard denied access to insert');
161
+ }
162
+
163
+ // Guard may have modified context.body (e.g., force tenantId/userId)
164
+ if (context.body && typeof context.body === 'object') {
165
+ // Merge guard modifications into data
166
+ return { ...data, ...context.body };
167
+ }
168
+
169
+ return data;
170
+ }
171
+
172
+ /**
173
+ * Execute guards for update operation
174
+ * @param {Resource} resource - Resource instance
175
+ * @param {Object} context - GuardContext
176
+ * @param {Object} record - Current record
177
+ * @returns {Promise<boolean>} True if allowed
178
+ */
179
+ export async function applyGuardsToUpdate(resource, context, record) {
180
+ if (!record) {
181
+ throw new Error('Resource not found');
182
+ }
183
+
184
+ // Execute update guard
185
+ const allowed = await resource.executeGuard('update', context, record);
186
+
187
+ if (!allowed) {
188
+ throw new Error('Forbidden: Guard denied access to update');
189
+ }
190
+
191
+ return true;
192
+ }
193
+
194
+ /**
195
+ * Execute guards for delete operation
196
+ * @param {Resource} resource - Resource instance
197
+ * @param {Object} context - GuardContext
198
+ * @param {Object} record - Record to delete
199
+ * @returns {Promise<boolean>} True if allowed
200
+ */
201
+ export async function applyGuardsToDelete(resource, context, record) {
202
+ if (!record) {
203
+ throw new Error('Resource not found');
204
+ }
205
+
206
+ // Execute delete guard
207
+ const allowed = await resource.executeGuard('delete', context, record);
208
+
209
+ if (!allowed) {
210
+ throw new Error('Forbidden: Guard denied access to delete');
211
+ }
212
+
213
+ return true;
214
+ }
215
+
216
+ /**
217
+ * Check if user has required scopes
218
+ *
219
+ * @param {Array<string>} requiredScopes - Required scopes
220
+ * @param {string} mode - 'any' or 'all' (default: 'any')
221
+ * @returns {Function} Guard function
222
+ *
223
+ * @example
224
+ * // Require admin scope
225
+ * guard: {
226
+ * delete: requireScopes(['admin'])
227
+ * }
228
+ *
229
+ * @example
230
+ * // Require ANY of multiple scopes
231
+ * guard: {
232
+ * update: requireScopes(['admin', 'moderator'], 'any')
233
+ * }
234
+ *
235
+ * @example
236
+ * // Require ALL scopes
237
+ * guard: {
238
+ * create: requireScopes(['write:urls', 'verified'], 'all')
239
+ * }
240
+ */
241
+ export function requireScopes(requiredScopes, mode = 'any') {
242
+ if (!Array.isArray(requiredScopes)) {
243
+ requiredScopes = [requiredScopes];
244
+ }
245
+
246
+ return (ctx) => {
247
+ const userScopes = ctx.user?.scopes || [];
248
+
249
+ if (mode === 'all') {
250
+ // User must have ALL required scopes
251
+ return requiredScopes.every(scope => userScopes.includes(scope));
252
+ }
253
+
254
+ // mode === 'any': User must have AT LEAST ONE required scope
255
+ return requiredScopes.some(scope => userScopes.includes(scope));
256
+ };
257
+ }
258
+
259
+ /**
260
+ * Check if user has required role
261
+ *
262
+ * @param {string|Array<string>} role - Required role(s)
263
+ * @returns {Function} Guard function
264
+ *
265
+ * @example
266
+ * guard: {
267
+ * delete: requireRole('admin')
268
+ * }
269
+ *
270
+ * @example
271
+ * // Multiple roles (any)
272
+ * guard: {
273
+ * update: requireRole(['admin', 'moderator'])
274
+ * }
275
+ */
276
+ export function requireRole(role) {
277
+ const roles = Array.isArray(role) ? role : [role];
278
+
279
+ return (ctx) => {
280
+ const userRole = ctx.user?.role;
281
+ const userRoles = ctx.user?.roles || [];
282
+
283
+ // Check single role field
284
+ if (userRole && roles.includes(userRole)) {
285
+ return true;
286
+ }
287
+
288
+ // Check roles array
289
+ return roles.some(r => userRoles.includes(r));
290
+ };
291
+ }
292
+
293
+ /**
294
+ * Require admin scope (shorthand for requireScopes(['admin']))
295
+ *
296
+ * @returns {Function} Guard function
297
+ *
298
+ * @example
299
+ * guard: {
300
+ * delete: requireAdmin()
301
+ * }
302
+ */
303
+ export function requireAdmin() {
304
+ return requireScopes(['admin']);
305
+ }
306
+
307
+ /**
308
+ * Check ownership (record.userId === ctx.user.sub)
309
+ *
310
+ * @param {string} field - Field to check (default: 'userId')
311
+ * @returns {Function} Guard function
312
+ *
313
+ * @example
314
+ * guard: {
315
+ * update: requireOwnership(),
316
+ * delete: requireOwnership('createdBy')
317
+ * }
318
+ */
319
+ export function requireOwnership(field = 'userId') {
320
+ return (ctx, resource) => {
321
+ if (!resource) return false;
322
+
323
+ const userId = ctx.user?.sub || ctx.user?.id;
324
+ if (!userId) return false;
325
+
326
+ return resource[field] === userId;
327
+ };
328
+ }
329
+
330
+ /**
331
+ * Combine guards with OR logic (any guard passes = allowed)
332
+ *
333
+ * @param {...Function} guards - Guard functions
334
+ * @returns {Function} Combined guard function
335
+ *
336
+ * @example
337
+ * guard: {
338
+ * delete: anyOf(
339
+ * requireAdmin(),
340
+ * requireOwnership()
341
+ * )
342
+ * }
343
+ */
344
+ export function anyOf(...guards) {
345
+ return async (ctx, resource) => {
346
+ for (const guard of guards) {
347
+ const result = await guard(ctx, resource);
348
+ if (result) return true;
349
+ }
350
+ return false;
351
+ };
352
+ }
353
+
354
+ /**
355
+ * Combine guards with AND logic (all guards must pass)
356
+ *
357
+ * @param {...Function} guards - Guard functions
358
+ * @returns {Function} Combined guard function
359
+ *
360
+ * @example
361
+ * guard: {
362
+ * create: allOf(
363
+ * requireScopes(['write:urls']),
364
+ * (ctx) => ctx.user.verified === true
365
+ * )
366
+ * }
367
+ */
368
+ export function allOf(...guards) {
369
+ return async (ctx, resource) => {
370
+ for (const guard of guards) {
371
+ const result = await guard(ctx, resource);
372
+ if (!result) return false;
373
+ }
374
+ return true;
375
+ };
376
+ }
377
+
378
+ /**
379
+ * Check if user belongs to specific tenant
380
+ *
381
+ * @param {string} tenantField - Field name in resource (default: 'tenantId')
382
+ * @returns {Function} Guard function
383
+ *
384
+ * @example
385
+ * guard: {
386
+ * '*': (ctx) => {
387
+ * ctx.tenantId = ctx.user.tenantId || ctx.user.tid;
388
+ * return !!ctx.tenantId;
389
+ * },
390
+ * update: requireTenant()
391
+ * }
392
+ */
393
+ export function requireTenant(tenantField = 'tenantId') {
394
+ return (ctx, resource) => {
395
+ if (!resource) return true; // Let wildcard/insert guards handle
396
+
397
+ const userTenantId = ctx.tenantId || ctx.user?.tenantId || ctx.user?.tid;
398
+ if (!userTenantId) return false;
399
+
400
+ return resource[tenantField] === userTenantId;
401
+ };
402
+ }