@astram/guard 0.2.0 → 0.3.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 (170) hide show
  1. package/dist/auth/account.controller.d.ts +245 -0
  2. package/dist/auth/account.controller.d.ts.map +1 -0
  3. package/dist/auth/account.controller.js +673 -0
  4. package/dist/auth/account.controller.js.map +1 -0
  5. package/dist/auth/auth.controller.d.ts +219 -5
  6. package/dist/auth/auth.controller.d.ts.map +1 -1
  7. package/dist/auth/auth.controller.js +790 -12
  8. package/dist/auth/auth.controller.js.map +1 -0
  9. package/dist/auth/auth.module.d.ts +100 -2
  10. package/dist/auth/auth.module.d.ts.map +1 -1
  11. package/dist/auth/auth.module.js +299 -7
  12. package/dist/auth/auth.module.js.map +1 -0
  13. package/dist/auth/connections.js.map +1 -0
  14. package/dist/auth/dto.d.ts +16 -6
  15. package/dist/auth/dto.d.ts.map +1 -1
  16. package/dist/auth/dto.js.map +1 -0
  17. package/dist/auth/pending-mfa-store.js.map +1 -0
  18. package/dist/auth/persistence.module.js.map +1 -0
  19. package/dist/auth/ports/audit-sink.d.ts +47 -0
  20. package/dist/auth/ports/audit-sink.d.ts.map +1 -0
  21. package/dist/auth/ports/audit-sink.js +20 -0
  22. package/dist/auth/ports/audit-sink.js.map +1 -0
  23. package/dist/auth/ports/credential-writer.d.ts +23 -0
  24. package/dist/auth/ports/credential-writer.d.ts.map +1 -0
  25. package/dist/auth/ports/credential-writer.js +2 -0
  26. package/dist/auth/ports/credential-writer.js.map +1 -0
  27. package/dist/auth/ports/email-sender.d.ts +14 -0
  28. package/dist/auth/ports/email-sender.d.ts.map +1 -0
  29. package/dist/auth/ports/email-sender.js +2 -0
  30. package/dist/auth/ports/email-sender.js.map +1 -0
  31. package/dist/auth/ports/mfa-device-store.d.ts +45 -0
  32. package/dist/auth/ports/mfa-device-store.d.ts.map +1 -0
  33. package/dist/auth/ports/mfa-device-store.js +13 -0
  34. package/dist/auth/ports/mfa-device-store.js.map +1 -0
  35. package/dist/auth/ports/profile-writer.d.ts +12 -0
  36. package/dist/auth/ports/profile-writer.d.ts.map +1 -0
  37. package/dist/auth/ports/profile-writer.js +2 -0
  38. package/dist/auth/ports/profile-writer.js.map +1 -0
  39. package/dist/auth/ports/recovery-code-store.d.ts +10 -0
  40. package/dist/auth/ports/recovery-code-store.d.ts.map +1 -0
  41. package/dist/auth/ports/recovery-code-store.js +2 -0
  42. package/dist/auth/ports/recovery-code-store.js.map +1 -0
  43. package/dist/auth/ports/reset-token-store.d.ts +12 -0
  44. package/dist/auth/ports/reset-token-store.d.ts.map +1 -0
  45. package/dist/auth/ports/reset-token-store.js +2 -0
  46. package/dist/auth/ports/reset-token-store.js.map +1 -0
  47. package/dist/auth/ports/session-facts-projector.d.ts +44 -0
  48. package/dist/auth/ports/session-facts-projector.d.ts.map +1 -0
  49. package/dist/auth/ports/session-facts-projector.js +30 -0
  50. package/dist/auth/ports/session-facts-projector.js.map +1 -0
  51. package/dist/auth/ports/session-sweeper.d.ts +12 -0
  52. package/dist/auth/ports/session-sweeper.d.ts.map +1 -0
  53. package/dist/auth/ports/session-sweeper.js +2 -0
  54. package/dist/auth/ports/session-sweeper.js.map +1 -0
  55. package/dist/auth/profile-resolver.d.ts +73 -0
  56. package/dist/auth/profile-resolver.d.ts.map +1 -0
  57. package/dist/auth/profile-resolver.js +28 -0
  58. package/dist/auth/profile-resolver.js.map +1 -0
  59. package/dist/auth/rate-limit/attempt-limiter.d.ts +91 -0
  60. package/dist/auth/rate-limit/attempt-limiter.d.ts.map +1 -0
  61. package/dist/auth/rate-limit/attempt-limiter.js +133 -0
  62. package/dist/auth/rate-limit/attempt-limiter.js.map +1 -0
  63. package/dist/auth/tokens.d.ts +174 -0
  64. package/dist/auth/tokens.d.ts.map +1 -1
  65. package/dist/auth/tokens.js +190 -1
  66. package/dist/auth/tokens.js.map +1 -0
  67. package/dist/auth/totp-enrollment.d.ts +42 -0
  68. package/dist/auth/totp-enrollment.d.ts.map +1 -0
  69. package/dist/auth/totp-enrollment.js +96 -0
  70. package/dist/auth/totp-enrollment.js.map +1 -0
  71. package/dist/authentication/argon2id-password-hasher.js.map +1 -0
  72. package/dist/authentication/mariadb-credential-resolver.d.ts +38 -24
  73. package/dist/authentication/mariadb-credential-resolver.d.ts.map +1 -1
  74. package/dist/authentication/mariadb-credential-resolver.js +42 -28
  75. package/dist/authentication/mariadb-credential-resolver.js.map +1 -0
  76. package/dist/authz/cross-tenant-grant-writer.d.ts +259 -0
  77. package/dist/authz/cross-tenant-grant-writer.d.ts.map +1 -0
  78. package/dist/authz/cross-tenant-grant-writer.js +397 -0
  79. package/dist/authz/cross-tenant-grant-writer.js.map +1 -0
  80. package/dist/authz/manifest-operation-authority-adapter.d.ts +65 -0
  81. package/dist/authz/manifest-operation-authority-adapter.d.ts.map +1 -0
  82. package/dist/authz/manifest-operation-authority-adapter.js +108 -0
  83. package/dist/authz/manifest-operation-authority-adapter.js.map +1 -0
  84. package/dist/authz/mariadb-cross-tenant-grant-adapter.d.ts +90 -0
  85. package/dist/authz/mariadb-cross-tenant-grant-adapter.d.ts.map +1 -0
  86. package/dist/authz/mariadb-cross-tenant-grant-adapter.js +211 -0
  87. package/dist/authz/mariadb-cross-tenant-grant-adapter.js.map +1 -0
  88. package/dist/authz/mariadb-record-rule-adapter.d.ts +60 -0
  89. package/dist/authz/mariadb-record-rule-adapter.d.ts.map +1 -0
  90. package/dist/authz/mariadb-record-rule-adapter.js +154 -0
  91. package/dist/authz/mariadb-record-rule-adapter.js.map +1 -0
  92. package/dist/authz/mariadb-record-share-adapter.d.ts +75 -0
  93. package/dist/authz/mariadb-record-share-adapter.d.ts.map +1 -0
  94. package/dist/authz/mariadb-record-share-adapter.js +106 -0
  95. package/dist/authz/mariadb-record-share-adapter.js.map +1 -0
  96. package/dist/authz/record-share-cascade.d.ts +52 -0
  97. package/dist/authz/record-share-cascade.d.ts.map +1 -0
  98. package/dist/authz/record-share-cascade.js +49 -0
  99. package/dist/authz/record-share-cascade.js.map +1 -0
  100. package/dist/authz/record-share-writer.d.ts +175 -0
  101. package/dist/authz/record-share-writer.d.ts.map +1 -0
  102. package/dist/authz/record-share-writer.js +225 -0
  103. package/dist/authz/record-share-writer.js.map +1 -0
  104. package/dist/authz/wire-record-authorization.d.ts +66 -0
  105. package/dist/authz/wire-record-authorization.d.ts.map +1 -0
  106. package/dist/authz/wire-record-authorization.js +34 -0
  107. package/dist/authz/wire-record-authorization.js.map +1 -0
  108. package/dist/boot/boot-sequence.js.map +1 -0
  109. package/dist/boot/boot.module.js.map +1 -0
  110. package/dist/completeness/catalog-reconciler.d.ts +21 -0
  111. package/dist/completeness/catalog-reconciler.d.ts.map +1 -0
  112. package/dist/completeness/catalog-reconciler.js +74 -0
  113. package/dist/completeness/catalog-reconciler.js.map +1 -0
  114. package/dist/completeness/operation-inventory-scanner.d.ts +17 -0
  115. package/dist/completeness/operation-inventory-scanner.d.ts.map +1 -0
  116. package/dist/completeness/operation-inventory-scanner.js +79 -0
  117. package/dist/completeness/operation-inventory-scanner.js.map +1 -0
  118. package/dist/completeness/ports/catalog-store.d.ts +33 -0
  119. package/dist/completeness/ports/catalog-store.d.ts.map +1 -0
  120. package/dist/completeness/ports/catalog-store.js +2 -0
  121. package/dist/completeness/ports/catalog-store.js.map +1 -0
  122. package/dist/config/app-config.js.map +1 -0
  123. package/dist/config/config.module.js.map +1 -0
  124. package/dist/enforcement/classification.decorator.d.ts +33 -14
  125. package/dist/enforcement/classification.decorator.d.ts.map +1 -1
  126. package/dist/enforcement/classification.decorator.js +34 -14
  127. package/dist/enforcement/classification.decorator.js.map +1 -0
  128. package/dist/enforcement/core-seam.js.map +1 -0
  129. package/dist/enforcement/enforcement.module.js.map +1 -0
  130. package/dist/enforcement/global-enforcement.guard.d.ts +29 -19
  131. package/dist/enforcement/global-enforcement.guard.d.ts.map +1 -1
  132. package/dist/enforcement/global-enforcement.guard.js +108 -25
  133. package/dist/enforcement/global-enforcement.guard.js.map +1 -0
  134. package/dist/enforcement/reenroll-allowlist.d.ts +49 -0
  135. package/dist/enforcement/reenroll-allowlist.d.ts.map +1 -0
  136. package/dist/enforcement/reenroll-allowlist.js +77 -0
  137. package/dist/enforcement/reenroll-allowlist.js.map +1 -0
  138. package/dist/index.d.ts +35 -3
  139. package/dist/index.d.ts.map +1 -1
  140. package/dist/index.js +159 -7
  141. package/dist/index.js.map +1 -0
  142. package/dist/module/shield-tokens.d.ts +15 -0
  143. package/dist/module/shield-tokens.d.ts.map +1 -0
  144. package/dist/module/shield-tokens.js +17 -0
  145. package/dist/module/shield-tokens.js.map +1 -0
  146. package/dist/module/shield.module.d.ts +153 -9
  147. package/dist/module/shield.module.d.ts.map +1 -1
  148. package/dist/module/shield.module.js +181 -24
  149. package/dist/module/shield.module.js.map +1 -0
  150. package/dist/request-auth/request-principal.d.ts +42 -0
  151. package/dist/request-auth/request-principal.d.ts.map +1 -0
  152. package/dist/request-auth/request-principal.js +87 -0
  153. package/dist/request-auth/request-principal.js.map +1 -0
  154. package/dist/request-auth/session-facts-als.d.ts +65 -0
  155. package/dist/request-auth/session-facts-als.d.ts.map +1 -0
  156. package/dist/request-auth/session-facts-als.js +57 -0
  157. package/dist/request-auth/session-facts-als.js.map +1 -0
  158. package/dist/request-auth/session-token-binder.d.ts +130 -0
  159. package/dist/request-auth/session-token-binder.d.ts.map +1 -0
  160. package/dist/request-auth/session-token-binder.js +277 -0
  161. package/dist/request-auth/session-token-binder.js.map +1 -0
  162. package/dist/request-auth/tokens.d.ts +14 -0
  163. package/dist/request-auth/tokens.d.ts.map +1 -0
  164. package/dist/request-auth/tokens.js +16 -0
  165. package/dist/request-auth/tokens.js.map +1 -0
  166. package/dist/session/crypto-adapters.js.map +1 -0
  167. package/dist/session/index.js.map +1 -0
  168. package/dist/session/mariadb-session-record-store.js.map +1 -0
  169. package/dist/session/redis-hot-session-store.js.map +1 -0
  170. package/package.json +2 -2
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.module.js","sourceRoot":"","sources":["../../src/config/config.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAgD;AAChD,mDAAgE;AAEhE,mDAAmD;AACtC,QAAA,UAAU,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC;AAE/C;;;;;;GAMG;AAWI,IAAM,YAAY,GAAlB,MAAM,YAAY;CAAG,CAAA;AAAf,oCAAY;uBAAZ,YAAY;IAVxB,IAAA,eAAM,GAAE;IACR,IAAA,eAAM,EAAC;QACN,SAAS,EAAE;YACT;gBACE,OAAO,EAAE,kBAAU;gBACnB,UAAU,EAAE,GAAc,EAAE,CAAC,IAAA,6BAAa,GAAE;aAC7C;SACF;QACD,OAAO,EAAE,CAAC,kBAAU,CAAC;KACtB,CAAC;GACW,YAAY,CAAG"}
@@ -4,22 +4,40 @@
4
4
  * Every HTTP handler in Shield must carry a classification. The global
