spine-framework 0.3.124 → 0.3.126

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 (319) hide show
  1. package/.framework/cli/commands/agents.ts +35 -37
  2. package/.framework/cli/commands/auth.ts +35 -34
  3. package/.framework/cli/commands/create-app.ts +104 -40
  4. package/.framework/cli/commands/dev.ts +104 -69
  5. package/.framework/cli/commands/doctor.ts +179 -69
  6. package/.framework/cli/commands/generate.ts +83 -143
  7. package/.framework/cli/commands/init.ts +72 -34
  8. package/.framework/cli/commands/install-app.ts +260 -125
  9. package/.framework/cli/commands/items.ts +33 -44
  10. package/.framework/cli/commands/migrate.ts +122 -40
  11. package/.framework/cli/commands/migrations.ts +33 -40
  12. package/.framework/cli/commands/pipelines.ts +31 -39
  13. package/.framework/cli/commands/status.ts +47 -23
  14. package/.framework/cli/commands/system.ts +47 -37
  15. package/.framework/cli/commands/test.ts +112 -68
  16. package/.framework/cli/commands/validate-app.ts +215 -121
  17. package/.framework/cli/context.ts +112 -130
  18. package/.framework/cli/env-loader.ts +47 -18
  19. package/.framework/cli/index.ts +81 -42
  20. package/.framework/docs/runtime-types.md +1024 -0
  21. package/.framework/functions/_shared/agent-runner.ts +1296 -961
  22. package/.framework/functions/_shared/app-manifest.ts +82 -68
  23. package/.framework/functions/_shared/audit.ts +135 -83
  24. package/.framework/functions/_shared/{cil-state.ts → cil.ts} +158 -56
  25. package/.framework/functions/_shared/db.ts +80 -126
  26. package/.framework/functions/_shared/hash-api-key.ts +63 -0
  27. package/.framework/functions/_shared/index.ts +224 -335
  28. package/.framework/functions/_shared/kb.ts +319 -0
  29. package/.framework/functions/_shared/llm.ts +92 -0
  30. package/.framework/functions/_shared/logging.ts +288 -0
  31. package/.framework/functions/_shared/middleware.ts +329 -365
  32. package/.framework/functions/_shared/outcomes.ts +36 -0
  33. package/.framework/functions/_shared/permissions.ts +359 -643
  34. package/.framework/functions/_shared/pipeline-runner.ts +243 -316
  35. package/.framework/functions/_shared/principal.ts +376 -468
  36. package/.framework/functions/_shared/resolve-credentials.ts +102 -0
  37. package/.framework/functions/_shared/resolve-ids.ts +188 -16
  38. package/.framework/functions/_shared/retry-deliveries.ts +104 -0
  39. package/.framework/functions/_shared/schema-utils.ts +611 -373
  40. package/.framework/functions/_shared/testing.ts +207 -137
  41. package/.framework/functions/_shared/tracing.ts +276 -0
  42. package/.framework/functions/_shared/trigger-engine.ts +159 -211
  43. package/.framework/functions/_shared/webhook-registration.ts +95 -81
  44. package/.framework/functions/_shared/webhook-registry.ts +153 -75
  45. package/.framework/functions/account-nodes.ts +31 -53
  46. package/.framework/functions/adapters/generic-webhook-inbound.ts +56 -0
  47. package/.framework/functions/adapters/generic-webhook-outbound.ts +43 -0
  48. package/.framework/functions/adapters/openai-outbound.ts +40 -0
  49. package/.framework/functions/admin-data.ts +685 -537
  50. package/.framework/functions/admin-funnel.ts +31 -20
  51. package/.framework/functions/admin-install-funnel.ts +47 -28
  52. package/.framework/functions/admin-signals.ts +47 -29
  53. package/.framework/functions/ai-agents.ts +122 -147
  54. package/.framework/functions/ai-interactions.ts +180 -90
  55. package/.framework/functions/api-keys.ts +250 -192
  56. package/.framework/functions/apps.ts +203 -197
  57. package/.framework/functions/auth.ts +98 -91
  58. package/.framework/functions/cil-agent-ingest.ts +773 -0
  59. package/.framework/functions/cil-agent.ts +592 -0
  60. package/.framework/functions/cil-callmap.ts +1227 -0
  61. package/.framework/functions/cil-config.ts +583 -0
  62. package/.framework/functions/cil-context.ts +445 -0
  63. package/.framework/functions/cil-doc.ts +1752 -0
  64. package/.framework/functions/cil-embeddings.ts +1202 -0
  65. package/.framework/functions/cil-graph.ts +585 -0
  66. package/.framework/functions/cil-ingest.ts +2416 -0
  67. package/.framework/functions/cil-pipeline.ts +318 -0
  68. package/.framework/functions/cil-project.ts +499 -0
  69. package/.framework/functions/cil-validate.ts +469 -0
  70. package/.framework/functions/debug-auth.ts +73 -53
  71. package/.framework/functions/deliveries.ts +117 -0
  72. package/.framework/functions/embeddings.ts +528 -516
  73. package/.framework/functions/eval-harness.ts +158 -106
  74. package/.framework/functions/integration-outbound.ts +311 -0
  75. package/.framework/functions/integration-routes.ts +240 -466
  76. package/.framework/functions/integrations.ts +132 -140
  77. package/.framework/functions/invites.ts +401 -201
  78. package/.framework/functions/item-progress.ts +113 -53
  79. package/.framework/functions/kb.ts +28 -0
  80. package/.framework/functions/logs.ts +239 -222
  81. package/.framework/functions/observability.ts +308 -259
  82. package/.framework/functions/pipeline-executions.ts +179 -235
  83. package/.framework/functions/pipelines.ts +163 -205
  84. package/.framework/functions/prompt-configs.ts +109 -156
  85. package/.framework/functions/roles.ts +97 -147
  86. package/.framework/functions/system-cron.ts +132 -218
  87. package/.framework/functions/system.ts +112 -52
  88. package/.framework/functions/tests.ts +32 -28
  89. package/.framework/functions/timers.ts +162 -210
  90. package/.framework/functions/triggers.ts +282 -346
  91. package/.framework/functions/types.ts +283 -400
  92. package/.framework/migrations/001_core_schema.sql +630 -0
  93. package/.framework/migrations/002_core_rls_and_functions.sql +532 -0
  94. package/.framework/migrations/{003_system_data.sql → 003_core_system_data.sql} +26 -153
  95. package/.framework/migrations/004_core_rpc_and_broker.sql +421 -0
  96. package/.framework/migrations/005_core_observability.sql +598 -0
  97. package/.framework/migrations/006_security_hardening_rls.sql +97 -0
  98. package/.framework/migrations/007_kb_article_list.sql +237 -0
  99. package/.framework/migrations/008_items_target.sql +8 -0
  100. package/.framework/migrations/009_roles_slug_unique.sql +11 -0
  101. package/.framework/public/spine.config.json +91 -0
  102. package/.framework/scripts/app-install-cli.ts +112 -98
  103. package/.framework/src/App.tsx +67 -20
  104. package/.framework/src/apps/admin/index.tsx +97 -27
  105. package/.framework/src/components/AppWrapper.tsx +50 -11
  106. package/.framework/src/components/CustomAppLoader.tsx +131 -22
  107. package/.framework/src/components/admin/AdminListPage.tsx +75 -8
  108. package/.framework/src/components/admin/AdminSidebar.tsx +148 -36
  109. package/.framework/src/components/admin/AdminStatsCard.tsx +48 -29
  110. package/.framework/src/components/admin/PageInstructions.tsx +49 -0
  111. package/.framework/src/components/admin/SortableTableHeader.tsx +49 -2
  112. package/.framework/src/components/app-shell/GenericAppShell.tsx +69 -11
  113. package/.framework/src/components/app-shell/GenericDetailPage.tsx +55 -10
  114. package/.framework/src/components/app-shell/GenericListPage.tsx +76 -9
  115. package/.framework/src/components/app-sidebar.tsx +50 -3
  116. package/.framework/src/components/auth/ProtectedRoute.tsx +53 -45
  117. package/.framework/src/components/kb/KBArticleEditor.tsx +409 -0
  118. package/.framework/src/components/kb/KBArticleList.tsx +557 -0
  119. package/.framework/src/components/layout/AppShell.tsx +54 -16
  120. package/.framework/src/components/layout/Header.tsx +50 -38
  121. package/.framework/src/components/layout/Layout.tsx +48 -32
  122. package/.framework/src/components/layout/Sidebar.tsx +167 -16
  123. package/.framework/src/components/runtime/DataDetailHeader.tsx +33 -1
  124. package/.framework/src/components/runtime/DataDetailPage.tsx +289 -4
  125. package/.framework/src/components/runtime/DataFilters.tsx +78 -1
  126. package/.framework/src/components/runtime/DataHeader.tsx +32 -29
  127. package/.framework/src/components/runtime/DataListPage.tsx +161 -2
  128. package/.framework/src/components/runtime/DataStats.tsx +48 -1
  129. package/.framework/src/components/runtime/DataTable.tsx +153 -1
  130. package/.framework/src/components/runtime/SchemaDetailForm.tsx +77 -46
  131. package/.framework/src/components/runtime/index.ts +16 -9
  132. package/.framework/src/components/search-form.tsx +33 -1
  133. package/.framework/src/components/shared/AgentView.tsx +48 -51
  134. package/.framework/src/components/shared/FieldRenderer.tsx +84 -67
  135. package/.framework/src/components/shared/InviteManager.tsx +113 -52
  136. package/.framework/src/components/shared/MemberList.tsx +182 -0
  137. package/.framework/src/components/shared/SchemaFields.tsx +96 -78
  138. package/.framework/src/components/ui/DataTable.tsx +86 -56
  139. package/.framework/src/components/ui/Form.tsx +188 -109
  140. package/.framework/src/components/ui/ItemCard.tsx +86 -58
  141. package/.framework/src/components/ui/ItemListView.tsx +88 -61
  142. package/.framework/src/components/ui/LoadingSpinner.tsx +52 -27
  143. package/.framework/src/components/ui/Modal.tsx +81 -4
  144. package/.framework/src/components/ui/RichTextEditor.tsx +101 -9
  145. package/.framework/src/components/ui/accordion.tsx +97 -5
  146. package/.framework/src/components/ui/alert-dialog.tsx +225 -13
  147. package/.framework/src/components/ui/alert.tsx +113 -6
  148. package/.framework/src/components/ui/aspect-ratio.tsx +49 -2
  149. package/.framework/src/components/ui/avatar.tsx +129 -7
  150. package/.framework/src/components/ui/badge.tsx +65 -3
  151. package/.framework/src/components/ui/breadcrumb.tsx +145 -8
  152. package/.framework/src/components/ui/button-group.tsx +81 -4
  153. package/.framework/src/components/ui/button.tsx +49 -2
  154. package/.framework/src/components/ui/calendar.tsx +65 -3
  155. package/.framework/src/components/ui/card.tsx +145 -8
  156. package/.framework/src/components/ui/carousel.tsx +129 -7
  157. package/.framework/src/components/ui/chart.tsx +161 -10
  158. package/.framework/src/components/ui/checkbox.tsx +49 -2
  159. package/.framework/src/components/ui/collapsible.tsx +81 -5
  160. package/.framework/src/components/ui/combobox.tsx +305 -20
  161. package/.framework/src/components/ui/command.tsx +177 -11
  162. package/.framework/src/components/ui/context-menu.tsx +273 -17
  163. package/.framework/src/components/ui/dialog.tsx +209 -27
  164. package/.framework/src/components/ui/direction.tsx +64 -4
  165. package/.framework/src/components/ui/drawer.tsx +193 -12
  166. package/.framework/src/components/ui/dropdown-menu.tsx +273 -18
  167. package/.framework/src/components/ui/empty.tsx +145 -9
  168. package/.framework/src/components/ui/field.tsx +209 -14
  169. package/.framework/src/components/ui/hover-card.tsx +81 -4
  170. package/.framework/src/components/ui/input-group.tsx +162 -10
  171. package/.framework/src/components/ui/input-otp.tsx +98 -5
  172. package/.framework/src/components/ui/input.tsx +49 -2
  173. package/.framework/src/components/ui/item.tsx +225 -13
  174. package/.framework/src/components/ui/kbd.tsx +65 -3
  175. package/.framework/src/components/ui/label.tsx +49 -2
  176. package/.framework/src/components/ui/menubar.tsx +289 -17
  177. package/.framework/src/components/ui/native-select.tsx +97 -5
  178. package/.framework/src/components/ui/navigation-menu.tsx +177 -10
  179. package/.framework/src/components/ui/pagination.tsx +161 -9
  180. package/.framework/src/components/ui/popover.tsx +145 -8
  181. package/.framework/src/components/ui/progress.tsx +49 -2
  182. package/.framework/src/components/ui/radio-group.tsx +65 -3
  183. package/.framework/src/components/ui/resizable.tsx +82 -4
  184. package/.framework/src/components/ui/scroll-area.tsx +65 -3
  185. package/.framework/src/components/ui/select.tsx +193 -11
  186. package/.framework/src/components/ui/separator.tsx +49 -2
  187. package/.framework/src/components/ui/sheet.tsx +193 -11
  188. package/.framework/src/components/ui/sidebar.tsx +481 -38
  189. package/.framework/src/components/ui/skeleton.tsx +49 -2
  190. package/.framework/src/components/ui/slider.tsx +49 -2
  191. package/.framework/src/components/ui/sonner.tsx +49 -2
  192. package/.framework/src/components/ui/spinner.tsx +49 -2
  193. package/.framework/src/components/ui/switch.tsx +49 -2
  194. package/.framework/src/components/ui/table-primitives.tsx +160 -33
  195. package/.framework/src/components/ui/table.tsx +161 -9
  196. package/.framework/src/components/ui/tabs.tsx +113 -6
  197. package/.framework/src/components/ui/textarea.tsx +49 -2
  198. package/.framework/src/components/ui/toggle-group.tsx +81 -4
  199. package/.framework/src/components/ui/toggle.tsx +65 -3
  200. package/.framework/src/components/ui/tooltip.tsx +97 -5
  201. package/.framework/src/contexts/AppContext.tsx +129 -45
  202. package/.framework/src/contexts/AuthContext.tsx +187 -144
  203. package/.framework/src/hooks/use-mobile.ts +48 -2
  204. package/.framework/src/hooks/useApi.ts +208 -159
  205. package/.framework/src/hooks/useAppPath.ts +32 -23
  206. package/.framework/src/hooks/useApps.ts +51 -20
  207. package/.framework/src/hooks/useEntityList.ts +32 -78
  208. package/.framework/src/hooks/useEntityRecord.ts +35 -96
  209. package/.framework/src/hooks/useForm.ts +32 -92
  210. package/.framework/src/hooks/useListSchema.ts +49 -78
  211. package/.framework/src/hooks/useSchemaRecord.ts +114 -105
  212. package/.framework/src/lib/api.ts +195 -116
  213. package/.framework/src/lib/supabase.ts +98 -64
  214. package/.framework/src/lib/utils.ts +287 -151
  215. package/.framework/src/main.tsx +49 -3
  216. package/.framework/src/pages/DashboardPage.tsx +52 -29
  217. package/.framework/src/pages/NotFoundPage.tsx +32 -10
  218. package/.framework/src/pages/SetupPage.tsx +32 -12
  219. package/.framework/src/pages/admin/AIAgentDetailPage.tsx +119 -10
  220. package/.framework/src/pages/admin/AIAgentsPage.tsx +48 -19
  221. package/.framework/src/pages/admin/APIKeyDetailPage.tsx +187 -17
  222. package/.framework/src/pages/admin/APIKeysPage.tsx +98 -22
  223. package/.framework/src/pages/admin/AlertsConfigPage.tsx +113 -26
  224. package/.framework/src/pages/admin/AppDetailPage.tsx +125 -8
  225. package/.framework/src/pages/admin/AppsPage.tsx +49 -2
  226. package/.framework/src/pages/admin/DeliveriesPage.tsx +196 -0
  227. package/.framework/src/pages/admin/DesignedPage.tsx +32 -18
  228. package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +215 -484
  229. package/.framework/src/pages/admin/EmbeddingsPage.tsx +116 -319
  230. package/.framework/src/pages/admin/ExtendedShadcnTestPage.tsx +34 -9
  231. package/.framework/src/pages/admin/IncrementalShadcnTestPage.tsx +34 -9
  232. package/.framework/src/pages/admin/IntegratedDashboard.tsx +32 -16
  233. package/.framework/src/pages/admin/IntegrationDetailPage.tsx +183 -26
  234. package/.framework/src/pages/admin/IntegrationsPage.tsx +48 -19
  235. package/.framework/src/pages/admin/InvitesPage.tsx +48 -14
  236. package/.framework/src/pages/admin/LogsPage.tsx +422 -75
  237. package/.framework/src/pages/admin/MinimalShadcnTestPage.tsx +34 -9
  238. package/.framework/src/pages/admin/ObservabilityDashboard.tsx +113 -24
  239. package/.framework/src/pages/admin/PipelineDetailPage.tsx +206 -34
  240. package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +50 -17
  241. package/.framework/src/pages/admin/PipelinesPage.tsx +160 -21
  242. package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +378 -88
  243. package/.framework/src/pages/admin/PromptConfigsPage.tsx +125 -16
  244. package/.framework/src/pages/admin/ProperlyDesignedPage.tsx +49 -10
  245. package/.framework/src/pages/admin/RoleDetailPage.tsx +130 -10
  246. package/.framework/src/pages/admin/RolesPage.tsx +171 -18
  247. package/.framework/src/pages/admin/SelectTestPage.tsx +34 -9
  248. package/.framework/src/pages/admin/ShadcnTestPage.tsx +49 -11
  249. package/.framework/src/pages/admin/SimpleDashboard.tsx +49 -10
  250. package/.framework/src/pages/admin/TestRunDetailPage.tsx +49 -4
  251. package/.framework/src/pages/admin/TestingDashboard.tsx +98 -28
  252. package/.framework/src/pages/admin/TimerDetailPage.tsx +143 -33
  253. package/.framework/src/pages/admin/TimersPage.tsx +48 -20
  254. package/.framework/src/pages/admin/TriggerDetailPage.tsx +118 -30
  255. package/.framework/src/pages/admin/TriggersPage.tsx +64 -19
  256. package/.framework/src/pages/admin/TypeDetailPage.tsx +138 -15
  257. package/.framework/src/pages/admin/TypesPage.tsx +48 -20
  258. package/.framework/src/pages/admin/cil/CILAgentPage.tsx +465 -0
  259. package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +787 -0
  260. package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +699 -0
  261. package/.framework/src/pages/admin/cil/CILFileListPage.tsx +647 -0
  262. package/.framework/src/pages/admin/cil/CILGraphPage.tsx +616 -0
  263. package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +1402 -0
  264. package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +614 -0
  265. package/.framework/src/pages/admin/cil/CILSearchPage.tsx +571 -0
  266. package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +731 -0
  267. package/.framework/src/pages/admin/kb/KBAdminPage.tsx +64 -0
  268. package/.framework/src/pages/auth/AuthConfirmPage.tsx +52 -51
  269. package/.framework/src/pages/auth/ForgotPasswordPage.tsx +31 -22
  270. package/.framework/src/pages/auth/LoginPage.tsx +50 -23
  271. package/.framework/src/pages/auth/PendingApprovalPage.tsx +31 -15
  272. package/.framework/src/pages/auth/RegisterPage.tsx +281 -317
  273. package/.framework/src/pages/spine-framework/APIPage.tsx +33 -0
  274. package/.framework/src/pages/spine-framework/CLIPage.tsx +33 -0
  275. package/.framework/src/types/auth.ts +128 -71
  276. package/.framework/src/types/types.ts +373 -157
  277. package/custom/.devin/skills/spine-workbench-discovery/SKILL.md +39 -0
  278. package/custom/.devin/skills/spine-workbench-idea/SKILL.md +36 -0
  279. package/custom/.devin/skills/spine-workbench-implement/SKILL.md +46 -0
  280. package/custom/.devin/skills/spine-workbench-initiative/SKILL.md +28 -0
  281. package/custom/.devin/skills/spine-workbench-plan/SKILL.md +38 -0
  282. package/docs/README.md +9 -130
  283. package/netlify.toml +5 -2
  284. package/package.json +13 -25
  285. package/scripts/kb-ingest-docs.ts +203 -0
  286. package/spine.config.json +65 -4
  287. package/.framework/migrations/001_schema.sql +0 -425
  288. package/.framework/migrations/002_rls_and_functions.sql +0 -763
  289. package/.framework/migrations/004_enhancements.sql +0 -43
  290. package/.framework/migrations/005_auth_user_trigger.sql +0 -105
  291. package/.framework/migrations/006_fix_current_actor_id.sql +0 -21
  292. package/.framework/migrations/016_invites.sql +0 -27
  293. package/.framework/migrations/017_fix_get_account_apps.sql +0 -78
  294. package/.framework/migrations/018_apps_required_roles.sql +0 -17
  295. package/.framework/migrations/019_triggers_unique_constraint.sql +0 -13
  296. package/.framework/migrations/020_get_account_apps_role_filter.sql +0 -117
  297. package/.framework/migrations/021_app_domains.sql +0 -112
  298. package/.framework/migrations/022_route_prefix_domain_unique.sql +0 -21
  299. package/.framework/migrations/023_match_embeddings_rpc.sql +0 -78
  300. package/.framework/migrations/024_messages_account_id.sql +0 -9
  301. package/.framework/migrations/025_retype_threads_messages.sql +0 -240
  302. package/.framework/migrations/026_fix_rls_policies.sql +0 -47
  303. package/.framework/migrations/027_embedding_lifecycle.sql +0 -176
  304. package/.framework/migrations/028_fix_message_data_permissions.sql +0 -49
  305. package/.framework/migrations/029_tighten_support_triage_prompt.sql +0 -41
  306. package/.framework/migrations/030_messages_updated_by.sql +0 -6
  307. package/.framework/migrations/031_seed_upsert_constraints.sql +0 -35
  308. package/docs/APPS.md +0 -232
  309. package/docs/ARCHITECTURE.md +0 -246
  310. package/docs/INSTALL.md +0 -155
  311. package/docs/design-schema-spec.md +0 -1837
  312. package/docs/dev-guides/01-create-an-app.md +0 -183
  313. package/docs/dev-guides/02-create-a-function.md +0 -178
  314. package/docs/dev-guides/03-create-a-component.md +0 -178
  315. package/docs/dev-guides/04-webhook-handlers.md +0 -129
  316. package/docs/dev-guides/05-testing.md +0 -168
  317. package/docs/dev-guides/README.md +0 -40
  318. package/docs/npm-packages-plan.md +0 -288
  319. package/docs/spine-core--api-reference.md +0 -822
