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,465 @@
1
+ /**
2
+ * Identity Provider UI – Tailwind baseline
3
+ */
4
+
5
+ :root {
6
+ color-scheme: dark;
7
+ }
8
+
9
+ *,
10
+ *::before,
11
+ *::after {
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ html,
16
+ body {
17
+ min-height: 100%;
18
+ }
19
+
20
+ body {
21
+ margin: 0;
22
+ font-family: var(--font-family);
23
+ font-size: var(--font-size-base);
24
+ color: var(--color-text, #e2e8f0);
25
+ background-color: transparent;
26
+ }
27
+
28
+ a {
29
+ color: inherit;
30
+ text-decoration: none;
31
+ }
32
+
33
+ a:hover {
34
+ text-decoration: none;
35
+ }
36
+
37
+ code {
38
+ font-family: 'Fira Code', 'Source Code Pro', Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
39
+ }
40
+
41
+ table {
42
+ border-collapse: collapse;
43
+ }
44
+
45
+ button {
46
+ font: inherit;
47
+ }
48
+
49
+ input,
50
+ textarea,
51
+ select {
52
+ font: inherit;
53
+ color: inherit;
54
+ background-color: transparent;
55
+ }
56
+
57
+ .auth-links {
58
+ text-align: center;
59
+ color: #475569;
60
+ }
61
+
62
+ .auth-links p {
63
+ margin-bottom: 0.35rem;
64
+ }
65
+
66
+ .auth-links .mt-3 {
67
+ margin-top: 1.25rem;
68
+ }
69
+
70
+ .flash {
71
+ width: 100%;
72
+ max-width: 420px;
73
+ border-radius: 14px;
74
+ padding: 0.85rem 1.2rem;
75
+ background: rgba(15, 23, 42, 0.8);
76
+ border: 1px solid rgba(148, 163, 184, 0.25);
77
+ color: var(--color-text);
78
+ box-shadow: 0 22px 45px rgba(15, 23, 42, 0.35);
79
+ backdrop-filter: blur(16px);
80
+ }
81
+
82
+ .flash-success {
83
+ border-color: rgba(74, 222, 128, 0.45);
84
+ background: rgba(22, 163, 74, 0.16);
85
+ }
86
+
87
+ .flash-error {
88
+ border-color: rgba(248, 113, 113, 0.5);
89
+ background: rgba(220, 38, 38, 0.18);
90
+ }
91
+
92
+ /* ========================================
93
+ Links & Text
94
+ ======================================== */
95
+
96
+ .text-center {
97
+ text-align: center;
98
+ }
99
+
100
+ .text-muted {
101
+ color: #999;
102
+ font-size: 0.9rem;
103
+ }
104
+ a {
105
+ color: var(--color-accent);
106
+ text-decoration: none;
107
+ transition: color 0.2s ease;
108
+ }
109
+
110
+ a:hover {
111
+ color: var(--color-primary-light);
112
+ text-decoration: underline;
113
+ }
114
+
115
+ /* ========================================
116
+ Identity Login Screen
117
+ ======================================== */
118
+
119
+ .identity-login {
120
+ width: 100%;
121
+ margin: 0 auto;
122
+ border-radius: 0;
123
+ overflow: hidden;
124
+ display: flex;
125
+ gap: 0;
126
+ background: linear-gradient(120deg, rgba(15, 23, 42, 0.88), rgba(15, 23, 42, 0.94));
127
+ border: none;
128
+ box-shadow: none;
129
+ backdrop-filter: none;
130
+ min-height: 100vh;
131
+ }
132
+
133
+ .identity-login__panel {
134
+ position: relative;
135
+ flex: 1 1 58%;
136
+ padding: clamp(3rem, 5vw, 4rem);
137
+ color: #f8fafc;
138
+ background: linear-gradient(135deg, var(--color-primary, #2563eb), var(--color-secondary, #7c3aed));
139
+ display: flex;
140
+ flex-direction: column;
141
+ gap: clamp(2.25rem, 4vw, 3rem);
142
+ border-radius: 0;
143
+ overflow: hidden;
144
+ min-height: 100%;
145
+ justify-content: space-between;
146
+ }
147
+
148
+ .identity-login__panel::before,
149
+ .identity-login__panel::after {
150
+ content: '';
151
+ position: absolute;
152
+ inset: 0;
153
+ background: radial-gradient(circle at 25% 18%, rgba(255, 255, 255, 0.35), transparent 55%);
154
+ opacity: 0.45;
155
+ pointer-events: none;
156
+ }
157
+
158
+ .identity-login__panel::after {
159
+ background: radial-gradient(circle at 85% 82%, rgba(255, 255, 255, 0.25), transparent 60%);
160
+ opacity: 0.55;
161
+ }
162
+
163
+ .identity-login__panel-content {
164
+ position: relative;
165
+ z-index: 1;
166
+ display: flex;
167
+ flex-direction: column;
168
+ gap: 2.5rem;
169
+ flex: 1;
170
+ }
171
+
172
+ .identity-login__panel-main {
173
+ display: flex;
174
+ flex-direction: column;
175
+ gap: 1.5rem;
176
+ max-width: 28rem;
177
+ }
178
+
179
+ .identity-login__badge {
180
+ display: inline-flex;
181
+ align-items: center;
182
+ gap: 0.4rem;
183
+ font-size: 0.75rem;
184
+ letter-spacing: 0.32em;
185
+ text-transform: uppercase;
186
+ padding: 0.45rem 0.9rem;
187
+ border-radius: 999px;
188
+ background: rgba(255, 255, 255, 0.18);
189
+ backdrop-filter: blur(8px);
190
+ font-weight: 600;
191
+ }
192
+
193
+ .identity-login__brand {
194
+ display: flex;
195
+ align-items: center;
196
+ gap: 1rem;
197
+ }
198
+
199
+ .identity-login__brand-logo {
200
+ width: 56px;
201
+ height: 56px;
202
+ border-radius: 18px;
203
+ object-fit: cover;
204
+ border: 1px solid rgba(255, 255, 255, 0.25);
205
+ background: rgba(255, 255, 255, 0.2);
206
+ backdrop-filter: blur(8px);
207
+ }
208
+
209
+ .identity-login__panel-title {
210
+ font-size: clamp(2.2rem, 3.1vw, 3rem);
211
+ font-weight: 600;
212
+ line-height: 1.15;
213
+ margin: 0;
214
+ }
215
+
216
+ .identity-login__panel-text {
217
+ font-size: 1rem;
218
+ line-height: 1.7;
219
+ color: rgba(241, 245, 249, 0.82);
220
+ margin: 0;
221
+ }
222
+
223
+ .identity-login__panel-footer {
224
+ position: relative;
225
+ z-index: 1;
226
+ font-size: 0.75rem;
227
+ letter-spacing: 0.18em;
228
+ text-transform: uppercase;
229
+ color: rgba(241, 245, 249, 0.7);
230
+ margin-top: auto;
231
+ padding-top: 2rem;
232
+ }
233
+
234
+ .identity-login__form {
235
+ flex: 1 1 42%;
236
+ padding: clamp(3rem, 5vw, 3.75rem);
237
+ background: rgba(5, 10, 28, 0.92);
238
+ color: #f8fafc;
239
+ display: flex;
240
+ flex-direction: column;
241
+ justify-content: center;
242
+ gap: clamp(2rem, 3.5vw, 2.5rem);
243
+ border-radius: 0;
244
+ box-shadow: inset 0 0 38px rgba(15, 23, 42, 0.45);
245
+ min-height: 100%;
246
+ }
247
+
248
+ .identity-login__form-header h2 {
249
+ margin: 0;
250
+ font-size: clamp(1.75rem, 2vw, 2.1rem);
251
+ font-weight: 600;
252
+ color: #f1f5f9;
253
+ }
254
+
255
+ .identity-login__form-header p {
256
+ margin: 0.75rem 0 0;
257
+ color: rgba(148, 163, 184, 0.85);
258
+ font-size: 0.95rem;
259
+ }
260
+
261
+ .identity-login__alert {
262
+ border-radius: 18px;
263
+ padding: 0.9rem 1.1rem;
264
+ font-size: 0.9rem;
265
+ line-height: 1.5;
266
+ box-shadow: 0 18px 48px rgba(15, 23, 42, 0.4);
267
+ }
268
+
269
+ .identity-login__alert--error {
270
+ border: 1px solid rgba(248, 113, 113, 0.6);
271
+ background: rgba(220, 38, 38, 0.16);
272
+ color: #fecaca;
273
+ }
274
+
275
+ .identity-login__form-body {
276
+ display: flex;
277
+ flex-direction: column;
278
+ gap: 1.35rem;
279
+ }
280
+
281
+ .identity-login__group label {
282
+ display: block;
283
+ margin-bottom: 0.55rem;
284
+ font-weight: 600;
285
+ font-size: 0.9rem;
286
+ color: rgba(226, 232, 240, 0.92);
287
+ }
288
+
289
+ .identity-login__input {
290
+ width: 100%;
291
+ border-radius: 16px;
292
+ border: 1px solid rgba(94, 106, 136, 0.38);
293
+ background: rgba(13, 21, 38, 0.82);
294
+ color: #f8fafc;
295
+ padding: 0.85rem 1rem;
296
+ font-size: 1rem;
297
+ transition: border 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
298
+ }
299
+
300
+ .identity-login__input:focus {
301
+ outline: none;
302
+ border-color: rgba(96, 165, 250, 0.6);
303
+ background: rgba(11, 19, 34, 0.95);
304
+ box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.2);
305
+ }
306
+
307
+ .identity-login__input::placeholder {
308
+ color: rgba(148, 163, 184, 0.7);
309
+ }
310
+
311
+ .identity-login__options {
312
+ display: flex;
313
+ align-items: center;
314
+ justify-content: space-between;
315
+ gap: 1rem;
316
+ font-size: 0.85rem;
317
+ color: rgba(148, 163, 184, 0.85);
318
+ }
319
+
320
+ .identity-login__checkbox {
321
+ display: inline-flex;
322
+ align-items: center;
323
+ gap: 0.55rem;
324
+ cursor: pointer;
325
+ }
326
+
327
+ .identity-login__checkbox input {
328
+ width: 18px;
329
+ height: 18px;
330
+ border-radius: 6px;
331
+ border: 1px solid rgba(148, 163, 184, 0.45);
332
+ background: rgba(15, 23, 42, 0.6);
333
+ accent-color: var(--color-primary, #2563eb);
334
+ }
335
+
336
+ .identity-login__forgot {
337
+ color: var(--color-primary, #60a5fa);
338
+ font-weight: 600;
339
+ }
340
+
341
+ .identity-login__forgot:hover {
342
+ color: #fff;
343
+ }
344
+
345
+ .identity-login__submit {
346
+ display: inline-flex;
347
+ align-items: center;
348
+ justify-content: center;
349
+ border: none;
350
+ border-radius: 18px;
351
+ padding: 0.9rem 1rem;
352
+ font-weight: 600;
353
+ font-size: 1rem;
354
+ color: #fff;
355
+ cursor: pointer;
356
+ background: linear-gradient(135deg, var(--color-primary, #2563eb), var(--color-secondary, #7c3aed));
357
+ box-shadow: 0 18px 48px rgba(37, 99, 235, 0.35);
358
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
359
+ }
360
+
361
+ .identity-login__submit:hover {
362
+ transform: translateY(-2px);
363
+ box-shadow: 0 22px 60px rgba(37, 99, 235, 0.45);
364
+ }
365
+
366
+ .identity-login__submit:focus {
367
+ outline: none;
368
+ box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.28);
369
+ }
370
+
371
+ .identity-login__divider {
372
+ display: flex;
373
+ align-items: center;
374
+ gap: 1rem;
375
+ font-size: 0.75rem;
376
+ letter-spacing: 0.32em;
377
+ text-transform: uppercase;
378
+ color: rgba(148, 163, 184, 0.55);
379
+ }
380
+
381
+ .identity-login__divider::before,
382
+ .identity-login__divider::after {
383
+ content: '';
384
+ flex: 1;
385
+ height: 1px;
386
+ background: rgba(148, 163, 184, 0.18);
387
+ }
388
+
389
+ .identity-login__meta {
390
+ text-align: center;
391
+ font-size: 0.9rem;
392
+ color: rgba(148, 163, 184, 0.8);
393
+ }
394
+
395
+ .identity-login__meta a {
396
+ color: var(--color-primary, #60a5fa);
397
+ font-weight: 600;
398
+ }
399
+
400
+ .identity-login__meta a:hover {
401
+ color: #fff;
402
+ }
403
+
404
+ .identity-login__support {
405
+ text-align: center;
406
+ font-size: 0.75rem;
407
+ color: rgba(148, 163, 184, 0.65);
408
+ }
409
+
410
+ .identity-login__support a {
411
+ color: var(--color-primary, #60a5fa);
412
+ font-weight: 600;
413
+ }
414
+
415
+ .identity-login__support a:hover {
416
+ color: #fff;
417
+ }
418
+
419
+ /* ========================================
420
+ Responsive
421
+ ======================================== */
422
+
423
+ @media (max-width: 768px) {
424
+ .identity-login {
425
+ flex-direction: column;
426
+ border-radius: 24px;
427
+ margin: clamp(2.5rem, 6vh, 3.5rem) auto;
428
+ backdrop-filter: blur(16px);
429
+ }
430
+
431
+ .identity-login__panel,
432
+ .identity-login__form {
433
+ flex: 1 1 auto;
434
+ padding: clamp(2.1rem, 3vw, 2.75rem);
435
+ border-radius: 24px 24px 0 0;
436
+ }
437
+
438
+ .identity-login__form {
439
+ border-radius: 0 0 24px 24px;
440
+ }
441
+
442
+ .identity-login__options {
443
+ flex-direction: column;
444
+ align-items: flex-start;
445
+ }
446
+
447
+ .identity-login__divider {
448
+ justify-content: center;
449
+ }
450
+ }
451
+
452
+ @media (max-width: 520px) {
453
+ .identity-login {
454
+ width: calc(100vw - 2rem);
455
+ }
456
+
457
+ .identity-login__panel,
458
+ .identity-login__form {
459
+ padding: 2rem 1.75rem;
460
+ }
461
+
462
+ .identity-login__panel-title {
463
+ font-size: 1.9rem;
464
+ }
465
+ }
@@ -2,8 +2,9 @@ export * from './plugin.class.js'
2
2
  export * from './plugin.obj.js'
3
3
 
4
4
  // plugins:
5
- // ApiPlugin is exported separately to avoid bundling hono dependencies
5
+ // ApiPlugin and IdentityPlugin are exported separately to avoid bundling hono dependencies
6
6
  export { ApiPlugin } from './api/index.js'
7
+ export { IdentityPlugin } from './identity/index.js'
7
8
  export * from './audit.plugin.js'
8
9
  export * from './backup.plugin.js'
9
10
  export * from './cache.plugin.js'
@@ -13,6 +14,7 @@ export * from './fulltext.plugin.js'
13
14
  export * from './geo.plugin.js'
14
15
  export * from './metrics.plugin.js'
15
16
  export * from './ml.plugin.js'
17
+ // export * from './cloud-inventory.plugin.js' // Temporarily disabled due to AWS SDK paginator issues
16
18
  export * from './queue-consumer.plugin.js'
17
19
  export * from './relation.plugin.js'
18
20
  export * from './replicator.plugin.js'
@@ -28,3 +30,4 @@ export * from './backup/index.js'
28
30
  export * from './cache/index.js'
29
31
  export * from './replicators/index.js'
30
32
  export * from './consumers/index.js'
33
+ // export * from './cloud-inventory/index.js' // Temporarily disabled due to AWS SDK paginator issues
@@ -25,11 +25,13 @@ export class BaseModel {
25
25
  resource: config.resource,
26
26
  features: config.features || [],
27
27
  target: config.target,
28
+ minSamples: Math.max(1, config.minSamples ?? 10),
28
29
  modelConfig: {
29
30
  epochs: 50,
30
31
  batchSize: 32,
31
32
  learningRate: 0.01,
32
33
  validationSplit: 0.2,
34
+ shuffle: true,
33
35
  ...config.modelConfig
34
36
  },
35
37
  verbose: config.verbose || false
@@ -51,22 +53,36 @@ export class BaseModel {
51
53
  errors: 0
52
54
  };
53
55
 
54
- // Validate TensorFlow.js
55
- this._validateTensorFlow();
56
+ // TensorFlow will be loaded lazily on first use
57
+ this.tf = null;
58
+ this._tfValidated = false;
56
59
  }
57
60
 
58
61
  /**
59
- * Validate TensorFlow.js is installed
62
+ * Validate and load TensorFlow.js (lazy loading)
60
63
  * @private
61
64
  */
62
- _validateTensorFlow() {
65
+ async _validateTensorFlow() {
66
+ if (this._tfValidated) {
67
+ return; // Already validated and loaded
68
+ }
69
+
63
70
  try {
71
+ // Try CommonJS require first (works in most environments)
64
72
  this.tf = require('@tensorflow/tfjs-node');
65
- } catch (error) {
66
- throw new TensorFlowDependencyError(
67
- 'TensorFlow.js is not installed. Run: pnpm add @tensorflow/tfjs-node',
68
- { originalError: error.message }
69
- );
73
+ this._tfValidated = true;
74
+ } catch (requireError) {
75
+ // If require fails (e.g., Jest VM modules), try dynamic import
76
+ try {
77
+ const tfModule = await import('@tensorflow/tfjs-node');
78
+ this.tf = tfModule.default || tfModule;
79
+ this._tfValidated = true;
80
+ } catch (importError) {
81
+ throw new TensorFlowDependencyError(
82
+ 'TensorFlow.js is not installed. Run: pnpm add @tensorflow/tfjs-node',
83
+ { originalError: importError.message }
84
+ );
85
+ }
70
86
  }
71
87
  }
72
88
 
@@ -85,6 +101,11 @@ export class BaseModel {
85
101
  * @returns {Object} Training results
86
102
  */
87
103
  async train(data) {
104
+ // Validate TensorFlow on first use (lazy loading)
105
+ if (!this._tfValidated) {
106
+ await this._validateTensorFlow();
107
+ }
108
+
88
109
  try {
89
110
  if (!data || data.length === 0) {
90
111
  throw new InsufficientDataError('No training data provided', {
@@ -93,7 +114,9 @@ export class BaseModel {
93
114
  }
94
115
 
95
116
  // Validate minimum samples
96
- const minSamples = this.config.modelConfig.batchSize || 10;
117
+ const configuredMin = this.config.minSamples ?? 10;
118
+ const batchSize = this.config.modelConfig.batchSize || configuredMin;
119
+ const minSamples = Math.max(1, Math.min(configuredMin, batchSize));
97
120
  if (data.length < minSamples) {
98
121
  throw new InsufficientDataError(
99
122
  `Insufficient training data: ${data.length} samples (minimum: ${minSamples})`,
@@ -114,6 +137,7 @@ export class BaseModel {
114
137
  epochs: this.config.modelConfig.epochs,
115
138
  batchSize: this.config.modelConfig.batchSize,
116
139
  validationSplit: this.config.modelConfig.validationSplit,
140
+ shuffle: this.config.modelConfig.shuffle,
117
141
  verbose: this.config.verbose ? 1 : 0,
118
142
  callbacks: {
119
143
  onEpochEnd: (epoch, logs) => {
@@ -171,6 +195,11 @@ export class BaseModel {
171
195
  * @returns {Object} Prediction result
172
196
  */
173
197
  async predict(input) {
198
+ // Validate TensorFlow on first use (lazy loading)
199
+ if (!this._tfValidated) {
200
+ await this._validateTensorFlow();
201
+ }
202
+
174
203
  if (!this.isTrained) {
175
204
  throw new ModelNotTrainedError(`Model "${this.config.name}" is not trained yet`, {
176
205
  model: this.config.name
@@ -421,14 +450,34 @@ export class BaseModel {
421
450
  * @param {Object} data - Serialized model data
422
451
  */
423
452
  async import(data) {
424
- this.config = data.config;
425
- this.normalizer = data.normalizer;
426
- this.stats = data.stats;
427
- this.isTrained = data.isTrained;
453
+ if (!this._tfValidated) {
454
+ await this._validateTensorFlow();
455
+ }
456
+
457
+ this.config = {
458
+ ...this.config,
459
+ ...data.config,
460
+ modelConfig: {
461
+ ...this.config.modelConfig,
462
+ ...(data.config?.modelConfig || {})
463
+ }
464
+ };
465
+
466
+ if (data.config?.minSamples) {
467
+ this.config.minSamples = Math.max(1, data.config.minSamples);
468
+ }
469
+
470
+ this.normalizer = data.normalizer || this.normalizer;
471
+ this.stats = data.stats || this.stats;
472
+ this.isTrained = data.isTrained ?? false;
473
+
474
+ if (this.model && typeof this.model.dispose === 'function') {
475
+ this.model.dispose();
476
+ }
428
477
 
429
478
  if (data.model) {
430
- // Note: Actual model reconstruction depends on the model type
431
- // This is a placeholder and should be overridden by subclasses
479
+ this.model = await this.tf.models.modelFromJSON(data.model);
480
+ } else {
432
481
  this.buildModel();
433
482
  }
434
483
  }
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { BaseModel } from './base-model.class.js';
9
- import { ModelConfigError, DataValidationError } from '../ml.errors.js';
9
+ import { ModelConfigError, DataValidationError, ModelNotTrainedError } from '../ml.errors.js';
10
10
 
11
11
  export class ClassificationModel extends BaseModel {
12
12
  constructor(config = {}) {
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { BaseModel } from './base-model.class.js';
9
- import { ModelConfigError, DataValidationError, InsufficientDataError } from '../ml.errors.js';
9
+ import { ModelConfigError, DataValidationError, InsufficientDataError, ModelNotTrainedError } from '../ml.errors.js';
10
10
 
11
11
  export class TimeSeriesModel extends BaseModel {
12
12
  constructor(config = {}) {
@@ -22,6 +22,8 @@ export class TimeSeriesModel extends BaseModel {
22
22
  recurrentDropout: config.modelConfig?.recurrentDropout || 0.2
23
23
  };
24
24
 
25
+ this.config.modelConfig.shuffle = config.modelConfig?.shuffle ?? false;
26
+
25
27
  // Validate lookback
26
28
  if (this.config.modelConfig.lookback < 2) {
27
29
  throw new ModelConfigError(