5
5
  * enforcement guard (see {@link GlobalEnforcementGuard}) reads these keys and,
6
6
  * for any route WITHOUT one, denies the request (deny-by-default). This is the
7
- * structurally-unavoidable, fail-closed posture ADR-0002 mandates — present from
8
- * day one, even though the Scaffold stub does not yet consult a real decision
9
- * authority.
7
+ * structurally-unavoidable, fail-closed posture ADR-0002 mandates.
8
+ *
9
+ * The `@Classified(...)` value now carries the {@link Classification} the guard
10
+ * maps to a `decide(principal, action, resource, context)` call (the request-auth
11
+ * layer): `{ typeKey, action }`. A bare string is still accepted (deprecated
12
+ * back-compat) and treated as `typeKey` with a default `action` of `'access'`.
10
13
  */
11
14
  export declare const CLASSIFICATION_METADATA: {
12
15
  /** Marks a route as an explicit, code-reviewed public opt-out. */
13
16
  readonly PUBLIC: "shield:classification:public";
14
17
  /**
15
- * Marks a route as classified against a permission/tier. In the Scaffold this
16
- * is a placeholder value only — a LATER layer (ADR-0004 tiers, ADR-0012
17
- * authoring) resolves it to a real decision. Its mere PRESENCE is what the
18
- * stub guard checks: a classified route is "declared", an undeclared route is
19
- * denied.
18
+ * Marks a route as classified. The value is a {@link Classification}
19
+ * (`{ typeKey, action }`) the guard uses to build the `decide()` request; a bare
20
+ * string is accepted for back-compat (→ `{ typeKey: <string>, action: 'access' }`).
20
21
  */
21
22
  readonly CLASSIFIED: "shield:classification:classified";
22
23
  };
