@astram/guard 0.4.1 → 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.
- package/dist/auth/auth.module.d.ts +6 -0
- package/dist/auth/auth.module.d.ts.map +1 -1
- package/dist/auth/auth.module.js +106 -15
- package/dist/auth/tokens.d.ts +9 -0
- package/dist/auth/tokens.d.ts.map +1 -1
- package/dist/auth/tokens.js +10 -1
- package/dist/authentication/log-email-sender.d.ts +13 -0
- package/dist/authentication/log-email-sender.d.ts.map +1 -0
- package/dist/authentication/log-email-sender.js +13 -0
- package/dist/authentication/mariadb-credential-writer.d.ts +16 -0
- package/dist/authentication/mariadb-credential-writer.d.ts.map +1 -0
- package/dist/authentication/mariadb-credential-writer.js +19 -0
- package/dist/authentication/mariadb-mfa-device-store.d.ts +16 -0
- package/dist/authentication/mariadb-mfa-device-store.d.ts.map +1 -0
- package/dist/authentication/mariadb-mfa-device-store.js +99 -0
- package/dist/authentication/mariadb-or-totp-verifier.d.ts +31 -0
- package/dist/authentication/mariadb-or-totp-verifier.d.ts.map +1 -0
- package/dist/authentication/mariadb-or-totp-verifier.js +48 -0
- package/dist/authentication/mariadb-profile-resolver.d.ts +24 -0
- package/dist/authentication/mariadb-profile-resolver.d.ts.map +1 -0
- package/dist/authentication/mariadb-profile-resolver.js +54 -0
- package/dist/authentication/mariadb-profile-writer.d.ts +18 -0
- package/dist/authentication/mariadb-profile-writer.d.ts.map +1 -0
- package/dist/authentication/mariadb-profile-writer.js +36 -0
- package/dist/authentication/mariadb-recovery-code-store.d.ts +47 -0
- package/dist/authentication/mariadb-recovery-code-store.d.ts.map +1 -0
- package/dist/authentication/mariadb-recovery-code-store.js +53 -0
- package/dist/authentication/mariadb-reset-token-store.d.ts +26 -0
- package/dist/authentication/mariadb-reset-token-store.d.ts.map +1 -0
- package/dist/authentication/mariadb-reset-token-store.js +43 -0
- package/dist/authentication/mariadb-session-sweeper.d.ts +33 -0
- package/dist/authentication/mariadb-session-sweeper.d.ts.map +1 -0
- package/dist/authentication/mariadb-session-sweeper.js +60 -0
- package/dist/identity/auth-facts.controller.d.ts +24 -0
- package/dist/identity/auth-facts.controller.d.ts.map +1 -0
- package/dist/identity/auth-facts.controller.js +106 -0
- package/dist/identity/identity-actions.d.ts +22 -0
- package/dist/identity/identity-actions.d.ts.map +1 -0
- package/dist/identity/identity-actions.js +32 -0
- package/dist/identity/module-registry.bridge.d.ts +4 -0
- package/dist/identity/module-registry.bridge.d.ts.map +1 -0
- package/dist/identity/module-registry.bridge.js +43 -0
- package/dist/identity/permission-facts.adapter.d.ts +47 -0
- package/dist/identity/permission-facts.adapter.d.ts.map +1 -0
- package/dist/identity/permission-facts.adapter.js +108 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -2
- package/dist/migrations-dir.d.ts +2 -0
- package/dist/migrations-dir.d.ts.map +1 -0
- package/dist/migrations-dir.js +13 -0
- package/dist/module/shield-tokens.d.ts +6 -1
- package/dist/module/shield-tokens.d.ts.map +1 -1
- package/dist/module/shield-tokens.js +7 -1
- package/dist/module/shield.module.d.ts +17 -0
- package/dist/module/shield.module.d.ts.map +1 -1
- package/dist/module/shield.module.js +12 -0
- package/migrations/1710000000006-CreateAuthzSchema.cjs +53 -0
- package/migrations/1710000000007-ExtendPermissionAndRolePermission.cjs +66 -0
- package/migrations/1710000000008-CreateAccessBundleTables.cjs +49 -0
- 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;
|
|
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"}
|
package/dist/auth/auth.module.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
package/dist/auth/tokens.d.ts
CHANGED
|
@@ -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"}
|
package/dist/auth/tokens.js
CHANGED
|
@@ -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;
|