@apart-tech/intelligence-core 1.11.3 → 1.11.5

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 (88) hide show
  1. package/dist/auth/ability.d.ts +148 -0
  2. package/dist/auth/ability.d.ts.map +1 -0
  3. package/dist/auth/ability.js +285 -0
  4. package/dist/auth/ability.js.map +1 -0
  5. package/dist/auth/ability.test.d.ts +2 -0
  6. package/dist/auth/ability.test.d.ts.map +1 -0
  7. package/dist/auth/ability.test.js +680 -0
  8. package/dist/auth/ability.test.js.map +1 -0
  9. package/dist/auth/delegation-jwt.d.ts +167 -0
  10. package/dist/auth/delegation-jwt.d.ts.map +1 -0
  11. package/dist/auth/delegation-jwt.js +237 -0
  12. package/dist/auth/delegation-jwt.js.map +1 -0
  13. package/dist/auth/delegation-jwt.test.d.ts +2 -0
  14. package/dist/auth/delegation-jwt.test.d.ts.map +1 -0
  15. package/dist/auth/delegation-jwt.test.js +283 -0
  16. package/dist/auth/delegation-jwt.test.js.map +1 -0
  17. package/dist/auth/principal.d.ts +94 -0
  18. package/dist/auth/principal.d.ts.map +1 -0
  19. package/dist/auth/principal.js +33 -0
  20. package/dist/auth/principal.js.map +1 -0
  21. package/dist/config/config.test.d.ts +2 -0
  22. package/dist/config/config.test.d.ts.map +1 -0
  23. package/dist/config/config.test.js +57 -0
  24. package/dist/config/config.test.js.map +1 -0
  25. package/dist/config/index.d.ts.map +1 -1
  26. package/dist/config/index.js +17 -0
  27. package/dist/config/index.js.map +1 -1
  28. package/dist/index.d.ts +13 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +6 -0
  31. package/dist/index.js.map +1 -1
  32. package/dist/lib/__tests__/jwt.test.d.ts +2 -0
  33. package/dist/lib/__tests__/jwt.test.d.ts.map +1 -0
  34. package/dist/lib/__tests__/jwt.test.js +97 -0
  35. package/dist/lib/__tests__/jwt.test.js.map +1 -0
  36. package/dist/lib/jwt.d.ts +20 -0
  37. package/dist/lib/jwt.d.ts.map +1 -1
  38. package/dist/lib/jwt.js +56 -3
  39. package/dist/lib/jwt.js.map +1 -1
  40. package/dist/services/__tests__/delegation-cleanup-service.test.d.ts +2 -0
  41. package/dist/services/__tests__/delegation-cleanup-service.test.d.ts.map +1 -0
  42. package/dist/services/__tests__/delegation-cleanup-service.test.js +211 -0
  43. package/dist/services/__tests__/delegation-cleanup-service.test.js.map +1 -0
  44. package/dist/services/agent-run-service.d.ts +44 -7
  45. package/dist/services/agent-run-service.d.ts.map +1 -1
  46. package/dist/services/agent-run-service.js +14 -0
  47. package/dist/services/agent-run-service.js.map +1 -1
  48. package/dist/services/agent-schedule-service.d.ts +21 -0
  49. package/dist/services/agent-schedule-service.d.ts.map +1 -1
  50. package/dist/services/agent-schedule-service.js +12 -0
  51. package/dist/services/agent-schedule-service.js.map +1 -1
  52. package/dist/services/audit-event-service.d.ts +76 -0
  53. package/dist/services/audit-event-service.d.ts.map +1 -0
  54. package/dist/services/audit-event-service.js +48 -0
  55. package/dist/services/audit-event-service.js.map +1 -0
  56. package/dist/services/cleaning-service.d.ts.map +1 -1
  57. package/dist/services/cleaning-service.js +5 -1
  58. package/dist/services/cleaning-service.js.map +1 -1
  59. package/dist/services/delegation-cleanup-service.d.ts +133 -0
  60. package/dist/services/delegation-cleanup-service.d.ts.map +1 -0
  61. package/dist/services/delegation-cleanup-service.js +111 -0
  62. package/dist/services/delegation-cleanup-service.js.map +1 -0
  63. package/dist/services/edge-service.d.ts.map +1 -1
  64. package/dist/services/edge-service.js +3 -0
  65. package/dist/services/edge-service.js.map +1 -1
  66. package/dist/services/org-agent-type-service.d.ts +15 -0
  67. package/dist/services/org-agent-type-service.d.ts.map +1 -1
  68. package/dist/services/org-agent-type-service.js +2 -0
  69. package/dist/services/org-agent-type-service.js.map +1 -1
  70. package/dist/services/usage-service.d.ts +48 -0
  71. package/dist/services/usage-service.d.ts.map +1 -0
  72. package/dist/services/usage-service.js +116 -0
  73. package/dist/services/usage-service.js.map +1 -0
  74. package/dist/services/user-service.d.ts.map +1 -1
  75. package/dist/services/user-service.js +24 -6
  76. package/dist/services/user-service.js.map +1 -1
  77. package/dist/services/user-service.test.d.ts +2 -0
  78. package/dist/services/user-service.test.d.ts.map +1 -0
  79. package/dist/services/user-service.test.js +86 -0
  80. package/dist/services/user-service.test.js.map +1 -0
  81. package/dist/types/index.d.ts +13 -0
  82. package/dist/types/index.d.ts.map +1 -1
  83. package/package.json +3 -2
  84. package/prisma/schema.prisma +158 -82
  85. package/dist/db/schema.d.ts +0 -507
  86. package/dist/db/schema.d.ts.map +0 -1
  87. package/dist/db/schema.js +0 -77
  88. package/dist/db/schema.js.map +0 -1