24
+ /**
25
+ * What a `@Classified` route gates on — the object-type key + the action the
26
+ * caller is attempting. The guard maps this to the Core `decide()` inputs:
27
+ * `Action = { key: action }`, `Resource = { level: 'object_type', typeKey,
28
+ * tenantId: <principal's tenant> }`. Stable keys (never route strings), matching
29
+ * the Core metadata contract (ADR-0004/ADR-0012).
30
+ */
31
+ export interface Classification {
32
+ /** Stable object-type key the action operates on (e.g. `'company'`). */
33
+ readonly typeKey: string;
34
+ /** Stable action key (e.g. `'read'`, `'create'`, `'update'`, `'delete'`). */
35
+ readonly action: string;
36
+ }
37
+ /** Default action when a bare-string `@Classified('typeKey')` is used (back-compat). */
38
+ export declare const DEFAULT_CLASSIFIED_ACTION = "access";
39
+ /** Normalize either form of the decorator value to a {@link Classification}. */
40
+ export declare function normalizeClassification(value: string | Classification | undefined): Classification | null;
23
41
  /**
24
42
  * `@Public()` — the explicit, deny-by-default opt-OUT (ADR-0002).
25
43
  *
@@ -30,12 +48,13 @@ export declare const CLASSIFICATION_METADATA: {
30
48
  */
31
49
  export declare const Public: () => MethodDecorator & ClassDecorator;
32
50
  /**
33
- * `@Classified(...)` — declare a route's permission/tier classification.
51
+ * `@Classified({ typeKey, action })` — declare what a route gates on.
34
52
  *
35
- * SCAFFOLD STUB: this only records that the route was classified; it does not
36
- * yet gate on a real permission. A later authorization layer replaces the stub
37
- * with an actual decision. Present here so business routes can be authored with
38
- * the final API shape from the start.
53
+ * The global enforcement guard reconstructs the caller's `Principal` from the
54
+ * request's bearer token, builds `Action`/`Resource` from this classification, and
55
+ * calls the Core `decide()` — allowing the request iff the decision is `allow`
56
+ * (deny-by-default otherwise). A bare string (`@Classified('company')`) is accepted
57
+ * for back-compat (→ action `'access'`), but the structured form is preferred.
39
58
  */
40
- export declare const Classified: (classification: string) => MethodDecorator & ClassDecorator;
59
+ export declare const Classified: (classification: string | Classification) => MethodDecorator & ClassDecorator;
41
60
  //# sourceMappingURL=classification.decorator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"classification.decorator.d.ts","sourceRoot":"","sources":["../../src/enforcement/classification.decorator.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,eAAO,MAAM,uBAAuB;IAClC,kEAAkE;;IAElE;;;;;;OAMG;;CAEK,CAAC;AAEX;;;;;;;GAOG;AACH,eAAO,MAAM,MAAM,QAAO,eAAe,GAAG,cACO,CAAC;AAEpD;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,GAAI,gBAAgB,MAAM,KAAG,eAAe,GAAG,cACL,CAAC"}
