@astram/guard 0.4.0 → 0.5.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 (64) hide show
  1. package/dist/auth/auth.module.d.ts +6 -0
  2. package/dist/auth/auth.module.d.ts.map +1 -1
  3. package/dist/auth/auth.module.js +106 -15
  4. package/dist/auth/tokens.d.ts +9 -0
  5. package/dist/auth/tokens.d.ts.map +1 -1
  6. package/dist/auth/tokens.js +10 -1
  7. package/dist/authentication/log-email-sender.d.ts +13 -0
  8. package/dist/authentication/log-email-sender.d.ts.map +1 -0
  9. package/dist/authentication/log-email-sender.js +13 -0
  10. package/dist/authentication/mariadb-credential-writer.d.ts +16 -0
  11. package/dist/authentication/mariadb-credential-writer.d.ts.map +1 -0
  12. package/dist/authentication/mariadb-credential-writer.js +19 -0
  13. package/dist/authentication/mariadb-mfa-device-store.d.ts +16 -0
  14. package/dist/authentication/mariadb-mfa-device-store.d.ts.map +1 -0
  15. package/dist/authentication/mariadb-mfa-device-store.js +99 -0
  16. package/dist/authentication/mariadb-or-totp-verifier.d.ts +31 -0
  17. package/dist/authentication/mariadb-or-totp-verifier.d.ts.map +1 -0
  18. package/dist/authentication/mariadb-or-totp-verifier.js +48 -0
  19. package/dist/authentication/mariadb-profile-resolver.d.ts +24 -0
  20. package/dist/authentication/mariadb-profile-resolver.d.ts.map +1 -0
  21. package/dist/authentication/mariadb-profile-resolver.js +54 -0
  22. package/dist/authentication/mariadb-profile-writer.d.ts +18 -0
  23. package/dist/authentication/mariadb-profile-writer.d.ts.map +1 -0
  24. package/dist/authentication/mariadb-profile-writer.js +36 -0
  25. package/dist/authentication/mariadb-recovery-code-store.d.ts +47 -0
  26. package/dist/authentication/mariadb-recovery-code-store.d.ts.map +1 -0
  27. package/dist/authentication/mariadb-recovery-code-store.js +53 -0
  28. package/dist/authentication/mariadb-reset-token-store.d.ts +26 -0
  29. package/dist/authentication/mariadb-reset-token-store.d.ts.map +1 -0
  30. package/dist/authentication/mariadb-reset-token-store.js +43 -0
  31. package/dist/authentication/mariadb-session-sweeper.d.ts +33 -0
  32. package/dist/authentication/mariadb-session-sweeper.d.ts.map +1 -0
  33. package/dist/authentication/mariadb-session-sweeper.js +60 -0
  34. package/dist/config/app-config.d.ts +5 -7
  35. package/dist/config/app-config.d.ts.map +1 -1
  36. package/dist/config/app-config.js +24 -57
  37. package/dist/identity/auth-facts.controller.d.ts +24 -0
  38. package/dist/identity/auth-facts.controller.d.ts.map +1 -0
  39. package/dist/identity/auth-facts.controller.js +106 -0
  40. package/dist/identity/identity-actions.d.ts +22 -0
  41. package/dist/identity/identity-actions.d.ts.map +1 -0
  42. package/dist/identity/identity-actions.js +32 -0
  43. package/dist/identity/module-registry.bridge.d.ts +4 -0
  44. package/dist/identity/module-registry.bridge.d.ts.map +1 -0
  45. package/dist/identity/module-registry.bridge.js +43 -0
  46. package/dist/identity/permission-facts.adapter.d.ts +47 -0
  47. package/dist/identity/permission-facts.adapter.d.ts.map +1 -0
  48. package/dist/identity/permission-facts.adapter.js +108 -0
  49. package/dist/index.d.ts +3 -0
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +20 -2
  52. package/dist/migrations-dir.d.ts +2 -0
  53. package/dist/migrations-dir.d.ts.map +1 -0
  54. package/dist/migrations-dir.js +13 -0
  55. package/dist/module/shield-tokens.d.ts +6 -1
  56. package/dist/module/shield-tokens.d.ts.map +1 -1
  57. package/dist/module/shield-tokens.js +7 -1
  58. package/dist/module/shield.module.d.ts +17 -0
  59. package/dist/module/shield.module.d.ts.map +1 -1
  60. package/dist/module/shield.module.js +12 -0
  61. package/migrations/1710000000006-CreateAuthzSchema.cjs +53 -0
  62. package/migrations/1710000000007-ExtendPermissionAndRolePermission.cjs +66 -0
  63. package/migrations/1710000000008-CreateAccessBundleTables.cjs +49 -0
  64. package/package.json +5 -3
@@ -173,6 +173,12 @@ export interface AuthModuleOptions {
173
173
  * behavior unchanged). Only meaningful under `auth: true`.
174
174
  */
175
175
  readonly rootIds?: readonly string[];