@@ -1,83 +1,97 @@
1
- /**
2
- * @module invites
3
- * @audience core-contributor
4
- * @layer api-handler
5
- * @stability stable
6
- *
7
- * Invite and access-request management. All invite/access-request state is
8
- * stored directly on `people` rows — no separate table. Invite metadata lives
9
- * in `people.data` under `invite_*` keys.
10
- *
11
- * **Routed by:** `GET/POST /.netlify/functions/invites?action=<action>`
12
- *
13
- * **Public actions (no auth required):**
14
- * | action | method | description |
15
- * |-------------------|--------|-------------|
16
- * | check-first-user | GET | Boolean: is this the first user registration? |
17
- * | check-domain | GET | Boolean: does this email domain have existing accounts? |
18
- * | validate-token | GET | Validate invite token, return preview |
19
- * | complete-registration | POST | Finalise walk-up registration after Supabase auth |
20
- *
21
- * **Protected actions (require *_admin or system_admin):**
22
- * | action | method | description |
23
- * |----------------|--------|-------------|
24
- * | create-invite | POST | Create invite stub + return shareable URL |
25
- * | list-invites | GET | List invites scoped to account (or all for system_admin) |
26
- * | revoke-invite | POST | Mark invite inactive |
27
- * | reinvite | POST | Issue new token on existing stub |
28
- * | block-invite | POST | Manually block an invite token |
29
- * | approve | POST | Approve access_request → activate person |
30
- * | deny | POST | Deny access_request → deactivate person |
31
- *
32
- * **Invite metadata in people.data:**
33
- * ```json
34
- * {
35
- * "invite_token": "abc123...",
36
- * "invite_kind": "invite" | "access_request",
37
- * "invite_expires_at": "2026-06-24T...",
38
- * "invite_attempts": 0,
39
- * "invite_blocked_at": null,
40
- * "invite_invited_by": "person-uuid"
41
- * }
42
- * ```
43
- *
44
- * **people.status values used here:**
45
- * invited — unclaimed admin invite
46
- * pending — walk-up access request awaiting approval
47
- * active — approved / invite redeemed
48
- * inactive — denied / revoked
49
- *
50
- * @seeAlso migrations/016_invites.sql (indexes)
51
- * @seeAlso migrations/005_auth_user_trigger.sql (stub resolution on signup)
52
- * @seeAlso src/pages/auth/RegisterPage.tsx (frontend)
53
- * @seeAlso src/components/shared/InviteManager.tsx (UI)
54
- */
55
1
 
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+ // CIL[BEGIN chunk_id="invites_moduleheader_1783968140" type="module_header" identity="imports" parent="none"]
18
+ // CIL[DOC-MACRO purpose="Import dependencies for invite management API"]
19
+ // CIL[DOC-RESPONSIBILITY responsibility="Import handler utilities and database access"]
20
+ // CIL[DOC-CONTEXT module="invites" layer="api-handler" related_files="[\"custom/apps/cortex/pages/operations/OperationsDashboard.tsx\",\".framework/src/pages/admin/TypeDetailPage.tsx\",\".framework/functions/_shared/principal.ts\",\".framework/functions/_shared/schema-utils.ts\",\"custom/apps/portal/pages/ContentPage.tsx\",\"\\\"]"]
21
+ // CIL[DOC-CONTRACT inputs="none" outputs="Imported modules" errors="Import failures" side_effects="none"]
22
+ // CIL[DOC-MICRO logic_summary="Imports createHandler, createPublicHandler, adminDb, emitLog" notable_decisions="[]"]
23
+ // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
24
+ // CIL[DOC-STABILITY stability="stable"]
25
+ // CIL[DOC-AUDIENCE audience="developer"]
26
+ // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: custom/apps/cortex/pages/operations/OperationsDashboard.tsx, .framework/src/pages/admin/TypeDetailPage.tsx, .framework/functions/_shared/principal.ts, .framework/functions/_shared/schema-utils.ts, custom/apps/portal/pages/ContentPage.tsx, \."]
27
+ // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
28
+ // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
29
+ // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
30
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
31
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
56
32
  import { createHandler, createPublicHandler } from './_shared/middleware'