1
+ {"version":3,"file":"classification.decorator.d.ts","sourceRoot":"","sources":["../../src/enforcement/classification.decorator.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,uBAAuB;IAClC,kEAAkE;;IAElE;;;;OAIG;;CAEK,CAAC;AAEX;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,wEAAwE;IACxE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,wFAAwF;AACxF,eAAO,MAAM,yBAAyB,WAAW,CAAC;AAElD,gFAAgF;AAChF,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,GACzC,cAAc,GAAG,IAAI,CAcvB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,MAAM,QAAO,eAAe,GAAG,cACO,CAAC;AAEpD;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,GACrB,gBAAgB,MAAM,GAAG,cAAc,KACtC,eAAe,GAAG,cAC4C,CAAC"}
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Classified = exports.Public = exports.CLASSIFICATION_METADATA = void 0;
3
+ exports.Classified = exports.Public = exports.DEFAULT_CLASSIFIED_ACTION = exports.CLASSIFICATION_METADATA = void 0;
4
+ exports.normalizeClassification = normalizeClassification;
4
5
  const common_1 = require("@nestjs/common");
5
6
  /**
6
7
  * Route-classification metadata keys (ADR-0002).
@@ -8,22 +9,40 @@ const common_1 = require("@nestjs/common");
8
9
  * Every HTTP handler in Shield must carry a classification. The global
9
10
  * enforcement guard (see {@link GlobalEnforcementGuard}) reads these keys and,
10
11
  * for any route WITHOUT one, denies the request (deny-by-default). This is the
11
- * structurally-unavoidable, fail-closed posture ADR-0002 mandates — present from
12
- * day one, even though the Scaffold stub does not yet consult a real decision
13
- * authority.
12
+ * structurally-unavoidable, fail-closed posture ADR-0002 mandates.
13
+ *
14
+ * The `@Classified(...)` value now carries the {@link Classification} the guard
15
+ * maps to a `decide(principal, action, resource, context)` call (the request-auth
16
+ * layer): `{ typeKey, action }`. A bare string is still accepted (deprecated
17
+ * back-compat) and treated as `typeKey` with a default `action` of `'access'`.
14
18
  */
15
19
  exports.CLASSIFICATION_METADATA = {
16
20
  /** Marks a route as an explicit, code-reviewed public opt-out. */
17
21
  PUBLIC: 'shield:classification:public',
18
22
  /**
19
- * Marks a route as classified against a permission/tier. In the Scaffold this
20
- * is a placeholder value only — a LATER layer (ADR-0004 tiers, ADR-0012
21
- * authoring) resolves it to a real decision. Its mere PRESENCE is what the
22
- * stub guard checks: a classified route is "declared", an undeclared route is
23
- * denied.
23
+ * Marks a route as classified. The value is a {@link Classification}
24
+ * (`{ typeKey, action }`) the guard uses to build the `decide()` request; a bare
25
+ * string is accepted for back-compat (→ `{ typeKey: <string>, action: 'access' }`).
24
26
  */
25
27
  CLASSIFIED: 'shield:classification:classified',
26
28
  };
29
+ /** Default action when a bare-string `@Classified('typeKey')` is used (back-compat). */
30
+ exports.DEFAULT_CLASSIFIED_ACTION = 'access';
31
+ /** Normalize either form of the decorator value to a {@link Classification}. */
32
+ function normalizeClassification(value) {
33
+ if (value === undefined)
34
+ return null;
35
+ if (typeof value === 'string') {
36
+ return value.length > 0 ? { typeKey: value, action: exports.DEFAULT_CLASSIFIED_ACTION } : null;
37
+ }
38
+ if (typeof value.typeKey === 'string' &&
39
+ value.typeKey.length > 0 &&
40
+ typeof value.action === 'string' &&
41
+ value.action.length > 0) {
42
+ return { typeKey: value.typeKey, action: value.action };
43
+ }
44
+ return null;
45
+ }
27
46
  /**
28
47
  * `@Public()` — the explicit, deny-by-default opt-OUT (ADR-0002).
29
48
  *
@@ -35,12 +54,13 @@ exports.CLASSIFICATION_METADATA = {
35
54
  const Public = () => (0, common_1.SetMetadata)(exports.CLASSIFICATION_METADATA.PUBLIC, true);
36
55
  exports.Public = Public;
37
56
  /**
38
- * `@Classified(...)` — declare a route's permission/tier classification.
57
+ * `@Classified({ typeKey, action })` — declare what a route gates on.
39
58
  *
40
- * SCAFFOLD STUB: this only records that the route was classified; it does not
41
- * yet gate on a real permission. A later authorization layer replaces the stub
42
- * with an actual decision. Present here so business routes can be authored with
43
- * the final API shape from the start.
59
+ * The global enforcement guard reconstructs the caller's `Principal` from the
60
+ * request's bearer token, builds `Action`/`Resource` from this classification, and
61
+ * calls the Core `decide()` — allowing the request iff the decision is `allow`
62
+ * (deny-by-default otherwise). A bare string (`@Classified('company')`) is accepted
63
+ * for back-compat (→ action `'access'`), but the structured form is preferred.
44
64
  */
45
65
  const Classified = (classification) => (0, common_1.SetMetadata)(exports.CLASSIFICATION_METADATA.CLASSIFIED, classification);
46
66
  exports.Classified = Classified;
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classification.decorator.js","sourceRoot":"","sources":["../../src/enforcement/classification.decorator.ts"],"names":[],"mappings":";;;AA4CA,0DAgBC;AA5DD,2CAA6C;AAE7C;;;;;;;;;;;;GAYG;AACU,QAAA,uBAAuB,GAAG;IACrC,kEAAkE;IAClE,MAAM,EAAE,8BAA8B;IACtC;;;;OAIG;IACH,UAAU,EAAE,kCAAkC;CACtC,CAAC;AAgBX,wFAAwF;AAC3E,QAAA,yBAAyB,GAAG,QAAQ,CAAC;AAElD,gFAAgF;AAChF,SAAgB,uBAAuB,CACrC,KAA0C;IAE1C,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACrC,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,iCAAyB,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IACzF,CAAC;IACD,IACE,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ;QACjC,KAAK,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC;QACxB,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ;QAChC,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EACvB,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC;IAC1D,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;GAOG;AACI,MAAM,MAAM,GAAG,GAAqC,EAAE,CAC3D,IAAA,oBAAW,EAAC,+BAAuB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;AADvC,QAAA,MAAM,UACiC;AAEpD;;;;;;;;GAQG;AACI,MAAM,UAAU,GAAG,CACxB,cAAuC,EACL,EAAE,CACpC,IAAA,oBAAW,EAAC,+BAAuB,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC;AAHrD,QAAA,UAAU,cAG2C"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core-seam.js","sourceRoot":"","sources":["../../src/enforcement/core-seam.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;GAiBG;;;AAqBH;;;;;;;;;;;GAWG;AACU,QAAA,qBAAqB,GAA0B;IAC1D,MAAM;QACJ,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC,CAAC;IACxE,CAAC;CACF,CAAC"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enforcement.module.js","sourceRoot":"","sources":["../../src/enforcement/enforcement.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,uCAAyC;AACzC,+EAAuE;AAEvE;;;;;;;;;;;;;;GAcG;AASI,IAAM,iBAAiB,GAAvB,MAAM,iBAAiB;CAAG,CAAA;AAApB,8CAAiB;4BAAjB,iBAAiB;IAR7B,IAAA,eAAM,EAAC;QACN,SAAS,EAAE;YACT;gBACE,OAAO,EAAE,gBAAS;gBAClB,QAAQ,EAAE,oDAAsB;aACjC;SACF;KACF,CAAC;GACW,iBAAiB,CAAG"}
@@ -1,31 +1,41 @@
1
1
  import { CanActivate, ExecutionContext } from '@nestjs/common';
2
2
  import { Reflector } from '@nestjs/core';
3
+ import type { CoreDecisionAuthority } from './core-seam.js';
4
+ import type { SessionTokenBinder } from '../request-auth/session-token-binder.js';
3
5
  /**
4
- * Global, fail-closed enforcement guard (ADR-0002).
6
+ * Global, fail-closed enforcement guard (ADR-0002) — now with REAL request
7
+ * authorization for classified routes.
5
8
  *
6
9
  * This guard is the SINGLE, structurally-unavoidable point every request passes
7
- * through. It is bound as an `APP_GUARD` through the ONE registration seam,
8
- * `ShieldModule.forRoot()` (which composes the internal `EnforcementModule` that
9
- * holds the sole binding) — NOT opt-in per controller — so no endpoint can escape
10
- * it. Its posture is **deny-by-default**:
10
+ * through (bound as the sole `APP_GUARD` via `ShieldModule.forRoot()`). Posture is
11
+ * **deny-by-default**:
11
12
  *
12
- * - A route marked `@Public()` → ALLOWED (explicit, code-reviewed opt-out).
13
- * - A route marked `@Classified()` → allowed to PROCEED in the Scaffold stub
14
- * (a LATER layer will consult the real Core `DecisionAuthority` here; see
15
- * {@link ../enforcement/core-seam.ts}). Its presence proves the route was
16
- * declared.
17
- * - A route with NO classification → DENIED (403). A forgotten declaration
18
- * locks the door instead of opening it.
13
+ * - `@Public()` → ALLOWED (explicit, code-reviewed opt-out).
14
+ * - `@Classified({typeKey, action})` → the request-auth path runs:
15
+ * 1. extract the bearer access token from `Authorization`,
16
+ * 2. resolve it to a `Principal` (via the {@link SessionTokenBinder}),
17
+ * 3. build `Action`/`Resource` from the classification,
18
+ * 4. call the Core `decide()` — ALLOW iff `effect === 'allow'`, else 403.
19
+ * No/invalid token → 401 (fail-closed). This replaces the old scaffold stub
20
+ * that let classified routes through unconditionally.
21
+ * - No classification → DENIED (403). A forgotten declaration locks the door.
19
22
  *
20
- * SCAFFOLD SCOPE: this does not yet perform real authorization. It only proves
21
- * the fail-closed, globally-unavoidable, deny-by-default seam exists from day
22
- * one. When the Authorization layer lands, the `@Classified` branch calls the
23
- * Core decision authority (wired via the core-seam) — the deny-by-default
24
- * structure here does not change.
23
+ * OPT-IN / GRACEFUL DEGRADE: the request-auth dependencies (the token binder) exist
24
+ * only when `auth: true`. When they are ABSENT (enforcement-only, no-`.env` boot),
25
+ * the guard cannot authenticate a request — so a classified route with the
26
+ * request-auth path unavailable is DENIED (403), never allowed-through. This keeps
27
+ * the enforcement-only app booting AND fail-closed: an app that declares classified
28
+ * routes but hasn't enabled auth gets a clear deny, not a silent open door. (The
29
+ * decision authority defaults to the deny-by-default stub, so even a wired-but-
30
+ * unconfigured authority denies.)
25
31
  */
26
32
  export declare class GlobalEnforcementGuard implements CanActivate {
27
33
  private readonly reflector;
28
- constructor(reflector: Reflector);
29
- canActivate(context: ExecutionContext): boolean;
34
+ private readonly decisionAuthority?;
35
+ private readonly tokenBinder?;
36
+ constructor(reflector: Reflector, decisionAuthority?: CoreDecisionAuthority | undefined, tokenBinder?: SessionTokenBinder | undefined);
37
+ canActivate(context: ExecutionContext): Promise<boolean>;
38
+ /** The real authorization path for a classified route (fail-closed throughout). */
39
+ private authorizeClassified;
30
40
  }
31
41
  //# sourceMappingURL=global-enforcement.guard.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"global-enforcement.guard.d.ts","sourceRoot":"","sources":["../../src/enforcement/global-enforcement.guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAkC,MAAM,gBAAgB,CAAC;AAC/F,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAGzC;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBACa,sBAAuB,YAAW,WAAW;IAC5C,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,SAAS;IAEjD,WAAW,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO;CAsChD"}
1
+ {"version":3,"file":"global-enforcement.guard.d.ts","sourceRoot":"","sources":["../../src/enforcement/global-enforcement.guard.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EACX,gBAAgB,EAMjB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAOzC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAE5D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAQlF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBACa,sBAAuB,YAAW,WAAW;IAEtD,OAAO,CAAC,QAAQ,CAAC,SAAS;IAG1B,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAC;IAGnC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;gBANZ,SAAS,EAAE,SAAS,EAGpB,iBAAiB,CAAC,EAAE,qBAAqB,YAAA,EAGzC,WAAW,CAAC,EAAE,kBAAkB,YAAA;IAG7C,WAAW,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC;IAuC9D,mFAAmF;YACrE,mBAAmB;CA0FlC"}
@@ -8,40 +8,56 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
8
8
  var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
10
  };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
11
14
  Object.defineProperty(exports, "__esModule", { value: true });
12
15
  exports.GlobalEnforcementGuard = void 0;
13
16
  const common_1 = require("@nestjs/common");
14
17
  const core_1 = require("@nestjs/core");
15
18
  const classification_decorator_js_1 = require("./classification.decorator.js");
19
+ const shield_tokens_js_1 = require("../module/shield-tokens.js");
20
+ const tokens_js_1 = require("../request-auth/tokens.js");
21
+ const request_principal_js_1 = require("../request-auth/request-principal.js");
22
+ const session_facts_als_js_1 = require("../request-auth/session-facts-als.js");
23
+ const reenroll_allowlist_js_1 = require("./reenroll-allowlist.js");
16
24
  /**
17
- * Global, fail-closed enforcement guard (ADR-0002).
25
+ * Global, fail-closed enforcement guard (ADR-0002) — now with REAL request
26
+ * authorization for classified routes.
18
27
  *
19
28
  * This guard is the SINGLE, structurally-unavoidable point every request passes
20
- * through. It is bound as an `APP_GUARD` through the ONE registration seam,
21
- * `ShieldModule.forRoot()` (which composes the internal `EnforcementModule` that
22
- * holds the sole binding) — NOT opt-in per controller — so no endpoint can escape
23
- * it. Its posture is **deny-by-default**:
29
+ * through (bound as the sole `APP_GUARD` via `ShieldModule.forRoot()`). Posture is
30
+ * **deny-by-default**:
24
31
  *
25
- * - A route marked `@Public()` → ALLOWED (explicit, code-reviewed opt-out).
26
- * - A route marked `@Classified()` → allowed to PROCEED in the Scaffold stub
27
- * (a LATER layer will consult the real Core `DecisionAuthority` here; see
28
- * {@link ../enforcement/core-seam.ts}). Its presence proves the route was
29
- * declared.
30
- * - A route with NO classification → DENIED (403). A forgotten declaration
31
- * locks the door instead of opening it.
32
+ * - `@Public()` → ALLOWED (explicit, code-reviewed opt-out).
33
+ * - `@Classified({typeKey, action})` → the request-auth path runs:
34
+ * 1. extract the bearer access token from `Authorization`,
35
+ * 2. resolve it to a `Principal` (via the {@link SessionTokenBinder}),
36
+ * 3. build `Action`/`Resource` from the classification,
37
+ * 4. call the Core `decide()` — ALLOW iff `effect === 'allow'`, else 403.
38
+ * No/invalid token → 401 (fail-closed). This replaces the old scaffold stub
39
+ * that let classified routes through unconditionally.
40
+ * - No classification → DENIED (403). A forgotten declaration locks the door.
32
41
  *
33
- * SCAFFOLD SCOPE: this does not yet perform real authorization. It only proves
34
- * the fail-closed, globally-unavoidable, deny-by-default seam exists from day
35
- * one. When the Authorization layer lands, the `@Classified` branch calls the
36
- * Core decision authority (wired via the core-seam) — the deny-by-default
37
- * structure here does not change.
42
+ * OPT-IN / GRACEFUL DEGRADE: the request-auth dependencies (the token binder) exist
43
+ * only when `auth: true`. When they are ABSENT (enforcement-only, no-`.env` boot),
44
+ * the guard cannot authenticate a request — so a classified route with the
45
+ * request-auth path unavailable is DENIED (403), never allowed-through. This keeps
46
+ * the enforcement-only app booting AND fail-closed: an app that declares classified
47
+ * routes but hasn't enabled auth gets a clear deny, not a silent open door. (The
48
+ * decision authority defaults to the deny-by-default stub, so even a wired-but-
49
+ * unconfigured authority denies.)
38
50
  */
39
51
  let GlobalEnforcementGuard = class GlobalEnforcementGuard {
40
52
  reflector;
41
- constructor(reflector) {
53
+ decisionAuthority;
54
+ tokenBinder;
55
+ constructor(reflector, decisionAuthority, tokenBinder) {
42
56
  this.reflector = reflector;
57
+ this.decisionAuthority = decisionAuthority;
58
+ this.tokenBinder = tokenBinder;
43
59
  }
44
- canActivate(context) {
60
+ async canActivate(context) {
45
61
  // Only HTTP requests are classified here. Non-HTTP contexts (if ever added)
46
62
  // must be handled explicitly rather than silently allowed.
47
63
  if (context.getType() !== 'http') {
@@ -57,20 +73,87 @@ let GlobalEnforcementGuard = class GlobalEnforcementGuard {
57
73
  if (isPublic === true) {
58
74
  return true;
59
75
  }
60
- // 2. Classified route → declared. Scaffold stub lets it proceed (a real
61
- // decision authority is wired in a later layer via the core-seam).
62
- const classification = this.reflector.getAllAndOverride(classification_decorator_js_1.CLASSIFICATION_METADATA.CLASSIFIED, [handler, controller]);
63
- if (typeof classification === 'string' && classification.length > 0) {
64
- return true;
76
+ // 2. Classified route → run the request-auth + decide() path.
77
+ const raw = this.reflector.getAllAndOverride(classification_decorator_js_1.CLASSIFICATION_METADATA.CLASSIFIED, [handler, controller]);
78
+ const classification = (0, classification_decorator_js_1.normalizeClassification)(raw);
79
+ if (classification !== null) {
80
+ return this.authorizeClassified(context, classification);
65
81
  }
66
82
  // 3. No classification at all → DENY (ADR-0002 deny-by-default / fail-closed).
67
83
  throw new common_1.ForbiddenException('Shield enforcement: this route carries no access classification. ' +
68
84
  'It is denied by default (ADR-0002). Mark it @Public() to opt out, or ' +
69
85
  '@Classified(...) to declare its permission.');
70
86
  }
87
+ /** The real authorization path for a classified route (fail-closed throughout). */
88
+ async authorizeClassified(context, classification) {
89
+ // Request-auth is only possible when the token binder is wired (auth: true).
90
+ // Absent → we cannot authenticate the caller → DENY (never allow-through).
91
+ if (this.tokenBinder === undefined || this.decisionAuthority === undefined) {
92
+ throw new common_1.ForbiddenException('Shield enforcement: this route is @Classified but request authentication is not ' +
93
+ "enabled (ShieldModule.forRoot({ auth: true })). It is denied by default.");
94
+ }
95
+ const req = context.switchToHttp().getRequest();
96
+ const authHeader = req?.headers?.['authorization'];
97
+ const resolved = await (0, request_principal_js_1.resolveRequestAuth)(authHeader, this.tokenBinder);
98
+ if (resolved === null) {
99
+ // No/invalid/expired token → not authenticated. 401 (distinct from a deny).
100
+ throw new common_1.UnauthorizedException('Shield enforcement: missing or invalid access token for a classified route.');
101
+ }
102
+ const { principal, claims } = resolved;
103
+ // ENFORCED-RE-ENROLL GATE (spec §4.2): a session established via a recovery-code
104
+ // login is restricted to the re-secure allowlist until a fresh device is confirmed.
105
+ // Checked AFTER authentication, BEFORE decide() — a marked session hitting any route
106
+ // NOT on REENROLL_ALLOWED is 403 `reenrollment_required` (a distinct code the
107
+ // frontend recognizes → forces the re-enroll screen). Fail-closed: this is an
108
+ // allowlist, so an unknown/unclassified route with a marked session also 403s. This
109
+ // runs only on @Classified routes (the @Public re-secure routes self-authenticate
110
+ // and check nothing here). The classified route being gated here is, by definition,
111
+ // NOT on the allowlist (the allowlist is @Public), so a marked session is always
112
+ // denied — the isReenrollAllowed check below is defense-in-depth for a future
113
+ // classified allowlisted route.
114
+ if (claims.mustReenroll === true) {
115
+ const method = req?.method;
116
+ const url = req?.originalUrl ?? req?.url;
117
+ if (!(0, reenroll_allowlist_js_1.isReenrollAllowed)(method, url)) {
118
+ throw new common_1.ForbiddenException({ error: 'reenrollment_required' });
119
+ }
120
+ }
121
+ // Attach the resolved principal so downstream handlers can act as this user.
122
+ req.principal = principal;
123
+ // ── ADR-0011 SESSION-FACTS HOT PATH ──────────────────────────────────────────
124
+ // Enter the per-request session-facts ALS scope with THIS request's cached grants
125
+ // (from the binding — same bearer token → same principal → same facts) around the
126
+ // decide() call. The app-owned `SessionFactsReader` (an ALS-backed reader wired into
127
+ // the decision authority) reads the SAME `SHIELD_SESSION_FACTS_ALS` store inside
128
+ // decide(), so gate 5 gets these facts and can serve the coarse decision from cache
129
+ // (no DB reload) when the cached version is still fresh.
130
+ //
131
+ // CROSS-PRINCIPAL ISOLATION: `als.run(store, cb)` binds `store` to this async
132
+ // context and everything decide() awaits, and ONLY that context — a concurrent
133
+ // request runs its own `run()` with its own store, so getStore() never returns
134
+ // another request's facts. The store is built HERE from THIS request's claims.
135
+ //
136
+ // FAIL-CLOSED: no projector wired / absent / malformed binding → `claims.cachedFacts`
137
+ // is undefined → the store carries `cachedFacts: null` → gate 5 DB-reloads (today's
138
+ // behavior). The cache is only ever consulted; it never fabricates a permit.
139
+ const factsStore = {
140
+ cachedFacts: claims.cachedFacts ?? null,
141
+ permissionsVersion: claims.cachedFacts?.permissionsVersion ?? 0,
142
+ };
143
+ const decision = await session_facts_als_js_1.SHIELD_SESSION_FACTS_ALS.run(factsStore, () => this.decisionAuthority.decide(principal, { key: classification.action }, { level: 'object_type', typeKey: classification.typeKey, tenantId: principal.tenantId }, { isAuthenticated: true }));
144
+ if (decision.effect === 'allow') {
145
+ return true;
146
+ }
147
+ // Deny (or any non-allow) → 403. Do NOT leak the reason detail to the caller.
148
+ throw new common_1.ForbiddenException(`Shield enforcement: access denied for ${classification.action} on ${classification.typeKey}.`);
149
+ }
71
150
  };
72
151
  exports.GlobalEnforcementGuard = GlobalEnforcementGuard;
73
152
  exports.GlobalEnforcementGuard = GlobalEnforcementGuard = __decorate([
74
153
  (0, common_1.Injectable)(),
75
- __metadata("design:paramtypes", [core_1.Reflector])
154
+ __param(1, (0, common_1.Optional)()),
155
+ __param(1, (0, common_1.Inject)(shield_tokens_js_1.SHIELD_DECISION_AUTHORITY)),
156
+ __param(2, (0, common_1.Optional)()),
157
+ __param(2, (0, common_1.Inject)(tokens_js_1.SHIELD_SESSION_TOKEN_BINDER)),
158
+ __metadata("design:paramtypes", [core_1.Reflector, Object, Function])
76
159
  ], GlobalEnforcementGuard);
@@ -0,0 +1 @@
1
+ {"version":3,"file":"global-enforcement.guard.js","sourceRoot":"","sources":["../../src/enforcement/global-enforcement.guard.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * REENROLL_ALLOWED — the enforced-re-enroll allowlist (spec §4.2).
3
+ *
4
+ * A session established via a recovery-code login carries `mustReenroll: true` in its
5
+ * bound claims (see {@link import('../request-auth/session-token-binder.js').BoundPrincipalClaims}).
6
+ * The {@link import('./global-enforcement.guard.js').GlobalEnforcementGuard} restricts
7
+ * such a session to ONLY the routes below — the minimal set needed to RE-SECURE the
8
+ * account (add a fresh authenticator device + refresh recovery codes) and to observe
9
+ * the restricted state (`/auth/me`, list devices) or leave it (`/auth/logout`). EVERY
10
+ * other authenticated route returns 403 `{ error: 'reenrollment_required' }`.
11
+ *
12
+ * FAIL-CLOSED: this is an allowlist, not a denylist — a route NOT listed here (incl. an
13
+ * unclassified/unknown route) is DENIED for a `mustReenroll` session. Adding a route
14
+ * that a re-enrolling user must reach means adding it HERE, deliberately.
15
+ *
16
+ * NOTE — allowlisted ≠ gate-clearing (spec §4.3): being on this list only lets the
17
+ * request THROUGH the gate; it does NOT clear `mustReenroll`. ONLY a successful
18
+ * `POST /account/mfa/devices/confirm` clears the flag (the account controller calls
19
+ * `binder.clearReenroll`). Regenerating codes is allowed but does not re-secure.
20
+ */
21
+ /** One allowlisted route: an HTTP method + the path SUFFIX it must end with. */
22
+ export interface ReenrollAllowedRoute {
23
+ readonly method: string;
24
+ /** The route path (no query string, no trailing slash), matched by suffix. */
25
+ readonly path: string;
26
+ }
27
+ /**
28
+ * The allowlist (spec §4.2). Paths are the framework-owned route paths; matched
29
+ * robustly by method + normalized-path suffix (see {@link isReenrollAllowed}) so a
30
+ * future global prefix, a trailing slash, or a query string never defeats the match.
31
+ */
32
+ export declare const REENROLL_ALLOWED: readonly ReenrollAllowedRoute[];
33
+ /**
34
+ * Normalize a raw request path for matching: drop the query string and any fragment,
35
+ * collapse a trailing slash (except the root). Never throws.
36
+ */
37
+ export declare function normalizeRequestPath(rawUrl: unknown): string;
38
+ /**
39
+ * True iff the (method, path) is on the enforced-re-enroll allowlist. Matching is
40
+ * case-insensitive on the method and SUFFIX-based on the normalized path — so
41
+ * `/api/account/mfa/devices/confirm` (a prefixed mount) still matches
42
+ * `/account/mfa/devices/confirm`. The path boundary is inherent: every `route.path`
43
+ * BEGINS with `/`, so `endsWith(route.path)` cannot spuriously match a longer segment
44
+ * name (`/evilaccount/...` does NOT end with `/account/...` — there is no `/` before
45
+ * `account`). `/account/mfa/devices/confirm-evil` also fails (its suffix is `confirm-evil`,
46
+ * not `/confirm`).
47
+ */
48
+ export declare function isReenrollAllowed(method: unknown, rawUrl: unknown): boolean;
49
+ //# sourceMappingURL=reenroll-allowlist.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reenroll-allowlist.d.ts","sourceRoot":"","sources":["../../src/enforcement/reenroll-allowlist.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,gFAAgF;AAChF,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,8EAA8E;IAC9E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,EAAE,SAAS,oBAAoB,EAO1D,CAAC;AAEH;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAO5D;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAU3E"}
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ /**
3
+ * REENROLL_ALLOWED — the enforced-re-enroll allowlist (spec §4.2).
4
+ *
5
+ * A session established via a recovery-code login carries `mustReenroll: true` in its
6
+ * bound claims (see {@link import('../request-auth/session-token-binder.js').BoundPrincipalClaims}).
7
+ * The {@link import('./global-enforcement.guard.js').GlobalEnforcementGuard} restricts
8
+ * such a session to ONLY the routes below — the minimal set needed to RE-SECURE the
9
+ * account (add a fresh authenticator device + refresh recovery codes) and to observe
10
+ * the restricted state (`/auth/me`, list devices) or leave it (`/auth/logout`). EVERY
11
+ * other authenticated route returns 403 `{ error: 'reenrollment_required' }`.
12
+ *
13
+ * FAIL-CLOSED: this is an allowlist, not a denylist — a route NOT listed here (incl. an
14
+ * unclassified/unknown route) is DENIED for a `mustReenroll` session. Adding a route
15
+ * that a re-enrolling user must reach means adding it HERE, deliberately.
16
+ *
17
+ * NOTE — allowlisted ≠ gate-clearing (spec §4.3): being on this list only lets the
18
+ * request THROUGH the gate; it does NOT clear `mustReenroll`. ONLY a successful
19
+ * `POST /account/mfa/devices/confirm` clears the flag (the account controller calls
20
+ * `binder.clearReenroll`). Regenerating codes is allowed but does not re-secure.
21
+ */
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.REENROLL_ALLOWED = void 0;
24
+ exports.normalizeRequestPath = normalizeRequestPath;
25
+ exports.isReenrollAllowed = isReenrollAllowed;
26
+ /**
27
+ * The allowlist (spec §4.2). Paths are the framework-owned route paths; matched
28
+ * robustly by method + normalized-path suffix (see {@link isReenrollAllowed}) so a
29
+ * future global prefix, a trailing slash, or a query string never defeats the match.
30
+ */
31
+ exports.REENROLL_ALLOWED = Object.freeze([
32
+ { method: 'POST', path: '/account/mfa/devices/begin' },
33
+ { method: 'POST', path: '/account/mfa/devices/confirm' },
34
+ { method: 'POST', path: '/account/mfa/recovery-codes/regenerate' },
35
+ { method: 'GET', path: '/account/mfa/devices' },
36
+ { method: 'GET', path: '/auth/me' },
37
+ { method: 'POST', path: '/auth/logout' },
38
+ ]);
39
+ /**
40
+ * Normalize a raw request path for matching: drop the query string and any fragment,
41
+ * collapse a trailing slash (except the root). Never throws.
42
+ */
43
+ function normalizeRequestPath(rawUrl) {
44
+ if (typeof rawUrl !== 'string' || rawUrl.length === 0)
45
+ return '';
46
+ // Strip query (?...) and fragment (#...).
47
+ let path = rawUrl.split('?', 1)[0].split('#', 1)[0];
48
+ // Collapse a single trailing slash (but keep the root "/").
49
+ if (path.length > 1 && path.endsWith('/'))
50
+ path = path.slice(0, -1);
51
+ return path;
52
+ }
53
+ /**
54
+ * True iff the (method, path) is on the enforced-re-enroll allowlist. Matching is
55
+ * case-insensitive on the method and SUFFIX-based on the normalized path — so
56
+ * `/api/account/mfa/devices/confirm` (a prefixed mount) still matches
57
+ * `/account/mfa/devices/confirm`. The path boundary is inherent: every `route.path`
58
+ * BEGINS with `/`, so `endsWith(route.path)` cannot spuriously match a longer segment
59
+ * name (`/evilaccount/...` does NOT end with `/account/...` — there is no `/` before
60
+ * `account`). `/account/mfa/devices/confirm-evil` also fails (its suffix is `confirm-evil`,
61
+ * not `/confirm`).
62
+ */
63
+ function isReenrollAllowed(method, rawUrl) {
64
+ if (typeof method !== 'string')
65
+ return false;
66
+ const m = method.toUpperCase();
67
+ const path = normalizeRequestPath(rawUrl);
68
+ if (path === '')
69
+ return false;
70
+ for (const route of exports.REENROLL_ALLOWED) {
71
+ if (route.method !== m)
72
+ continue;
73
+ if (path === route.path || path.endsWith(route.path))
74
+ return true;
75
+ }
76
+ return false;
77
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reenroll-allowlist.js","sourceRoot":"","sources":["../../src/enforcement/reenroll-allowlist.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;GAmBG;;;AA2BH,oDAOC;AAYD,8CAUC;AA/CD;;;;GAIG;AACU,QAAA,gBAAgB,GAAoC,MAAM,CAAC,MAAM,CAAC;IAC7E,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,4BAA4B,EAAE;IACtD,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,8BAA8B,EAAE;IACxD,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,wCAAwC,EAAE;IAClE,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,sBAAsB,EAAE;IAC/C,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE;IACnC,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE;CACzC,CAAC,CAAC;AAEH;;;GAGG;AACH,SAAgB,oBAAoB,CAAC,MAAe;IAClD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACjE,0CAA0C;IAC1C,IAAI,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAE,CAAC;IACtD,4DAA4D;IAC5D,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACpE,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;GASG;AACH,SAAgB,iBAAiB,CAAC,MAAe,EAAE,MAAe;IAChE,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC7C,MAAM,CAAC,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC;IAC/B,MAAM,IAAI,GAAG,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC1C,IAAI,IAAI,KAAK,EAAE;QAAE,OAAO,KAAK,CAAC;IAC9B,KAAK,MAAM,KAAK,IAAI,wBAAgB,EAAE,CAAC;QACrC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QACjC,IAAI,IAAI,KAAK,KAAK,CAAC,IAAI,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;IACpE,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}