176
+ /** The identity permission-key universe (for auth-facts wildcard expansion). Supplied by the
177
+ * consumer from `@astram/identity`'s `identityAuthActionKeys` (guard never imports identity).
178
+ * Absent → the adapter gets `[]` (wildcards expand to nothing; exact grants still resolve). */
179
+ readonly identityActionKeys?: readonly string[];
180
+ /** The control-plane subset (root gets these grant-independently). From `identityControlPlaneKeys`. */
181
+ readonly identityControlPlaneKeys?: readonly string[];
176
182
  /**
177
183
  * OPTIONAL eager session-facts projector (ADR-0011, the {@link SessionFactsProjector}
178
184
  * port). Threaded exactly like the other seams: supplied → bound app-wide under
@@ -1 +1 @@
1
- {"version":3,"file":"auth.module.d.ts","sourceRoot":"","sources":["../../src/auth/auth.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+DG;AAEH,OAAO,EAAU,KAAK,aAAa,EAAiB,MAAM,gBAAgB,CAAC;AAa3E,OAAO,KAAK,EAGV,kBAAkB,EAOlB,QAAQ,EACR,cAAc,EACf,MAAM,cAAc,CAAC;AAkDtB,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAEhF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AACpE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAIvD;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,EAAE,QACqE,CAAC;AAE7F;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,cAAc,GAAG,IAAI,GAAG,SAAS,EAC3C,eAAe,EAAE,cAAc,GAC9B,cAAc,CAEhB;AAED;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,cAAc,CAAC,CAGvE;AAyUD;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;;OAMG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACjD;;;;;;OAMG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,eAAe,CAAC;IAC3C;;;;;;OAMG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IAC7C,QAAQ,CAAC,eAAe,CAAC,EAAE,eAAe,CAAC;IAC3C,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,CAAC;IACnC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IAC/C,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC;IACvC,QAAQ,CAAC,cAAc,CAAC,EAAE,cAAc,CAAC;IACzC;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;IAC/B;;;;;OAKG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,cAAc,CAAC;IACzC;;;;;;;;OAQG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,cAAc,CAAC;IACvC;;;;;;;;OAQG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC;;;;;;;OAOG;IACH,QAAQ,CAAC,qBAAqB,CAAC,EAAE,qBAAqB,CAAC;CACxD;AAgED;;;;;;GAMG;AACH,qBACa,UAAU;IACrB;;;;;OAKG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,iBAAsB,GAAG,aAAa;CA6F/D"}
1
+ {"version":3,"file":"auth.module.d.ts","sourceRoot":"","sources":["../../src/auth/auth.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+DG;AAEH,OAAO,EAAU,KAAK,aAAa,EAAiB,MAAM,gBAAgB,CAAC;AAa3E,OAAO,KAAK,EAGV,kBAAkB,EAOlB,QAAQ,EACR,cAAc,EACf,MAAM,cAAc,CAAC;AA8DtB,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAEhF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AACpE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAIvD;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,EAAE,QACqE,CAAC;AAE7F;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,cAAc,GAAG,IAAI,GAAG,SAAS,EAC3C,eAAe,EAAE,cAAc,GAC9B,cAAc,CAEhB;AAED;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,cAAc,CAAC,CAGvE;AAiVD;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;;OAMG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACjD;;;;;;OAMG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,eAAe,CAAC;IAC3C;;;;;;OAMG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IAC7C,QAAQ,CAAC,eAAe,CAAC,EAAE,eAAe,CAAC;IAC3C,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,CAAC;IACnC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IAC/C,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC;IACvC,QAAQ,CAAC,cAAc,CAAC,EAAE,cAAc,CAAC;IACzC;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;IAC/B;;;;;OAKG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,cAAc,CAAC;IACzC;;;;;;;;OAQG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,cAAc,CAAC;IACvC;;;;;;;;OAQG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC;;oGAEgG;IAChG,QAAQ,CAAC,kBAAkB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAChD,uGAAuG;IACvG,QAAQ,CAAC,wBAAwB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtD;;;;;;;OAOG;IACH,QAAQ,CAAC,qBAAqB,CAAC,EAAE,qBAAqB,CAAC;CACxD;AAuHD;;;;;;GAMG;AACH,qBACa,UAAU;IACrB;;;;;OAKG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,iBAAsB,GAAG,aAAa;CAkH/D"}
@@ -113,6 +113,17 @@ const app_config_js_1 = require("../config/app-config.js");
113
113
  const config_module_js_1 = require("../config/config.module.js");
114
114
  const argon2id_password_hasher_js_1 = require("../authentication/argon2id-password-hasher.js");
115
115
  const mariadb_credential_resolver_js_1 = require("../authentication/mariadb-credential-resolver.js");
116
+ const mariadb_profile_resolver_js_1 = require("../authentication/mariadb-profile-resolver.js");
117
+ const mariadb_credential_writer_js_1 = require("../authentication/mariadb-credential-writer.js");
118
+ const mariadb_profile_writer_js_1 = require("../authentication/mariadb-profile-writer.js");
119
+ const mariadb_reset_token_store_js_1 = require("../authentication/mariadb-reset-token-store.js");
120
+ const mariadb_recovery_code_store_js_1 = require("../authentication/mariadb-recovery-code-store.js");
121
+ const mariadb_mfa_device_store_js_1 = require("../authentication/mariadb-mfa-device-store.js");
122
+ const mariadb_session_sweeper_js_1 = require("../authentication/mariadb-session-sweeper.js");
123
+ const log_email_sender_js_1 = require("../authentication/log-email-sender.js");
124
+ const mariadb_or_totp_verifier_js_1 = require("../authentication/mariadb-or-totp-verifier.js");
125
+ const permission_facts_adapter_js_1 = require("../identity/permission-facts.adapter.js");
126
+ const auth_facts_controller_js_1 = require("../identity/auth-facts.controller.js");
116
127
  const index_js_1 = require("../session/index.js");
117
128
  const persistence_module_js_1 = require("./persistence.module.js");
118
129
  const auth_controller_js_1 = require("./auth.controller.js");
@@ -380,9 +391,15 @@ const sessionAuthorityProvider = {
380
391
  const authenticatorProvider = {
381
392
  provide: tokens_js_1.SHIELD_AUTHENTICATOR,
382
393
  // async: loads the core factories lazily (CJS→ESM, see loadCore()); Nest awaits it.
383
- useFactory: async (hasher, resolver, totpOverride, rootIds) => {
394
+ useFactory: async (hasher, resolver, totpOverride, rootIds, mfaDeviceStore) => {
384
395
  const { createAuthenticator, createIdentityBroker, createTotpFactorVerifier } = await loadCore();
385
- const totp = resolveTotpVerifier(totpOverride, createTotpFactorVerifier({ hmacSha1: exports.hmacSha1 }));
396
+ // The login totp verifier is ALWAYS the OR-across-devices wrapper: it delegates
397
+ // to the resolved base (the consumer `totpOverride` when supplied, else the
398
+ // framework's single-secret RFC-6238 verifier) per device secret, so a
399
+ // principal's multiple confirmed devices OR-match. A consumer override still wins
400
+ // (it's the base inside the OR-wrap); multi-device works by default.
401
+ const base = resolveTotpVerifier(totpOverride, createTotpFactorVerifier({ hmacSha1: exports.hmacSha1 }));
402
+ const totp = new mariadb_or_totp_verifier_js_1.OrTotpVerifier(base, mfaDeviceStore);
386
403
  return createAuthenticator({
387
404
  hasher,
388
405
  resolver,
@@ -399,6 +416,7 @@ const authenticatorProvider = {
399
416
  tokens_js_1.SHIELD_CREDENTIAL_RESOLVER,
400
417
  tokens_js_1.SHIELD_TOTP_VERIFIER_OVERRIDE,
401
418
  tokens_js_1.SHIELD_ROOT_IDS,
419
+ tokens_js_1.SHIELD_MFA_DEVICE_STORE,
402
420
  ],
403
421
  };
404
422
  /**
@@ -438,13 +456,28 @@ const totpVerifierProvider = {
438
456
  },
439
457
  };
440
458
  /**
441
- * Bind the OPTIONAL current-user profile resolver. When the consumer supplies one it
442
- * is a value provider under {@link SHIELD_PROFILE_RESOLVER}; when absent we bind
443
- * `null` so the `@Optional()` injection in the controller resolves cleanly to
444
- * "no seam" (→ `/auth/me` returns 501). Always provided so the DI token exists.
459
+ * Bind the current-user profile resolver backing `GET /auth/me`.
460
+ *
461
+ * DEFAULT (no consumer override): a `useFactory` that constructs the framework's
462
+ * {@link MariaDbProfileResolver} injected with the live {@link SHIELD_DB_POOL} — it
463
+ * names the signed-in user/company/roles from the framework-owned `@astram/identity`
464
+ * org tables in `shield_app`. This is the paved-road default (mirrors
465
+ * {@link makeCredentialResolverProvider}): `/auth/me` resolves out of the box, so the
466
+ * historical honest-501 is now the fail-closed path only when the resolver truly can't
467
+ * name a profile (deleted user → 404; store outage → 500), never the default.
468
+ *
469
+ * OVERRIDE: when the consumer supplies a `profileResolver` through
470
+ * {@link ShieldModuleOptions.profileResolver} → {@link AuthModule.forRoot}, the token
471
+ * binds that instance verbatim so `/auth/me` reads the consumer's OWN org schema.
445
472
  */