57
33
  import { adminDb } from './_shared/db'
58
34
  import { emitLog } from './_shared/audit'
59
-
60
- // ─── CONSTANTS ────────────────────────────────────────────────────────────────
61
-
35
+ import { create as adminDataCreate, list as adminDataList, update as adminDataUpdate } from './admin-data'
36
+ // CIL[END chunk_id="invites_moduleheader_1783968140"]
37
+ // CIL[BEGIN chunk_id="invites_constants_1783968140" type="constants" identity="constants" parent="none"]
38
+ // CIL[DOC-MACRO purpose="Define constants for invite token generation and validation"]
39
+ // CIL[DOC-RESPONSIBILITY responsibility="Configure invite security parameters and domain exclusions"]
40
+ // CIL[DOC-CONTEXT module="invites" layer="api-handler" related_files="[\".framework/functions/_shared/schema-utils.ts\",\"custom/apps/portal/pages/ContentPage.tsx\",\"custom/apps/cortex/pages/operations/OperationsDashboard.tsx\",\".framework/functions/_shared/principal.ts\",\".framework/src/pages/admin/TypeDetailPage.tsx\",\"\\\"]"]
41
+ // CIL[DOC-CONTRACT inputs="none" outputs="Constants" errors="none" side_effects="none"]
42
+ // CIL[DOC-MICRO logic_summary="Sets token length, max attempts, expiry days, and personal email domains" notable_decisions="[]"]
43
+ // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
44
+ // CIL[DOC-STABILITY stability="stable"]
45
+ // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: .framework/functions/_shared/schema-utils.ts, custom/apps/portal/pages/ContentPage.tsx, custom/apps/cortex/pages/operations/OperationsDashboard.tsx, .framework/functions/_shared/principal.ts, .framework/src/pages/admin/TypeDetailPage.tsx, \."]
46
+ // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
47
+ // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
48
+ // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
49
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
50
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
62
51
  const TOKEN_LENGTH = 16
