@o3co/auth.policy-verifier.builtins 0.3.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.
Files changed (128) hide show
  1. package/README.ja.md +70 -14
  2. package/README.md +75 -15
  3. package/dist/collectors/PayloadScopeCollector.d.mts +5 -0
  4. package/dist/collectors/PayloadScopeCollector.d.mts.map +1 -1
  5. package/dist/collectors/PayloadScopeCollector.mjs +8 -1
  6. package/dist/collectors/PayloadSubjectIdCollector.d.mts +9 -0
  7. package/dist/collectors/PayloadSubjectIdCollector.d.mts.map +1 -1
  8. package/dist/collectors/PayloadSubjectIdCollector.mjs +16 -4
  9. package/dist/collectors/RequestContextAttributeCollector.d.mts +47 -0
  10. package/dist/collectors/RequestContextAttributeCollector.d.mts.map +1 -0
  11. package/dist/collectors/RequestContextAttributeCollector.mjs +100 -0
  12. package/dist/collectors/StaticPermissionCollector.d.mts +5 -0
  13. package/dist/collectors/StaticPermissionCollector.d.mts.map +1 -1
  14. package/dist/collectors/StaticPermissionCollector.mjs +7 -0
  15. package/dist/collectors/StaticRoleCollector.d.mts +5 -0
  16. package/dist/collectors/StaticRoleCollector.d.mts.map +1 -1
  17. package/dist/collectors/StaticRoleCollector.mjs +7 -0
  18. package/dist/index.d.mts +3 -2
  19. package/dist/index.d.mts.map +1 -1
  20. package/dist/index.mjs +4 -1
  21. package/dist/module.d.mts +5 -0
  22. package/dist/module.d.mts.map +1 -1
  23. package/dist/module.mjs +10 -1
  24. package/dist/resource/DotNotationResourceParser.d.mts +48 -0
  25. package/dist/resource/DotNotationResourceParser.d.mts.map +1 -1
  26. package/dist/resource/DotNotationResourceParser.mjs +94 -10
  27. package/dist/rules/AttrLiteralCompare.d.mts +2 -2
  28. package/dist/rules/AttrLiteralCompare.d.mts.map +1 -1
  29. package/dist/rules/AttrLiteralCompare.mjs +2 -0
  30. package/dist/rules/AttrLiteralEqual.d.mts +2 -2
  31. package/dist/rules/AttrLiteralEqual.d.mts.map +1 -1
  32. package/dist/rules/AttrLiteralEqual.mjs +2 -0
  33. package/dist/rules/AttrLiteralIn.d.mts +2 -2
  34. package/dist/rules/AttrLiteralIn.d.mts.map +1 -1
  35. package/dist/rules/AttrLiteralIn.mjs +2 -0
  36. package/dist/rules/AttrLiteralNotEqual.d.mts +2 -2
  37. package/dist/rules/AttrLiteralNotEqual.d.mts.map +1 -1
  38. package/dist/rules/AttrLiteralNotEqual.mjs +2 -0
  39. package/dist/rules/AttrLiteralNotIn.d.mts +2 -2
  40. package/dist/rules/AttrLiteralNotIn.d.mts.map +1 -1
  41. package/dist/rules/AttrLiteralNotIn.mjs +2 -0
  42. package/dist/rules/AttrMatchRule.d.mts.map +1 -1
  43. package/dist/rules/AttrMatchRule.mjs +2 -0
  44. package/dist/rules/AttrPairCompare.d.mts +2 -2
  45. package/dist/rules/AttrPairCompare.d.mts.map +1 -1
  46. package/dist/rules/AttrPairCompare.mjs +2 -0
  47. package/dist/rules/AttrPairEqual.d.mts +2 -2
  48. package/dist/rules/AttrPairEqual.d.mts.map +1 -1
  49. package/dist/rules/AttrPairEqual.mjs +2 -0
  50. package/dist/rules/AttrPairNotEqual.d.mts +2 -2
  51. package/dist/rules/AttrPairNotEqual.d.mts.map +1 -1
  52. package/dist/rules/AttrPairNotEqual.mjs +2 -0
  53. package/dist/rules/HasPermission.d.mts +36 -2
  54. package/dist/rules/HasPermission.d.mts.map +1 -1
  55. package/dist/rules/HasPermission.mjs +57 -6
  56. package/dist/rules/HasScope.d.mts +45 -4
  57. package/dist/rules/HasScope.d.mts.map +1 -1
  58. package/dist/rules/HasScope.mjs +46 -9
  59. package/dist/rules/_sharedValidation.d.mts +12 -9
  60. package/dist/rules/_sharedValidation.d.mts.map +1 -1
  61. package/dist/rules/_sharedValidation.mjs +46 -9
  62. package/dist/rules/collectors/ResourceActionPermissionRuleCollector.d.mts +6 -0
  63. package/dist/rules/collectors/ResourceActionPermissionRuleCollector.d.mts.map +1 -1
  64. package/dist/rules/collectors/ResourceActionPermissionRuleCollector.mjs +8 -0
  65. package/dist/rules/collectors/ResourceActionScopeRuleCollector.d.mts +52 -4
  66. package/dist/rules/collectors/ResourceActionScopeRuleCollector.d.mts.map +1 -1
  67. package/dist/rules/collectors/ResourceActionScopeRuleCollector.mjs +54 -6
  68. package/package.json +8 -5
  69. package/dist/__tests__/collectors/PayloadScopeCollector.test.d.mts +0 -2
  70. package/dist/__tests__/collectors/PayloadScopeCollector.test.d.mts.map +0 -1
  71. package/dist/__tests__/collectors/PayloadScopeCollector.test.mjs +0 -24
  72. package/dist/__tests__/collectors/PayloadSubjectIdCollector.test.d.mts +0 -2
  73. package/dist/__tests__/collectors/PayloadSubjectIdCollector.test.d.mts.map +0 -1
  74. package/dist/__tests__/collectors/PayloadSubjectIdCollector.test.mjs +0 -33
  75. package/dist/__tests__/collectors/StaticPermissionCollector.test.d.mts +0 -2
  76. package/dist/__tests__/collectors/StaticPermissionCollector.test.d.mts.map +0 -1
  77. package/dist/__tests__/collectors/StaticPermissionCollector.test.mjs +0 -22
  78. package/dist/__tests__/collectors/StaticRoleCollector.test.d.mts +0 -2
  79. package/dist/__tests__/collectors/StaticRoleCollector.test.d.mts.map +0 -1
  80. package/dist/__tests__/collectors/StaticRoleCollector.test.mjs +0 -24
  81. package/dist/__tests__/module.test.d.mts +0 -2
  82. package/dist/__tests__/module.test.d.mts.map +0 -1
  83. package/dist/__tests__/module.test.mjs +0 -71
  84. package/dist/__tests__/resource/DotNotationResourceParser.test.d.mts +0 -2
  85. package/dist/__tests__/resource/DotNotationResourceParser.test.d.mts.map +0 -1
  86. package/dist/__tests__/resource/DotNotationResourceParser.test.mjs +0 -34
  87. package/dist/__tests__/rules/AttrLiteralCompare.test.d.mts +0 -2
  88. package/dist/__tests__/rules/AttrLiteralCompare.test.d.mts.map +0 -1
  89. package/dist/__tests__/rules/AttrLiteralCompare.test.mjs +0 -253
  90. package/dist/__tests__/rules/AttrLiteralEqual.test.d.mts +0 -2
  91. package/dist/__tests__/rules/AttrLiteralEqual.test.d.mts.map +0 -1
  92. package/dist/__tests__/rules/AttrLiteralEqual.test.mjs +0 -138
  93. package/dist/__tests__/rules/AttrLiteralIn.test.d.mts +0 -2
  94. package/dist/__tests__/rules/AttrLiteralIn.test.d.mts.map +0 -1
  95. package/dist/__tests__/rules/AttrLiteralIn.test.mjs +0 -160
  96. package/dist/__tests__/rules/AttrLiteralNotEqual.test.d.mts +0 -2
  97. package/dist/__tests__/rules/AttrLiteralNotEqual.test.d.mts.map +0 -1
  98. package/dist/__tests__/rules/AttrLiteralNotEqual.test.mjs +0 -157
  99. package/dist/__tests__/rules/AttrLiteralNotIn.test.d.mts +0 -2
  100. package/dist/__tests__/rules/AttrLiteralNotIn.test.d.mts.map +0 -1
  101. package/dist/__tests__/rules/AttrLiteralNotIn.test.mjs +0 -168
  102. package/dist/__tests__/rules/AttrMatchRule.test.d.mts +0 -2
  103. package/dist/__tests__/rules/AttrMatchRule.test.d.mts.map +0 -1
  104. package/dist/__tests__/rules/AttrMatchRule.test.mjs +0 -79
  105. package/dist/__tests__/rules/AttrPairCompare.test.d.mts +0 -2
  106. package/dist/__tests__/rules/AttrPairCompare.test.d.mts.map +0 -1
  107. package/dist/__tests__/rules/AttrPairCompare.test.mjs +0 -319
  108. package/dist/__tests__/rules/AttrPairEqual.test.d.mts +0 -2
  109. package/dist/__tests__/rules/AttrPairEqual.test.d.mts.map +0 -1
  110. package/dist/__tests__/rules/AttrPairEqual.test.mjs +0 -155
  111. package/dist/__tests__/rules/AttrPairNotEqual.test.d.mts +0 -2
  112. package/dist/__tests__/rules/AttrPairNotEqual.test.d.mts.map +0 -1
  113. package/dist/__tests__/rules/AttrPairNotEqual.test.mjs +0 -155
  114. package/dist/__tests__/rules/HasPermission.test.d.mts +0 -2
  115. package/dist/__tests__/rules/HasPermission.test.d.mts.map +0 -1
  116. package/dist/__tests__/rules/HasPermission.test.mjs +0 -86
  117. package/dist/__tests__/rules/HasScope.test.d.mts +0 -2
  118. package/dist/__tests__/rules/HasScope.test.d.mts.map +0 -1
  119. package/dist/__tests__/rules/HasScope.test.mjs +0 -42
  120. package/dist/__tests__/rules/_sharedValidation.test.d.mts +0 -2
  121. package/dist/__tests__/rules/_sharedValidation.test.d.mts.map +0 -1
  122. package/dist/__tests__/rules/_sharedValidation.test.mjs +0 -325
  123. package/dist/__tests__/rules/collectors/ResourceActionPermissionRuleCollector.test.d.mts +0 -2
  124. package/dist/__tests__/rules/collectors/ResourceActionPermissionRuleCollector.test.d.mts.map +0 -1
  125. package/dist/__tests__/rules/collectors/ResourceActionPermissionRuleCollector.test.mjs +0 -27
  126. package/dist/__tests__/rules/collectors/ResourceActionScopeRuleCollector.test.d.mts +0 -2
  127. package/dist/__tests__/rules/collectors/ResourceActionScopeRuleCollector.test.d.mts.map +0 -1
  128. package/dist/__tests__/rules/collectors/ResourceActionScopeRuleCollector.test.mjs +0 -48