@@ -0,0 +1,148 @@
1
+ import { type MongoAbility, type RawRuleOf } from "@casl/ability";
2
+ import type { Principal } from "./principal.js";
3
+ /**
4
+ * In-process authorization engine for Apart Intelligence (Phase 1b).
5
+ *
6
+ * Every permission check in the API service layer goes through
7
+ * `principal.can(action, subject)` via the `AppAbility` returned by
8
+ * `buildAbility(principal)`. CASL owns the rule engine; `buildAbility` owns
9
+ * the per-principal-type rule construction.
10
+ *
11
+ * The action/subject vocabulary is intentionally the minimal starter set —
12
+ * just enough to cover the five existing `requireRole` call sites in
13
+ * `members.ts` and `invites.ts`, plus the accept-invite flow for users
14
+ * without memberships. Phase 1e grows the vocabulary per-route as the
15
+ * broader route-handler sweep lands.
16
+ *
17
+ * **No field conditions in Phase 1b rules.** Fine-grained row scoping
18
+ * (e.g., "this user can only read their own membership") is not expressed
19
+ * in CASL rules. Tenant isolation at the query level — Prisma's org-id
20
+ * injection extension — already restricts every DB read to the active
21
+ * organization, so a `can('read', 'Membership')` check is implicitly
22
+ * scoped to the caller's org at the data layer. Route handlers that need
23
+ * self-ID checks (e.g., "this must be the caller's own user row") do them
24
+ * explicitly. This keeps Phase 1b's authorization surface narrow and the
25
+ * CASL type wiring simple; Phase 1e can add conditions back via the
26
+ * `subject()` runtime helper once the per-route refactor reveals the right
27
+ * shapes.
28
+ *
29
+ * **Do not** add an action or a subject to these types without also thinking
30
+ * through the role rules below. CASL rules that reference an unknown action
31
+ * or subject become dead code silently, which is the opposite of what
32
+ * authorization code should do.
33
+ */
34
+ /**
35
+ * Canonical action verbs. `manage` is CASL's wildcard matching any action.
36
+ *
37
+ * `bypass` was added in Phase 1e as a first-class action so that the
38
+ * `X-Pii-Bypass` header (Phase 1 Security Review finding M3) can be gated
39
+ * with the natural phrasing `can('bypass', 'Pii')`. It is granted
40
+ * implicitly by the `manage` wildcard, which is the correct CASL semantic
41
+ * and preserves the legacy-api-key-grants-everything invariant without
42
+ * an explicit rule. See decision `500bfa31` for the full rationale,
43
+ * including which alternatives were rejected.
44
+ */
45
+ export type AppAction = "manage" | "create" | "read" | "update" | "delete" | "bypass";
46
+ /**
47
+ * Canonical subject names. `all` is CASL's wildcard matching any subject.
48
+ *
49
+ * Phase 1e added `OrgConfig`, `AgentSchedule`, and `Pii` to close the
50
+ * Phase 1 Security Review findings H6 (`/api/org/config` authz), H7
51
+ * (`/api/agent/schedules` authz), and M3 (`X-Pii-Bypass` gating). Any new
52
+ * subject added here **must also** be added to `CONCRETE_SUBJECTS` below
53
+ * or `intersect()` will silently drop any rule that targets it.
54
+ */
55
+ export type AppSubject = "Organization" | "Membership" | "Invite" | "User" | "OrgConfig" | "AgentSchedule" | "Pii" | "Node" | "Search" | "Import" | "Domain" | "Workspace" | "AgentRun" | "OrgAgentConfig" | "OrgEmbedding" | "UserSecret" | "all";
56
+ /** The CASL ability type used everywhere in the API. */
57
+ export type AppAbility = MongoAbility<[AppAction, AppSubject]>;
58
+ /** A CASL raw rule typed for our ability — used when serializing a
59
+ * DelegatedAgent's captured ability snapshot. */
60
+ export type AppRawRule = RawRuleOf<AppAbility>;
61
+ /**
62
+ * Build a CASL `AppAbility` from a `Principal`. Pure function — no I/O, no
63
+ * DB reads. The principal must be fully constructed by the auth middleware
64
+ * before this is called; the result can be cached per-request.
65
+ *
66
+ * Rule semantics by principal type:
67
+ *
68
+ * - **UserPrincipal**: rules derive from the user's `role` on their active
69
+ * `organizationId`. `owner` grants `manage all` (including `bypass Pii`
70
+ * by wildcard). `admin` grants management of invites and agent schedules
71
+ * plus read of org/membership/OrgConfig/Pii. `member` grants read of
72
+ * org/membership/OrgConfig/AgentSchedule/Pii. `none` (no active org)
73
+ * grants read of self and create of memberships for invite acceptance.
74
+ * Scoping to the caller's org happens at the query layer via Prisma
75
+ * tenant isolation. Admins and members cannot bypass PII scrubbing —
76
+ * the `bypass` action is reserved for `manage`-grade principals only,
77
+ * per the Phase 1e decision `500bfa31`.
78
+ *
79
+ * - **OrgAgentPrincipal**: if `legacyApiKey` is true (the default for
80
+ * pre-Phase-1c API keys), grants `manage all` to match current behavior.
81
+ * Once Phase 1c binds keys to real `OrgAgentType` rows, this branch will
82
+ * read from `OrgAgentType.intrinsicPolicy` instead — the legacy path stays
83
+ * as a fallback until the backfill completes.
84
+ *
85
+ * - **DelegatedAgentPrincipal**: rehydrates CASL rules from
86
+ * `capturedAbility`. The snapshot was computed at agent spawn time as
87
+ * `intersect(userAbility, agentIntrinsicPolicy)` and serialized into
88
+ * `AgentRun.capturedAbility` (Phase 1c column). This function is a pure
89
+ * deserializer for that snapshot; it does not recompute anything.
90
+ */
91
+ export declare function buildAbility(principal: Principal): AppAbility;
92
+ /**
93
+ * Thrown when `intersect()` is asked to combine rule sets whose shape is
94
+ * outside the starter CASL vocabulary — currently any rule that carries
95
+ * CASL `conditions` or `fields`. See `intersect` docstring.
96
+ */
97
+ export declare class UnsupportedIntersectionError extends Error {
98
+ constructor(reason: string);
99
+ }
100
+ /**
101
+ * Compute the intersection of two raw CASL rule sets, producing a rule set
102
+ * that grants access only where BOTH inputs grant access.
103
+ *
104
+ * Used at agent spawn time to capture the effective authority of a
105
+ * `DelegatedAgentPrincipal`: the user's ability at spawn time, intersected
106
+ * with the agent's intrinsic policy, is the ceiling on what the agent can
107
+ * do for the life of the run. The result is persisted on
108
+ * `AgentRun.captured_ability` (the Phase 1c column) and rehydrated by
109
+ * `buildAbility(DelegatedAgentPrincipal)` on every callback from the
110
+ * sandbox. See the Phase 1d user story `ed8fcc68` for the full rationale.
111
+ *
112
+ * **Semantics.** The intersection is computed by building a CASL ability
113
+ * from each input and enumerating every concrete `(action, subject)` pair
114
+ * in the `AppAction × AppSubject` cross product. A rule is emitted for
115
+ * each pair where both abilities grant access. CASL's `manage` and `all`
116
+ * wildcards are handled implicitly because `.can("read", "Organization")`
117
+ * on a `manage all` ability returns true.
118
+ *
119
+ * The returned rules are NOT compressed — a full-overlap intersection
120
+ * yields `CONCRETE_ACTIONS.length × CONCRETE_SUBJECTS.length` concrete
121
+ * rules (currently 5 × 7 = 35) rather than
122
+ * `[{action:"manage", subject:"all"}]`. This is semantically equivalent
123
+ * but more verbose. If audit-log readability ever becomes a concern, a
124
+ * `compress()` helper can be added as a separate function; for now,
125
+ * verbosity is the price of simplicity.
126
+ *
127
+ * **Phase 1d limitation.** CASL conditions and field scopes are not
128
+ * supported on either side of the intersection. The current `AppAbility`
129
+ * vocabulary explicitly does not use them (see the docstring at the top
130
+ * of this file — "No field conditions in Phase 1b rules"), so this is
131
+ * the correct shape for the current starter vocabulary. If an input
132
+ * rule carries `conditions` or `fields`, this function throws
133
+ * `UnsupportedIntersectionError` rather than silently producing an
134
+ * over-permissive result. When a future phase grows the CASL vocabulary
135
+ * to use conditions, this function grows with it.
136
+ *
137
+ * **Edge cases.**
138
+ * - Empty ∩ X → empty (deny by default).
139
+ * - X ∩ empty → empty.
140
+ * - Disjoint inputs → empty.
141
+ * - Full overlap (both sides grant `manage all`) → all concrete rules
142
+ * in the cross product (5 × 7 = 35 today), which rehydrates to a
143
+ * functionally-manage-all ability.
144
+ * - One side is `manage all`, the other is a specific rule → the specific
145
+ * rule (expanded to concrete (action, subject) form).
146
+ */
147
+ export declare function intersect(a: AppRawRule[], b: AppRawRule[]): AppRawRule[];
148
+ //# sourceMappingURL=ability.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ability.d.ts","sourceRoot":"","sources":["../../src/auth/ability.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,SAAS,EACf,MAAM,eAAe,CAAC;AAEvB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH;;;;;;;;;;GAUG;AACH,MAAM,MAAM,SAAS,GACjB,QAAQ,GACR,QAAQ,GACR,MAAM,GACN,QAAQ,GACR,QAAQ,GACR,QAAQ,CAAC;AAEb;;;;;;;;GAQG;AACH,MAAM,MAAM,UAAU,GAClB,cAAc,GACd,YAAY,GACZ,QAAQ,GACR,MAAM,GACN,WAAW,GACX,eAAe,GACf,KAAK,GACL,MAAM,GACN,QAAQ,GACR,QAAQ,GACR,QAAQ,GACR,WAAW,GACX,UAAU,GACV,gBAAgB,GAChB,cAAc,GACd,YAAY,GACZ,KAAK,CAAC;AAEV,wDAAwD;AACxD,MAAM,MAAM,UAAU,GAAG,YAAY,CAAC,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC,CAAC;AAE/D;kDACkD;AAClD,MAAM,MAAM,UAAU,GAAG,SAAS,CAAC,UAAU,CAAC,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,SAAS,GAAG,UAAU,CA6G7D;AA2BD;;;;GAIG;AACH,qBAAa,4BAA6B,SAAQ,KAAK;gBACzC,MAAM,EAAE,MAAM;CAI3B;AAyCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,UAAU,EAAE,EAAE,CAAC,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,CAgBxE"}
@@ -0,0 +1,285 @@
1
+ import { AbilityBuilder, createMongoAbility, } from "@casl/ability";
2
+ /**
3
+ * Build a CASL `AppAbility` from a `Principal`. Pure function — no I/O, no
4
+ * DB reads. The principal must be fully constructed by the auth middleware
5
+ * before this is called; the result can be cached per-request.
6
+ *
7
+ * Rule semantics by principal type:
8
+ *
9
+ * - **UserPrincipal**: rules derive from the user's `role` on their active
10
+ * `organizationId`. `owner` grants `manage all` (including `bypass Pii`
11
+ * by wildcard). `admin` grants management of invites and agent schedules
12
+ * plus read of org/membership/OrgConfig/Pii. `member` grants read of
13
+ * org/membership/OrgConfig/AgentSchedule/Pii. `none` (no active org)
14
+ * grants read of self and create of memberships for invite acceptance.
15
+ * Scoping to the caller's org happens at the query layer via Prisma
16
+ * tenant isolation. Admins and members cannot bypass PII scrubbing —
17
+ * the `bypass` action is reserved for `manage`-grade principals only,
18
+ * per the Phase 1e decision `500bfa31`.
19
+ *
20
+ * - **OrgAgentPrincipal**: if `legacyApiKey` is true (the default for
21
+ * pre-Phase-1c API keys), grants `manage all` to match current behavior.
22
+ * Once Phase 1c binds keys to real `OrgAgentType` rows, this branch will
23
+ * read from `OrgAgentType.intrinsicPolicy` instead — the legacy path stays
24
+ * as a fallback until the backfill completes.
25
+ *
26
+ * - **DelegatedAgentPrincipal**: rehydrates CASL rules from
27
+ * `capturedAbility`. The snapshot was computed at agent spawn time as
28
+ * `intersect(userAbility, agentIntrinsicPolicy)` and serialized into
29
+ * `AgentRun.capturedAbility` (Phase 1c column). This function is a pure
30
+ * deserializer for that snapshot; it does not recompute anything.
31
+ */
32
+ export function buildAbility(principal) {
33
+ const { can, build } = new AbilityBuilder(createMongoAbility);
34
+ switch (principal.type) {
35
+ case "user": {
36
+ switch (principal.role) {
37
+ case "owner":
38
+ // Full access — matches today's `requireRole("owner")` behavior.
39
+ can("manage", "all");
40
+ break;
41
+ case "admin":
42
+ // Admins manage the invite lifecycle and can read the org and
43
+ // members. They cannot touch memberships directly; role changes
44
+ // are owner-only. Phase 1e: admins can also manage agent
45
+ // schedules (scheduled delegated agents are an operational
46
+ // concern admins need to own), and can read org-level config
47
+ // surfaces without being able to change them.
48
+ // Phase 1b: admins get manage on the full knowledge-graph
49
+ // surface (Node, Import, Domain, Workspace, AgentRun) and
50
+ // org-level config (OrgAgentConfig, OrgEmbedding). They get
51
+ // read on Search (a read-only operation).
52
+ can("manage", "Invite");
53
+ can("manage", "AgentSchedule");
54
+ can("manage", "Node");
55
+ can("manage", "Import");
56
+ can("manage", "Domain");
57
+ can("manage", "Workspace");
58
+ can("manage", "AgentRun");
59
+ can("manage", "OrgAgentConfig");
60
+ can("manage", "OrgEmbedding");
61
+ can("manage", "UserSecret");
62
+ can("read", "Organization");
63
+ can("read", "Membership");
64
+ can("read", "OrgConfig");
65
+ can("read", "Pii");
66
+ can("read", "Search");
67
+ break;
68
+ case "member":
69
+ // Members can read their org and memberships and the org-level
70
+ // config surfaces (so that the UI can show them). They cannot
71
+ // create schedules in Phase 1e — CASL conditions for "own
72
+ // schedules" are not in the starter vocabulary, so member-
73
+ // created schedules would need handler-level self-ID
74
+ // enforcement that this phase does not introduce. Tenant
75
+ // isolation at the query layer scopes all reads to the
76
+ // active org.
77
+ // Phase 1b: members get full manage on Node (graph CRUD is
78
+ // the core work) and Workspace, create+read on AgentRun,
79
+ // and read on everything else.
80
+ can("manage", "Node");
81
+ can("manage", "Workspace");
82
+ can("manage", "UserSecret");
83
+ can("create", "AgentRun");
84
+ can("read", "AgentRun");
85
+ can("read", "Organization");
86
+ can("read", "Membership");
87
+ can("read", "OrgConfig");
88
+ can("read", "AgentSchedule");
89
+ can("read", "Pii");
90
+ can("read", "Search");
91
+ can("read", "Import");
92
+ can("read", "Domain");
93
+ can("read", "OrgAgentConfig");
94
+ can("read", "OrgEmbedding");
95
+ break;
96
+ case "none":
97
+ // No active org: the user is either pre-invite-acceptance or
98
+ // holds multiple memberships without choosing one. Allow reading
99
+ // their own user record (handler must verify self-ID) and
100
+ // creating a membership for the accept-invite flow.
101
+ // Phase 1b: UserSecret is self-service and not org-scoped.
102
+ can("read", "User");
103
+ can("create", "Membership");
104
+ can("manage", "UserSecret");
105
+ break;
106
+ }
107
+ break;
108
+ }
109
+ case "org_agent": {
110
+ if (principal.legacyApiKey) {
111
+ // Pre-Phase-1c API key: match today's full-org-access behavior so
112
+ // nothing breaks during the migration. Phase 1c will bind keys to
113
+ // real OrgAgentType rows with intrinsicPolicy; this branch becomes
114
+ // the fallback for unbound legacy keys only.
115
+ can("manage", "all");
116
+ }
117
+ else {
118
+ // Phase 1c reads `OrgAgentType.intrinsicPolicy` here and constructs
119
+ // rules from it. For Phase 1b there is no such column yet, so the
120
+ // non-legacy branch is a placeholder with no rules — callers get a
121
+ // "deny by default" ability if they construct a non-legacy OrgAgent
122
+ // principal. This is the correct failure mode until 1c lands.
123
+ }
124
+ break;
125
+ }
126
+ case "delegated_agent": {
127
+ // Rehydrate from the captured snapshot. We bypass the AbilityBuilder
128
+ // because the rules are already fully formed; pass them straight to
129
+ // `createMongoAbility` to get an ability with those exact rules.
130
+ const rules = normalizeCapturedAbility(principal.capturedAbility);
131
+ return createMongoAbility(rules);
132
+ }
133
+ }
134
+ return build();
135
+ }
136
+ /**
137
+ * Coerce a `DelegatedAgentPrincipal.capturedAbility` payload (typed
138
+ * `unknown` at the principal boundary) into a `RawRuleOf<AppAbility>[]` that
139
+ * CASL can consume. If the payload is malformed, returns an empty rule set —
140
+ * which yields a deny-by-default ability. This is intentionally strict:
141
+ * garbage in means no access, not crash.
142
+ *
143
+ * Phase 1d adds the real end-to-end test of the capture-and-rehydrate loop;
144
+ * for Phase 1b this function just has to round-trip a sensible array and
145
+ * reject obvious garbage.
146
+ */
147
+ function normalizeCapturedAbility(captured) {
148
+ if (!Array.isArray(captured)) {
149
+ return [];
150
+ }
151
+ // We do not re-validate each rule's shape here — CASL itself will throw
152
+ // at `createMongoAbility` time if a rule is fundamentally malformed, and
153
+ // in Phase 1b the delegation path is synthetic (tests) or not exercised
154
+ // (no code mints delegation tokens yet). When Phase 1d wires this for
155
+ // real, consider adding a Zod schema here.
156
+ return captured;
157
+ }
158
+ // ── Rule-set intersection (Phase 1d) ────────────────────────────────────────
159
+ /**
160
+ * Thrown when `intersect()` is asked to combine rule sets whose shape is
161
+ * outside the starter CASL vocabulary — currently any rule that carries
162
+ * CASL `conditions` or `fields`. See `intersect` docstring.
163
+ */
164
+ export class UnsupportedIntersectionError extends Error {
165
+ constructor(reason) {
166
+ super(`intersect(): ${reason}`);
167
+ this.name = "UnsupportedIntersectionError";
168
+ }
169
+ }
170
+ /**
171
+ * The non-wildcard subjects we enumerate when computing an intersection.
172
+ * Keep this in sync with the `AppSubject` type definition above — if a new
173
+ * concrete subject is added to `AppSubject`, it must be added here too or
174
+ * the intersection will silently drop any rules that target it.
175
+ *
176
+ * The `satisfies` clause gives us a compile error if a listed element is
177
+ * not a valid `AppSubject`, but it does NOT enforce exhaustiveness. The
178
+ * comment above is the current guardrail; an exhaustiveness check can be
179
+ * added later if the vocabulary grows past a handful of subjects.
180
+ */
181
+ const CONCRETE_SUBJECTS = [
182
+ "Organization",
183
+ "Membership",
184
+ "Invite",
185
+ "User",
186
+ "OrgConfig",
187
+ "AgentSchedule",
188
+ "Pii",
189
+ "Node",
190
+ "Search",
191
+ "Import",
192
+ "Domain",
193
+ "Workspace",
194
+ "AgentRun",
195
+ "OrgAgentConfig",
196
+ "OrgEmbedding",
197
+ "UserSecret",
198
+ ];
199
+ /** The non-wildcard actions. Same sync-with-type-definition caveat as above. */
200
+ const CONCRETE_ACTIONS = [
201
+ "create",
202
+ "read",
203
+ "update",
204
+ "delete",
205
+ "bypass",
206
+ ];
207
+ /**
208
+ * Compute the intersection of two raw CASL rule sets, producing a rule set
209
+ * that grants access only where BOTH inputs grant access.
210
+ *
211
+ * Used at agent spawn time to capture the effective authority of a
212
+ * `DelegatedAgentPrincipal`: the user's ability at spawn time, intersected
213
+ * with the agent's intrinsic policy, is the ceiling on what the agent can
214
+ * do for the life of the run. The result is persisted on
215
+ * `AgentRun.captured_ability` (the Phase 1c column) and rehydrated by
216
+ * `buildAbility(DelegatedAgentPrincipal)` on every callback from the
217
+ * sandbox. See the Phase 1d user story `ed8fcc68` for the full rationale.
218
+ *
219
+ * **Semantics.** The intersection is computed by building a CASL ability
220
+ * from each input and enumerating every concrete `(action, subject)` pair
221
+ * in the `AppAction × AppSubject` cross product. A rule is emitted for
222
+ * each pair where both abilities grant access. CASL's `manage` and `all`
223
+ * wildcards are handled implicitly because `.can("read", "Organization")`
224
+ * on a `manage all` ability returns true.
225
+ *
226
+ * The returned rules are NOT compressed — a full-overlap intersection
227
+ * yields `CONCRETE_ACTIONS.length × CONCRETE_SUBJECTS.length` concrete
228
+ * rules (currently 5 × 7 = 35) rather than
229
+ * `[{action:"manage", subject:"all"}]`. This is semantically equivalent
230
+ * but more verbose. If audit-log readability ever becomes a concern, a
231
+ * `compress()` helper can be added as a separate function; for now,
232
+ * verbosity is the price of simplicity.
233
+ *
234
+ * **Phase 1d limitation.** CASL conditions and field scopes are not
235
+ * supported on either side of the intersection. The current `AppAbility`
236
+ * vocabulary explicitly does not use them (see the docstring at the top
237
+ * of this file — "No field conditions in Phase 1b rules"), so this is
238
+ * the correct shape for the current starter vocabulary. If an input
239
+ * rule carries `conditions` or `fields`, this function throws
240
+ * `UnsupportedIntersectionError` rather than silently producing an
241
+ * over-permissive result. When a future phase grows the CASL vocabulary
242
+ * to use conditions, this function grows with it.
243
+ *
244
+ * **Edge cases.**
245
+ * - Empty ∩ X → empty (deny by default).
246
+ * - X ∩ empty → empty.
247
+ * - Disjoint inputs → empty.
248
+ * - Full overlap (both sides grant `manage all`) → all concrete rules
249
+ * in the cross product (5 × 7 = 35 today), which rehydrates to a
250
+ * functionally-manage-all ability.
251
+ * - One side is `manage all`, the other is a specific rule → the specific
252
+ * rule (expanded to concrete (action, subject) form).
253
+ */
254
+ export function intersect(a, b) {
255
+ assertUnconditional(a, "left");
256
+ assertUnconditional(b, "right");
257
+ const abilityA = createMongoAbility(a);
258
+ const abilityB = createMongoAbility(b);
259
+ const result = [];
260
+ for (const subject of CONCRETE_SUBJECTS) {
261
+ for (const action of CONCRETE_ACTIONS) {
262
+ if (abilityA.can(action, subject) && abilityB.can(action, subject)) {
263
+ result.push({ action, subject });
264
+ }
265
+ }
266
+ }
267
+ return result;
268
+ }
269
+ /**
270
+ * Walk a rule set and throw if any rule carries `conditions` or `fields`.
271
+ * The `side` label is threaded into the error message so callers can tell
272
+ * which argument was the offender.
273
+ */
274
+ function assertUnconditional(rules, side) {
275
+ for (let i = 0; i < rules.length; i++) {
276
+ const rule = rules[i];
277
+ if (rule.conditions !== undefined) {
278
+ throw new UnsupportedIntersectionError(`${side}[${i}] has CASL conditions, which the Phase 1d starter vocabulary does not support`);
279
+ }
280
+ if (rule.fields !== undefined) {
281
+ throw new UnsupportedIntersectionError(`${side}[${i}] has CASL field scoping, which the Phase 1d starter vocabulary does not support`);
282
+ }
283
+ }
284
+ }
285
+ //# sourceMappingURL=ability.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ability.js","sourceRoot":"","sources":["../../src/auth/ability.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ability.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ability.test.d.ts","sourceRoot":"","sources":["../../src/auth/ability.test.ts"],"names":[],"mappings":""}