446
473
  function makeProfileResolverProvider(override) {
447
- return { provide: tokens_js_1.SHIELD_PROFILE_RESOLVER, useValue: override ?? null };
474
+ return override
475
+ ? { provide: tokens_js_1.SHIELD_PROFILE_RESOLVER, useValue: override }
476
+ : {
477
+ provide: tokens_js_1.SHIELD_PROFILE_RESOLVER,
478
+ useFactory: (pool) => new mariadb_profile_resolver_js_1.MariaDbProfileResolver(pool),
479
+ inject: [tokens_js_1.SHIELD_DB_POOL],
480
+ };
448
481
  }
449
482
  /**
450
483
  * Bind each OPTIONAL account/credential-surface seam. Same pattern as
@@ -453,25 +486,63 @@ function makeProfileResolverProvider(override) {
453
486
  * resolves cleanly to "no seam" (→ 501). Always provided so the DI token exists.
454
487
  */
455
488
  function makeCredentialWriterProvider(override) {
456
- return { provide: tokens_js_1.SHIELD_CREDENTIAL_WRITER, useValue: override ?? null };
489
+ return override
490
+ ? { provide: tokens_js_1.SHIELD_CREDENTIAL_WRITER, useValue: override }
491
+ : {
492
+ provide: tokens_js_1.SHIELD_CREDENTIAL_WRITER,
493
+ useFactory: (pool) => new mariadb_credential_writer_js_1.MariaDbCredentialWriter(pool),
494
+ inject: [tokens_js_1.SHIELD_DB_POOL],
495
+ };
457
496
  }
458
497
  function makeResetTokenStoreProvider(override) {
459
- return { provide: tokens_js_1.SHIELD_RESET_TOKEN_STORE, useValue: override ?? null };
498
+ return override
499
+ ? { provide: tokens_js_1.SHIELD_RESET_TOKEN_STORE, useValue: override }
500
+ : {
501
+ provide: tokens_js_1.SHIELD_RESET_TOKEN_STORE,
502
+ useFactory: (pool) => new mariadb_reset_token_store_js_1.MariaDbResetTokenStore(pool),
503
+ inject: [tokens_js_1.SHIELD_DB_POOL],
504
+ };
460
505
  }
461
506
  function makeEmailSenderProvider(override) {
462
- return { provide: tokens_js_1.SHIELD_EMAIL_SENDER, useValue: override ?? null };
507
+ return override
508
+ ? { provide: tokens_js_1.SHIELD_EMAIL_SENDER, useValue: override }
509
+ : { provide: tokens_js_1.SHIELD_EMAIL_SENDER, useFactory: () => new log_email_sender_js_1.LogEmailSender() };
463
510
  }
464
511
  function makeRecoveryCodeStoreProvider(override) {
465
- return { provide: tokens_js_1.SHIELD_RECOVERY_CODE_STORE, useValue: override ?? null };
512
+ return override
513
+ ? { provide: tokens_js_1.SHIELD_RECOVERY_CODE_STORE, useValue: override }
514
+ : {
515
+ provide: tokens_js_1.SHIELD_RECOVERY_CODE_STORE,
516
+ useFactory: (pool, hasher) => new mariadb_recovery_code_store_js_1.MariaDbRecoveryCodeStore(pool, hasher),
517
+ inject: [tokens_js_1.SHIELD_DB_POOL, argon2id_password_hasher_js_1.SHIELD_PASSWORD_HASHER],
518
+ };
466
519
  }
467
520
  function makeProfileWriterProvider(override) {
468
- return { provide: tokens_js_1.SHIELD_PROFILE_WRITER, useValue: override ?? null };
521
+ return override
522
+ ? { provide: tokens_js_1.SHIELD_PROFILE_WRITER, useValue: override }
523
+ : {
524
+ provide: tokens_js_1.SHIELD_PROFILE_WRITER,
525
+ useFactory: (pool) => new mariadb_profile_writer_js_1.MariaDbProfileWriter(pool),
526
+ inject: [tokens_js_1.SHIELD_DB_POOL],
527
+ };
469
528
  }
470
529
  function makeSessionSweeperProvider(override) {
471
- return { provide: tokens_js_1.SHIELD_SESSION_SWEEPER, useValue: override ?? null };
530
+ return override
531
+ ? { provide: tokens_js_1.SHIELD_SESSION_SWEEPER, useValue: override }
532
+ : {
533
+ provide: tokens_js_1.SHIELD_SESSION_SWEEPER,
534
+ useFactory: (pool, binder) => new mariadb_session_sweeper_js_1.MariaDbSessionSweeper(pool, binder),
535
+ inject: [tokens_js_1.SHIELD_DB_POOL, tokens_js_2.SHIELD_SESSION_TOKEN_BINDER],
536
+ };
472
537
  }
473
538
  function makeMfaDeviceStoreProvider(override) {
474
- return { provide: tokens_js_1.SHIELD_MFA_DEVICE_STORE, useValue: override ?? null };
539
+ return override
540
+ ? { provide: tokens_js_1.SHIELD_MFA_DEVICE_STORE, useValue: override }
541
+ : {
542
+ provide: tokens_js_1.SHIELD_MFA_DEVICE_STORE,
543
+ useFactory: (pool) => new mariadb_mfa_device_store_js_1.MariaDbMfaDeviceStore(pool),
544
+ inject: [tokens_js_1.SHIELD_DB_POOL],
545
+ };
475
546
  }