@@ -1,11 +1,52 @@
1
- import type { Attributes, Rule } from "@o3co/auth.policy-verifier.core";
1
+ import type { ReadonlyAttributes, Rule } from "@o3co/auth.policy-verifier.core";
2
+ /** Options accepted by `HasScope`. */
3
+ export interface HasScopeOptions {
4
+ /**
5
+ * Opt in to treating a bare granted scope (one containing no `:`) as
6
+ * `read:<scope>` in addition to its literal value. Defaults to `false`.
7
+ *
8
+ * This exists only for deployments whose issuer emits bare resource names
9
+ * and that relied on the rewrite before it became opt-in. It is off by
10
+ * default because the rewrite invents an action the issuer never wrote:
11
+ * a token granted `project` is silently promoted to `read:project`.
12
+ */
13
+ allowBareScopeRewrite?: boolean;
14
+ }
15
+ /**
16
+ * Rule that passes when the token carries the required scope.
17
+ *
18
+ * ## Matching
19
+ *
20
+ * Comparison is an **exact, case-sensitive string equality** against each value
21
+ * in `ATTR_SCOPES`. OAuth 2.0 scope values are case-sensitive opaque strings
22
+ * (RFC 6749 §3.3), so the verifier compares what the issuer wrote rather than a
23
+ * normalized form of it:
24
+ *
25
+ * - `read:PROJECT` does **not** satisfy a `read:project` requirement.
26
+ * - `read:project:restricted` is a value in its own right. It does not satisfy
27
+ * `read:project`, and `read:project` does not satisfy it. Nothing is split
28
+ * off at the second `:`; a scope the issuer deliberately narrowed must not
29
+ * collapse into the broader one.
30
+ *
31
+ * ## Bare-scope rewrite
32
+ *
33
+ * A granted scope carrying no `:` is compared literally unless
34
+ * `{ allowBareScopeRewrite: true }` is passed, in which case it also matches
35
+ * `read:<scope>`. Even then, only a scope with **no** `:` is ever rewritten —
36
+ * a value such as `project:restricted` is left alone, because which of its
37
+ * segments is the action is unknowable and guessing would over-grant.
38
+ *
39
+ * Non-string entries in `ATTR_SCOPES` never match and never throw: the map is
40
+ * untyped, and a malformed value must produce a denial, not a crash.
41
+ */
2
42
  export declare class HasScope implements Rule {
3
43
  private scope;
4
44
  readonly ruleType = "scope";
5
45
  readonly code = "invalid_scope";
6
46
  readonly message: string;
7
- constructor(scope: string);
8
- verify(attrs: Attributes): boolean;
9
- private matchScopes;
47
+ private readonly allowBareScopeRewrite;
48
+ constructor(scope: string, options?: HasScopeOptions);
49
+ verify(attrs: ReadonlyAttributes): boolean;
50
+ private matchScope;
10
51
  }