63
52
  const MAX_ATTEMPTS = 5
64
53
  const INVITE_EXPIRY_DAYS = 7
65
54
 
66
- // Personal/free email domains — excluded from domain-match logic
67
55
  const PERSONAL_DOMAINS = new Set([
68
56
  'gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com', 'icloud.com',
69
57
  'aol.com', 'protonmail.com', 'me.com', 'mac.com', 'live.com',
70
58
  'msn.com', 'ymail.com', 'googlemail.com', 'fastmail.com',
71
59
  ])
72
-
73
- // ─── HELPERS ──────────────────────────────────────────────────────────────────
74
-
60
+ // CIL[END chunk_id="invites_constants_1783968140"]
61
+ // CIL[BEGIN chunk_id="invites_generatetoken_1783968140" type="function" identity="generateToken" parent="none"]
62
+ // CIL[DOC-MACRO purpose="Generate cryptographically secure random token for invites"]
63
+ // CIL[DOC-RESPONSIBILITY responsibility="Create unique invite tokens"]
64
+ // CIL[DOC-CONTEXT module="invites" layer="api-handler" related_files="[\".framework/src/pages/admin/TypeDetailPage.tsx\",\".framework/src/pages/admin/RolesPage.tsx\",\".framework/functions/_shared/principal.ts\",\"custom/apps/cortex/pages/operations/OperationsDashboard.tsx\",\".framework/functions/_shared/schema-utils.ts\",\"\\\"]"]
65
+ // CIL[DOC-CONTRACT inputs="none" outputs="Random token string" errors="Crypto API failures" side_effects="none"]
66
+ // CIL[DOC-MICRO logic_summary="Uses crypto.getRandomValues to generate alphanumeric token" notable_decisions="[]"]
67
+ // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
68
+ // CIL[DOC-STABILITY stability="stable"]
69
+ // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 1. Direct calls: 0. Related files: .framework/src/pages/admin/TypeDetailPage.tsx, .framework/src/pages/admin/RolesPage.tsx, .framework/functions/_shared/principal.ts, custom/apps/cortex/pages/operations/OperationsDashboard.tsx, .framework/functions/_shared/schema-utils.ts, \."]
70
+ // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 1. Total affected: 1."]
71
+ // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
72
+ // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
73
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
74
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
75
75
  function generateToken(): string {
76
76
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
77
77
  const bytes = crypto.getRandomValues(new Uint8Array(TOKEN_LENGTH))
78
78
  return Array.from(bytes).map(b => chars[b % chars.length]).join('')
79
79
  }