476
547
  function makeAuditSinkProvider(override) {
477
548
  return { provide: tokens_js_1.SHIELD_AUDIT_SINK, useValue: override ?? null };
@@ -511,6 +582,13 @@ let AuthModule = AuthModule_1 = class AuthModule {
511
582
  * EXACTLY unchanged (zero behavior change for existing consumers).
512
583
  */
513
584
  static forRoot(options = {}) {
585
+ // The identity permission-key universe for the auth-facts projection. SAFE
586
+ // DEFAULT: absent → `[]` (the adapter's wildcard expansion yields nothing, exact
587
+ // grants still resolve — never a crash). The consumer supplies these from
588
+ // `@astram/identity`'s `identityAuthActionKeys` / `identityControlPlaneKeys`
589
+ // through `ShieldModule.forRoot` — guard NEVER imports `@astram/identity`.
590
+ const identityActionKeys = options.identityActionKeys ?? [];
591
+ const identityControlPlaneKeys = options.identityControlPlaneKeys ?? [];
514
592
  return {
515
593
  module: AuthModule_1,
516
594
  imports: [persistence_module_js_1.PersistenceModule.forRoot()],
@@ -518,7 +596,9 @@ let AuthModule = AuthModule_1 = class AuthModule {
518
596
  // Team C's four `@Public()` /auth/* endpoints. Registered here (integration
519
597
  // wiring) so the routes exist the moment auth is opted in — the controller
520
598
  // injects the two composed engines (below) + PendingMfaStore.
521
- controllers: [auth_controller_js_1.AuthController, account_controller_js_1.AccountController],
599
+ // AuthFactsController serves the framework-default `/identity/auth-facts` +
600
+ // `/identity/permissions-version` (over the SHIELD_PERMISSION_FACTS adapter below).
601
+ controllers: [auth_controller_js_1.AuthController, account_controller_js_1.AccountController, auth_facts_controller_js_1.AuthFactsController],
522
602
  providers: [
523
603
  passwordHasherProvider,
524
604
  makeCredentialResolverProvider(options.credentialResolver),
@@ -545,6 +625,17 @@ let AuthModule = AuthModule_1 = class AuthModule {
545
625
  // The eager session-facts projector (ADR-0011; null when absent → the auth
546
626
  // controller omits cachedFacts → gate 5 DB-reloads). Consumed by AuthController.
547
627
  makeSessionFactsProjectorProvider(options.sessionFactsProjector),
628
+ // The framework-DEFAULT permission-facts adapter backing AuthFactsController
629
+ // (`/identity/auth-facts` + `/identity/permissions-version`). Reads role grants +
630
+ // per-tenant version from the pool; expands `identity:*` wildcards against the
631
+ // supplied action-key universe (empty when the consumer didn't pass keys — wildcards
632
+ // then expand to nothing, exact grants still resolve) and unions root's control-plane
633
+ // keys. The keys are CLOSED-OVER option values (never a guard→identity import).
634
+ {
635
+ provide: tokens_js_1.SHIELD_PERMISSION_FACTS,
636
+ useFactory: (pool) => new permission_facts_adapter_js_1.MariaDbPermissionFactsAdapter(pool, () => identityActionKeys, identityControlPlaneKeys),
637
+ inject: [tokens_js_1.SHIELD_DB_POOL],
638
+ },
548
639
  // The reset-link base URL value the forgot-password handler composes into the
549
640
  // emailed link (framework-owned URL composition; EmailSender stays dumb).
550
641
  resetLinkBaseUrlProvider,
@@ -233,4 +233,13 @@ export declare const SHIELD_FORGOT_HINT_LIMITER: unique symbol;
233
233
  * `req.ip`.
234
234
  */
235
235
  export declare const SHIELD_RESET_LIMITER: unique symbol;
236
+ /**
237
+ * The `MariaDbPermissionFactsAdapter` (`../identity/permission-facts.adapter.ts`) — the
238
+ * framework-DEFAULT source of a caller's coarse permission facts, backing
239
+ * `GET /identity/auth-facts` + `GET /identity/permissions-version`
240
+ * ({@link AuthFactsController}). Consumed `@Optional()`: absent → those routes fail
241
+ * closed with a `503 authorization_unavailable` (never a fabricated empty projection
242
+ * or version, which would fail-OPEN).
243
+ */
244
+ export declare const SHIELD_PERMISSION_FACTS: unique symbol;
236
245
  //# sourceMappingURL=tokens.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/auth/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,+CAA+C,CAAC;AAEvF;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,eAA2B,CAAC;AAEvD;;;;GAIG;AACH,eAAO,MAAM,YAAY,eAAyB,CAAC;AAEnD;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAE/E;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,eAAoC,CAAC;AAEzE;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,eAAiC,CAAC;AAEnE;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,eAAqC,CAAC;AAW3E;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,eAAqC,CAAC;AAE3E;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,eAAqC,CAAC;AAE3E;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,eAAgC,CAAC;AAEjE;;;GAGG;AACH,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAE/E;;;GAGG;AACH,eAAO,MAAM,qBAAqB,eAAkC,CAAC;AAErE;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,eAA8B,CAAC;AAE7D;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,eAAoC,CAAC;AAEzE;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,eAAmC,CAAC;AAEvE;;;;;;;GAOG;AACH,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAE/E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,oBAAoB,eAAiC,CAAC;AAEnE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,6BAA6B,eAA0C,CAAC;AAErF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,eAAe,eAA4B,CAAC;AAEzD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,8BAA8B,eAA2C,CAAC;AAUvF;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,eAAiC,CAAC;AAEnE;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,eAAoC,CAAC;AAEzE;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB,eAAkC,CAAC;AAErE;;;;;;;GAOG;AACH,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAE/E;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,eAAiC,CAAC"}
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/auth/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,+CAA+C,CAAC;AAEvF;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,eAA2B,CAAC;AAEvD;;;;GAIG;AACH,eAAO,MAAM,YAAY,eAAyB,CAAC;AAEnD;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAE/E;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,eAAoC,CAAC;AAEzE;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,eAAiC,CAAC;AAEnE;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,eAAqC,CAAC;AAW3E;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,eAAqC,CAAC;AAE3E;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,eAAqC,CAAC;AAE3E;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,eAAgC,CAAC;AAEjE;;;GAGG;AACH,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAE/E;;;GAGG;AACH,eAAO,MAAM,qBAAqB,eAAkC,CAAC;AAErE;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,eAA8B,CAAC;AAE7D;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,eAAoC,CAAC;AAEzE;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,eAAmC,CAAC;AAEvE;;;;;;;GAOG;AACH,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAE/E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,oBAAoB,eAAiC,CAAC;AAEnE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,6BAA6B,eAA0C,CAAC;AAErF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,eAAe,eAA4B,CAAC;AAEzD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,8BAA8B,eAA2C,CAAC;AAUvF;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,eAAiC,CAAC;AAEnE;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,eAAoC,CAAC;AAEzE;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB,eAAkC,CAAC;AAErE;;;;;;;GAOG;AACH,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAE/E;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,eAAiC,CAAC;AAEnE;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,eAAoC,CAAC"}
@@ -23,7 +23,7 @@
23
23
  * ADR-0003: no framework/DB/crypto imports here — just Symbols (+ one re-export).
24
24
  */
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.SHIELD_RESET_LIMITER = exports.SHIELD_FORGOT_HINT_LIMITER = exports.SHIELD_FORGOT_LIMITER = exports.SHIELD_RECOVERY_LIMITER = exports.SHIELD_LOGIN_LIMITER = exports.SHIELD_SESSION_FACTS_PROJECTOR = exports.SHIELD_ROOT_IDS = exports.SHIELD_TOTP_VERIFIER_OVERRIDE = exports.SHIELD_TOTP_VERIFIER = exports.SHIELD_RESET_LINK_BASE_URL = exports.SHIELD_SESSION_SWEEPER = exports.SHIELD_MFA_DEVICE_STORE = exports.SHIELD_AUDIT_SINK = exports.SHIELD_PROFILE_WRITER = exports.SHIELD_RECOVERY_CODE_STORE = exports.SHIELD_EMAIL_SENDER = exports.SHIELD_RESET_TOKEN_STORE = exports.SHIELD_CREDENTIAL_WRITER = exports.SHIELD_SESSION_AUTHORITY = exports.SHIELD_AUTHENTICATOR = exports.SHIELD_PROFILE_RESOLVER = exports.SHIELD_CREDENTIAL_RESOLVER = exports.SHIELD_REDIS = exports.SHIELD_DB_POOL = exports.SHIELD_PASSWORD_HASHER = void 0;
26
+ exports.SHIELD_PERMISSION_FACTS = exports.SHIELD_RESET_LIMITER = exports.SHIELD_FORGOT_HINT_LIMITER = exports.SHIELD_FORGOT_LIMITER = exports.SHIELD_RECOVERY_LIMITER = exports.SHIELD_LOGIN_LIMITER = exports.SHIELD_SESSION_FACTS_PROJECTOR = exports.SHIELD_ROOT_IDS = exports.SHIELD_TOTP_VERIFIER_OVERRIDE = exports.SHIELD_TOTP_VERIFIER = exports.SHIELD_RESET_LINK_BASE_URL = exports.SHIELD_SESSION_SWEEPER = exports.SHIELD_MFA_DEVICE_STORE = exports.SHIELD_AUDIT_SINK = exports.SHIELD_PROFILE_WRITER = exports.SHIELD_RECOVERY_CODE_STORE = exports.SHIELD_EMAIL_SENDER = exports.SHIELD_RESET_TOKEN_STORE = exports.SHIELD_CREDENTIAL_WRITER = exports.SHIELD_SESSION_AUTHORITY = exports.SHIELD_AUTHENTICATOR = exports.SHIELD_PROFILE_RESOLVER = exports.SHIELD_CREDENTIAL_RESOLVER = exports.SHIELD_REDIS = exports.SHIELD_DB_POOL = exports.SHIELD_PASSWORD_HASHER = void 0;
27
27
  var argon2id_password_hasher_js_1 = require("../authentication/argon2id-password-hasher.js");
28
28
  Object.defineProperty(exports, "SHIELD_PASSWORD_HASHER", { enumerable: true, get: function () { return argon2id_password_hasher_js_1.SHIELD_PASSWORD_HASHER; } });
29
29
  /**
@@ -252,3 +252,12 @@ exports.SHIELD_FORGOT_HINT_LIMITER = Symbol('SHIELD_FORGOT_HINT_LIMITER');
252
252
  * `req.ip`.
253
253
  */
254
254
  exports.SHIELD_RESET_LIMITER = Symbol('SHIELD_RESET_LIMITER');
255
+ /**
256
+ * The `MariaDbPermissionFactsAdapter` (`../identity/permission-facts.adapter.ts`) — the
257
+ * framework-DEFAULT source of a caller's coarse permission facts, backing
258
+ * `GET /identity/auth-facts` + `GET /identity/permissions-version`
259
+ * ({@link AuthFactsController}). Consumed `@Optional()`: absent → those routes fail
260
+ * closed with a `503 authorization_unavailable` (never a fabricated empty projection
261
+ * or version, which would fail-OPEN).
262
+ */
263
+ exports.SHIELD_PERMISSION_FACTS = Symbol('SHIELD_PERMISSION_FACTS');
@@ -0,0 +1,13 @@
1
+ /**
2
+ * LogEmailSender — the framework DEV-DEFAULT EmailSender. Does NOT deliver mail; it LOGS the outgoing
3
+ * message (recipient + subject + link) so forgot/reset-password works end-to-end in development with no
4
+ * external provider. A real SMTP/SendGrid sender stays a consumer override for production. Never throws
5
+ * (a logging failure must not fail the forgot-password flow, which is fail-closed/no-op-safe by design).
6
+ */
7
+ import type { EmailSender, EmailMessage } from '../auth/ports/email-sender.js';
8
+ export declare class LogEmailSender implements EmailSender {
9
+ private readonly log;
10
+ constructor(log?: (msg: string) => void);
11
+ send(message: EmailMessage): Promise<void>;
12
+ }
13
+ //# sourceMappingURL=log-email-sender.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log-email-sender.d.ts","sourceRoot":"","sources":["../../src/authentication/log-email-sender.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAE/E,qBAAa,cAAe,YAAW,WAAW;IACpC,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,GAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAA4B;IACzE,IAAI,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;CAGjD"}
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LogEmailSender = void 0;
4
+ class LogEmailSender {
5
+ log;
6
+ constructor(log = (m) => console.log(m)) {
7
+ this.log = log;
8
+ }
9
+ async send(message) {
10
+ this.log(`[astram][dev-email] (NOT delivered — dev LogEmailSender) to=${message.to} subject=${message.subject}\n${message.text ?? message.html ?? ''}`);
11
+ }
12
+ }
13
+ exports.LogEmailSender = LogEmailSender;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * MariaDbCredentialWriter — the framework-DEFAULT CredentialWriter (backs POST /account/change-password
3
+ * + password reset). Writes ONLY the Argon2id encoded hash to shield_identity.identity_credential; never
4
+ * plaintext, never logs the hash. Auto-wired on auth:true; a consumer override still wins. Mirrors
5
+ * MariaDbCredentialResolver (same pool, same table).
6
+ */
7
+ import type { Pool } from 'mariadb';
8
+ import type { CredentialWriter } from '../auth/ports/credential-writer.js';
9
+ export declare class MariaDbCredentialWriter implements CredentialWriter {
10
+ private readonly pool;
11
+ private readonly now;
12
+ constructor(pool: Pool, now?: () => number);
13
+ setPassword(principalId: string, passwordHash: string): Promise<void>;
14
+ getPasswordHash(principalId: string): Promise<string | null>;
15
+ }
16
+ //# sourceMappingURL=mariadb-credential-writer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mariadb-credential-writer.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-credential-writer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;AAI3E,qBAAa,uBAAwB,YAAW,gBAAgB;IAClD,OAAO,CAAC,QAAQ,CAAC,IAAI;IAAQ,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAhC,IAAI,EAAE,IAAI,EAAmB,GAAG,GAAE,MAAM,MAAiB;IAEhF,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOrE,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;CAOnE"}
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MariaDbCredentialWriter = void 0;
4
+ class MariaDbCredentialWriter {
5
+ pool;
6
+ now;
7
+ constructor(pool, now = Date.now) {
8
+ this.pool = pool;
9
+ this.now = now;
10
+ }
11
+ async setPassword(principalId, passwordHash) {
12
+ await this.pool.query('UPDATE `shield_identity`.`identity_credential` SET ic_password_hash = ?, ic_updated_at_ms = ? WHERE ic_principal_id = ?', [passwordHash, this.now(), principalId]);
13
+ }
14
+ async getPasswordHash(principalId) {
15
+ const rows = (await this.pool.query('SELECT ic_password_hash FROM `shield_identity`.`identity_credential` WHERE ic_principal_id = ? LIMIT 1', [principalId]));
16
+ return rows[0]?.ic_password_hash ?? null;
17
+ }
18
+ }
19
+ exports.MariaDbCredentialWriter = MariaDbCredentialWriter;
@@ -0,0 +1,16 @@
1
+ import type { Pool } from 'mariadb';
2
+ import type { MfaDevice, MfaDeviceStore } from '../auth/ports/mfa-device-store.js';
3
+ export declare class MariaDbMfaDeviceStore implements MfaDeviceStore {
4
+ private readonly pool;
5
+ constructor(pool: Pool);
6
+ listAll(principalId: string): Promise<MfaDevice[]>;
7
+ listConfirmedSecrets(principalId: string): Promise<string[]>;
8
+ getSecret(principalId: string, deviceId: string): Promise<string | null>;
9
+ begin(principalId: string, name: string, secret: string): Promise<string>;
10
+ confirm(principalId: string, deviceId: string): Promise<boolean>;
11
+ rename(principalId: string, deviceId: string, name: string): Promise<MfaDevice | null>;
12
+ remove(principalId: string, deviceId: string): Promise<boolean>;
13
+ countConfirmed(principalId: string): Promise<number>;
14
+ stampUsed(secret: string): Promise<void>;
15
+ }
16
+ //# sourceMappingURL=mariadb-mfa-device-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mariadb-mfa-device-store.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-mfa-device-store.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAuBnF,qBAAa,qBAAsB,YAAW,cAAc;IAC9C,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,IAAI;IAEjC,OAAO,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IASlD,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAS5D,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IASxE,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAiBzE,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAShE,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAetF,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAQ/D,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IASpD,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAU/C"}
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MariaDbMfaDeviceStore = void 0;
4
+ /**
5
+ * MariaDbMfaDeviceStore — the framework-DEFAULT MfaDeviceStore (multi-device MFA).
6
+ * Persists a principal's N TOTP authenticator devices in `shield_identity`.`mfa_device`
7
+ * (the secure credential schema — a device secret is server-side verifier material,
8
+ * ADR-0014). Each device has its OWN base32 secret; the login OR-match (any confirmed
9
+ * device satisfies the factor) lives in the resolver bundle + the consumer's OR-verifier,
10
+ * NOT here — this class is pure persistence. Backs all `GET/POST/PATCH/DELETE
11
+ * /account/mfa/devices*` routes.
12
+ *
13
+ * All queries are parameterized. `begin` self-prunes the caller's stale PENDING rows (a
14
+ * `begin` that is never confirmed) before inserting, so pending rows can't accumulate —
15
+ * no cron needed. `listConfirmedSecrets` / `countConfirmed` filter to
16
+ * `md_confirmed_at_ms IS NOT NULL`, so a pending device never satisfies a login nor
17
+ * counts toward "MFA enabled". Device ids are `crypto.randomUUID()` (never a hard-coded
18
+ * UUID).
19
+ *
20
+ * SECURITY — the display shape {@link MfaDevice} NEVER carries `md_secret`; only
21
+ * {@link listConfirmedSecrets} and {@link getSecret} return secrets, and both are
22
+ * server-side-only callers (login bundle, confirm-time code check). `stampUsed` is
23
+ * best-effort (wrapped in try/catch) — a stamp failure must never fail a login. Every
24
+ * other method's genuine store outage propagates (fails loud), never swallowed.
25
+ */
26
+ const node_crypto_1 = require("node:crypto");
27
+ /** Pending-device TTL — a `begin` never confirmed within this window is self-pruned. */
28
+ const PENDING_TTL_MS = 15 * 60 * 1000;
29
+ /** Map a DB row → the display shape (NO secret). BIGINT columns arrive as bigint → Number. */
30
+ const toDevice = (r) => ({
31
+ id: r.md_id,
32
+ name: r.md_name,
33
+ createdAtMs: Number(r.md_created_at_ms),
34
+ lastUsedAtMs: r.md_last_used_at_ms == null ? null : Number(r.md_last_used_at_ms),
35
+ confirmed: r.md_confirmed_at_ms != null,
36
+ });
37
+ class MariaDbMfaDeviceStore {
38
+ pool;
39
+ constructor(pool) {
40
+ this.pool = pool;
41
+ }
42
+ async listAll(principalId) {
43
+ const rows = (await this.pool.query(`SELECT md_id, md_name, md_secret, md_created_at_ms, md_last_used_at_ms, md_confirmed_at_ms
44
+ FROM \`shield_identity\`.\`mfa_device\` WHERE md_principal_id = ? ORDER BY md_created_at_ms ASC`, [principalId]));
45
+ return rows.map(toDevice);
46
+ }
47
+ async listConfirmedSecrets(principalId) {
48
+ const rows = (await this.pool.query(`SELECT md_secret FROM \`shield_identity\`.\`mfa_device\`
49
+ WHERE md_principal_id = ? AND md_confirmed_at_ms IS NOT NULL`, [principalId]));
50
+ return rows.map((r) => r.md_secret);
51
+ }
52
+ async getSecret(principalId, deviceId) {
53
+ const rows = (await this.pool.query(`SELECT md_secret FROM \`shield_identity\`.\`mfa_device\`
54
+ WHERE md_id = ? AND md_principal_id = ? LIMIT 1`, [deviceId, principalId]));
55
+ return rows[0]?.md_secret ?? null;
56
+ }
57
+ async begin(principalId, name, secret) {
58
+ // Prune the caller's stale pending rows first (self-pruning; no cron).
59
+ await this.pool.query(`DELETE FROM \`shield_identity\`.\`mfa_device\`
60
+ WHERE md_principal_id = ? AND md_confirmed_at_ms IS NULL AND md_created_at_ms < ?`, [principalId, Date.now() - PENDING_TTL_MS]);
61
+ const id = (0, node_crypto_1.randomUUID)();
62
+ await this.pool.query(`INSERT INTO \`shield_identity\`.\`mfa_device\`
63
+ (md_id, md_principal_id, md_name, md_secret, md_created_at_ms, md_last_used_at_ms, md_confirmed_at_ms)
64
+ VALUES (?, ?, ?, ?, ?, NULL, NULL)`, [id, principalId, name, secret, Date.now()]);
65
+ return id;
66
+ }
67
+ async confirm(principalId, deviceId) {
68
+ const res = (await this.pool.query(`UPDATE \`shield_identity\`.\`mfa_device\` SET md_confirmed_at_ms = ?
69
+ WHERE md_id = ? AND md_principal_id = ? AND md_confirmed_at_ms IS NULL`, [Date.now(), deviceId, principalId]));
70
+ return res.affectedRows === 1;
71
+ }
72
+ async rename(principalId, deviceId, name) {
73
+ const res = (await this.pool.query(`UPDATE \`shield_identity\`.\`mfa_device\` SET md_name = ?
74
+ WHERE md_id = ? AND md_principal_id = ?`, [name, deviceId, principalId]));
75
+ if (res.affectedRows !== 1)
76
+ return null;
77
+ const rows = (await this.pool.query(`SELECT md_id, md_name, md_secret, md_created_at_ms, md_last_used_at_ms, md_confirmed_at_ms
78
+ FROM \`shield_identity\`.\`mfa_device\` WHERE md_id = ? AND md_principal_id = ? LIMIT 1`, [deviceId, principalId]));
79
+ return rows[0] ? toDevice(rows[0]) : null;
80
+ }
81
+ async remove(principalId, deviceId) {
82
+ const res = (await this.pool.query(`DELETE FROM \`shield_identity\`.\`mfa_device\` WHERE md_id = ? AND md_principal_id = ?`, [deviceId, principalId]));
83
+ return res.affectedRows === 1;
84
+ }
85
+ async countConfirmed(principalId) {
86
+ const rows = (await this.pool.query(`SELECT COUNT(*) AS c FROM \`shield_identity\`.\`mfa_device\`
87
+ WHERE md_principal_id = ? AND md_confirmed_at_ms IS NOT NULL`, [principalId]));
88
+ return Number(rows[0]?.c ?? 0);
89
+ }
90
+ async stampUsed(secret) {
91
+ try {
92
+ await this.pool.query(`UPDATE \`shield_identity\`.\`mfa_device\` SET md_last_used_at_ms = ? WHERE md_secret = ?`, [Date.now(), secret]);
93
+ }
94
+ catch {
95
+ // best-effort; a stamp failure must never fail a login.
96
+ }
97
+ }
98
+ }
99
+ exports.MariaDbMfaDeviceStore = MariaDbMfaDeviceStore;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * OrTotpVerifier — the framework-DEFAULT `totp` {@link FactorVerifier} for multi-device
3
+ * MFA. The core authenticator treats one verifier per factor, and multiple
4
+ * `RequiredFactor`s of the same factor as AND — the OPPOSITE of "match ANY device". So
5
+ * the OR-match lives here: the caller bundles a principal's N confirmed device secrets
6
+ * into ONE required-factor whose `secret` is the newline-joined bundle, and this
7
+ * verifier unpacks the bundle and returns `{ satisfied: true }` if the presented code
8
+ * matches ANY device secret — delegating the RFC-6238 arithmetic per secret to the
9
+ * wrapped BASE single-secret verifier.
10
+ *
11
+ * On a match it best-effort stamps `stampUsed` for the matched device (matched by its
12
+ * unique secret — the verify signature is `(credential, secret)` and carries no
13
+ * principalId; secrets are high-entropy random so the match is exact). The stamp is
14
+ * fire-and-forget: a stamp failure must NEVER fail a login.
15
+ */
16
+ import type { Credential, FactorVerdict, FactorVerifier } from '@astram/core';
17
+ import type { MfaDeviceStore } from '../auth/ports/mfa-device-store.js';
18
+ export declare class OrTotpVerifier implements FactorVerifier {
19
+ /** The single-secret RFC-6238 TOTP verifier (delegated to per device secret). */
20
+ private readonly base;
21
+ /** The device store — only `stampUsed` is needed (best-effort last-used stamp). */
22
+ private readonly devices;
23
+ readonly factor: "totp";
24
+ constructor(
25
+ /** The single-secret RFC-6238 TOTP verifier (delegated to per device secret). */
26
+ base: FactorVerifier,
27
+ /** The device store — only `stampUsed` is needed (best-effort last-used stamp). */
28
+ devices: Pick<MfaDeviceStore, 'stampUsed'>);
29
+ verify(credential: Credential, bundledSecret: string): Promise<FactorVerdict>;
30
+ }
31
+ //# sourceMappingURL=mariadb-or-totp-verifier.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mariadb-or-totp-verifier.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-or-totp-verifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAExE,qBAAa,cAAe,YAAW,cAAc;IAIjD,iFAAiF;IACjF,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,mFAAmF;IACnF,OAAO,CAAC,QAAQ,CAAC,OAAO;IAN1B,QAAQ,CAAC,MAAM,EAAG,MAAM,CAAU;;IAGhC,iFAAiF;IAChE,IAAI,EAAE,cAAc;IACrC,mFAAmF;IAClE,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,WAAW,CAAC;IAGvD,MAAM,CAAC,UAAU,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;CAgBpF"}
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ /**
3
+ * OrTotpVerifier — the framework-DEFAULT `totp` {@link FactorVerifier} for multi-device
4
+ * MFA. The core authenticator treats one verifier per factor, and multiple
5
+ * `RequiredFactor`s of the same factor as AND — the OPPOSITE of "match ANY device". So
6
+ * the OR-match lives here: the caller bundles a principal's N confirmed device secrets
7
+ * into ONE required-factor whose `secret` is the newline-joined bundle, and this
8
+ * verifier unpacks the bundle and returns `{ satisfied: true }` if the presented code
9
+ * matches ANY device secret — delegating the RFC-6238 arithmetic per secret to the
10
+ * wrapped BASE single-secret verifier.
11
+ *
12
+ * On a match it best-effort stamps `stampUsed` for the matched device (matched by its
13
+ * unique secret — the verify signature is `(credential, secret)` and carries no
14
+ * principalId; secrets are high-entropy random so the match is exact). The stamp is
15
+ * fire-and-forget: a stamp failure must NEVER fail a login.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.OrTotpVerifier = void 0;
19
+ class OrTotpVerifier {
20
+ base;
21
+ devices;
22
+ factor = 'totp';
23
+ constructor(
24
+ /** The single-secret RFC-6238 TOTP verifier (delegated to per device secret). */
25
+ base,
26
+ /** The device store — only `stampUsed` is needed (best-effort last-used stamp). */
27
+ devices) {
28
+ this.base = base;
29
+ this.devices = devices;
30
+ }
31
+ async verify(credential, bundledSecret) {
32
+ const secrets = String(bundledSecret ?? '')
33
+ .split('\n')
34
+ .map((s) => s.trim())
35
+ .filter(Boolean);
36
+ for (const secret of secrets) {
37
+ const verdict = await this.base.verify(credential, secret);
38
+ if (verdict.satisfied) {
39
+ // Best-effort last-used stamp for the device whose secret verified; never
40
+ // awaited into the deny path (a stamp failure must not fail the login).
41
+ void this.devices.stampUsed(secret);
42
+ return { satisfied: true };
43
+ }
44
+ }
45
+ return { satisfied: false };
46
+ }
47
+ }
48
+ exports.OrTotpVerifier = OrTotpVerifier;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * MariaDbProfileResolver — the framework-DEFAULT ProfileResolver backing `GET /auth/me`
3
+ * (the seam opened in `../auth/profile-resolver.ts`). Turns an authenticated Principal into
4
+ * the signed-in user's DISPLAY profile (name / company name / role names) by reading the
5
+ * framework-owned `@astram/identity` org tables in `shield_app`.
6
+ *
7
+ * FRAMEWORK-OWNED (no consumer resolver): auto-wired as the default when `auth: true`, exactly
8
+ * like MariaDbCredentialResolver. A consumer-supplied `profileResolver` still overrides it.
9
+ *
10
+ * ⛔ NEVER-SHOW-UUIDS: returns display NAMES for name/company.name/roles[].name. The bare `id`
11
+ * fields are machine-only (keys/routing). CONTRACT: null = no resolvable profile (deleted user →
12
+ * endpoint 404s); a store OUTAGE THROWS (fail-closed → endpoint 500, never a partial profile).
13
+ */
14
+ import type { Pool } from 'mariadb';
15
+ import type { Principal } from '@astram/core';
16
+ import type { CurrentUserProfile, ProfileResolver } from '../auth/profile-resolver.js';
17
+ export declare class MariaDbProfileResolver implements ProfileResolver {
18
+ private readonly pool;
19
+ private readonly now;
20
+ constructor(pool: Pool, now?: () => number);
21
+ resolve(principal: Principal): Promise<CurrentUserProfile | null>;
22
+ private resolveRoleNames;
23
+ }
24
+ //# sourceMappingURL=mariadb-profile-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mariadb-profile-resolver.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-profile-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,eAAe,EAAe,MAAM,6BAA6B,CAAC;AAMpG,qBAAa,sBAAuB,YAAW,eAAe;IAChD,OAAO,CAAC,QAAQ,CAAC,IAAI;IAAQ,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAhC,IAAI,EAAE,IAAI,EAAmB,GAAG,GAAE,MAAM,MAAiB;IAEhF,OAAO,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;YA4BzD,gBAAgB;CAc/B"}
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ /**
3
+ * MariaDbProfileResolver — the framework-DEFAULT ProfileResolver backing `GET /auth/me`
4
+ * (the seam opened in `../auth/profile-resolver.ts`). Turns an authenticated Principal into
5
+ * the signed-in user's DISPLAY profile (name / company name / role names) by reading the
6
+ * framework-owned `@astram/identity` org tables in `shield_app`.
7
+ *
8
+ * FRAMEWORK-OWNED (no consumer resolver): auto-wired as the default when `auth: true`, exactly
9
+ * like MariaDbCredentialResolver. A consumer-supplied `profileResolver` still overrides it.
10
+ *
11
+ * ⛔ NEVER-SHOW-UUIDS: returns display NAMES for name/company.name/roles[].name. The bare `id`
12
+ * fields are machine-only (keys/routing). CONTRACT: null = no resolvable profile (deleted user →
13
+ * endpoint 404s); a store OUTAGE THROWS (fail-closed → endpoint 500, never a partial profile).
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.MariaDbProfileResolver = void 0;
17
+ class MariaDbProfileResolver {
18
+ pool;
19
+ now;
20
+ constructor(pool, now = Date.now) {
21
+ this.pool = pool;
22
+ this.now = now;
23
+ }
24
+ async resolve(principal) {
25
+ const userUuid = principal.id;
26
+ const companyUuid = principal.tenantId;
27
+ const users = (await this.pool.query('SELECT u_username, u_email FROM `shield_app`.`users` WHERE u_uuid = ? LIMIT 1', [userUuid]));
28
+ const user = users[0];
29
+ if (!user)
30
+ return null; // deleted user → no profile (never id-leak; endpoint 404s)
31
+ const companies = (await this.pool.query('SELECT c_name FROM `shield_app`.`companies` WHERE c_uuid = ? LIMIT 1', [companyUuid]));
32
+ const companyName = companies[0]?.c_name ?? '(unknown company)';
33
+ const roles = await this.resolveRoleNames(userUuid, companyUuid);
34
+ return {
35
+ id: userUuid,
36
+ name: user.u_username,
37
+ ...(user.u_email ? { email: user.u_email } : {}),
38
+ company: { id: companyUuid, name: companyName },
39
+ roles,
40
+ };
41
+ }
42
+ async resolveRoleNames(userUuid, companyUuid) {
43
+ const nowMs = this.now();
44
+ const rows = (await this.pool.query(`SELECT DISTINCT r.r_uuid, r.r_name
45
+ FROM \`shield_app\`.\`membership_posting\` mp
46
+ JOIN \`shield_app\`.\`company_membership\` cm ON cm.cm_uuid = mp.mp_membership_uuid
47
+ JOIN \`shield_app\`.\`roles\` r ON r.r_uuid = mp.mp_role_uuid
48
+ WHERE cm.cm_user_uuid = ? AND cm.cm_company_uuid = ?
49
+ AND (mp.mp_effective_from_ms IS NULL OR mp.mp_effective_from_ms <= ?)
50
+ AND (mp.mp_effective_until_ms IS NULL OR mp.mp_effective_until_ms > ?)`, [userUuid, companyUuid, nowMs, nowMs]));
51
+ return rows.map((r) => ({ id: r.r_uuid, name: r.r_name }));
52
+ }
53
+ }
54
+ exports.MariaDbProfileResolver = MariaDbProfileResolver;