11
52
  //# sourceMappingURL=HasScope.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"HasScope.d.mts","sourceRoot":"","sources":["../../src/rules/HasScope.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,iCAAiC,CAAC;AAGxE,qBAAa,QAAS,YAAW,IAAI;IAKxB,OAAO,CAAC,KAAK;IAJzB,QAAQ,CAAC,QAAQ,WAAW;IAC5B,QAAQ,CAAC,IAAI,mBAAmB;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBAEL,KAAK,EAAE,MAAM;IAIjC,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO;IAKlC,OAAO,CAAC,WAAW;CASnB"}
1
+ {"version":3,"file":"HasScope.d.mts","sourceRoot":"","sources":["../../src/rules/HasScope.mts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,IAAI,EAAE,MAAM,iCAAiC,CAAC;AAGhF,sCAAsC;AACtC,MAAM,WAAW,eAAe;IAC/B;;;;;;;;OAQG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBAAa,QAAS,YAAW,IAAI;IAQnC,OAAO,CAAC,KAAK;IAPd,QAAQ,CAAC,QAAQ,WAAW;IAC5B,QAAQ,CAAC,IAAI,mBAAmB;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAEzB,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAU;gBAGvC,KAAK,EAAE,MAAM,EACrB,OAAO,CAAC,EAAE,eAAe;IAM1B,MAAM,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO;IAM1C,OAAO,CAAC,UAAU;CAYlB"}
@@ -1,22 +1,59 @@
1
+ // SPDX-FileCopyrightText: 2026 1o1 Co. Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { ATTR_SCOPES } from "@o3co/auth.policy-verifier.core";
4
+ /**
5
+ * Rule that passes when the token carries the required scope.
6
+ *
7
+ * ## Matching
8
+ *
9
+ * Comparison is an **exact, case-sensitive string equality** against each value
10
+ * in `ATTR_SCOPES`. OAuth 2.0 scope values are case-sensitive opaque strings
11
+ * (RFC 6749 §3.3), so the verifier compares what the issuer wrote rather than a
12
+ * normalized form of it:
13
+ *
14
+ * - `read:PROJECT` does **not** satisfy a `read:project` requirement.
15
+ * - `read:project:restricted` is a value in its own right. It does not satisfy
16
+ * `read:project`, and `read:project` does not satisfy it. Nothing is split
17
+ * off at the second `:`; a scope the issuer deliberately narrowed must not
18
+ * collapse into the broader one.
19
+ *
20
+ * ## Bare-scope rewrite
21
+ *
22
+ * A granted scope carrying no `:` is compared literally unless
23
+ * `{ allowBareScopeRewrite: true }` is passed, in which case it also matches
24
+ * `read:<scope>`. Even then, only a scope with **no** `:` is ever rewritten —
25
+ * a value such as `project:restricted` is left alone, because which of its
26
+ * segments is the action is unknowable and guessing would over-grant.
27
+ *
28
+ * Non-string entries in `ATTR_SCOPES` never match and never throw: the map is
29
+ * untyped, and a malformed value must produce a denial, not a crash.
30
+ */
2
31
  export class HasScope {
3
32
  scope;
4
33
  ruleType = "scope";
5
34
  code = "invalid_scope";
6
35
  message;
7
- constructor(scope) {
36
+ allowBareScopeRewrite;
37
+ constructor(scope, options) {
8
38
  this.scope = scope;
9
39
  this.message = `Token does not have required scope: ${scope}`;
40
+ this.allowBareScopeRewrite = options?.allowBareScopeRewrite ?? false;
10
41
  }
11
42
  verify(attrs) {
12
- const scopes = attrs.get(ATTR_SCOPES) ?? [];
13
- return scopes.some((s) => this.matchScopes(s, this.scope));
43
+ const scopes = attrs.get(ATTR_SCOPES);
44
+ if (!Array.isArray(scopes))
45
+ return false;
46
+ return scopes.some((s) => typeof s === "string" && this.matchScope(s, this.scope));
14
47
  }
15
- matchScopes(scope, required) {
16
- scope = scope.toLowerCase();
17
- required = required.toLowerCase();
18
- const parts = scope.split(":");
19
- const [perm, resource] = parts.length === 1 ? ["read", parts[0]] : parts;
20
- return required === `${perm}:${resource}`;
48
+ matchScope(granted, required) {
49
+ if (granted === required)
50
+ return true;
51
+ // The rewrite applies to bare scopes only. A granted scope that already
52
+ // contains ":" is never re-interpreted: splitting it to guess an action
53
+ // is what let "read:project:restricted" pass as "read:project".
54
+ if (this.allowBareScopeRewrite && !granted.includes(":")) {
55
+ return `read:${granted}` === required;
56
+ }
57
+ return false;
21
58
  }
22
59
  }
@@ -1,6 +1,4 @@
1
- /**
2
- * Shared validation helpers for attribute-based rule constructors.
3
- */
1
+ export declare function fnv1a64(s: string): string;
4
2
  export type LiteralValue = string | number | boolean;
5
3
  export type CompareOp = "lt" | "le" | "gt" | "ge";
6
4
  export declare const COMPARE_OPS: readonly CompareOp[];
@@ -69,15 +67,20 @@ export declare function requireCompareOp(className: string, value: unknown): Com
69
67
  export declare function requireOptionalGroup(className: string, value: unknown): string | undefined;
70
68
  /**
71
69
  * Computes a stable cache/dedup key for an array of LiteralValues.
72
- * Format: `{type}:{count}:{hashPrefix}` — SHA-256 over a canonical serialization
73
- * of `values`. The canonical form first deduplicates `values` (to mirror the
74
- * Set-based semantics of AttrLiteralIn / AttrLiteralNotIn — duplicates do not
75
- * change the rule's behavior, so they must not change its `ruleType`), then
76
- * applies `JSON.stringify(String(v))` to each remaining element before sorting
77
- * and joining with `,`. The per-element quoting prevents separator collisions
70
+ * Format: `{type}:{count}:{hashPrefix}` — `hashPrefix` is a 16-hex-character
71
+ * FNV-1a 64-bit hash over a canonical serialization of `values`. The canonical
72
+ * form first deduplicates `values` (to mirror the Set-based semantics of
73
+ * AttrLiteralIn / AttrLiteralNotIn — duplicates do not change the rule's
74
+ * behavior, so they must not change its `ruleType`), then applies
75
+ * `JSON.stringify(String(v))` to each remaining element before sorting and
76
+ * joining with `,`. The per-element quoting prevents separator collisions
78
77
  * across different arrays (e.g. `["a,b", "c"]` vs `["a", "b,c"]`).
79
78
  * The `{count}` segment reflects the post-dedup element count.
80
79
  * Caller must guarantee values is a non-empty homogeneous LiteralValue[].
80
+ *
81
+ * The hash is non-cryptographic by design, but uses 64-bit output to keep
82
+ * the birthday-collision bound (~2^32) far above any realistic policy size
83
+ * and to resist deliberate collision construction.
81
84
  */
82
85
  export declare function computeValuesKey(values: LiteralValue[]): string;
83
86
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"_sharedValidation.d.mts","sourceRoot":"","sources":["../../src/rules/_sharedValidation.mts"],"names":[],"mappings":"AAEA;;GAEG;AAEH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAarD,MAAM,MAAM,SAAS,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAElD,eAAO,MAAM,WAAW,EAAE,SAAS,SAAS,EAA6B,CAAC;AAE1E;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,GAAG,GAAG,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAe/F;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAClC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,GAAG,EACd,KAAK,EAAE,OAAO,GACZ,YAAY,CAad;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAQvF;AAED;;;;;;;GAOG;AACH,wBAAgB,8BAA8B,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,YAAY,EAAE,CAkChG;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,SAAS,CAU7E;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAe1F;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,CAS/D;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,OAAO,CAWxF"}
1
+ {"version":3,"file":"_sharedValidation.d.mts","sourceRoot":"","sources":["../../src/rules/_sharedValidation.mts"],"names":[],"mappings":"AA4BA,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAOzC;AAED,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAarD,MAAM,MAAM,SAAS,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAElD,eAAO,MAAM,WAAW,EAAE,SAAS,SAAS,EAA6B,CAAC;AAE1E;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,GAAG,GAAG,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAe/F;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAClC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,GAAG,EACd,KAAK,EAAE,OAAO,GACZ,YAAY,CAad;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAQvF;AAED;;;;;;;GAOG;AACH,wBAAgB,8BAA8B,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,YAAY,EAAE,CAkChG;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,SAAS,CAU7E;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAe1F;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,CAQ/D;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,OAAO,CAWxF"}
@@ -1,4 +1,37 @@
1
- import { createHash } from "node:crypto";
1
+ // SPDX-FileCopyrightText: 2026 1o1 Co. Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Shared validation helpers for attribute-based rule constructors.
5
+ */
6
+ /**
7
+ * FNV-1a 64-bit non-cryptographic hash, computed with BigInt. Sync,
8
+ * runtime-agnostic (no `node:*` imports), deterministic. Returns 16
9
+ * lowercase hex characters, zero-padded.
10
+ *
11
+ * Used by `computeValuesKey` to build a stable grouping suffix for `ruleType`.
12
+ * The 64-bit output gives ~2^32 birthday-collision bound, which is
13
+ * effectively non-colliding for any realistic policy size and resists
14
+ * deliberate collision construction by a hostile policy author or supply
15
+ * chain. (A 32-bit variant was rejected during review because random short
16
+ * strings can be made to collide within seconds, weakening evaluator
17
+ * grouping when two distinct rules on the same attribute share a `ruleType`.)
18
+ *
19
+ * Iterates UTF-16 code units via `charCodeAt`. BigInt is used because
20
+ * 64-bit multiplication overflows IEEE-754 doubles; `Math.imul` only
21
+ * supports 32-bit. BigInt is available in every modern JavaScript runtime
22
+ * (Node 10.4+, all evergreen browsers, Cloudflare Workers, Deno, Bun).
23
+ */
24
+ const FNV_OFFSET_BASIS_64 = 0xcbf29ce484222325n;
25
+ const FNV_PRIME_64 = 0x100000001b3n;
26
+ const FNV_MASK_64 = 0xffffffffffffffffn;
27
+ export function fnv1a64(s) {
28
+ let hash = FNV_OFFSET_BASIS_64;
29
+ for (let i = 0; i < s.length; i++) {
30
+ hash ^= BigInt(s.charCodeAt(i));
31
+ hash = (hash * FNV_PRIME_64) & FNV_MASK_64;
32
+ }
33
+ return hash.toString(16).padStart(16, "0");
34
+ }
2
35
  /**
3
36
  * Returns a human-readable type label for use in error messages.
4
37
  * Distinguishes null and undefined from other types instead of returning
@@ -152,15 +185,20 @@ export function requireOptionalGroup(className, value) {
152
185
  }
153
186
  /**
154
187
  * Computes a stable cache/dedup key for an array of LiteralValues.
155
- * Format: `{type}:{count}:{hashPrefix}` — SHA-256 over a canonical serialization
156
- * of `values`. The canonical form first deduplicates `values` (to mirror the
157
- * Set-based semantics of AttrLiteralIn / AttrLiteralNotIn — duplicates do not
158
- * change the rule's behavior, so they must not change its `ruleType`), then
159
- * applies `JSON.stringify(String(v))` to each remaining element before sorting
160
- * and joining with `,`. The per-element quoting prevents separator collisions
188
+ * Format: `{type}:{count}:{hashPrefix}` — `hashPrefix` is a 16-hex-character
189
+ * FNV-1a 64-bit hash over a canonical serialization of `values`. The canonical
190
+ * form first deduplicates `values` (to mirror the Set-based semantics of
191
+ * AttrLiteralIn / AttrLiteralNotIn — duplicates do not change the rule's
192
+ * behavior, so they must not change its `ruleType`), then applies
193
+ * `JSON.stringify(String(v))` to each remaining element before sorting and
194
+ * joining with `,`. The per-element quoting prevents separator collisions
161
195
  * across different arrays (e.g. `["a,b", "c"]` vs `["a", "b,c"]`).
162
196
  * The `{count}` segment reflects the post-dedup element count.
163
197
  * Caller must guarantee values is a non-empty homogeneous LiteralValue[].
198
+ *
199
+ * The hash is non-cryptographic by design, but uses 64-bit output to keep
200
+ * the birthday-collision bound (~2^32) far above any realistic policy size
201
+ * and to resist deliberate collision construction.
164
202
  */
165
203
  export function computeValuesKey(values) {
166
204
  const elementType = typeof values[0];
@@ -169,8 +207,7 @@ export function computeValuesKey(values) {
169
207
  .map((v) => JSON.stringify(String(v)))
170
208
  .sort()
171
209
  .join(",");
172
- const hash = createHash("sha256").update(joined).digest("hex").slice(0, 8);
173
- return `${elementType}:${unique.length}:${hash}`;
210
+ return `${elementType}:${unique.length}:${fnv1a64(joined)}`;
174
211
  }
175
212
  /**
176
213
  * Applies a numeric comparison operator between left and right.
@@ -1,4 +1,10 @@
1
1
  import type { CollectorContext, Rule, RuleCollector } from "@o3co/auth.policy-verifier.core";
2
+ /**
3
+ * Rule collector that emits a single `HasPermission` rule for the
4
+ * `{resource}.perm:{action}` permission string. Use when the effective
5
+ * permission is encoded directly into the token's permission attribute rather
6
+ * than derived from scopes.
7
+ */
2
8
  export declare class ResourceActionPermissionRuleCollector implements RuleCollector {
3
9
  collect(context: CollectorContext): Promise<Rule[]>;
4
10
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ResourceActionPermissionRuleCollector.d.mts","sourceRoot":"","sources":["../../../src/rules/collectors/ResourceActionPermissionRuleCollector.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAG7F,qBAAa,qCAAsC,YAAW,aAAa;IACpE,OAAO,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;CAIzD"}
1
+ {"version":3,"file":"ResourceActionPermissionRuleCollector.d.mts","sourceRoot":"","sources":["../../../src/rules/collectors/ResourceActionPermissionRuleCollector.mts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAG7F;;;;;GAKG;AACH,qBAAa,qCAAsC,YAAW,aAAa;IACpE,OAAO,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;CAIzD"}
@@ -1,4 +1,12 @@
1
+ // SPDX-FileCopyrightText: 2026 1o1 Co. Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { HasPermission } from "../HasPermission.mjs";
4
+ /**
5
+ * Rule collector that emits a single `HasPermission` rule for the
6
+ * `{resource}.perm:{action}` permission string. Use when the effective
7
+ * permission is encoded directly into the token's permission attribute rather
8
+ * than derived from scopes.
9
+ */
2
10
  export class ResourceActionPermissionRuleCollector {
3
11
  async collect(context) {
4
12
  const permission = `${context.resource.raw}.perm:${context.action}`;
@@ -1,14 +1,62 @@
1
1
  import type { CollectorContext, Rule, RuleCollector } from "@o3co/auth.policy-verifier.core";
2
+ /** How the collector treats a token that carries no `scope` claim. */
3
+ export type ScopelessPolicy = "deny" | "skip";
4
+ /** Config entry accepted by `ResourceActionScopeRuleCollector`. */
5
+ export interface ResourceActionScopeRuleCollectorConfig {
6
+ /**
7
+ * `"deny"` (default) emits the scope rule for every request, so a scopeless
8
+ * token fails it. `"skip"` emits no rule for a scopeless token, leaving the
9
+ * scope group out of AND-evaluation.
10
+ */
11
+ scopeless?: ScopelessPolicy;
12
+ /**
13
+ * Forwarded to `HasScope`. `false` (default) compares granted scopes
14
+ * literally; `true` additionally treats a bare granted scope `x` as
15
+ * `read:x`. See `HasScopeOptions.allowBareScopeRewrite`.
16
+ */
17
+ allowBareScopeRewrite?: boolean;
18
+ }
2
19
  /**
3
20
  * Generates a HasScope rule derived from the request action and resource type.
4
21
  *
5
- * When the JWT payload has no `scope` claim (e.g. DID-grant tokens), no rules
6
- * are generated. The scope rule group is therefore absent from AND-evaluation,
7
- * allowing other rule groups to decide independently.
22
+ * ## Scope as capability ceiling
23
+ *
24
+ * The JWT `scope` claim represents what the session **can request** — a
25
+ * capability ceiling — not what the session **has been granted**. Whether the
26
+ * operation is ultimately permitted is determined by the full rule pipeline
27
+ * (other collectors, resource-owner policy, etc.). This collector enforces the
28
+ * ceiling only: it produces a `HasScope` rule for the requested
29
+ * `{action}:{resourceType}`, which must be satisfied by the token's scopes.
30
+ *
31
+ * ## Matching
32
+ *
33
+ * The emitted `HasScope` compares scopes exactly and case-sensitively. An
34
+ * issuer that emits bare resource names (`project` rather than `read:project`)
35
+ * must opt in with `{ allowBareScopeRewrite: true }`; without it a bare scope
36
+ * satisfies nothing of the `{action}:{resourceType}` form.
37
+ *
38
+ * ## Behavior for scopeless tokens
39
+ *
40
+ * By default the rule is emitted regardless of whether the token carries a
41
+ * `scope` claim, so a scopeless token fails it. Dropping the rule instead would
42
+ * remove the scope group from AND-evaluation, and in a scope-only pipeline that
43
+ * turns "the token asserts no capability" into "every capability is allowed".
44
+ *
45
+ * Flows where the IdP does not issue a `scope` claim (e.g. DID-grant tokens)
46
+ * must opt out explicitly with `{ scopeless: "skip" }`, and only in a pipeline
47
+ * where another rule group authorizes the request — otherwise the request is
48
+ * left with no applicable rule, which the evaluator denies.
49
+ *
50
+ * For pipelines that exclusively serve scopeless flows, prefer collectors that
51
+ * derive rules from identity claims (e.g. DID, `sub`, role) instead.
8
52
  *
9
- * OAuth JWTs that carry a `scope` claim are validated as before.
53
+ * See https://github.com/o3co/auth.provider/issues/56 for background on why
54
+ * the IdP asserts identity, not permissions.
10
55
  */
11
56
  export declare class ResourceActionScopeRuleCollector implements RuleCollector {
57
+ private readonly scopeless;
58
+ private readonly allowBareScopeRewrite;
59
+ constructor(config?: ResourceActionScopeRuleCollectorConfig);
12
60
  collect(context: CollectorContext): Promise<Rule[]>;
13
61
  }
14
62
  //# sourceMappingURL=ResourceActionScopeRuleCollector.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ResourceActionScopeRuleCollector.d.mts","sourceRoot":"","sources":["../../../src/rules/collectors/ResourceActionScopeRuleCollector.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAG7F;;;;;;;;GAQG;AACH,qBAAa,gCAAiC,YAAW,aAAa;IAC/D,OAAO,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;CAOzD"}
1
+ {"version":3,"file":"ResourceActionScopeRuleCollector.d.mts","sourceRoot":"","sources":["../../../src/rules/collectors/ResourceActionScopeRuleCollector.mts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAG7F,sEAAsE;AACtE,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,MAAM,CAAC;AAI9C,mEAAmE;AACnE,MAAM,WAAW,sCAAsC;IACtD;;;;OAIG;IACH,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,qBAAa,gCAAiC,YAAW,aAAa;IACrE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAkB;IAC5C,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAU;gBAEpC,MAAM,CAAC,EAAE,sCAAsC;IAqBrD,OAAO,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;CAOzD"}
@@ -1,19 +1,67 @@
1
+ // SPDX-FileCopyrightText: 2026 1o1 Co. Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { HasScope } from "../HasScope.mjs";
4
+ const SCOPELESS_POLICIES = ["deny", "skip"];
2
5
  /**
3
6
  * Generates a HasScope rule derived from the request action and resource type.
4
7
  *
5
- * When the JWT payload has no `scope` claim (e.g. DID-grant tokens), no rules
6
- * are generated. The scope rule group is therefore absent from AND-evaluation,
7
- * allowing other rule groups to decide independently.
8
+ * ## Scope as capability ceiling
8
9
  *
9
- * OAuth JWTs that carry a `scope` claim are validated as before.
10
+ * The JWT `scope` claim represents what the session **can request** — a
11
+ * capability ceiling — not what the session **has been granted**. Whether the
12
+ * operation is ultimately permitted is determined by the full rule pipeline
13
+ * (other collectors, resource-owner policy, etc.). This collector enforces the
14
+ * ceiling only: it produces a `HasScope` rule for the requested
15
+ * `{action}:{resourceType}`, which must be satisfied by the token's scopes.
16
+ *
17
+ * ## Matching
18
+ *
19
+ * The emitted `HasScope` compares scopes exactly and case-sensitively. An
20
+ * issuer that emits bare resource names (`project` rather than `read:project`)
21
+ * must opt in with `{ allowBareScopeRewrite: true }`; without it a bare scope
22
+ * satisfies nothing of the `{action}:{resourceType}` form.
23
+ *
24
+ * ## Behavior for scopeless tokens
25
+ *
26
+ * By default the rule is emitted regardless of whether the token carries a
27
+ * `scope` claim, so a scopeless token fails it. Dropping the rule instead would
28
+ * remove the scope group from AND-evaluation, and in a scope-only pipeline that
29
+ * turns "the token asserts no capability" into "every capability is allowed".
30
+ *
31
+ * Flows where the IdP does not issue a `scope` claim (e.g. DID-grant tokens)
32
+ * must opt out explicitly with `{ scopeless: "skip" }`, and only in a pipeline
33
+ * where another rule group authorizes the request — otherwise the request is
34
+ * left with no applicable rule, which the evaluator denies.
35
+ *
36
+ * For pipelines that exclusively serve scopeless flows, prefer collectors that
37
+ * derive rules from identity claims (e.g. DID, `sub`, role) instead.
38
+ *
39
+ * See https://github.com/o3co/auth.provider/issues/56 for background on why
40
+ * the IdP asserts identity, not permissions.
10
41
  */
11
42
  export class ResourceActionScopeRuleCollector {
43
+ scopeless;
44
+ allowBareScopeRewrite;
45
+ constructor(config) {
46
+ const scopeless = config?.scopeless ?? "deny";
47
+ if (!SCOPELESS_POLICIES.includes(scopeless)) {
48
+ throw new Error(`ResourceActionScopeRuleCollector: scopeless must be one of ${SCOPELESS_POLICIES.join(", ")}, got "${scopeless}"`);
49
+ }
50
+ this.scopeless = scopeless;
51
+ // Validate the raw value before defaulting: `?? false` alone would treat an
52
+ // explicit `null` as "unset" and silently accept a misconfiguration. Only
53
+ // an absent key may fall back to the default.
54
+ const rawRewrite = config?.allowBareScopeRewrite;
55
+ if (rawRewrite !== undefined && typeof rawRewrite !== "boolean") {
56
+ throw new Error(`ResourceActionScopeRuleCollector: allowBareScopeRewrite must be a boolean, got "${rawRewrite}"`);
57
+ }
58
+ this.allowBareScopeRewrite = rawRewrite ?? false;
59
+ }
12
60
  async collect(context) {
13
- if (context.payload.scope === undefined) {
61
+ if (this.scopeless === "skip" && context.subject.scope === undefined) {
14
62
  return [];
15
63
  }
16
64
  const scope = `${context.action}:${context.resource.resourceType}`;
17
- return [new HasScope(scope)];
65
+ return [new HasScope(scope, { allowBareScopeRewrite: this.allowBareScopeRewrite })];
18
66
  }
19
67
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@o3co/auth.policy-verifier.builtins",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "engines": {
@@ -31,15 +31,18 @@
31
31
  }
32
32
  },
33
33
  "dependencies": {
34
- "@o3co/auth.policy-verifier.core": "0.3.1"
34
+ "@o3co/auth.policy-verifier.core": "0.5.0"
35
35
  },
36
36
  "devDependencies": {
37
- "@types/node": "^25.1.0",
38
- "vitest": "^4.1.2"
37
+ "@types/node": "^26.4.0",
38
+ "@vitest/coverage-v8": "^4.1.11",
39
+ "vitest": "^4.1.11"
39
40
  },
40
41
  "scripts": {
41
42
  "prebuild": "rimraf dist",
42
43
  "build": "tsc",
43
- "test": "vitest run"
44
+ "typecheck": "tsc --noEmit -p tsconfig.typecheck.json",
45
+ "test": "vitest run",
46
+ "test:coverage": "vitest run --coverage"
44
47
  }
45
48
  }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=PayloadScopeCollector.test.d.mts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"PayloadScopeCollector.test.d.mts","sourceRoot":"","sources":["../../../src/__tests__/collectors/PayloadScopeCollector.test.mts"],"names":[],"mappings":""}
@@ -1,24 +0,0 @@
1
- import { ATTR_SCOPES } from "@o3co/auth.policy-verifier.core";
2
- import { describe, expect, it } from "vitest";
3
- import { PayloadScopeCollector } from "#/collectors/PayloadScopeCollector.mjs";
4
- const makeContext = (scope) => ({
5
- payload: { scope },
6
- resource: { raw: "test:1", resourceType: "test", resourceId: "1" },
7
- action: "read",
8
- });
9
- describe("PayloadScopeCollector", () => {
10
- const collector = new PayloadScopeCollector();
11
- it("extracts scopes from payload", async () => {
12
- const attrs = await collector.collect(makeContext("read:user write:doc"));
13
- expect(attrs.get(ATTR_SCOPES)).toEqual(["read:user", "write:doc"]);
14
- });
15
- it("returns empty array when scope missing", async () => {
16
- const ctx = {
17
- payload: {},
18
- resource: { raw: "test:1", resourceType: "test", resourceId: "1" },
19
- action: "read",
20
- };
21
- const attrs = await collector.collect(ctx);
22
- expect(attrs.get(ATTR_SCOPES)).toEqual([]);
23
- });
24
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=PayloadSubjectIdCollector.test.d.mts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"PayloadSubjectIdCollector.test.d.mts","sourceRoot":"","sources":["../../../src/__tests__/collectors/PayloadSubjectIdCollector.test.mts"],"names":[],"mappings":""}
@@ -1,33 +0,0 @@
1
- import { ATTR_CLIENT_ID, ATTR_USER_ID } from "@o3co/auth.policy-verifier.core";
2
- import { describe, expect, it } from "vitest";
3
- import { PayloadSubjectIdCollector } from "#/collectors/PayloadSubjectIdCollector.mjs";
4
- describe("PayloadSubjectIdCollector", () => {
5
- const collector = new PayloadSubjectIdCollector();
6
- it("extracts userId from payload.sub", async () => {
7
- const ctx = {
8
- payload: { sub: "u1" },
9
- resource: { raw: "test:1", resourceType: "test", resourceId: "1" },
10
- action: "read",
11
- };
12
- const attrs = await collector.collect(ctx);
13
- expect(attrs.get(ATTR_USER_ID)).toBe("u1");
14
- });
15
- it("extracts clientId from payload.azp", async () => {
16
- const ctx = {
17
- payload: { azp: "c1" },
18
- resource: { raw: "test:1", resourceType: "test", resourceId: "1" },
19
- action: "read",
20
- };
21
- const attrs = await collector.collect(ctx);
22
- expect(attrs.get(ATTR_CLIENT_ID)).toBe("c1");
23
- });
24
- it("returns empty map when neither sub nor azp present", async () => {
25
- const ctx = {
26
- payload: {},
27
- resource: { raw: "test:1", resourceType: "test", resourceId: "1" },
28
- action: "read",
29
- };
30
- const attrs = await collector.collect(ctx);
31
- expect(attrs.size).toBe(0);
32
- });
33
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=StaticPermissionCollector.test.d.mts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"StaticPermissionCollector.test.d.mts","sourceRoot":"","sources":["../../../src/__tests__/collectors/StaticPermissionCollector.test.mts"],"names":[],"mappings":""}
@@ -1,22 +0,0 @@
1
- import { ATTR_PERMISSIONS } from "@o3co/auth.policy-verifier.core";
2
- import { describe, expect, it } from "vitest";
3
- import { StaticPermissionCollector } from "#/collectors/StaticPermissionCollector.mjs";
4
- const stubContext = {
5
- payload: {},
6
- resource: { raw: "test:1", resourceType: "test", resourceId: "1" },
7
- action: "read",
8
- };
9
- describe("StaticPermissionCollector", () => {
10
- it("returns configured permissions", async () => {
11
- const collector = new StaticPermissionCollector({
12
- permissions: ["project:*.perm:read", "document:*.perm:write"],
13
- });
14
- const attrs = await collector.collect(stubContext);
15
- expect(attrs.get(ATTR_PERMISSIONS)).toEqual(["project:*.perm:read", "document:*.perm:write"]);
16
- });
17
- it("returns empty array when no permissions configured", async () => {
18
- const collector = new StaticPermissionCollector({ permissions: [] });
19
- const attrs = await collector.collect(stubContext);
20
- expect(attrs.get(ATTR_PERMISSIONS)).toEqual([]);
21
- });
22
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=StaticRoleCollector.test.d.mts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"StaticRoleCollector.test.d.mts","sourceRoot":"","sources":["../../../src/__tests__/collectors/StaticRoleCollector.test.mts"],"names":[],"mappings":""}
@@ -1,24 +0,0 @@
1
- import { ATTR_ROLES } from "@o3co/auth.policy-verifier.core";
2
- import { describe, expect, it } from "vitest";
3
- import { StaticRoleCollector } from "#/collectors/StaticRoleCollector.mjs";
4
- const stubContext = {
5
- payload: {},
6
- resource: { raw: "test:1", resourceType: "test", resourceId: "1" },
7
- action: "read",
8
- };
9
- describe("StaticRoleCollector", () => {
10
- it("returns configured roles", async () => {
11
- const roles = [
12
- { name: "admin", permissions: ["*"] },
13
- { name: "viewer", permissions: ["project:*.perm:read"] },
14
- ];
15
- const collector = new StaticRoleCollector({ roles });
16
- const attrs = await collector.collect(stubContext);
17
- expect(attrs.get(ATTR_ROLES)).toEqual(roles);
18
- });
19
- it("returns empty array when no roles configured", async () => {
20
- const collector = new StaticRoleCollector({ roles: [] });
21
- const attrs = await collector.collect(stubContext);
22
- expect(attrs.get(ATTR_ROLES)).toEqual([]);
23
- });
24
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=module.test.d.mts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"module.test.d.mts","sourceRoot":"","sources":["../../src/__tests__/module.test.mts"],"names":[],"mappings":""}