@apart-tech/intelligence-core 1.11.4 → 1.11.6

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 (152) 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 +291 -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 +693 -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 +22 -1
  27. package/dist/config/index.js.map +1 -1
  28. package/dist/db/tenant.d.ts.map +1 -1
  29. package/dist/db/tenant.js +8 -0
  30. package/dist/db/tenant.js.map +1 -1
  31. package/dist/index.d.ts +19 -1
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +10 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/lib/__tests__/jwt.test.d.ts +2 -0
  36. package/dist/lib/__tests__/jwt.test.d.ts.map +1 -0
  37. package/dist/lib/__tests__/jwt.test.js +97 -0
  38. package/dist/lib/__tests__/jwt.test.js.map +1 -0
  39. package/dist/lib/jwt.d.ts +20 -0
  40. package/dist/lib/jwt.d.ts.map +1 -1
  41. package/dist/lib/jwt.js +56 -3
  42. package/dist/lib/jwt.js.map +1 -1
  43. package/dist/services/__tests__/chunk-service.test.d.ts +2 -0
  44. package/dist/services/__tests__/chunk-service.test.d.ts.map +1 -0
  45. package/dist/services/__tests__/chunk-service.test.js +111 -0
  46. package/dist/services/__tests__/chunk-service.test.js.map +1 -0
  47. package/dist/services/__tests__/chunker.test.d.ts +2 -0
  48. package/dist/services/__tests__/chunker.test.d.ts.map +1 -0
  49. package/dist/services/__tests__/chunker.test.js +113 -0
  50. package/dist/services/__tests__/chunker.test.js.map +1 -0
  51. package/dist/services/__tests__/delegation-cleanup-service.test.d.ts +2 -0
  52. package/dist/services/__tests__/delegation-cleanup-service.test.d.ts.map +1 -0
  53. package/dist/services/__tests__/delegation-cleanup-service.test.js +211 -0
  54. package/dist/services/__tests__/delegation-cleanup-service.test.js.map +1 -0
  55. package/dist/services/__tests__/node-service.test.d.ts +2 -0
  56. package/dist/services/__tests__/node-service.test.d.ts.map +1 -0
  57. package/dist/services/__tests__/node-service.test.js +207 -0
  58. package/dist/services/__tests__/node-service.test.js.map +1 -0
  59. package/dist/services/__tests__/pii-detector-service.test.js +51 -0
  60. package/dist/services/__tests__/pii-detector-service.test.js.map +1 -1
  61. package/dist/services/__tests__/pii-encryption-service.test.js +37 -0
  62. package/dist/services/__tests__/pii-encryption-service.test.js.map +1 -1
  63. package/dist/services/__tests__/search-service.test.d.ts +2 -0
  64. package/dist/services/__tests__/search-service.test.d.ts.map +1 -0
  65. package/dist/services/__tests__/search-service.test.js +163 -0
  66. package/dist/services/__tests__/search-service.test.js.map +1 -0
  67. package/dist/services/agent-run-service.d.ts +44 -7
  68. package/dist/services/agent-run-service.d.ts.map +1 -1
  69. package/dist/services/agent-run-service.js +14 -0
  70. package/dist/services/agent-run-service.js.map +1 -1
  71. package/dist/services/agent-schedule-service.d.ts +21 -0
  72. package/dist/services/agent-schedule-service.d.ts.map +1 -1
  73. package/dist/services/agent-schedule-service.js +12 -0
  74. package/dist/services/agent-schedule-service.js.map +1 -1
  75. package/dist/services/audit-event-service.d.ts +76 -0
  76. package/dist/services/audit-event-service.d.ts.map +1 -0
  77. package/dist/services/audit-event-service.js +48 -0
  78. package/dist/services/audit-event-service.js.map +1 -0
  79. package/dist/services/backfill-chunks.d.ts +30 -0
  80. package/dist/services/backfill-chunks.d.ts.map +1 -0
  81. package/dist/services/backfill-chunks.js +55 -0
  82. package/dist/services/backfill-chunks.js.map +1 -0
  83. package/dist/services/chunk-service.d.ts +45 -0
  84. package/dist/services/chunk-service.d.ts.map +1 -0
  85. package/dist/services/chunk-service.js +111 -0
  86. package/dist/services/chunk-service.js.map +1 -0
  87. package/dist/services/chunker.d.ts +32 -0
  88. package/dist/services/chunker.d.ts.map +1 -0
  89. package/dist/services/chunker.js +289 -0
  90. package/dist/services/chunker.js.map +1 -0
  91. package/dist/services/context-service.d.ts +3 -1
  92. package/dist/services/context-service.d.ts.map +1 -1
  93. package/dist/services/context-service.js +17 -1
  94. package/dist/services/context-service.js.map +1 -1
  95. package/dist/services/delegation-cleanup-service.d.ts +133 -0
  96. package/dist/services/delegation-cleanup-service.d.ts.map +1 -0
  97. package/dist/services/delegation-cleanup-service.js +111 -0
  98. package/dist/services/delegation-cleanup-service.js.map +1 -0
  99. package/dist/services/edge-service.d.ts.map +1 -1
  100. package/dist/services/edge-service.js +3 -0
  101. package/dist/services/edge-service.js.map +1 -1
  102. package/dist/services/node-service.d.ts +12 -1
  103. package/dist/services/node-service.d.ts.map +1 -1
  104. package/dist/services/node-service.js +54 -11
  105. package/dist/services/node-service.js.map +1 -1
  106. package/dist/services/org-agent-type-service.d.ts +15 -0
  107. package/dist/services/org-agent-type-service.d.ts.map +1 -1
  108. package/dist/services/org-agent-type-service.js +2 -0
  109. package/dist/services/org-agent-type-service.js.map +1 -1
  110. package/dist/services/pii-detector-service.d.ts +1 -0
  111. package/dist/services/pii-detector-service.d.ts.map +1 -1
  112. package/dist/services/pii-detector-service.js +95 -2
  113. package/dist/services/pii-detector-service.js.map +1 -1
  114. package/dist/services/pii-encryption-service.d.ts +10 -0
  115. package/dist/services/pii-encryption-service.d.ts.map +1 -1
  116. package/dist/services/pii-encryption-service.js +32 -0
  117. package/dist/services/pii-encryption-service.js.map +1 -1
  118. package/dist/services/search-service.d.ts +30 -1
  119. package/dist/services/search-service.d.ts.map +1 -1
  120. package/dist/services/search-service.js +262 -45
  121. package/dist/services/search-service.js.map +1 -1
  122. package/dist/services/tag-service.d.ts +78 -0
  123. package/dist/services/tag-service.d.ts.map +1 -0
  124. package/dist/services/tag-service.js +639 -0
  125. package/dist/services/tag-service.js.map +1 -0
  126. package/dist/services/tag-service.test.d.ts +2 -0
  127. package/dist/services/tag-service.test.d.ts.map +1 -0
  128. package/dist/services/tag-service.test.js +448 -0
  129. package/dist/services/tag-service.test.js.map +1 -0
  130. package/dist/services/usage-service.d.ts +48 -0
  131. package/dist/services/usage-service.d.ts.map +1 -0
  132. package/dist/services/usage-service.js +116 -0
  133. package/dist/services/usage-service.js.map +1 -0
  134. package/dist/services/user-service.d.ts.map +1 -1
  135. package/dist/services/user-service.js +24 -6
  136. package/dist/services/user-service.js.map +1 -1
  137. package/dist/services/user-service.test.d.ts +2 -0
  138. package/dist/services/user-service.test.d.ts.map +1 -0
  139. package/dist/services/user-service.test.js +86 -0
  140. package/dist/services/user-service.test.js.map +1 -0
  141. package/dist/services/workspace-service.d.ts +2 -0
  142. package/dist/services/workspace-service.d.ts.map +1 -1
  143. package/dist/services/workspace-service.js +7 -1
  144. package/dist/services/workspace-service.js.map +1 -1
  145. package/dist/types/index.d.ts +80 -2
  146. package/dist/types/index.d.ts.map +1 -1
  147. package/package.json +3 -2
  148. package/prisma/schema.prisma +335 -82
  149. package/dist/db/schema.d.ts +0 -507
  150. package/dist/db/schema.d.ts.map +0 -1
  151. package/dist/db/schema.js +0 -77
  152. 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" | "Tag" | "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, agent schedules,