80
-
80
+ // CIL[END chunk_id="invites_generatetoken_1783968140"]
81
+ // CIL[BEGIN chunk_id="invites_generateaccountslug_1783968140" type="function" identity="generateAccountSlug" parent="none"]
82
+ // CIL[DOC-MACRO purpose="Generate unique account slug from name with random suffix"]
83
+ // CIL[DOC-RESPONSIBILITY responsibility="Create URL-safe account identifiers"]
84
+ // CIL[DOC-CONTEXT module="invites" layer="api-handler" related_files="[\".framework/src/pages/admin/RolesPage.tsx\",\".framework/functions/_shared/schema-utils.ts\",\".framework/src/components/ui/dialog.tsx\",\".framework/functions/_shared/principal.ts\",\".framework/src/components/ui/table-primitives.tsx\",\"\\\"]"]
85
+ // CIL[DOC-CONTRACT inputs="none" outputs="Unique account slug" errors="Invalid name format" side_effects="none"]
86
+ // CIL[DOC-MICRO logic_summary="Normalizes name to lowercase, adds hyphens, appends random hash" notable_decisions="[]"]
87
+ // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
88
+ // CIL[DOC-STABILITY stability="stable"]
89
+ // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: .framework/src/pages/admin/RolesPage.tsx, .framework/functions/_shared/schema-utils.ts, .framework/src/components/ui/dialog.tsx, .framework/functions/_shared/principal.ts, .framework/src/components/ui/table-primitives.tsx, \."]
90
+ // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
91
+ // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
92
+ // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
93
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
94
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
81
95
  function generateAccountSlug(name: string): string {
82
96
  const base = name
83
97
  .toLowerCase()
@@ -87,20 +101,42 @@ function generateAccountSlug(name: string): string {
87
101
  const hash = Math.random().toString(36).slice(2, 8)
88
102
  return `${base}-${hash}`
89
103
  }
90
-
104
+ // CIL[END chunk_id="invites_generateaccountslug_1783968140"]
105
+ // CIL[BEGIN chunk_id="invites_isadminrole_1783968140" type="function" identity="isAdminRole" parent="none"]
106
+ // CIL[DOC-MACRO purpose="Check if role slug indicates admin privileges"]
107
+ // CIL[DOC-RESPONSIBILITY responsibility="Validate admin role permissions"]
108
+ // CIL[DOC-CONTEXT module="invites" layer="api-handler" related_files="[\".framework/src/pages/admin/RolesPage.tsx\",\".framework/migrations/015_simplify_apps_table.sql\",\".framework/src/pages/admin/TypeDetailPage.tsx\",\".framework/src/pages/admin/IntegrationsPage.tsx\",\"\\\"]"]
109
+ // CIL[DOC-CONTRACT inputs="none" outputs="boolean - admin status" errors="none" side_effects="none"]
110
+ // CIL[DOC-MICRO logic_summary="Checks for system_admin or role ending with _admin" notable_decisions="[]"]
111
+ // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
112
+ // CIL[DOC-STABILITY stability="stable"]
113
+ // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: .framework/src/pages/admin/RolesPage.tsx, .framework/migrations/015_simplify_apps_table.sql, .framework/src/pages/admin/TypeDetailPage.tsx, .framework/src/pages/admin/IntegrationsPage.tsx, \."]
114
+ // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
115
+ // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
116
+ // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
117
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
118
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
91
119
  function isAdminRole(roleSlug: string | undefined): boolean {
92
120
  return !!roleSlug && (
93
121
  roleSlug === 'system_admin' ||
94
122
  roleSlug.endsWith('_admin')
95
123
  )
96
124
  }
97
-
98
- // ─── PUBLIC: check-first-user ─────────────────────────────────────────────────
99
-
100
- /**
101
- * Returns { is_first_user: boolean } — true if there are zero people in the system.
102
- * Used by RegisterPage to show simplified first-user flow.
103
- */
125
+ // CIL[END chunk_id="invites_isadminrole_1783968140"]
126
+ // CIL[BEGIN chunk_id="invites_checkfirstuser_1783968140" type="function" identity="checkFirstUser" parent="none"]
127
+ // CIL[DOC-MACRO purpose="Check if system has any users - enables first-user flow"]
128
+ // CIL[DOC-RESPONSIBILITY responsibility="Determine if this is initial system setup"]
129
+ // CIL[DOC-CONTEXT module="invites" layer="api-handler" related_files="[]"]
130
+ // CIL[DOC-CONTRACT inputs="none" outputs="{is_first_user: boolean}" errors="DB query failures" side_effects="DB read operation"]
131
+ // CIL[DOC-MICRO logic_summary="Counts people records, returns true if count is zero" notable_decisions="[]"]
132
+ // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
133
+ // CIL[DOC-STABILITY stability="stable"]
134
+ // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
135
+ // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
136
+ // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
137
+ // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
138
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
139
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
104
140
  export const checkFirstUser = createHandler(async (_ctx, _body) => {
105
141
  const { count, error } = await adminDb
106
142
  .from('people')
@@ -110,14 +146,21 @@ export const checkFirstUser = createHandler(async (_ctx, _body) => {
110
146
 
111
147
  return { is_first_user: (count ?? 0) === 0 }
112
148
  })
113
-
114
- // ─── PUBLIC: check-domain ─────────────────────────────────────────────────────
115
-
116
- /**
117
- * Returns { match: boolean } — whether any active people rows exist with the
118
- * same email domain AND it's not a known personal domain.
119
- * Does NOT return account names or IDs to avoid leaking structure.
120
- */
149
+ // CIL[END chunk_id="invites_checkfirstuser_1783968140"]
150
+ // CIL[BEGIN chunk_id="invites_checkdomain_1783968140" type="function" identity="checkDomain" parent="none"]
151
+ // CIL[DOC-MACRO purpose="Check if email domain has existing active accounts"]
152
+ // CIL[DOC-RESPONSIBILITY responsibility="Validate domain for account matching logic"]
153
+ // CIL[DOC-CONTEXT module="invites" layer="api-handler" related_files="[]"]
154
+ // CIL[DOC-CONTRACT inputs="none" outputs="{match: boolean}" errors="Invalid email, DB errors" side_effects="DB read operation"]
155
+ // CIL[DOC-MICRO logic_summary="Excludes personal domains, checks for active accounts with same domain" notable_decisions="[]"]
156
+ // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
157
+ // CIL[DOC-STABILITY stability="stable"]
158
+ // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
159
+ // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
160
+ // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
161
+ // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
162
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
163
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
121
164
  export const checkDomain = createHandler(async (_ctx, _body) => {
122
165
  const email = _ctx.query?.email as string | undefined
123
166
  if (!email || !email.includes('@')) {
@@ -139,13 +182,21 @@ export const checkDomain = createHandler(async (_ctx, _body) => {
139
182
 
140
183
  return { match: (data?.length ?? 0) > 0 }
141
184
  })
142
-
143
- // ─── PUBLIC: validate-token ───────────────────────────────────────────────────
144
-
145
- /**
146
- * Looks up a people row by invite_token. Returns a safe preview (no UUIDs).
147
- * Increments invite_attempts; blocks the token after MAX_ATTEMPTS failures.
148
- */
185
+ // CIL[END chunk_id="invites_checkdomain_1783968140"]
186
+ // CIL[BEGIN chunk_id="invites_validatetoken_1783968140" type="function" identity="validateToken" parent="none"]
187
+ // CIL[DOC-MACRO purpose="Validate invite token and return safe preview data"]
188
+ // CIL[DOC-RESPONSIBILITY responsibility="Authenticate invite tokens and track attempts"]
189
+ // CIL[DOC-CONTEXT module="invites" layer="api-handler" related_files="[]"]
190
+ // CIL[DOC-CONTRACT inputs="none" outputs="Invite preview data" errors="Token invalid/expired/blocked" side_effects="Updates attempt count, blocks after max attempts"]
191
+ // CIL[DOC-MICRO logic_summary="Validates token, checks expiry/blocked status, increments attempts, blocks after max" notable_decisions="[]"]
192
+ // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="complex_dependencies"]
193
+ // CIL[DOC-STABILITY stability="stable"]
194
+ // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
195
+ // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
196
+ // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
197
+ // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
198
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
199
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
149
200
  export const validateToken = createHandler(async (_ctx, _body) => {
150
201
  const token = _ctx.query?.token as string | undefined
151
202
  if (!token) throw new Error('Token is required')
@@ -163,17 +214,14 @@ export const validateToken = createHandler(async (_ctx, _body) => {
163
214
 
164
215
  const meta = person.data || {}
165
216
 
166
- // Check blocked
167
217
  if (meta.invite_blocked_at) {
168
218
  throw new Error('This invite link has been blocked. Please contact the administrator.')
169
219
  }
170
220
 
171
- // Check expiry
172
221
  if (meta.invite_expires_at && new Date(meta.invite_expires_at) < new Date()) {
173
222
  throw new Error('This invite link has expired. Please request a new one.')
174
223
  }
175
224
 
176
- // Increment attempts
177
225
  const attempts = (meta.invite_attempts || 0) + 1
178
226
  const blocked = attempts >= MAX_ATTEMPTS
179
227
 
@@ -204,16 +252,23 @@ export const validateToken = createHandler(async (_ctx, _body) => {
204
252
  kind: meta.invite_kind,
205
253
  }
206
254
  })
207
-
208
- // ─── PUBLIC: complete-registration ───────────────────────────────────────────
209
-
210
- /**
211
- * Called by RegisterPage after Supabase auth.signUp completes.
212
- * Handles three walk-up paths:
213
- * new_account — creates account + activates person as *_admin
214
- * join_request — creates access_request stub (person stays pending)
215
- * (invite path is handled entirely in the DB trigger)
216
- */
255
+ // CIL[END chunk_id="invites_validatetoken_1783968140"]
256
+ // CIL[BEGIN chunk_id="invites_completeregistration_1783968140" type="function" identity="completeRegistration" parent="none"]
257
+ // CIL[DOC-LEARNING topic="runtime-type:person/person" content="completeRegistration creates people records stamped with the person type's design_schema: full_name(text,required,system), email(email,required,system), phone(phone,system), status(text,required,system), is_active(boolean,required,system), avatar_url(url,system), created_at/updated_at(datetime,readonly,system). record_permissions allows member/member_admin/support/support_admin create/read/update and system_admin delete."]
258
+ // CIL[DOC-NOTE type="design_schema_caveat" content="The person record must have an account_id and role. The person type uses system=true for all fields, so values live on the people row, not in person.data."]
259
+ // CIL[DOC-MACRO purpose="Complete user registration after Supabase auth with multiple paths"]
260
+ // CIL[DOC-RESPONSIBILITY responsibility="Handle walk-up registration flows and account assignment"]
261
+ // CIL[DOC-CONTEXT module="invites" layer="api-handler" related_files="[]"]
262
+ // CIL[DOC-CONTRACT inputs="none" outputs="Registration result" errors="Invalid path, missing params, DB errors" side_effects="DB updates, role resolution"]
263
+ // CIL[DOC-MICRO logic_summary="Handles new_account, existing_account, and join_request paths with role resolution" notable_decisions="[]"]
264
+ // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="complex_dependencies"]
265
+ // CIL[DOC-STABILITY stability="stable"]
266
+ // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
267
+ // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
268
+ // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
269
+ // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
270
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
271
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
217
272
  export const completeRegistration = createHandler(async (ctx, body) => {
218
273
  const { path, account_name, email, role_slug, target_account } = body
219
274
 
@@ -224,7 +279,6 @@ export const completeRegistration = createHandler(async (ctx, body) => {
224
279
  if (path === 'existing_account') {
225
280
  if (!target_account) throw new Error('target_account is required for existing_account path')
226
281
 
227
- // Resolve the target account
228
282
  const { data: targetAccount } = await adminDb
229
283
  .from('accounts')
230
284
  .select('id, slug')
@@ -235,7 +289,6 @@ export const completeRegistration = createHandler(async (ctx, body) => {
235
289
  throw new Error(`Target account '${target_account}' not found`)
236
290
  }
237
291
 
238
- // Resolve person type_id
239
292
  const { data: personType } = await adminDb
240
293
  .from('types')
241
294
  .select('id, design_schema, validation_schema')
@@ -243,7 +296,6 @@ export const completeRegistration = createHandler(async (ctx, body) => {
243
296
  .eq('slug', 'person')
244
297
  .single()
245
298
 
246
- // Resolve role from role_slug param
247
299
  const targetRoleSlug = role_slug || 'member'
248
300
 
249
301
  let { data: roles } = await adminDb
@@ -254,7 +306,6 @@ export const completeRegistration = createHandler(async (ctx, body) => {
254
306
 
255
307
  let role = roles?.[0]
256
308
 
257
- // If specific role not found, try member_admin or member as fallback
258
309
  if (!role) {
259
310
  const { data: fallbackRoles } = await adminDb
260
311
  .from('roles')
@@ -265,7 +316,6 @@ export const completeRegistration = createHandler(async (ctx, body) => {
265
316
  role = fallbackRoles?.[0]
266
317
  }
267
318
 
268
- // Update the walk-up shell to be an active user on the target account
269
319
  const { error: updateErr } = await adminDb
270
320
  .from('people')
271
321
  .update({
@@ -291,7 +341,6 @@ export const completeRegistration = createHandler(async (ctx, body) => {
291
341
  if (path === 'new_account') {
292
342
  if (!account_name) throw new Error('account_name is required for new_account path')
293
343
 
294
- // Resolve person type_id
295
344
  const { data: personType } = await adminDb
296
345
  .from('types')
297
346
  .select('id, design_schema, validation_schema')
@@ -299,7 +348,6 @@ export const completeRegistration = createHandler(async (ctx, body) => {
299
348
  .eq('slug', 'person')
300
349
  .single()
301
350
 
302
- // Resolve account type_id
303
351
  const { data: accountType } = await adminDb
304
352
  .from('types')
305
353
  .select('id')
@@ -307,7 +355,6 @@ export const completeRegistration = createHandler(async (ctx, body) => {
307
355
  .eq('slug', 'account')
308
356
  .single()
309
357
 
310
- // Resolve role from role_slug param, or fall back to member_admin/member lookup
311
358
  const targetRoleSlug = role_slug || 'member'
312
359
 
313
360
  let { data: roles } = await adminDb
@@ -318,7 +365,6 @@ export const completeRegistration = createHandler(async (ctx, body) => {
318
365
 
319
366
  let role = roles?.[0]
320
367
 
321
- // If specific role not found, try member_admin or member as fallback
322
368
  if (!role) {
323
369
  const { data: fallbackRoles } = await adminDb
324
370
  .from('roles')
@@ -329,8 +375,6 @@ export const completeRegistration = createHandler(async (ctx, body) => {
329
375
  role = fallbackRoles?.[0]
330
376
  }
331
377
 
332
- // If no roles exist yet and no specific role was requested, create a default member role
333
- // But only if we're not trying to assign a specific app role
334
378
  if (!role && !role_slug) {
335
379
  const { data: newRole, error: roleErr } = await adminDb
336
380
  .from('roles')
@@ -349,7 +393,6 @@ export const completeRegistration = createHandler(async (ctx, body) => {
349
393
  }
350
394
  }
351
395
 
352
- // Create account
353
396
  const slug = generateAccountSlug(account_name)
354
397
  const { data: account, error: accErr } = await adminDb
355
398
  .from('accounts')
@@ -364,7 +407,6 @@ export const completeRegistration = createHandler(async (ctx, body) => {
364
407
 
365
408
  if (accErr || !account) throw new Error('Failed to create account: ' + accErr?.message)
366
409
 
367
- // Update the walk-up person shell (created by trigger as minimal record)
368
410
  const { error: pErr } = await adminDb
369
411
  .from('people')
370
412
  .update({
@@ -375,7 +417,7 @@ export const completeRegistration = createHandler(async (ctx, body) => {
375
417
  })
376
418
  .eq('email', email)
377
419
  .eq('status', 'active')
378
- .is('role_id', null) // only the newly created shell
420
+ .is('role_id', null)
379
421
 
380
422
  if (pErr) throw new Error('Failed to update person: ' + pErr.message)
381
423
 
@@ -386,7 +428,6 @@ export const completeRegistration = createHandler(async (ctx, body) => {
386
428
  const { target_account_id, role_slug } = body
387
429
  if (!target_account_id) throw new Error('target_account_id is required for join_request')
388
430
 
389
- // Resolve role from role_slug param, or fall back to member
390
431
  const targetRoleSlug = role_slug || 'member'
391
432
  let { data: roles } = await adminDb
392
433
  .from('roles')
@@ -396,7 +437,6 @@ export const completeRegistration = createHandler(async (ctx, body) => {
396
437
 
397
438
  let role = roles?.[0]
398
439
 
399
- // Fallback to member if specific role not found
400
440
  if (!role) {
401
441
  const { data: fallbackRoles } = await adminDb
402
442
  .from('roles')
@@ -406,7 +446,6 @@ export const completeRegistration = createHandler(async (ctx, body) => {
406
446
  role = fallbackRoles?.[0]
407
447
  }
408
448
 
409
- // Resolve type_id
410
449
  const { data: personType } = await adminDb
411
450
  .from('types')
412
451
  .select('id, design_schema, validation_schema')
@@ -414,7 +453,6 @@ export const completeRegistration = createHandler(async (ctx, body) => {
414
453
  .eq('slug', 'person')
415
454
  .single()
416
455
 
417
- // Update the walk-up shell to be a pending access request on the target account
418
456
  const { error: pErr } = await adminDb
419
457
  .from('people')
420
458
  .update({
@@ -439,21 +477,21 @@ export const completeRegistration = createHandler(async (ctx, body) => {
439
477
 
440
478
  throw new Error('Unknown registration path: ' + path)
441
479
  })
442
-
443
- // ─── PROTECTED: create-invite ─────────────────────────────────────────────────
444
-
445
- /**
446
- * Creates a stub people row (status: 'invited') with invite metadata.
447
- * Returns the shareable token URL.
448
- * Requires *_admin role scoped to the target account, or system_admin.
449
- *
450
- * @param full_name - Invitee's name (admin-entered, shown on register page)
451
- * @param email - Invitee's email address (locked on register page)
452
- * @param account_id - Account to invite into
453
- * @param role_slug - Role slug to assign (e.g. 'member', 'support')
454
- * @param allowed_role_slugs - Optional whitelist of permitted role slugs for this context
455
- * @param base_url - Origin to use for the invite URL (window.location.origin from client)
456
- */
480
+ // CIL[END chunk_id="invites_completeregistration_1783968140"]
481
+ // CIL[BEGIN chunk_id="invites_createinvite_1783968140" type="function" identity="createInvite" parent="none"]
482
+ // CIL[DOC-MACRO purpose="Create invite stub with metadata and return shareable URL"]
483
+ // CIL[DOC-RESPONSIBILITY responsibility="Generate new invite tokens and create person records"]
484
+ // CIL[DOC-CONTEXT module="invites" layer="api-handler" related_files="[]"]
485
+ // CIL[DOC-CONTRACT inputs="none" outputs="{id, token, invite_url, expires_at}" errors="Auth required, permission denied, DB errors" side_effects="DB insert, audit logging"]
486
+ // CIL[DOC-MICRO logic_summary="Validates admin permissions, creates person stub with invite metadata, generates token" notable_decisions="[]"]
487
+ // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="complex_dependencies"]
488
+ // CIL[DOC-STABILITY stability="stable"]
489
+ // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
490
+ // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
491
+ // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
492
+ // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
493
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
494
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
457
495
  export const createInvite = createHandler(async (ctx, body) => {
458
496
  if (!ctx.principal || ctx.principal.id === 'anonymous') {
459
497
  throw new Error('Authentication required')
@@ -464,7 +502,6 @@ export const createInvite = createHandler(async (ctx, body) => {
464
502
  throw new Error('email, account_id, and role_slug are required')
465
503
  }
466
504
 
467
- // Verify caller is admin for this account
468
505
  const { data: caller } = await adminDb
469
506
  .from('people')
470
507
  .select('role:roles(slug)')
@@ -483,12 +520,10 @@ export const createInvite = createHandler(async (ctx, body) => {
483
520
  throw new Error('Insufficient permissions — *_admin role required to create invites')
484
521
  }
485
522
 
486
- // Enforce allowed_role_slugs whitelist if provided
487
523
  if (allowed_role_slugs && Array.isArray(allowed_role_slugs) && !allowed_role_slugs.includes(role_slug)) {
488
524
  throw new Error(`Role '${role_slug}' is not permitted in this context`)
489
525
  }
490
526
 
491
- // Resolve role_id from slug
492
527
  const { data: roleRows } = await adminDb
493
528
  .from('roles')
494
529
  .select('id, slug')
@@ -498,7 +533,6 @@ export const createInvite = createHandler(async (ctx, body) => {
498
533
  const resolvedRole = roleRows?.[0]
499
534
  if (!resolvedRole) throw new Error(`Role '${role_slug}' not found`)
500
535
 
501
- // Check for existing unclaimed invite for this email+account
502
536
  const { data: existing } = await adminDb
503
537
  .from('people')
504
538
  .select('id')
@@ -511,7 +545,6 @@ export const createInvite = createHandler(async (ctx, body) => {
511
545
  throw new Error('A person with this email already exists or has a pending invite for this account')
512
546
  }
513
547
 
514
- // Resolve type_id for person
515
548
  const { data: personType } = await adminDb
516
549
  .from('types')
517
550
  .select('id, design_schema, validation_schema')
@@ -523,31 +556,27 @@ export const createInvite = createHandler(async (ctx, body) => {
523
556
  const expiresAt = new Date(Date.now() + INVITE_EXPIRY_DAYS * 86400 * 1000).toISOString()
524
557
  const stubName = (full_name && full_name.trim()) ? full_name.trim() : email.split('@')[0]
525
558
 
526
- const { data: stub, error: insertErr } = await adminDb
527
- .from('people')
528
- .insert({
529
- email,
530
- full_name: stubName,
531
- account_id,
532
- role_id: resolvedRole.id,
533
- type_id: personType?.id,
534
- status: 'invited',
535
- is_active: true,
536
- design_schema: personType?.design_schema ?? {},
537
- validation_schema: personType?.validation_schema ?? {},
538
- data: {
539
- invite_token: token,
540
- invite_kind: 'invite',
541
- invite_expires_at: expiresAt,
542
- invite_attempts: 0,
543
- invite_blocked_at: null,
544
- invite_invited_by: ctx.principal.id,
545
- },
546
- })
547
- .select('id')
548
- .single()
559
+ const adminCtx = { ...ctx, query: { ...ctx.query, entity: 'people' } }
560
+ const stub = await adminDataCreate(adminCtx, {
561
+ entity: 'people',
562
+ type_id: personType?.id,
563
+ account_id,
564
+ full_name: stubName,
565
+ email,
566
+ role_id: resolvedRole.id,
567
+ status: 'invited',
568
+ is_active: true,
569
+ data: {
570
+ invite_token: token,
571
+ invite_kind: 'invite',
572
+ invite_expires_at: expiresAt,
573
+ invite_attempts: 0,
574
+ invite_blocked_at: null,
575
+ invite_invited_by: ctx.principal.id,
576
+ },
577
+ })
549
578
 
550
- if (insertErr || !stub) throw new Error('Failed to create invite: ' + insertErr?.message)
579
+ if (!stub) throw new Error('Failed to create invite')
551
580
 
552
581
  const appBaseUrl = base_url || process.env.APP_BASE_URL || 'https://yourapp.com'
553
582
  const inviteUrl = `${appBaseUrl}/register?t=${token}`
@@ -558,17 +587,21 @@ export const createInvite = createHandler(async (ctx, body) => {
558
587
 
559
588
  return { id: stub.id, token, invite_url: inviteUrl, expires_at: expiresAt }
560
589
  })
561
-
562
- // ─── PROTECTED: list-invites ──────────────────────────────────────────────────
563
-
564
- /**
565
- * Lists people rows with invite metadata.
566
- * system_admin sees all accounts; others see only their account.
567
- *
568
- * @param account_id - Filter to a specific account (required for non-system_admin)
569
- * @param role_slugs - Comma-separated role slugs to filter by (e.g. 'support,support_admin')
570
- * When provided only people with one of these roles are returned.
571
- */
590
+ // CIL[END chunk_id="invites_createinvite_1783968140"]
591
+ // CIL[BEGIN chunk_id="invites_listinvites_1783968140" type="function" identity="listInvites" parent="none"]
592
+ // CIL[DOC-MACRO purpose="List invite records with filtering and role-based access control"]
593
+ // CIL[DOC-RESPONSIBILITY responsibility="Query and return scoped invite data"]
594
+ // CIL[DOC-CONTEXT module="invites" layer="api-handler" related_files="[]"]
595
+ // CIL[DOC-CONTRACT inputs="none" outputs="Array of invite records" errors="Auth required, DB errors" side_effects="DB read operations"]
596
+ // CIL[DOC-MICRO logic_summary="Filters by account scope, role slugs, returns mapped invite data" notable_decisions="[]"]
597
+ // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="complex_dependencies"]
598
+ // CIL[DOC-STABILITY stability="stable"]
599
+ // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
600
+ // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
601
+ // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
602
+ // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
603
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
604
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
572
605
  export const listInvites = createHandler(async (ctx, _body) => {
573
606
  if (!ctx.principal || ctx.principal.id === 'anonymous') {
574
607
  throw new Error('Authentication required')
@@ -577,12 +610,10 @@ export const listInvites = createHandler(async (ctx, _body) => {
577
610
  const isSystemAdmin = ctx.principal.permissions?.includes('system')
578
611
  const { account_id: queryAccountId, role_slugs: queryRoleSlugs } = ctx.query || {}
579
612
 
580
- // Parse optional role slug filter (comma-separated string from query param)
581
613
  const roleSlugsFilter: string[] | null = queryRoleSlugs
582
614
  ? (queryRoleSlugs as string).split(',').map((s: string) => s.trim()).filter(Boolean)
583
615
  : null
584
616
 
585
- // If role slug filter is provided, resolve to role IDs first
586
617
  let roleIdFilter: string[] | null = null
587
618
  if (roleSlugsFilter && roleSlugsFilter.length > 0) {
588
619
  const { data: roleRows } = await adminDb
@@ -592,7 +623,6 @@ export const listInvites = createHandler(async (ctx, _body) => {
592
623
 
593
624
  roleIdFilter = (roleRows || []).map(r => r.id)
594
625
  if (roleIdFilter.length === 0) {
595
- // No matching roles — return empty list rather than unfiltered
596
626
  return []
597
627
  }
598
628
  }
@@ -610,7 +640,6 @@ export const listInvites = createHandler(async (ctx, _body) => {
610
640
  .order('created_at', { ascending: false })
611
641
 
612
642
  if (!isSystemAdmin) {
613
- // Scoped to caller's account
614
643
  const scopedAccountId = queryAccountId || ctx.accountId
615
644
  if (!scopedAccountId) throw new Error('Account scope required')
616
645
  query = query.eq('account_id', scopedAccountId)
@@ -638,9 +667,21 @@ export const listInvites = createHandler(async (ctx, _body) => {
638
667
  role: Array.isArray(row.role) ? row.role[0] : row.role,
639
668
  }))
640
669
  })
641
-
642
- // ─── PROTECTED: revoke-invite ─────────────────────────────────────────────────
643
-
670
+ // CIL[END chunk_id="invites_listinvites_1783968140"]
671
+ // CIL[BEGIN chunk_id="invites_revokeinvite_1783968140" type="function" identity="revokeInvite" parent="none"]
672
+ // CIL[DOC-MACRO purpose="Soft-delete invite by setting status to inactive"]
673
+ // CIL[DOC-RESPONSIBILITY responsibility="Revoke pending or invited person records"]
674
+ // CIL[DOC-CONTEXT module="invites" layer="api-handler" related_files="[]"]
675
+ // CIL[DOC-CONTRACT inputs="none" outputs="{success: boolean}" errors="Auth required, person not found, DB errors" side_effects="DB update, audit logging"]
676
+ // CIL[DOC-MICRO logic_summary="Sets person status to inactive, clears invite token, logs revocation" notable_decisions="[]"]
677
+ // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
678
+ // CIL[DOC-STABILITY stability="stable"]
679
+ // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
680
+ // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
681
+ // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
682
+ // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
683
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
684
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
644
685
  export const revokeInvite = createHandler(async (ctx, body) => {
645
686
  if (!ctx.principal || ctx.principal.id === 'anonymous') throw new Error('Authentication required')
646
687
 
@@ -672,9 +713,21 @@ export const revokeInvite = createHandler(async (ctx, body) => {
672
713
 
673
714
  return { success: true }
674
715
  })
675
-
676
- // ─── PROTECTED: reinvite ──────────────────────────────────────────────────────
677
-
716
+ // CIL[END chunk_id="invites_revokeinvite_1783968140"]
717
+ // CIL[BEGIN chunk_id="invites_reinvite_1783968140" type="function" identity="reinvite" parent="none"]
718
+ // CIL[DOC-MACRO purpose="Issue new token for existing invite record"]
719
+ // CIL[DOC-RESPONSIBILITY responsibility="Regenerate invite tokens for expired invites"]
720
+ // CIL[DOC-CONTEXT module="invites" layer="api-handler" related_files="[]"]
721
+ // CIL[DOC-CONTRACT inputs="none" outputs="{token, invite_url, expires_at}" errors="Auth required, person not found, DB errors" side_effects="DB update, audit logging"]
722
+ // CIL[DOC-MICRO logic_summary="Generates new token, resets attempts, updates expiry, returns new URL" notable_decisions="[]"]
723
+ // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
724
+ // CIL[DOC-STABILITY stability="stable"]
725
+ // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
726
+ // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
727
+ // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
728
+ // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
729
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
730
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
678
731
  export const reinvite = createHandler(async (ctx, body) => {
679
732
  if (!ctx.principal || ctx.principal.id === 'anonymous') throw new Error('Authentication required')
680
733
 
@@ -721,9 +774,21 @@ export const reinvite = createHandler(async (ctx, body) => {
721
774
 
722
775
  return { token: newToken, invite_url: inviteUrl, expires_at: expiresAt }
723
776
  })
724
-
725
- // ─── PROTECTED: approve ───────────────────────────────────────────────────────
726
-
777
+ // CIL[END chunk_id="invites_reinvite_1783968140"]
778
+ // CIL[BEGIN chunk_id="invites_approve_1783968140" type="function" identity="approve" parent="none"]
779
+ // CIL[DOC-MACRO purpose="Approve pending access request and activate person"]
780
+ // CIL[DOC-RESPONSIBILITY responsibility="Convert pending access requests to active users"]
781
+ // CIL[DOC-CONTEXT module="invites" layer="api-handler" related_files="[]"]
782
+ // CIL[DOC-CONTRACT inputs="none" outputs="{success: boolean}" errors="Auth required, request not found, DB errors" side_effects="DB update, audit logging"]
783
+ // CIL[DOC-MICRO logic_summary="Sets status to active, strips invite metadata, logs approval" notable_decisions="[]"]
784
+ // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
785
+ // CIL[DOC-STABILITY stability="stable"]
786
+ // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
787
+ // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
788
+ // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
789
+ // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
790
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
791
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
727
792
  export const approve = createHandler(async (ctx, body) => {
728
793
  if (!ctx.principal || ctx.principal.id === 'anonymous') throw new Error('Authentication required')
729
794
 
@@ -739,7 +804,6 @@ export const approve = createHandler(async (ctx, body) => {
739
804
 
740
805
  if (!person) throw new Error('Access request not found or already processed')
741
806
 
742
- // Strip invite metadata on approval
743
807
  const { invite_token, invite_kind, invite_expires_at, invite_attempts, invite_blocked_at, invite_invited_by, ...remainingData } =
744
808
  (person.data as any) || {}
745
809
 
@@ -761,14 +825,21 @@ export const approve = createHandler(async (ctx, body) => {
761
825
 
762
826
  return { success: true }
763
827
  })
764
-
765
- // ─── PROTECTED: block-invite ─────────────────────────────────────────────────
766
-
767
- /**
768
- * Manually blocks an invite — sets invite_blocked_at on the people.data JSONB.
769
- * The token remains but validate-token will refuse it.
770
- * Useful for flagging suspicious or failed invite attempts before they auto-block.
771
- */
828
+ // CIL[END chunk_id="invites_approve_1783968140"]
829
+ // CIL[BEGIN chunk_id="invites_blockinvite_1783968140" type="function" identity="blockInvite" parent="none"]
830
+ // CIL[DOC-MACRO purpose="Manually block an invite token from being used"]
831
+ // CIL[DOC-RESPONSIBILITY responsibility="Flag suspicious or failed invite attempts"]
832
+ // CIL[DOC-CONTEXT module="invites" layer="api-handler" related_files="[]"]
833
+ // CIL[DOC-CONTRACT inputs="none" outputs="{success: boolean}" errors="Auth required, person not found, DB errors" side_effects="DB update, audit logging"]
834
+ // CIL[DOC-MICRO logic_summary="Sets invite_blocked_at timestamp to prevent token validation" notable_decisions="[]"]
835
+ // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
836
+ // CIL[DOC-STABILITY stability="stable"]
837
+ // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
838
+ // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
839
+ // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
840
+ // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
841
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
842
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
772
843
  export const blockInvite = createHandler(async (ctx, body) => {
773
844
  if (!ctx.principal || ctx.principal.id === 'anonymous') throw new Error('Authentication required')
774
845
 
@@ -804,9 +875,21 @@ export const blockInvite = createHandler(async (ctx, body) => {
804
875
 
805
876
  return { success: true }
806
877
  })
807
-
808
- // ─── PROTECTED: deny ──────────────────────────────────────────────────────────
809
-
878
+ // CIL[END chunk_id="invites_blockinvite_1783968140"]
879
+ // CIL[BEGIN chunk_id="invites_deny_1783968140" type="function" identity="deny" parent="none"]
880
+ // CIL[DOC-MACRO purpose="Deny pending access request and deactivate person"]
881
+ // CIL[DOC-RESPONSIBILITY responsibility="Reject access requests and mark users inactive"]
882
+ // CIL[DOC-CONTEXT module="invites" layer="api-handler" related_files="[]"]
883
+ // CIL[DOC-CONTRACT inputs="none" outputs="{success: boolean}" errors="Auth required, request not found, DB errors" side_effects="DB update, audit logging"]
884
+ // CIL[DOC-MICRO logic_summary="Sets status to inactive and is_active false, logs denial" notable_decisions="[]"]
885
+ // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
886
+ // CIL[DOC-STABILITY stability="stable"]
887
+ // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
888
+ // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
889
+ // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
890
+ // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
891
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
892
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
810
893
  export const deny = createHandler(async (ctx, body) => {
811
894
  if (!ctx.principal || ctx.principal.id === 'anonymous') throw new Error('Authentication required')
812
895
 
@@ -839,19 +922,29 @@ export const deny = createHandler(async (ctx, body) => {
839
922
 
840
923
  return { success: true }
841
924
  })
842
-
843
- // ─── MAIN HANDLER ────────────────────────────────────────────────────────────
844
-
925
+ // CIL[END chunk_id="invites_deny_1783968140"]
926
+ // CIL[BEGIN chunk_id="invites_handler_1783968140" type="function" identity="handler" parent="none"]
927
+ // CIL[DOC-MACRO purpose="Route invite actions to appropriate handler functions"]
928
+ // CIL[DOC-RESPONSIBILITY responsibility="Dispatch invite API requests to correct handlers"]
929
+ // CIL[DOC-CONTEXT module="invites" layer="api-handler" related_files="[]"]
930
+ // CIL[DOC-CONTRACT inputs="none" outputs="Varies by action" errors="Unknown action" side_effects="Delegates to handlers"]
931
+ // CIL[DOC-MICRO logic_summary="Switch statement routes action parameter to appropriate handler function" notable_decisions="[]"]
932
+ // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
933
+ // CIL[DOC-STABILITY stability="stable"]
934
+ // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
935
+ // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
936
+ // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
937
+ // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
938
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
939
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
845
940
  export const handler = createPublicHandler(async (ctx, body) => {
846
941
  const action = ctx.query?.action
847
942
 
848
943
  switch (action) {
849
- // Public endpoints (no auth required)
850
944
  case 'check-first-user': return await checkFirstUser(ctx, body)
851
945
  case 'check-domain': return await checkDomain(ctx, body)
852
946
  case 'validate-token': return await validateToken(ctx, body)
853
947
  case 'complete-registration': return await completeRegistration(ctx, body)
854
- // Protected endpoints (require auth)
855
948
  case 'create-invite': return await createInvite(ctx, body)
856
949
  case 'list-invites': return await listInvites(ctx, body)
857
950
  case 'revoke-invite': return await revokeInvite(ctx, body)
@@ -859,7 +952,114 @@ export const handler = createPublicHandler(async (ctx, body) => {
859
952
  case 'block-invite': return await blockInvite(ctx, body)
860
953
  case 'approve': return await approve(ctx, body)
861
954
  case 'deny': return await deny(ctx, body)
955
+ case 'list-members': return await listMembers(ctx, body)
956
+ case 'update-member': return await updateMember(ctx, body)
957
+ case 'deactivate-member': return await deactivateMember(ctx, body)
862
958
  default:
863
959
  throw new Error(`Unknown action: ${action}`)
864
960
  }
865
961
  })
962
+ // CIL[END chunk_id="invites_handler_1783968140"]
963
+
964
+ // ─── Member management actions (runtime CRUD via admin-data) ─────────────────
965
+
966
+ function canManageMembers(ctx: any) {
967
+ if (!ctx.principal || ctx.principal.id === 'anonymous') return false
968
+ if (ctx.principal.permissions?.includes('system')) return true
969
+ return (ctx.principal.roles || []).some((r: string) => isAdminRole(r))
970
+ }
971
+
972
+ async function listMembers(ctx: any, body: any) {
973
+ if (!canManageMembers(ctx)) {
974
+ throw new Error('Insufficient permissions — *_admin role required to list members')
975
+ }
976
+
977
+ const accountId = ctx.query?.account_id || body?.account_id || ctx.principal?.accountId
978
+ if (!accountId) throw new Error('account_id is required')
979
+
980
+ const adminCtx = { ...ctx, query: { ...ctx.query, entity: 'people', type_slug: 'person', account_id: accountId } }
981
+ const records = await adminDataList(adminCtx, {})
982
+
983
+ const roleIds = (records || [])
984
+ .map((r: any) => r.role_id)
985
+ .filter(Boolean)
986
+ const roleMap: Record<string, { slug: string; name: string }> = {}
987
+ if (roleIds.length) {
988
+ const { data: roles } = await adminDb
989
+ .from('roles')
990
+ .select('id, slug, name')
991
+ .in('id', roleIds)
992
+ for (const r of roles || []) {
993
+ roleMap[r.id] = { slug: r.slug, name: r.name }
994
+ }
995
+ }
996
+
997
+ const data = (records || []).map((r: any) => ({
998
+ ...r,
999
+ role: r.role_id ? roleMap[r.role_id] || { id: r.role_id } : null
1000
+ }))
1001
+
1002
+ return { data }
1003
+ }
1004
+
1005
+ async function resolveRoleId(slug: string) {
1006
+ const { data: roleRows } = await adminDb
1007
+ .from('roles')
1008
+ .select('id, slug')
1009
+ .eq('slug', slug)
1010
+ .limit(1)
1011
+ const resolved = roleRows?.[0]
1012
+ if (!resolved) throw new Error(`Role '${slug}' not found`)
1013
+ return resolved.id
1014
+ }
1015
+
1016
+ async function updateMember(ctx: any, body: any) {
1017
+ if (!canManageMembers(ctx)) {
1018
+ throw new Error('Insufficient permissions — *_admin role required to update members')
1019
+ }
1020
+
1021
+ const { person_id, full_name, role_slug } = body
1022
+ if (!person_id) throw new Error('person_id is required')
1023
+
1024
+ const recordData: any = {}
1025
+ if (full_name !== undefined) recordData.full_name = full_name
1026
+ if (role_slug !== undefined) recordData.role_id = await resolveRoleId(role_slug)
1027
+
1028
+ if (Object.keys(recordData).length === 0) {
1029
+ throw new Error('No update fields provided')
1030
+ }
1031
+
1032
+ const adminCtx = {
1033
+ ...ctx,
1034
+ query: { ...ctx.query, entity: 'people', id: person_id }
1035
+ }
1036
+
1037
+ const updated = await adminDataUpdate(adminCtx, recordData)
1038
+ if (!updated) throw new Error('Failed to update member')
1039
+
1040
+ await emitLog(ctx, 'member.updated', { type: 'person', id: person_id }, { after: recordData })
1041
+ return { success: true, data: updated }
1042
+ }
1043
+
1044
+ async function deactivateMember(ctx: any, body: any) {
1045
+ if (!canManageMembers(ctx)) {
1046
+ throw new Error('Insufficient permissions — *_admin role required to deactivate members')
1047
+ }
1048
+
1049
+ const { person_id } = body
1050
+ if (!person_id) throw new Error('person_id is required')
1051
+
1052
+ const adminCtx = {
1053
+ ...ctx,
1054
+ query: { ...ctx.query, entity: 'people', id: person_id }
1055
+ }
1056
+
1057
+ const updated = await adminDataUpdate(adminCtx, {
1058
+ status: 'inactive',
1059
+ is_active: false,
1060
+ })
1061
+ if (!updated) throw new Error('Failed to deactivate member')
1062
+
1063
+ await emitLog(ctx, 'member.deactivated', { type: 'person', id: person_id }, {})
1064
+ return { success: true, data: updated }
1065
+ }