71
+ * and PII config, plus read of org/membership/OrgConfig. `member` grants
72
+ * read of 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,GACL,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,CAkH7D;AA2BD;;;;GAIG;AACH,qBAAa,4BAA6B,SAAQ,KAAK;gBACzC,MAAM,EAAE,MAAM;CAI3B;AA0CD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,UAAU,EAAE,EAAE,CAAC,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,CAgBxE"}
@@ -0,0 +1,291 @@
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, agent schedules,
12
+ * and PII config, plus read of org/membership/OrgConfig. `member` grants
13
+ * read of 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 and PII config (operational concerns admins need
46
+ // to own). Phase 1b: admins get manage on the full knowledge-
47
+ // graph surface (Node, Import, Domain, Workspace, AgentRun)
48
+ // and org-level config (OrgAgentConfig, OrgEmbedding). They
49
+ // get read on Search (a read-only operation).
50
+ can("manage", "Invite");
51
+ can("manage", "AgentSchedule");
52
+ can("manage", "Node");
53
+ can("manage", "Import");
54
+ can("manage", "Domain");
55
+ can("manage", "Workspace");
56
+ can("manage", "AgentRun");
57
+ can("manage", "OrgAgentConfig");
58
+ can("manage", "OrgEmbedding");
59
+ can("manage", "UserSecret");
60
+ can("manage", "Tag");
61
+ can("create", "Pii");
62
+ can("read", "Pii");
63
+ can("update", "Pii");
64
+ can("delete", "Pii");
65
+ can("read", "Organization");
66
+ can("read", "Membership");
67
+ can("read", "OrgConfig");
68
+ can("read", "Search");
69
+ break;
70
+ case "member":
71
+ // Members can read their org and memberships and the org-level
72
+ // config surfaces (so that the UI can show them). They cannot
73
+ // create schedules in Phase 1e — CASL conditions for "own
74
+ // schedules" are not in the starter vocabulary, so member-
75
+ // created schedules would need handler-level self-ID
76
+ // enforcement that this phase does not introduce. Tenant
77
+ // isolation at the query layer scopes all reads to the
78
+ // active org.
79
+ // Phase 1b: members get full manage on Node (graph CRUD is
80
+ // the core work) and Workspace, create+read on AgentRun,
81
+ // and read on everything else.
82
+ can("manage", "Node");
83
+ can("manage", "Workspace");
84
+ can("manage", "UserSecret");
85
+ can("create", "Tag");
86
+ can("read", "Tag");
87
+ can("update", "Tag");
88
+ can("create", "AgentRun");
89
+ can("read", "AgentRun");
90
+ can("read", "Organization");
91
+ can("read", "Membership");
92
+ can("read", "OrgConfig");
93
+ can("read", "AgentSchedule");
94
+ can("read", "Pii");
95
+ can("read", "Search");
96
+ can("read", "Import");
97
+ can("read", "Domain");
98
+ can("read", "OrgAgentConfig");
99
+ can("read", "OrgEmbedding");
100
+ break;
101
+ case "none":
102
+ // No active org: the user is either pre-invite-acceptance or
103
+ // holds multiple memberships without choosing one. Allow reading
104
+ // their own user record (handler must verify self-ID) and
105
+ // creating a membership for the accept-invite flow.
106
+ // Phase 1b: UserSecret is self-service and not org-scoped.
107
+ can("read", "User");
108
+ can("create", "Membership");
109
+ can("manage", "UserSecret");
110
+ break;
111
+ }
112
+ break;
113
+ }
114
+ case "org_agent": {
115
+ if (principal.legacyApiKey) {
116
+ // Pre-Phase-1c API key: match today's full-org-access behavior so
117
+ // nothing breaks during the migration. Phase 1c will bind keys to
118
+ // real OrgAgentType rows with intrinsicPolicy; this branch becomes
119
+ // the fallback for unbound legacy keys only.
120
+ can("manage", "all");
121
+ }
122
+ else {
123
+ // Phase 1c reads `OrgAgentType.intrinsicPolicy` here and constructs
124
+ // rules from it. For Phase 1b there is no such column yet, so the
125
+ // non-legacy branch is a placeholder with no rules — callers get a
126
+ // "deny by default" ability if they construct a non-legacy OrgAgent
127
+ // principal. This is the correct failure mode until 1c lands.
128
+ }
129
+ break;
130
+ }
131
+ case "delegated_agent": {
132
+ // Rehydrate from the captured snapshot. We bypass the AbilityBuilder
133
+ // because the rules are already fully formed; pass them straight to
134
+ // `createMongoAbility` to get an ability with those exact rules.
135
+ const rules = normalizeCapturedAbility(principal.capturedAbility);
136
+ return createMongoAbility(rules);
137
+ }
138
+ }
139
+ return build();
140
+ }
141
+ /**
142
+ * Coerce a `DelegatedAgentPrincipal.capturedAbility` payload (typed
143
+ * `unknown` at the principal boundary) into a `RawRuleOf<AppAbility>[]` that
144
+ * CASL can consume. If the payload is malformed, returns an empty rule set —
145
+ * which yields a deny-by-default ability. This is intentionally strict:
146
+ * garbage in means no access, not crash.
147
+ *
148
+ * Phase 1d adds the real end-to-end test of the capture-and-rehydrate loop;
149
+ * for Phase 1b this function just has to round-trip a sensible array and
150
+ * reject obvious garbage.
151
+ */
152
+ function normalizeCapturedAbility(captured) {
153
+ if (!Array.isArray(captured)) {
154
+ return [];
155
+ }
156
+ // We do not re-validate each rule's shape here — CASL itself will throw
157
+ // at `createMongoAbility` time if a rule is fundamentally malformed, and
158
+ // in Phase 1b the delegation path is synthetic (tests) or not exercised
159
+ // (no code mints delegation tokens yet). When Phase 1d wires this for
160
+ // real, consider adding a Zod schema here.
161
+ return captured;
162
+ }
163
+ // ── Rule-set intersection (Phase 1d) ────────────────────────────────────────
164
+ /**
165
+ * Thrown when `intersect()` is asked to combine rule sets whose shape is
166
+ * outside the starter CASL vocabulary — currently any rule that carries
167
+ * CASL `conditions` or `fields`. See `intersect` docstring.
168
+ */
169
+ export class UnsupportedIntersectionError extends Error {
170
+ constructor(reason) {
171
+ super(`intersect(): ${reason}`);
172
+ this.name = "UnsupportedIntersectionError";
173
+ }
174
+ }
175
+ /**
176
+ * The non-wildcard subjects we enumerate when computing an intersection.
177
+ * Keep this in sync with the `AppSubject` type definition above — if a new
178
+ * concrete subject is added to `AppSubject`, it must be added here too or
179
+ * the intersection will silently drop any rules that target it.
180
+ *
181
+ * The `satisfies` clause gives us a compile error if a listed element is
182
+ * not a valid `AppSubject`, but it does NOT enforce exhaustiveness. The
183
+ * comment above is the current guardrail; an exhaustiveness check can be
184
+ * added later if the vocabulary grows past a handful of subjects.
185
+ */
186
+ const CONCRETE_SUBJECTS = [
187
+ "Organization",
188
+ "Membership",
189
+ "Invite",
190
+ "User",
191
+ "OrgConfig",
192
+ "AgentSchedule",
193
+ "Pii",
194
+ "Node",
195
+ "Search",
196
+ "Import",
197
+ "Domain",
198
+ "Workspace",
199
+ "AgentRun",
200
+ "OrgAgentConfig",
201
+ "OrgEmbedding",
202
+ "UserSecret",
203
+ "Tag",
204
+ ];
205
+ /** The non-wildcard actions. Same sync-with-type-definition caveat as above. */
206
+ const CONCRETE_ACTIONS = [
207
+ "create",
208
+ "read",
209
+ "update",
210
+ "delete",
211
+ "bypass",
212
+ ];
213
+ /**
214
+ * Compute the intersection of two raw CASL rule sets, producing a rule set
215
+ * that grants access only where BOTH inputs grant access.
216
+ *
217
+ * Used at agent spawn time to capture the effective authority of a
218
+ * `DelegatedAgentPrincipal`: the user's ability at spawn time, intersected
219
+ * with the agent's intrinsic policy, is the ceiling on what the agent can
220
+ * do for the life of the run. The result is persisted on
221
+ * `AgentRun.captured_ability` (the Phase 1c column) and rehydrated by
222
+ * `buildAbility(DelegatedAgentPrincipal)` on every callback from the
223
+ * sandbox. See the Phase 1d user story `ed8fcc68` for the full rationale.
224
+ *
225
+ * **Semantics.** The intersection is computed by building a CASL ability
226
+ * from each input and enumerating every concrete `(action, subject)` pair
227
+ * in the `AppAction × AppSubject` cross product. A rule is emitted for
228
+ * each pair where both abilities grant access. CASL's `manage` and `all`
229
+ * wildcards are handled implicitly because `.can("read", "Organization")`
230
+ * on a `manage all` ability returns true.
231
+ *
232
+ * The returned rules are NOT compressed — a full-overlap intersection
233
+ * yields `CONCRETE_ACTIONS.length × CONCRETE_SUBJECTS.length` concrete
234
+ * rules (currently 5 × 7 = 35) rather than
235
+ * `[{action:"manage", subject:"all"}]`. This is semantically equivalent
236
+ * but more verbose. If audit-log readability ever becomes a concern, a
237
+ * `compress()` helper can be added as a separate function; for now,
238
+ * verbosity is the price of simplicity.
239
+ *
240
+ * **Phase 1d limitation.** CASL conditions and field scopes are not
241
+ * supported on either side of the intersection. The current `AppAbility`
242
+ * vocabulary explicitly does not use them (see the docstring at the top
243
+ * of this file — "No field conditions in Phase 1b rules"), so this is
244
+ * the correct shape for the current starter vocabulary. If an input
245
+ * rule carries `conditions` or `fields`, this function throws
246
+ * `UnsupportedIntersectionError` rather than silently producing an
247
+ * over-permissive result. When a future phase grows the CASL vocabulary
248
+ * to use conditions, this function grows with it.
249
+ *
250
+ * **Edge cases.**
251
+ * - Empty ∩ X → empty (deny by default).
252
+ * - X ∩ empty → empty.
253
+ * - Disjoint inputs → empty.
254
+ * - Full overlap (both sides grant `manage all`) → all concrete rules
255
+ * in the cross product (5 × 7 = 35 today), which rehydrates to a
256
+ * functionally-manage-all ability.
257
+ * - One side is `manage all`, the other is a specific rule → the specific
258
+ * rule (expanded to concrete (action, subject) form).
259
+ */
260
+ export function intersect(a, b) {
261
+ assertUnconditional(a, "left");
262
+ assertUnconditional(b, "right");
263
+ const abilityA = createMongoAbility(a);
264
+ const abilityB = createMongoAbility(b);
265
+ const result = [];
266
+ for (const subject of CONCRETE_SUBJECTS) {
267
+ for (const action of CONCRETE_ACTIONS) {
268
+ if (abilityA.can(action, subject) && abilityB.can(action, subject)) {
269
+ result.push({ action, subject });
270
+ }
271
+ }
272
+ }
273
+ return result;
274
+ }
275
+ /**
276
+ * Walk a rule set and throw if any rule carries `conditions` or `fields`.
277
+ * The `side` label is threaded into the error message so callers can tell
278
+ * which argument was the offender.
279
+ */
280
+ function assertUnconditional(rules, side) {
281
+ for (let i = 0; i < rules.length; i++) {
282
+ const rule = rules[i];
283
+ if (rule.conditions !== undefined) {
284
+ throw new UnsupportedIntersectionError(`${side}[${i}] has CASL conditions, which the Phase 1d starter vocabulary does not support`);
285
+ }
286
+ if (rule.fields !== undefined) {
287
+ throw new UnsupportedIntersectionError(`${side}[${i}] has CASL field scoping, which the Phase 1d starter vocabulary does not support`);
288
+ }
289
+ }
290
+ }
291
+ //# 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":""}