spine-framework 0.3.145 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (451) hide show
  1. package/.env.example +76 -0
  2. package/.framework/.spine-manifest.json +19 -0
  3. package/.framework/cli/commands/agents.ts +61 -33
  4. package/.framework/cli/commands/auth.ts +60 -28
  5. package/.framework/cli/commands/create-app.ts +212 -80
  6. package/.framework/cli/commands/dev.ts +184 -68
  7. package/.framework/cli/commands/doctor.ts +324 -152
  8. package/.framework/cli/commands/generate.ts +119 -68
  9. package/.framework/cli/commands/init.ts +141 -242
  10. package/.framework/cli/commands/install-app.ts +1336 -314
  11. package/.framework/cli/commands/items.ts +31 -29
  12. package/.framework/cli/commands/migrate.ts +299 -156
  13. package/.framework/cli/commands/migrations.ts +62 -30
  14. package/.framework/cli/commands/pipelines.ts +31 -29
  15. package/.framework/cli/commands/status.ts +105 -45
  16. package/.framework/cli/commands/system.ts +47 -42
  17. package/.framework/cli/commands/test.ts +161 -102
  18. package/.framework/cli/commands/upgrade.ts +260 -0
  19. package/.framework/cli/commands/validate-app.ts +517 -161
  20. package/.framework/cli/context.ts +117 -85
  21. package/.framework/cli/env-loader.ts +74 -43
  22. package/.framework/cli/index.ts +70 -46
  23. package/.framework/cli/lib/project-files.ts +404 -0
  24. package/.framework/docs/guides/testing-guide.md +1 -1
  25. package/.framework/docs/runtime-types.md +72 -72
  26. package/.framework/functions/_shared/access.ts +84 -0
  27. package/.framework/functions/_shared/agent-runner.ts +5051 -1026
  28. package/.framework/functions/_shared/app-manifest.ts +214 -94
  29. package/.framework/functions/_shared/ast.ts +214 -0
  30. package/.framework/functions/_shared/audit.ts +91 -55
  31. package/.framework/functions/_shared/cil.ts +368 -127
  32. package/.framework/functions/_shared/db.ts +337 -68
  33. package/.framework/functions/_shared/funnel.ts +228 -0
  34. package/.framework/functions/_shared/geo.ts +105 -0
  35. package/.framework/functions/_shared/hash-api-key.ts +94 -17
  36. package/.framework/functions/_shared/index.ts +22 -224
  37. package/.framework/functions/_shared/kb.ts +832 -58
  38. package/.framework/functions/_shared/llm.ts +118 -18
  39. package/.framework/functions/_shared/logging.ts +317 -19
  40. package/.framework/functions/_shared/middleware.ts +701 -131
  41. package/.framework/functions/_shared/notify.ts +70 -0
  42. package/.framework/functions/_shared/outcomes.ts +34 -19
  43. package/.framework/functions/_shared/permissions.ts +501 -315
  44. package/.framework/functions/_shared/pipeline-runner.ts +704 -155
  45. package/.framework/functions/_shared/principal.ts +436 -263
  46. package/.framework/functions/_shared/rate-limit.ts +233 -0
  47. package/.framework/functions/_shared/resolve-credentials.ts +86 -19
  48. package/.framework/functions/_shared/resolve-ids.ts +203 -139
  49. package/.framework/functions/_shared/retry-deliveries.ts +51 -16
  50. package/.framework/functions/_shared/schema-utils.ts +602 -521
  51. package/.framework/functions/_shared/security.ts +345 -0
  52. package/.framework/functions/_shared/testing.ts +197 -172
  53. package/.framework/functions/_shared/tracing.ts +351 -21
  54. package/.framework/functions/_shared/trigger-engine.ts +425 -136
  55. package/.framework/functions/_shared/webhook-registration.ts +71 -70
  56. package/.framework/functions/_shared/webhook-registry.ts +126 -83
  57. package/.framework/functions/account-nodes.ts +30 -30
  58. package/.framework/functions/adapters/generic-webhook-inbound.ts +49 -16
  59. package/.framework/functions/adapters/generic-webhook-outbound.ts +17 -17
  60. package/.framework/functions/adapters/openai-outbound.ts +15 -16
  61. package/.framework/functions/admin-data.ts +596 -173
  62. package/.framework/functions/admin-funnel.ts +64 -60
  63. package/.framework/functions/admin-install-funnel.ts +84 -66
  64. package/.framework/functions/admin-signals.ts +69 -51
  65. package/.framework/functions/ai-agents.ts +431 -106
  66. package/.framework/functions/ai-interactions.ts +186 -112
  67. package/.framework/functions/api-keys.ts +107 -107
  68. package/.framework/functions/apps.ts +154 -136
  69. package/.framework/functions/auth.ts +83 -65
  70. package/.framework/functions/cil-agent-ingest.ts +189 -232
  71. package/.framework/functions/cil-agent.ts +433 -177
  72. package/.framework/functions/cil-callmap.ts +642 -430
  73. package/.framework/functions/cil-config.ts +139 -135
  74. package/.framework/functions/cil-context.ts +214 -127
  75. package/.framework/functions/cil-doc.ts +587 -458
  76. package/.framework/functions/cil-embeddings.ts +453 -364
  77. package/.framework/functions/cil-graph.ts +427 -142
  78. package/.framework/functions/cil-ingest.ts +1418 -1154
  79. package/.framework/functions/cil-pipeline.ts +276 -21
  80. package/.framework/functions/cil-project.ts +659 -132
  81. package/.framework/functions/cil-validate.ts +198 -164
  82. package/.framework/functions/debug-auth.ts +57 -55
  83. package/.framework/functions/deliveries.ts +80 -21
  84. package/.framework/functions/embeddings.ts +1023 -318
  85. package/.framework/functions/eval-harness.ts +146 -108
  86. package/.framework/functions/integration-outbound.ts +205 -75
  87. package/.framework/functions/integration-routes.ts +128 -23
  88. package/.framework/functions/integrations.ts +125 -100
  89. package/.framework/functions/invites.ts +1095 -460
  90. package/.framework/functions/item-progress.ts +158 -102
  91. package/.framework/functions/kb.ts +34 -10
  92. package/.framework/functions/logs.ts +133 -133
  93. package/.framework/functions/observability.ts +152 -122
  94. package/.framework/functions/pipeline-executions.ts +147 -132
  95. package/.framework/functions/pipelines.ts +134 -119
  96. package/.framework/functions/prompt-configs.ts +96 -80
  97. package/.framework/functions/roles.ts +112 -82
  98. package/.framework/functions/security.ts +450 -0
  99. package/.framework/functions/system-cron.ts +117 -80
  100. package/.framework/functions/system.ts +158 -98
  101. package/.framework/functions/tests.ts +29 -29
  102. package/.framework/functions/timers.ts +94 -95
  103. package/.framework/functions/trigger-scheduler.ts +10 -0
  104. package/.framework/functions/triggers.ts +179 -149
  105. package/.framework/functions/types.ts +137 -125
  106. package/.framework/mcp/index.ts +133 -0
  107. package/.framework/mcp/load-env.ts +60 -0
  108. package/.framework/mcp/shared/app-manifest.ts +275 -0
  109. package/.framework/mcp/shared/db.ts +477 -0
  110. package/.framework/mcp/shared/kb.ts +118 -0
  111. package/.framework/mcp/tools/architect.ts +431 -0
  112. package/.framework/mcp/tools/cil/context.ts +345 -0
  113. package/.framework/mcp/tools/cil/graph.ts +242 -0
  114. package/.framework/mcp/tools/cil/ingest.ts +1217 -0
  115. package/.framework/mcp/tools/cil/pipeline-orchestrator.ts +693 -0
  116. package/.framework/mcp/tools/cil/pipeline.ts +767 -0
  117. package/.framework/mcp/tools/cil/project.ts +791 -0
  118. package/.framework/mcp/tools/cil/retrieval.ts +336 -0
  119. package/.framework/mcp/tools/cil/search-core.ts +454 -0
  120. package/.framework/mcp/tools/cil/search.ts +97 -0
  121. package/.framework/mcp/tools/cil/suggest.ts +186 -0
  122. package/.framework/mcp/tools/cil/validate.ts +1369 -0
  123. package/.framework/mcp/tools/kb/delete.ts +139 -0
  124. package/.framework/mcp/tools/kb/index.ts +52 -0
  125. package/.framework/mcp/tools/kb/ingest.ts +84 -0
  126. package/.framework/mcp/tools/kb/list-types.ts +102 -0
  127. package/.framework/mcp/tools/kb/publish.ts +100 -0
  128. package/.framework/mcp/tools/kb/reindex.ts +86 -0
  129. package/.framework/mcp/tools/kb/revoke.ts +85 -0
  130. package/.framework/mcp/tools/kb/search.ts +101 -0
  131. package/.framework/mcp/tools/kb/upsert.ts +125 -0
  132. package/.framework/mcp/tools/observability.ts +510 -0
  133. package/.framework/migrations/001_core_schema.sql +15 -0
  134. package/.framework/migrations/002_core_rls_and_functions.sql +15 -0
  135. package/.framework/migrations/003_core_system_data.sql +16 -1
  136. package/.framework/migrations/004_core_rpc_and_broker.sql +17 -1
  137. package/.framework/migrations/005_core_observability.sql +17 -1
  138. package/.framework/migrations/006_security_hardening_rls.sql +15 -0
  139. package/.framework/migrations/007_kb_article_list.sql +17 -1
  140. package/.framework/migrations/008_items_target.sql +15 -0
  141. package/.framework/migrations/009_roles_slug_unique.sql +15 -0
  142. package/.framework/migrations/010_auth_user_trigger.sql +15 -0
  143. package/.framework/migrations/011_spine_system_account.sql +16 -0
  144. package/.framework/migrations/012_service_role_grants.sql +16 -0
  145. package/.framework/migrations/013_get_account_apps_role_filter.sql +15 -0
  146. package/.framework/migrations/014_apps_required_roles.sql +16 -0
  147. package/.framework/migrations/015_types_kind_slug_unique.sql +15 -0
  148. package/.framework/migrations/016_cil_types.sql +281 -0
  149. package/.framework/migrations/017_cil_stable_ids.sql +37 -0
  150. package/.framework/migrations/018_runtime_tables.sql +129 -0
  151. package/.framework/migrations/019_agent_prompt_columns.sql +56 -0
  152. package/.framework/migrations/020_schema_reconciliation.sql +67 -0
  153. package/.framework/migrations/021_kb_embedding_lifecycle.sql +31 -0
  154. package/.framework/migrations/032_cil_status_migration.sql +73 -0
  155. package/.framework/migrations/033_cil_versioning.sql +73 -0
  156. package/.framework/migrations/034_match_embeddings_knn.sql +115 -0
  157. package/.framework/migrations/035_rate_limits.sql +55 -0
  158. package/.framework/migrations/036_demo_record_flags.sql +72 -0
  159. package/.framework/migrations/037_kb_types_canonical.sql +244 -0
  160. package/.framework/migrations/038_drop_demo_record_flags.sql +38 -0
  161. package/.framework/migrations/042_registration_hierarchy.sql +309 -0
  162. package/.framework/migrations/043_policy_actor_cleanup.sql +39 -0
  163. package/.framework/migrations/044_match_embeddings_filtered_knn.sql +151 -0
  164. package/.framework/migrations/045_security_critical_rls.sql +139 -0
  165. package/.framework/migrations/046_security_missing_rls.sql +79 -0
  166. package/.framework/migrations/047_people_updated_by.sql +27 -0
  167. package/.framework/migrations/048_watcher_last_seen_at.sql +24 -0
  168. package/.framework/migrations/049_security_blocklist.sql +148 -0
  169. package/.framework/migrations/050_notifications.sql +49 -0
  170. package/.framework/migrations/051_message_agent_fields.sql +74 -0
  171. package/.framework/migrations/052_registration_security.sql +421 -0
  172. package/.framework/migrations/053_builtin_action_seeds.sql +66 -0
  173. package/.framework/migrations/054_kb_ingest_lease.sql +157 -0
  174. package/.framework/migrations/055_type_id_drift_reconciliation.sql +33 -0
  175. package/.framework/migrations/056_slug_column_reconciliation.sql +38 -0
  176. package/.framework/migrations/057_pipelines_slug_identity.sql +14 -0
  177. package/.framework/migrations/058_account_data_field_declarations.sql +53 -0
  178. package/.framework/migrations/059_registration_placement_fixes.sql +269 -0
  179. package/.framework/migrations/060_account_person_record_permissions.sql +38 -0
  180. package/.framework/migrations/061_fk_dedup.sql +32 -0
  181. package/.framework/public/spine-logo.svg +10 -3
  182. package/.framework/scripts/app-install-cli.ts +190 -93
  183. package/.framework/scripts/assemble-frontend.sh +127 -44
  184. package/.framework/scripts/assemble-functions.sh +128 -30
  185. package/.framework/scripts/boundary-check.sh +6 -2
  186. package/.framework/scripts/build-manifest.sh +24 -68
  187. package/.framework/scripts/check-core-integrity.sh +15 -76
  188. package/.framework/scripts/checksum-manifest.mts +343 -0
  189. package/.framework/scripts/cil-migrate-markers.ts +403 -0
  190. package/.framework/scripts/fix-cil-markers.mjs +176 -0
  191. package/.framework/scripts/verify-integrity.sh +21 -57
  192. package/.framework/scripts/verify-manifest.mts +305 -0
  193. package/.framework/seeds/types/funnel_signal_use.json +74 -21
  194. package/.framework/seeds/types/spine_instance.json +73 -21
  195. package/.framework/src/App.tsx +118 -72
  196. package/.framework/src/apps/admin/index.tsx +855 -58
  197. package/.framework/src/components/AppWrapper.tsx +43 -45
  198. package/.framework/src/components/CustomAppLoader.tsx +160 -116
  199. package/.framework/src/components/admin/AdminListPage.tsx +46 -44
  200. package/.framework/src/components/admin/AdminSidebar.tsx +57 -47
  201. package/.framework/src/components/admin/AdminStatsCard.tsx +46 -44
  202. package/.framework/src/components/admin/PageInstructions.tsx +98 -2
  203. package/.framework/src/components/admin/SortableTableHeader.tsx +44 -45
  204. package/.framework/src/components/app-shell/GenericAppShell.tsx +64 -65
  205. package/.framework/src/components/app-shell/GenericDetailPage.tsx +44 -47
  206. package/.framework/src/components/app-shell/GenericListPage.tsx +45 -49
  207. package/.framework/src/components/app-sidebar.tsx +42 -45
  208. package/.framework/src/components/auth/ProtectedRoute.tsx +48 -41
  209. package/.framework/src/components/auth/TurnstileWidget.tsx +27 -0
  210. package/.framework/src/components/kb/KBArticleEditor.tsx +231 -22
  211. package/.framework/src/components/kb/KBArticleList.tsx +251 -10
  212. package/.framework/src/components/layout/AppShell.tsx +51 -45
  213. package/.framework/src/components/layout/Header.tsx +42 -43
  214. package/.framework/src/components/layout/Layout.tsx +46 -46
  215. package/.framework/src/components/layout/Sidebar.tsx +196 -96
  216. package/.framework/src/components/runtime/DataDetailHeader.tsx +55 -30
  217. package/.framework/src/components/runtime/DataDetailPage.tsx +31 -269
  218. package/.framework/src/components/runtime/DataFilters.tsx +49 -74
  219. package/.framework/src/components/runtime/DataHeader.tsx +53 -32
  220. package/.framework/src/components/runtime/DataListPage.tsx +51 -155
  221. package/.framework/src/components/runtime/DataStats.tsx +66 -44
  222. package/.framework/src/components/runtime/DataTable.tsx +49 -149
  223. package/.framework/src/components/runtime/SchemaDetailForm.tsx +45 -75
  224. package/.framework/src/components/runtime/index.ts +119 -16
  225. package/.framework/src/components/search-form.tsx +46 -29
  226. package/.framework/src/components/shared/AgentView.tsx +65 -48
  227. package/.framework/src/components/shared/FieldRenderer.tsx +144 -50
  228. package/.framework/src/components/shared/InviteManager.tsx +207 -97
  229. package/.framework/src/components/shared/MemberList.tsx +97 -0
  230. package/.framework/src/components/shared/SchemaFields.tsx +107 -86
  231. package/.framework/src/components/ui/DataTable.tsx +80 -68
  232. package/.framework/src/components/ui/Form.tsx +157 -146
  233. package/.framework/src/components/ui/ItemCard.tsx +74 -67
  234. package/.framework/src/components/ui/ItemListView.tsx +73 -68
  235. package/.framework/src/components/ui/LoadingSpinner.tsx +43 -48
  236. package/.framework/src/components/ui/Modal.tsx +78 -68
  237. package/.framework/src/components/ui/RichTextEditor.tsx +89 -69
  238. package/.framework/src/components/ui/accordion.tsx +85 -86
  239. package/.framework/src/components/ui/alert-dialog.tsx +197 -184
  240. package/.framework/src/components/ui/alert.tsx +105 -94
  241. package/.framework/src/components/ui/aspect-ratio.tsx +57 -43
  242. package/.framework/src/components/ui/avatar.tsx +122 -107
  243. package/.framework/src/components/ui/badge.tsx +60 -55
  244. package/.framework/src/components/ui/breadcrumb.tsx +127 -128
  245. package/.framework/src/components/ui/button-group.tsx +86 -72
  246. package/.framework/src/components/ui/button.tsx +71 -50
  247. package/.framework/src/components/ui/calendar.tsx +72 -57
  248. package/.framework/src/components/ui/card.tsx +136 -129
  249. package/.framework/src/components/ui/carousel.tsx +226 -107
  250. package/.framework/src/components/ui/chart.tsx +252 -147
  251. package/.framework/src/components/ui/checkbox.tsx +43 -44
  252. package/.framework/src/components/ui/collapsible.tsx +86 -71
  253. package/.framework/src/components/ui/combobox.tsx +282 -249
  254. package/.framework/src/components/ui/command.tsx +158 -146
  255. package/.framework/src/components/ui/context-menu.tsx +239 -240
  256. package/.framework/src/components/ui/dialog.tsx +174 -157
  257. package/.framework/src/components/ui/direction.tsx +72 -56
  258. package/.framework/src/components/ui/drawer.tsx +168 -169
  259. package/.framework/src/components/ui/dropdown-menu.tsx +254 -223
  260. package/.framework/src/components/ui/empty.tsx +131 -120
  261. package/.framework/src/components/ui/field.tsx +205 -173
  262. package/.framework/src/components/ui/hover-card.tsx +71 -72
  263. package/.framework/src/components/ui/input-group.tsx +148 -133
  264. package/.framework/src/components/ui/input-otp.tsx +99 -86
  265. package/.framework/src/components/ui/input.tsx +43 -41
  266. package/.framework/src/components/ui/item.tsx +207 -185
  267. package/.framework/src/components/ui/kbd.tsx +57 -56
  268. package/.framework/src/components/ui/label.tsx +45 -41
  269. package/.framework/src/components/ui/menubar.tsx +268 -236
  270. package/.framework/src/components/ui/native-select.tsx +85 -86
  271. package/.framework/src/components/ui/navigation-menu.tsx +166 -156
  272. package/.framework/src/components/ui/pagination.tsx +131 -141
  273. package/.framework/src/components/ui/popover.tsx +125 -128
  274. package/.framework/src/components/ui/progress.tsx +47 -44
  275. package/.framework/src/components/ui/radio-group.tsx +60 -55
  276. package/.framework/src/components/ui/resizable.tsx +85 -72
  277. package/.framework/src/components/ui/scroll-area.tsx +57 -58
  278. package/.framework/src/components/ui/select.tsx +182 -169
  279. package/.framework/src/components/ui/separator.tsx +42 -43
  280. package/.framework/src/components/ui/sheet.tsx +170 -159
  281. package/.framework/src/components/ui/sidebar.tsx +395 -398
  282. package/.framework/src/components/ui/skeleton.tsx +42 -43
  283. package/.framework/src/components/ui/slider.tsx +61 -44
  284. package/.framework/src/components/ui/sonner.tsx +44 -44
  285. package/.framework/src/components/ui/spinner.tsx +47 -42
  286. package/.framework/src/components/ui/switch.tsx +56 -42
  287. package/.framework/src/components/ui/table-primitives.tsx +272 -132
  288. package/.framework/src/components/ui/table.tsx +140 -142
  289. package/.framework/src/components/ui/tabs.tsx +110 -93
  290. package/.framework/src/components/ui/textarea.tsx +43 -44
  291. package/.framework/src/components/ui/toggle-group.tsx +87 -71
  292. package/.framework/src/components/ui/toggle.tsx +56 -56
  293. package/.framework/src/components/ui/tooltip.tsx +99 -85
  294. package/.framework/src/contexts/AppContext.tsx +166 -114
  295. package/.framework/src/contexts/AuthContext.tsx +232 -137
  296. package/.framework/src/hooks/use-mobile.ts +43 -44
  297. package/.framework/src/hooks/useApi.ts +185 -172
  298. package/.framework/src/hooks/useAppPath.ts +31 -29
  299. package/.framework/src/hooks/useApps.ts +77 -42
  300. package/.framework/src/hooks/useEntityList.ts +66 -29
  301. package/.framework/src/hooks/useEntityRecord.ts +76 -32
  302. package/.framework/src/hooks/useForm.ts +62 -30
  303. package/.framework/src/hooks/useListSchema.ts +60 -43
  304. package/.framework/src/hooks/useSchemaRecord.ts +115 -95
  305. package/.framework/src/lib/api.ts +258 -134
  306. package/.framework/src/lib/registration.ts +181 -0
  307. package/.framework/src/lib/supabase.ts +118 -81
  308. package/.framework/src/lib/utils.ts +266 -266
  309. package/.framework/src/main.tsx +43 -44
  310. package/.framework/src/pages/DashboardPage.tsx +44 -43
  311. package/.framework/src/pages/NoAppPage.tsx +50 -0
  312. package/.framework/src/pages/NotFoundPage.tsx +32 -29
  313. package/.framework/src/pages/SetupPage.tsx +34 -33
  314. package/.framework/src/pages/admin/AIAgentDetailPage.tsx +69 -58
  315. package/.framework/src/pages/admin/AIAgentsPage.tsx +43 -40
  316. package/.framework/src/pages/admin/APIKeyDetailPage.tsx +63 -55
  317. package/.framework/src/pages/admin/APIKeysPage.tsx +44 -42
  318. package/.framework/src/pages/admin/AlertsConfigPage.tsx +117 -94
  319. package/.framework/src/pages/admin/AppDetailPage.tsx +48 -42
  320. package/.framework/src/pages/admin/AppsPage.tsx +45 -42
  321. package/.framework/src/pages/admin/DeliveriesPage.tsx +49 -17
  322. package/.framework/src/pages/admin/DesignedPage.tsx +31 -30
  323. package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +58 -0
  324. package/.framework/src/pages/admin/EmbeddingsPage.tsx +49 -0
  325. package/.framework/src/pages/admin/ExtendedShadcnTestPage.tsx +31 -29
  326. package/.framework/src/pages/admin/IncrementalShadcnTestPage.tsx +46 -29
  327. package/.framework/src/pages/admin/IntegratedDashboard.tsx +32 -29
  328. package/.framework/src/pages/admin/IntegrationDetailPage.tsx +68 -55
  329. package/.framework/src/pages/admin/IntegrationsPage.tsx +45 -42
  330. package/.framework/src/pages/admin/InvitesPage.tsx +60 -44
  331. package/.framework/src/pages/admin/LogsPage.tsx +87 -137
  332. package/.framework/src/pages/admin/MinimalShadcnTestPage.tsx +31 -29
  333. package/.framework/src/pages/admin/ObservabilityDashboard.tsx +150 -89
  334. package/.framework/src/pages/admin/PipelineDetailPage.tsx +77 -104
  335. package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +44 -43
  336. package/.framework/src/pages/admin/PipelinesPage.tsx +59 -145
  337. package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +53 -42
  338. package/.framework/src/pages/admin/PromptConfigsPage.tsx +47 -117
  339. package/.framework/src/pages/admin/ProperlyDesignedPage.tsx +31 -44
  340. package/.framework/src/pages/admin/RoleDetailPage.tsx +49 -92
  341. package/.framework/src/pages/admin/RolesPage.tsx +47 -164
  342. package/.framework/src/pages/admin/SecurityPage.tsx +310 -0
  343. package/.framework/src/pages/admin/SelectTestPage.tsx +30 -32
  344. package/.framework/src/pages/admin/ShadcnTestPage.tsx +37 -44
  345. package/.framework/src/pages/admin/SimpleDashboard.tsx +32 -44
  346. package/.framework/src/pages/admin/TestRunDetailPage.tsx +97 -44
  347. package/.framework/src/pages/admin/TestingDashboard.tsx +126 -71
  348. package/.framework/src/pages/admin/TimerDetailPage.tsx +67 -55
  349. package/.framework/src/pages/admin/TimersPage.tsx +44 -42
  350. package/.framework/src/pages/admin/TriggerDetailPage.tsx +47 -42
  351. package/.framework/src/pages/admin/TriggersPage.tsx +58 -57
  352. package/.framework/src/pages/admin/TypeDetailPage.tsx +91 -55
  353. package/.framework/src/pages/admin/TypesPage.tsx +45 -42
  354. package/.framework/src/pages/admin/cil/CILAgentPage.tsx +258 -128
  355. package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +127 -123
  356. package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +308 -235
  357. package/.framework/src/pages/admin/cil/CILFileListPage.tsx +159 -110
  358. package/.framework/src/pages/admin/cil/CILFileSelectPage.tsx +432 -0
  359. package/.framework/src/pages/admin/cil/CILGraphPage.tsx +200 -178
  360. package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +286 -239
  361. package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +211 -90
  362. package/.framework/src/pages/admin/cil/CILSearchPage.tsx +156 -143
  363. package/.framework/src/pages/admin/cil/CILSuggestionsPage.tsx +379 -0
  364. package/.framework/src/pages/admin/cil/CILVersionsPage.tsx +337 -0
  365. package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +219 -214
  366. package/.framework/src/pages/admin/kb/KBAdminPage.tsx +64 -0
  367. package/.framework/src/pages/auth/AuthConfirmPage.tsx +135 -65
  368. package/.framework/src/pages/auth/ForgotPasswordPage.tsx +39 -31
  369. package/.framework/src/pages/auth/LoginPage.tsx +38 -32
  370. package/.framework/src/pages/auth/PendingApprovalPage.tsx +31 -28
  371. package/.framework/src/pages/auth/RegisterPage.tsx +429 -155
  372. package/.framework/src/pages/spine-framework/APIPage.tsx +33 -30
  373. package/.framework/src/pages/spine-framework/CLIPage.tsx +32 -30
  374. package/.framework/src/types/auth.ts +101 -93
  375. package/.framework/src/types/types.ts +283 -262
  376. package/.framework/templates/project.json +87 -0
  377. package/.framework/templates/spine.config.neutral.json +7 -0
  378. package/.framework/tests/api/api-surface.test.ts +381 -0
  379. package/.framework/tests/fixtures/seed.ts +427 -0
  380. package/.framework/tests/fixtures/teardown.ts +228 -0
  381. package/.framework/tests/reporter.ts +351 -0
  382. package/.framework/tests/setup.ts +135 -0
  383. package/.framework/tests/ui/ui-sweep.ts +278 -0
  384. package/.framework/tests/unit/admin-data-logger.test.ts +310 -0
  385. package/.framework/tests/unit/agent-runner.test.ts +2389 -0
  386. package/.framework/tests/unit/ai-interactions.test.ts +81 -0
  387. package/.framework/tests/unit/app-manifest.test.ts +359 -0
  388. package/.framework/tests/unit/assembly-scripts.test.ts +172 -0
  389. package/.framework/tests/unit/cil-determinism.test.ts +85 -0
  390. package/.framework/tests/unit/core-isolation.test.ts +153 -0
  391. package/.framework/tests/unit/funnel-signals.test.ts +135 -0
  392. package/.framework/tests/unit/install-app.test.ts +1759 -0
  393. package/.framework/tests/unit/invites-security.test.ts +572 -0
  394. package/.framework/tests/unit/kb-ingest-lease.test.ts +287 -0
  395. package/.framework/tests/unit/kb-search.test.ts +119 -0
  396. package/.framework/tests/unit/migrate.test.ts +509 -0
  397. package/.framework/tests/unit/notify.test.ts +136 -0
  398. package/.framework/tests/unit/permissions.test.ts +269 -0
  399. package/.framework/tests/unit/pipeline-runner.test.ts +755 -0
  400. package/.framework/tests/unit/principal-cache.test.ts +138 -0
  401. package/.framework/tests/unit/retrieval-golden.test.ts +520 -0
  402. package/.framework/tests/unit/runtime-isolation.test.ts +642 -0
  403. package/.framework/tests/unit/schema-utils.test.ts +225 -0
  404. package/.framework/tests/unit/trigger-engine.test.ts +513 -0
  405. package/.framework/tests/unit/validate-app.test.ts +527 -0
  406. package/README.md +34 -3
  407. package/bin/spine-framework.cjs +1 -0
  408. package/config/tailwind.config.cjs +7 -0
  409. package/config/tsconfig.functions.json +42 -0
  410. package/config/tsconfig.json +37 -9
  411. package/config/vite.config.ts +31 -2
  412. package/netlify.toml +31 -3
  413. package/package.json +46 -29
  414. package/scripts/assemble-frontend.sh +127 -44
  415. package/scripts/assemble-functions.sh +127 -29
  416. package/scripts/boundary-check.sh +6 -2
  417. package/scripts/build-manifest.sh +24 -68
  418. package/scripts/checksum-manifest.mts +345 -0
  419. package/scripts/dev-clean.sh +86 -0
  420. package/scripts/dev-netlify.sh +49 -0
  421. package/scripts/netlify-dev-wrapper.sh +2 -0
  422. package/scripts/verify-install.ts +358 -0
  423. package/scripts/verify-integrity.sh +21 -57
  424. package/scripts/verify-manifest.mts +305 -0
  425. package/templates/project/gitignore +45 -0
  426. package/{config → templates/project}/package-lock.json +22335 -9366
  427. package/vitest.config.ts +46 -0
  428. package/.framework/public/spine.config.json +0 -91
  429. package/config/deno.lock +0 -108
  430. package/config/tsconfig.node.tsbuildinfo +0 -1
  431. package/config/tsconfig.tsbuildinfo +0 -1
  432. package/config/vite.config.d.ts +0 -2
  433. package/custom/.devin/AGENT.md +0 -357
  434. package/custom/.devin/skills/install-app/SKILL.md +0 -39
  435. package/custom/.devin/skills/spine-workbench-discovery/SKILL.md +0 -39
  436. package/custom/.devin/skills/spine-workbench-idea/SKILL.md +0 -36
  437. package/custom/.devin/skills/spine-workbench-implement/SKILL.md +0 -46
  438. package/custom/.devin/skills/spine-workbench-initiative/SKILL.md +0 -28
  439. package/custom/.devin/skills/spine-workbench-plan/SKILL.md +0 -38
  440. package/custom/.devin/workflows/app-install.yaml +0 -83
  441. package/custom/README.md +0 -79
  442. package/docs/README.md +0 -15
  443. package/scripts/app-install-cli.ts +0 -286
  444. package/scripts/check-core-integrity.sh +0 -82
  445. package/scripts/ingest-chunks.cjs +0 -202
  446. package/scripts/kb-chunk-parser.cjs +0 -312
  447. package/scripts/kb-chunk-parser.ts +0 -330
  448. package/scripts/kb-ingest-docs.ts +0 -203
  449. package/scripts/load-test-app-install.ts +0 -484
  450. package/scripts/test-login.js +0 -33
  451. package/spine.config.json +0 -91
@@ -1,3 +1,26 @@
1
+ /**
2
+ * @module invites
3
+ * @audience internal
4
+ * @layer app-specific
5
+ * @stability stable
6
+ *
7
+ * This module handles invite-related functionalities within the application, integrating with various services to manage invites effectively. It serves as an API handler for processing invite requests and includes middleware for authentication and rate limiting. INVARIANT: All invite requests must be authenticated before processing.
8
+ *
9
+ * @seeAlso middleware.ts (provides authentication and rate limiting)
10
+ * @seeAlso db.ts (database interactions)
11
+ * @seeAlso audit.ts (logging and auditing actions)
12
+ *
13
+ * @cil-generated
14
+ */
15
+ // CIL[BEGIN stable_id="~/.framework/functions/invites.ts:module_header:module_header" type="module_header" identity="module_header" parent="~/.framework/functions/invites.ts"]
16
+ // CIL[DOC-MACRO purpose="This module handles invite-related functionalities within the application, integrating with various services to manage invites effectively."]
17
+ // CIL[DOC-RESPONSIBILITY responsibility="This module handles invite-related functionalities within the application, integrating with various services to manage invites effectively. It serves as an API handler for processing invite requests and includes middleware for authentication and rate limiting. INVARIANT: All invite requests must be authenticated before processing."]
18
+ // CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files="middleware.ts (provides authentication and rate limiting),db.ts (database interactions),audit.ts (logging and auditing actions)"]
19
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
20
+ // CIL[DOC-MICRO logic_summary="This module handles invite-related functionalities within the application, integrating with various services to manage invites effectively. It serves as an API handler for processing invite requests and includes middleware for authentication and rate limiting. INVARIANT: All invite requests must be authenticated before processing." notable_decisions="none"]
21
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
22
+ // CIL[DOC-STABILITY stability="stable"]
23
+ // CIL[DOC-AUDIENCE audience="internal"]
1
24
 
2
25
 
3
26
 
@@ -14,84 +37,230 @@
14
37
 
15
38
 
16
39
 
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"]
32
40
  import { createHandler, createPublicHandler } from './_shared/middleware'
33
41
  import { adminDb } from './_shared/db'
34
42
  import { emitLog } from './_shared/audit'
43
+ import { checkRateLimit } from './_shared/rate-limit'
44
+ import { getClientGeo } from './_shared/geo'
45
+ import { checkRegistrationBlocklist, recordRegistrationGeo } from './_shared/security'
46
+ import { execute as executeOutbound } from './integration-outbound'
35
47
  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."]
48
+ // CIL[END stable_id="~/.framework/functions/invites.ts:module_header:module_header"]
49
+ // CIL[DOC-VECTOR type="context" content="purpose='This module handles invite-related functionalities within the application, integrating with various services to manage invites effectively.'; No verified external callers" status="ready"]
50
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
51
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
52
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
49
53
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
50
54
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
55
+ // CIL[DOC-MACRO purpose="TOKEN_LENGTH defines the length of the token used in the invite process, ensuring consistency across the application."]
56
+ // CIL[DOC-RESPONSIBILITY responsibility="TOKEN_LENGTH defines the length of the token used in the invite process, ensuring consistency across the application. This constant exists to centralize the token length configuration for easier maintenance and updates."]
57
+ // CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
58
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
59
+ // CIL[DOC-MICRO logic_summary="TOKEN_LENGTH defines the length of the token used in the invite process, ensuring consistency across the application. This constant exists to centralize the token length configuration for easier maintenance and updates." notable_decisions="none"]
60
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
61
+ // CIL[DOC-STABILITY stability="stable"]
62
+ /**
63
+ * TOKEN_LENGTH defines the length of the token used in the invite process, ensuring consistency across the application. This constant exists to centralize the token length configuration for easier maintenance and updates.
64
+ *
65
+ * @sideEffects none
66
+ * @stability stable
67
+ * @cil-generated
68
+ */
69
+ // CIL[BEGIN stable_id="~/.framework/functions/invites.ts:token_length:constant" type="constant" identity="TOKEN_LENGTH" parent="~/.framework/functions/invites.ts"]
70
+ // CIL[DOC-MACRO purpose="TOKEN_LENGTH constant"]
71
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the TOKEN_LENGTH constant."]
72
+ // CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
73
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
74
+ // CIL[DOC-MICRO logic_summary="Implements the TOKEN_LENGTH constant." notable_decisions="[]"]
75
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
76
+ // CIL[DOC-STABILITY stability="stable"]
51
77
  const TOKEN_LENGTH = 16
78
+ // CIL[END stable_id="~/.framework/functions/invites.ts:token_length:constant"]
79
+ // CIL[DOC-MACRO purpose="TOKEN_LENGTH constant"]
80
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the TOKEN_LENGTH constant."]
81
+ // CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
82
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
83
+ // CIL[DOC-MICRO logic_summary="Implements the TOKEN_LENGTH constant." notable_decisions="[]"]
84
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
85
+ // CIL[DOC-STABILITY stability="stable"]
86
+ // CIL[DOC-VECTOR type="context" content="purpose='TOKEN_LENGTH defines the length of the token used in the invite process, ensuring consistency across the application.'; No verified external callers" status="ready"]
87
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
88
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
89
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
90
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
91
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
92
+ // CIL[DOC-MACRO purpose="MAX_ATTEMPTS defines the maximum number of attempts allowed for a specific operation in the invites module."]
93
+ // CIL[DOC-RESPONSIBILITY responsibility="MAX_ATTEMPTS defines the maximum number of attempts allowed for a specific operation in the invites module. This constant helps to enforce limits and prevent excessive retries."]
94
+ // CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
95
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
96
+ // CIL[DOC-MICRO logic_summary="MAX_ATTEMPTS defines the maximum number of attempts allowed for a specific operation in the invites module. This constant helps to enforce limits and prevent excessive retries." notable_decisions="none"]
97
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
98
+ // CIL[DOC-STABILITY stability="stable"]
99
+ /**
100
+ * MAX_ATTEMPTS defines the maximum number of attempts allowed for a specific operation in the invites module. This constant helps to enforce limits and prevent excessive retries.
101
+ *
102
+ * @sideEffects none
103
+ * @stability stable
104
+ * @cil-generated
105
+ */
106
+ // CIL[BEGIN stable_id="~/.framework/functions/invites.ts:max_attempts:constant" type="constant" identity="MAX_ATTEMPTS" parent="~/.framework/functions/invites.ts"]
107
+ // CIL[DOC-MACRO purpose="MAX_ATTEMPTS constant"]
108
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the MAX_ATTEMPTS constant."]
109
+ // CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
110
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
111
+ // CIL[DOC-MICRO logic_summary="Implements the MAX_ATTEMPTS constant." notable_decisions="[]"]
112
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
113
+ // CIL[DOC-STABILITY stability="stable"]
52
114
  const MAX_ATTEMPTS = 5
115
+ // CIL[END stable_id="~/.framework/functions/invites.ts:max_attempts:constant"]
116
+ // CIL[DOC-MACRO purpose="MAX_ATTEMPTS constant"]
117
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the MAX_ATTEMPTS constant."]
118
+ // CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
119
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
120
+ // CIL[DOC-MICRO logic_summary="Implements the MAX_ATTEMPTS constant." notable_decisions="[]"]
121
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
122
+ // CIL[DOC-STABILITY stability="stable"]
123
+ // CIL[DOC-VECTOR type="context" content="purpose='MAX_ATTEMPTS defines the maximum number of attempts allowed for a specific operation in the invites module.'; No verified external callers" status="ready"]
124
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
125
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
126
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
127
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
128
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
129
+ // CIL[DOC-MACRO purpose="INVITE_EXPIRY_DAYS defines the duration in days that an invite remains valid."]
130
+ // CIL[DOC-RESPONSIBILITY responsibility="INVITE_EXPIRY_DAYS defines the duration in days that an invite remains valid. This constant is used to manage the expiration of invites in the application."]
131
+ // CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
132
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
133
+ // CIL[DOC-MICRO logic_summary="INVITE_EXPIRY_DAYS defines the duration in days that an invite remains valid. This constant is used to manage the expiration of invites in the application." notable_decisions="none"]
134
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
135
+ // CIL[DOC-STABILITY stability="stable"]
136
+ /**
137
+ * INVITE_EXPIRY_DAYS defines the duration in days that an invite remains valid. This constant is used to manage the expiration of invites in the application.
138
+ *
139
+ * @sideEffects none
140
+ * @stability stable
141
+ * @cil-generated
142
+ */
143
+ // CIL[BEGIN stable_id="~/.framework/functions/invites.ts:invite_expiry_days:constant" type="constant" identity="INVITE_EXPIRY_DAYS" parent="~/.framework/functions/invites.ts"]
144
+ // CIL[DOC-MACRO purpose="INVITE_EXPIRY_DAYS constant"]
145
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the INVITE_EXPIRY_DAYS constant."]
146
+ // CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
147
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
148
+ // CIL[DOC-MICRO logic_summary="Implements the INVITE_EXPIRY_DAYS constant." notable_decisions="[]"]
149
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
150
+ // CIL[DOC-STABILITY stability="stable"]
53
151
  const INVITE_EXPIRY_DAYS = 7
152
+ // CIL[END stable_id="~/.framework/functions/invites.ts:invite_expiry_days:constant"]
153
+ // CIL[DOC-MACRO purpose="INVITE_EXPIRY_DAYS constant"]
154
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the INVITE_EXPIRY_DAYS constant."]
155
+ // CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
156
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
157
+ // CIL[DOC-MICRO logic_summary="Implements the INVITE_EXPIRY_DAYS constant." notable_decisions="[]"]
158
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
159
+ // CIL[DOC-STABILITY stability="stable"]
160
+ // CIL[DOC-VECTOR type="context" content="purpose='INVITE_EXPIRY_DAYS defines the duration in days that an invite remains valid.'; No verified external callers" status="ready"]
161
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
162
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
163
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
164
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
165
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
54
166
 
167
+ // CIL[DOC-MACRO purpose="PERSONAL_DOMAINS is a set of common email domains used to identify personal email addresses."]
168
+ // CIL[DOC-RESPONSIBILITY responsibility="PERSONAL_DOMAINS is a set of common email domains used to identify personal email addresses. It exists to facilitate validation or filtering of email inputs in the module."]
169
+ // CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
170
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
171
+ // CIL[DOC-MICRO logic_summary="PERSONAL_DOMAINS is a set of common email domains used to identify personal email addresses. It exists to facilitate validation or filtering of email inputs in the module." notable_decisions="none"]
172
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
173
+ // CIL[DOC-STABILITY stability="stable"]
174
+ /**
175
+ * PERSONAL_DOMAINS is a set of common email domains used to identify personal email addresses. It exists to facilitate validation or filtering of email inputs in the module.
176
+ *
177
+ * @sideEffects none
178
+ * @stability stable
179
+ * @cil-generated
180
+ */
181
+ // CIL[BEGIN stable_id="~/.framework/functions/invites.ts:personal_domains:constant" type="constant" identity="PERSONAL_DOMAINS" parent="~/.framework/functions/invites.ts"]
182
+ // CIL[DOC-MACRO purpose="PERSONAL_DOMAINS constant"]
183
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the PERSONAL_DOMAINS constant."]
184
+ // CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
185
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
186
+ // CIL[DOC-MICRO logic_summary="Implements the PERSONAL_DOMAINS constant." notable_decisions="[]"]
187
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
188
+ // CIL[DOC-STABILITY stability="stable"]
55
189
  const PERSONAL_DOMAINS = new Set([
56
190
  'gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com', 'icloud.com',
57
191
  'aol.com', 'protonmail.com', 'me.com', 'mac.com', 'live.com',
58
192
  'msn.com', 'ymail.com', 'googlemail.com', 'fastmail.com',
59
193
  ])
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."]
194
+ // CIL[END stable_id="~/.framework/functions/invites.ts:personal_domains:constant"]
195
+ // CIL[DOC-VECTOR type="context" content="purpose='PERSONAL_DOMAINS is a set of common email domains used to identify personal email addresses.'; No verified external callers" status="ready"]
196
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
197
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
198
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
73
199
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
74
200
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
201
+ // CIL[DOC-MACRO purpose="Generates a random token string composed of alphanumeric characters."]
202
+ // CIL[DOC-RESPONSIBILITY responsibility="Generates a random token string composed of alphanumeric characters. This function is used to create unique identifiers for invites."]
203
+ // CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
204
+ // CIL[DOC-CONTRACT inputs="none" outputs="string" errors="none" side_effects="none dependencies="crypto.getRandomValues"]
205
+ // CIL[DOC-MICRO logic_summary="Generates a random token string composed of alphanumeric characters. This function is used to create unique identifiers for invites." notable_decisions="none"]
206
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
207
+ // CIL[DOC-STABILITY stability="stable"]
208
+ /**
209
+ * Generates a random token string composed of alphanumeric characters. This function is used to create unique identifiers for invites.
210
+ *
211
+ * @outputSpec string
212
+ * @sideEffects none
213
+ * @stability stable
214
+ * @calls crypto.getRandomValues
215
+ * @cil-generated
216
+ */
217
+ // CIL[BEGIN stable_id="~/.framework/functions/invites.ts:generatetoken:function" type="function" identity="generateToken" parent="~/.framework/functions/invites.ts"]
218
+ // CIL[DOC-MACRO purpose="generateToken function"]
219
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the generateToken function."]
220
+ // CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
221
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
222
+ // CIL[DOC-MICRO logic_summary="Implements the generateToken function." notable_decisions="[]"]
223
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
224
+ // CIL[DOC-STABILITY stability="stable"]
75
225
  function generateToken(): string {
76
226
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
77
227
  const bytes = crypto.getRandomValues(new Uint8Array(TOKEN_LENGTH))
78
228
  return Array.from(bytes).map(b => chars[b % chars.length]).join('')
79
229
  }
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."]
230
+ // CIL[END stable_id="~/.framework/functions/invites.ts:generatetoken:function"]
231
+ // CIL[DOC-VECTOR type="context" content="purpose='Generates a random token string composed of alphanumeric characters.'; No verified external callers" status="ready"]
232
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
233
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
234
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
93
235
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
94
236
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
237
+ // CIL[DOC-MACRO purpose="Slug generator for account names."]
238
+ // CIL[DOC-RESPONSIBILITY responsibility="Generates a URL-friendly slug from a given account name by normalizing and truncating it. This function ensures that the slug is unique by appending a random hash."]
239
+ // CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
240
+ // CIL[DOC-CONTRACT inputs="name: string (required) — the account name to be converted into a slug." outputs="string — a URL-friendly slug with a random hash appended." errors="none" side_effects="none"]
241
+ // CIL[DOC-MICRO logic_summary="Normalizes the name by converting it to lowercase and replacing non-alphanumeric characters with hyphens." notable_decisions="none"]
242
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
243
+ // CIL[DOC-STABILITY stability="stable"]
244
+ /**
245
+ * Generates a URL-friendly slug from a given account name by normalizing and truncating it. This function ensures that the slug is unique by appending a random hash.
246
+ *
247
+ * @macro Slug generator for account names.
248
+ * @micro Normalizes the name by converting it to lowercase and replacing non-alphanumeric characters with hyphens.
249
+ * @inputSpec
250
+ * name: string (required) — the account name to be converted into a slug.
251
+ * @outputSpec string — a URL-friendly slug with a random hash appended.
252
+ * @sideEffects none
253
+ * @stability stable
254
+ * @cil-generated
255
+ */
256
+ // CIL[BEGIN stable_id="~/.framework/functions/invites.ts:generateaccountslug:function" type="function" identity="generateAccountSlug" parent="~/.framework/functions/invites.ts"]
257
+ // CIL[DOC-MACRO purpose="generateAccountSlug function"]
258
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the generateAccountSlug function."]
259
+ // CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
260
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
261
+ // CIL[DOC-MICRO logic_summary="Implements the generateAccountSlug function." notable_decisions="[]"]
262
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
263
+ // CIL[DOC-STABILITY stability="stable"]
95
264
  function generateAccountSlug(name: string): string {
96
265
  const base = name
97
266
  .toLowerCase()
@@ -101,42 +270,195 @@ function generateAccountSlug(name: string): string {
101
270
  const hash = Math.random().toString(36).slice(2, 8)
102
271
  return `${base}-${hash}`
103
272
  }
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."]
273
+ // CIL[END stable_id="~/.framework/functions/invites.ts:generateaccountslug:function"]
274
+ // CIL[DOC-VECTOR type="context" content="purpose='Generates a URL-friendly slug from a given account name by normalizing and truncating it.'; No verified external callers" status="ready"]
275
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
276
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
277
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
117
278
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
118
279
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
280
+ // CIL[DOC-MACRO purpose="Role validation function for admin permissions."]
281
+ // CIL[DOC-RESPONSIBILITY responsibility="Determines if a given role slug corresponds to an admin role. This function is used to validate user permissions based on their role."]
282
+ // CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
283
+ // CIL[DOC-CONTRACT inputs="roleSlug: string | undefined (optional) — the role identifier to check for admin status." outputs="boolean — true if the role is an admin role, false otherwise." errors="none" side_effects="none"]
284
+ // CIL[DOC-MICRO logic_summary="Checks if the role slug is defined and matches specific admin criteria." notable_decisions="none"]
285
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
286
+ // CIL[DOC-STABILITY stability="stable"]
287
+ /**
288
+ * Determines if a given role slug corresponds to an admin role. This function is used to validate user permissions based on their role.
289
+ *
290
+ * @macro Role validation function for admin permissions.
291
+ * @micro Checks if the role slug is defined and matches specific admin criteria.
292
+ * @inputSpec
293
+ * roleSlug: string | undefined (optional) — the role identifier to check for admin status.
294
+ * @outputSpec boolean — true if the role is an admin role, false otherwise.
295
+ * @sideEffects none
296
+ * @stability stable
297
+ * @cil-generated
298
+ */
299
+ // CIL[BEGIN stable_id="~/.framework/functions/invites.ts:isadminrole:function" type="function" identity="isAdminRole" parent="~/.framework/functions/invites.ts"]
300
+ // CIL[DOC-MACRO purpose="isAdminRole function"]
301
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the isAdminRole function."]
302
+ // CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
303
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
304
+ // CIL[DOC-MICRO logic_summary="Implements the isAdminRole function." notable_decisions="[]"]
305
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
306
+ // CIL[DOC-STABILITY stability="stable"]
119
307
  function isAdminRole(roleSlug: string | undefined): boolean {
120
308
  return !!roleSlug && (
121
309
  roleSlug === 'system_admin' ||
122
310
  roleSlug.endsWith('_admin')
123
311
  )
124
312
  }
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"]
313
+ // CIL[END stable_id="~/.framework/functions/invites.ts:isadminrole:function"]
314
+
315
+ // CIL[BEGIN stable_id="~/.framework/functions/invites.ts:requireaccountadmin:function" type="function" identity="requireAccountAdmin" parent="~/.framework/functions/invites.ts"]
316
+ // CIL[DOC-MACRO purpose="Account-scoped admin gate for invite and member mutations"]
317
+ // CIL[DOC-RESPONSIBILITY responsibility="Require system_admin, or an *_admin role on the principal bound to the SAME account being mutated — closes cross-account invite/member management."]
318
+ // CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=".framework/functions/_shared/middleware.ts"]
319
+ // CIL[DOC-CONTRACT inputs="ctx (server-resolved principal), accountId" outputs="void" errors="Error — Authentication required; Error — Insufficient permissions — *_admin role required for this account" side_effects="none"]
320
+ // CIL[DOC-MICRO logic_summary="Anonymous callers throw; system principals (permissions 'system' or roles 'system_admin') pass; otherwise the principal must carry an *_admin role AND principal.accountId must equal the target account_id" notable_decisions="principal.accountId is server-resolved from the people row, so it is trustworthy — no client input feeds the check"]
321
+ // CIL[DOC-CONFIDENCE pass="first" score="0.85" flags="none"]
322
+ // CIL[DOC-STABILITY stability="stable"]
323
+ // CIL[DOC-NOTE type="security" content="Added in the registration/invitation hardening batch — previously invite mutations only required a non-anonymous principal, so any authenticated user could revoke/approve/block invites in any account."]
324
+ function requireAccountAdmin(ctx: any, accountId: string | null | undefined): void {
325
+ const principal = ctx?.principal
326
+ if (!principal || principal.id === 'anonymous') {
327
+ throw new Error('Authentication required')
328
+ }
329
+ if (
330
+ principal.permissions?.includes('system') ||
331
+ (principal.roles || []).includes('system_admin')
332
+ ) {
333
+ return
334
+ }
335
+ const isAdmin = (principal.roles || []).some((r: string) => isAdminRole(r))
336
+ if (isAdmin && accountId && principal.accountId === accountId) {
337
+ return
338
+ }
339
+ throw new Error('Insufficient permissions — *_admin role required for this account')
340
+ }
341
+ // CIL[END stable_id="~/.framework/functions/invites.ts:requireaccountadmin:function"]
342
+
343
+ // CIL[BEGIN stable_id="~/.framework/functions/invites.ts:approleallowlist:function" type="function" identity="appRoleAllowlist" parent="~/.framework/functions/invites.ts"]
344
+ // CIL[DOC-MACRO purpose="Extract the declared invitable-role allowlist from an app config"]
345
+ // CIL[DOC-RESPONSIBILITY responsibility="Return config.invitation.allowed_roles when declared, else config.registration.roles, else null (no declared allowlist)."]
346
+ // CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
347
+ // CIL[DOC-CONTRACT inputs="appConfig (apps.config jsonb)" outputs="string[] | null" errors="none" side_effects="none"]
348
+ // CIL[DOC-MICRO logic_summary="Reads invitation.allowed_roles first, falls back to registration.roles, filters non-strings, returns null when undeclared or empty" notable_decisions="null means no allowlist declared — callers still enforce the system_admin/is_system rejection"]
349
+ // CIL[DOC-CONFIDENCE pass="first" score="0.85" flags="none"]
350
+ // CIL[DOC-STABILITY stability="stable"]
351
+ function appRoleAllowlist(appConfig: any): string[] | null {
352
+ const declared = appConfig?.invitation?.allowed_roles ?? appConfig?.registration?.roles
353
+ if (!Array.isArray(declared)) return null
354
+ const slugs = declared.filter((r: any) => typeof r === 'string')
355
+ return slugs.length ? slugs : null
356
+ }
357
+ // CIL[END stable_id="~/.framework/functions/invites.ts:approleallowlist:function"]
358
+
359
+ // CIL[BEGIN stable_id="~/.framework/functions/invites.ts:resolveapproleallowlist:function" type="function" identity="resolveAppRoleAllowlist" parent="~/.framework/functions/invites.ts"]
360
+ // CIL[DOC-MACRO purpose="Resolve the calling app's invitable-role allowlist"]
361
+ // CIL[DOC-RESPONSIBILITY responsibility="Look up apps.config by the request's app context (ctx.appId header, else body.app slug) and return its declared role allowlist."]
362
+ // CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=".framework/functions/_shared/db.ts"]
363
+ // CIL[DOC-CONTRACT inputs="ctx, body" outputs="Promise<string[] | null>" errors="none (unknown app resolves to null)" side_effects="one apps read"]
364
+ // CIL[DOC-MICRO logic_summary="ctx.appId (x-app-id header) wins over body.app; queries apps by slug; delegates to appRoleAllowlist" notable_decisions="an unresolved app context yields null — the non-system checks still apply, matching the pre-hardening default of no app allowlist"]
365
+ // CIL[DOC-CONFIDENCE pass="first" score="0.85" flags="none"]
366
+ // CIL[DOC-STABILITY stability="stable"]
367
+ async function resolveAppRoleAllowlist(ctx: any, body: any): Promise<string[] | null> {
368
+ const appSlug = ctx?.appId || body?.app
369
+ if (!appSlug || typeof appSlug !== 'string') return null
370
+ const { data: appRow } = await adminDb
371
+ .from('apps')
372
+ .select('config')
373
+ .eq('slug', appSlug)
374
+ .maybeSingle()
375
+ return appRoleAllowlist(appRow?.config)
376
+ }
377
+ // CIL[END stable_id="~/.framework/functions/invites.ts:resolveapproleallowlist:function"]
378
+
379
+ // CIL[BEGIN stable_id="~/.framework/functions/invites.ts:roleallowlistforaccount:function" type="function" identity="roleAllowlistForAccount" parent="~/.framework/functions/invites.ts"]
380
+ // CIL[DOC-MACRO purpose="Union of invitable-role allowlists declared by apps owned by an account"]
381
+ // CIL[DOC-RESPONSIBILITY responsibility="Collect config.invitation.allowed_roles / config.registration.roles from every app whose owner_account_id is the account, returning the merged set or null when none is declared."]
382
+ // CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=".framework/functions/_shared/db.ts"]
383
+ // CIL[DOC-CONTRACT inputs="accountId" outputs="Promise<string[] | null>" errors="none" side_effects="one apps read"]
384
+ // CIL[DOC-MICRO logic_summary="Queries apps by owner_account_id and unions each app's declared allowlist" notable_decisions="used by updateMember to scope role_slug to the member's account apps"]
385
+ // CIL[DOC-CONFIDENCE pass="first" score="0.85" flags="none"]
386
+ // CIL[DOC-STABILITY stability="stable"]
387
+ async function roleAllowlistForAccount(accountId: string | null | undefined): Promise<string[] | null> {
388
+ if (!accountId) return null
389
+ const { data: appRows } = await adminDb
390
+ .from('apps')
391
+ .select('config')
392
+ .eq('owner_account_id', accountId)
393
+ const merged = new Set<string>()
394
+ for (const row of appRows || []) {
395
+ for (const slug of appRoleAllowlist(row?.config) || []) merged.add(slug)
396
+ }
397
+ return merged.size ? Array.from(merged) : null
398
+ }
399
+ // CIL[END stable_id="~/.framework/functions/invites.ts:roleallowlistforaccount:function"]
400
+
401
+ // CIL[BEGIN stable_id="~/.framework/functions/invites.ts:resolveinvitebaseurl:function" type="function" identity="resolveInviteBaseUrl" parent="~/.framework/functions/invites.ts"]
402
+ // CIL[DOC-MACRO purpose="Validate a client-supplied invite base URL against the origin allowlist"]
403
+ // CIL[DOC-RESPONSIBILITY responsibility="Accept base_url only when its origin matches APP_BASE_URL, an ALLOWED_ORIGINS csv entry, or localhost/127.0.0.1; otherwise fall back to APP_BASE_URL or http://localhost:8888."]
404
+ // CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
405
+ // CIL[DOC-CONTRACT inputs="baseUrl: string | undefined" outputs="string — validated origin or fallback base URL" errors="none (malformed input falls back)" side_effects="none"]
406
+ // CIL[DOC-MICRO logic_summary="Parses the origin; localhost/127.0.0.1 always allowed for dev; otherwise the origin must be in the APP_BASE_URL + ALLOWED_ORIGINS allowlist; any miss or parse failure returns the fallback" notable_decisions="drops the 'https://yourapp.com' placeholder; never returns the attacker-supplied URL when it is not allowlisted — invite links can no longer be pointed at phishing origins"]
407
+ // CIL[DOC-CONFIDENCE pass="first" score="0.85" flags="none"]
133
408
  // 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."]
409
+ // CIL[DOC-NOTE type="security" content="base_url is client-controlled; without this check an admin tricked into creating an invite could hand out a token link on an arbitrary origin."]
410
+ function resolveInviteBaseUrl(baseUrl: string | undefined): string {
411
+ const fallback = process.env.APP_BASE_URL || 'http://localhost:8888'
412
+ if (!baseUrl) return fallback
413
+ try {
414
+ const origin = new URL(baseUrl).origin
415
+ const hostname = new URL(origin).hostname
416
+ if (hostname === 'localhost' || hostname === '127.0.0.1') return origin
417
+ const allowed = new Set<string>()
418
+ if (process.env.APP_BASE_URL) {
419
+ try { allowed.add(new URL(process.env.APP_BASE_URL).origin) } catch { /* malformed env value */ }
420
+ }
421
+ for (const entry of (process.env.ALLOWED_ORIGINS || '').split(',')) {
422
+ const trimmed = entry.trim()
423
+ if (!trimmed) continue
424
+ try { allowed.add(new URL(trimmed).origin) } catch { /* skip malformed entry */ }
425
+ }
426
+ return allowed.has(origin) ? origin : fallback
427
+ } catch {
428
+ return fallback
429
+ }
430
+ }
431
+ // CIL[END stable_id="~/.framework/functions/invites.ts:resolveinvitebaseurl:function"]
432
+ // CIL[DOC-VECTOR type="context" content="purpose='Determines if a given role slug corresponds to an admin role.'; No verified external callers" status="ready"]
433
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
434
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
435
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
138
436
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
139
437
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
438
+ // CIL[DOC-MACRO purpose="This constant checks if there are any users in the database and determines if the current user is the first one."]
439
+ // CIL[DOC-RESPONSIBILITY responsibility="This constant checks if there are any users in the database and determines if the current user is the first one. It exists to facilitate user onboarding by identifying if the application is being used for the first time."]
440
+ // CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
441
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="Error — failed to check first user: error message" side_effects="none dependencies="adminDb.from('people')"]
442
+ // CIL[DOC-MICRO logic_summary="This constant checks if there are any users in the database and determines if the current user is the first one. It exists to facilitate user onboarding by identifying if the application is being used for the first time." notable_decisions="none"]
443
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
444
+ // CIL[DOC-STABILITY stability="stable"]
445
+ /**
446
+ * This constant checks if there are any users in the database and determines if the current user is the first one. It exists to facilitate user onboarding by identifying if the application is being used for the first time.
447
+ *
448
+ * @throws Error — failed to check first user: error message
449
+ * @sideEffects none
450
+ * @stability stable
451
+ * @calls adminDb.from('people')
452
+ * @cil-generated
453
+ */
454
+ // CIL[BEGIN stable_id="~/.framework/functions/invites.ts:checkfirstuser:constant" type="constant" identity="checkFirstUser" parent="~/.framework/functions/invites.ts"]
455
+ // CIL[DOC-MACRO purpose="checkFirstUser constant"]
456
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the checkFirstUser constant."]
457
+ // CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
458
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
459
+ // CIL[DOC-MICRO logic_summary="Implements the checkFirstUser constant." notable_decisions="[]"]
460
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
461
+ // CIL[DOC-STABILITY stability="stable"]
140
462
  export const checkFirstUser = createPublicHandler(async (_ctx, _body) => {
141
463
  const { data, error } = await adminDb
142
464
  .from('people')
@@ -147,21 +469,37 @@ export const checkFirstUser = createPublicHandler(async (_ctx, _body) => {
147
469
 
148
470
  return { is_first_user: (data?.length ?? 0) === 0 }
149
471
  })
150
- // CIL[END chunk_id="invites_checkfirstuser_1783968140"]
151
- // CIL[BEGIN chunk_id="invites_checkdomain_1783968140" type="function" identity="checkDomain" parent="none"]
152
- // CIL[DOC-MACRO purpose="Check if email domain has existing active accounts"]
153
- // CIL[DOC-RESPONSIBILITY responsibility="Validate domain for account matching logic"]
154
- // CIL[DOC-CONTEXT module="invites" layer="api-handler" related_files="[]"]
155
- // CIL[DOC-CONTRACT inputs="none" outputs="{match: boolean}" errors="Invalid email, DB errors" side_effects="DB read operation"]
156
- // CIL[DOC-MICRO logic_summary="Excludes personal domains, checks for active accounts with same domain" notable_decisions="[]"]
157
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
158
- // CIL[DOC-STABILITY stability="stable"]
159
- // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
160
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
161
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
162
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
472
+ // CIL[END stable_id="~/.framework/functions/invites.ts:checkfirstuser:constant"]
473
+ // CIL[DOC-VECTOR type="context" content="purpose='This constant checks if there are any users in the database and determines if the current user is the first one.'; No verified external callers" status="ready"]
474
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
475
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
476
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
163
477
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
164
478
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
479
+ // CIL[DOC-MACRO purpose="Validates the provided email domain against a list of personal domains and checks for an active account associated with that domain."]
480
+ // CIL[DOC-RESPONSIBILITY responsibility="Validates the provided email domain against a list of personal domains and checks for an active account associated with that domain. This function exists to ensure that only valid and non-personal email domains are processed for account matching."]
481
+ // CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
482
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="Error — Valid email is required if the email is missing or invalid." side_effects="Database read operation to check for active accounts. dependencies="adminDb.from('people')"]
483
+ // CIL[DOC-MICRO logic_summary="Validates the provided email domain against a list of personal domains and checks for an active account associated with that domain. This function exists to ensure that only valid and non-personal email domains are processed for account matching." notable_decisions="none"]
484
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
485
+ // CIL[DOC-STABILITY stability="stable"]
486
+ /**
487
+ * Validates the provided email domain against a list of personal domains and checks for an active account associated with that domain. This function exists to ensure that only valid and non-personal email domains are processed for account matching.
488
+ *
489
+ * @throws Error — Valid email is required if the email is missing or invalid.
490
+ * @sideEffects Database read operation to check for active accounts.
491
+ * @stability stable
492
+ * @calls adminDb.from('people')
493
+ * @cil-generated
494
+ */
495
+ // CIL[BEGIN stable_id="~/.framework/functions/invites.ts:checkdomain:constant" type="constant" identity="checkDomain" parent="~/.framework/functions/invites.ts"]
496
+ // CIL[DOC-MACRO purpose="checkDomain constant"]
497
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the checkDomain constant."]
498
+ // CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
499
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
500
+ // CIL[DOC-MICRO logic_summary="Implements the checkDomain constant." notable_decisions="[]"]
501
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
502
+ // CIL[DOC-STABILITY stability="stable"]
165
503
  export const checkDomain = createPublicHandler(async (_ctx, _body) => {
166
504
  const email = _ctx.query?.email as string | undefined
167
505
  if (!email || !email.includes('@')) {
@@ -184,28 +522,45 @@ export const checkDomain = createPublicHandler(async (_ctx, _body) => {
184
522
  const first = data?.[0]
185
523
  return { match: !!first, account_id: first?.account_id ?? null }
186
524
  })
187
- // CIL[END chunk_id="invites_checkdomain_1783968140"]
188
- // CIL[BEGIN chunk_id="invites_validatetoken_1783968140" type="function" identity="validateToken" parent="none"]
189
- // CIL[DOC-MACRO purpose="Validate invite token and return safe preview data"]
190
- // CIL[DOC-RESPONSIBILITY responsibility="Authenticate invite tokens and track attempts"]
191
- // CIL[DOC-CONTEXT module="invites" layer="api-handler" related_files="[]"]
192
- // CIL[DOC-CONTRACT inputs="none" outputs="Invite preview data" errors="Token invalid/expired/blocked" side_effects="Updates attempt count, blocks after max attempts"]
193
- // CIL[DOC-MICRO logic_summary="Validates token, checks expiry/blocked status, increments attempts, blocks after max" notable_decisions="[]"]
194
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="complex_dependencies"]
195
- // CIL[DOC-STABILITY stability="stable"]
196
- // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
197
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
198
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
199
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
525
+ // CIL[END stable_id="~/.framework/functions/invites.ts:checkdomain:constant"]
526
+ // CIL[DOC-VECTOR type="context" content="purpose='Validates the provided email domain against a list of personal domains and checks for an active account associated with that domain.'; No verified external callers" status="ready"]
527
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
528
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
529
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
200
530
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
201
531
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
532
+ // CIL[DOC-MACRO purpose="Validates an invite token by checking its existence and status in the database — read-only, no invite-attempt writes."]
533
+ // CIL[DOC-RESPONSIBILITY responsibility="Validates an invite token by checking its existence and status in the database. This function ensures that the invite link is valid, not expired, and not blocked before returning user details."]
534
+ // CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
535
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="Error — Token is required, Error — Invite not found or already used, Error — This invite link has been blocked. Please contact the administrator., Error — This invite link has expired. Please request a new one., Error — Too many failed attempts. This invite has been blocked." side_effects="Read-only single people lookup. dependencies="adminDb.from('people')"]
536
+ // CIL[DOC-MICRO logic_summary="Validates an invite token by checking its existence and status in the database. This function ensures that the invite link is valid, not expired, and not blocked before returning user details." notable_decisions="attempts-counter writes removed so previews cannot self-block the invite"]
537
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
538
+ // CIL[DOC-STABILITY stability="stable"]
539
+ /**
540
+ * Validates an invite token by checking its existence and status in the database. This function ensures that the invite link is valid, not expired, and not blocked before returning user details. Read-only: performs no invite-attempt writes.
541
+ *
542
+ * @throws Error — Token is required, Error — Invite not found or already used, Error — This invite link has been blocked. Please contact the administrator., Error — This invite link has expired. Please request a new one., Error — Too many failed attempts. This invite has been blocked.
543
+ * @sideEffects none — read-only preview (the invite_attempts write was removed in the registration hardening batch)
544
+ * @stability stable
545
+ * @calls adminDb.from('people')
546
+ * @cil-generated
547
+ */
548
+ // CIL[BEGIN stable_id="~/.framework/functions/invites.ts:validatetoken:constant" type="constant" identity="validateToken" parent="~/.framework/functions/invites.ts"]
549
+ // CIL[DOC-MACRO purpose="validateToken constant"]
550
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the validateToken constant."]
551
+ // CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
552
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
553
+ // CIL[DOC-MICRO logic_summary="Read-only invite preview: looks up the token on invited/pending people and reports blocked/expired/attempt-cap states without any writes." notable_decisions="attempts increment + auto-block write removed — previews no longer consume the attempts budget"]
554
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
555
+ // CIL[DOC-STABILITY stability="stable"]
556
+ // CIL[DOC-NOTE type="security" content="Registration/invitation hardening: the per-call invite_attempts write was removed entirely; MAX_ATTEMPTS is kept only to surface the blocked state on legacy rows."]
202
557
  export const validateToken = createPublicHandler(async (_ctx, _body) => {
203
558
  const token = _ctx.query?.token as string | undefined
204
559
  if (!token) throw new Error('Token is required')
205
560
 
206
561
  const { data: person } = await adminDb
207
562
  .from('people')
208
- .select('id, email, account_id, role_id, status, data, account:accounts!people_account_id_fkey(display_name), role:roles(slug, name)')
563
+ .select('id, email, account_id, role_id, status, data, account:accounts!people_account_id_fkey(display_name), role:roles!people_role_id_fkey(slug, name)')
209
564
  .filter('data->>invite_token', 'eq', token)
210
565
  .in('status', ['invited', 'pending'])
211
566
  .single()
@@ -224,22 +579,11 @@ export const validateToken = createPublicHandler(async (_ctx, _body) => {
224
579
  throw new Error('This invite link has expired. Please request a new one.')
225
580
  }
226
581
 
227
- const attempts = (meta.invite_attempts || 0) + 1
228
- const blocked = attempts >= MAX_ATTEMPTS
229
-
230
- await adminDb
231
- .from('people')
232
- .update({
233
- data: {
234
- ...meta,
235
- invite_attempts: attempts,
236
- invite_blocked_at: blocked ? new Date().toISOString() : null,
237
- },
238
- updated_at: new Date().toISOString(),
239
- })
240
- .eq('id', person.id)
241
-
242
- if (blocked) {
582
+ // Read-only preview: no invite_attempts increment and no auto-block write.
583
+ // Previously every preview call — including harmless page refreshes —
584
+ // counted toward MAX_ATTEMPTS and could self-block the invite. Legacy rows
585
+ // that already carry a counter at/above the cap still surface blocked.
586
+ if ((meta.invite_attempts || 0) >= MAX_ATTEMPTS) {
243
587
  throw new Error('Too many failed attempts. This invite has been blocked.')
244
588
  }
245
589
 
@@ -254,286 +598,180 @@ export const validateToken = createPublicHandler(async (_ctx, _body) => {
254
598
  kind: meta.invite_kind,
255
599
  }
256
600
  })
257
- // CIL[END chunk_id="invites_validatetoken_1783968140"]
258
- // CIL[BEGIN chunk_id="invites_completeregistration_1783968140" type="function" identity="completeRegistration" parent="none"]
259
- // 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."]
260
- // 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."]
261
- // CIL[DOC-MACRO purpose="Complete user registration after Supabase auth with multiple paths"]
262
- // CIL[DOC-RESPONSIBILITY responsibility="Handle walk-up registration flows and account assignment"]
263
- // CIL[DOC-CONTEXT module="invites" layer="api-handler" related_files="[]"]
264
- // CIL[DOC-CONTRACT inputs="none" outputs="Registration result" errors="Invalid path, missing params, DB errors" side_effects="DB updates, role resolution"]
265
- // CIL[DOC-MICRO logic_summary="Handles new_account, existing_account, and join_request paths with role resolution" notable_decisions="[]"]
266
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="complex_dependencies"]
267
- // CIL[DOC-STABILITY stability="stable"]
268
- // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
269
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
270
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
271
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
601
+ // CIL[END stable_id="~/.framework/functions/invites.ts:validatetoken:constant"]
602
+ // CIL[DOC-VECTOR type="context" content="purpose='Validates an invite token by checking its existence and status in the database, and updates the invite attempts.'; No verified external callers" status="ready"]
603
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
604
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
605
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
272
606
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
273
607
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
608
+ // CIL[DOC-MACRO purpose="The completeRegistration function handles the registration process for users, either linking them to an existing account or creating a new account based on the provided path."]
609
+ // CIL[DOC-RESPONSIBILITY responsibility="The completeRegistration function handles the registration process for users, either linking them to an existing account or creating a new account based on the provided path. It ensures that all required fields are validated and manages role assignments and rate limiting."]
610
+ // CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
611
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="Error — Authentication required; Error — app and path are required; Error (400) — any RPC error from complete_registration_placement." side_effects="Rate limit checks; placement writes happen inside the service-role-only complete_registration_placement RPC (migration 052); emits registration.completed audit. dependencies="adminDb.rpc('complete_registration_placement'), checkRateLimit, getClientGeo"]
612
+ // CIL[DOC-MICRO logic_summary="The completeRegistration function handles the registration process for users, either linking them to an existing account or creating a new account based on the provided path. It validates app+path, runs blocklist/rate-limit checks, and delegates all placement to the RPC bound by ctx.principal.id." notable_decisions="body email never steers placement; role/account/join-target are server-resolved in the RPC"]
613
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
614
+ // CIL[DOC-STABILITY stability="stable"]
615
+ /**
616
+ * The completeRegistration function handles the registration process for users, either linking them to an existing account or creating a new account based on the provided path. It ensures that all required fields are validated and manages role assignments and rate limiting.
617
+ *
618
+ * @throws Error — Authentication required; Error — app and path are required; Error (400) — RPC placement errors from complete_registration_placement.
619
+ * @sideEffects Rate limit checks; placement writes happen inside the complete_registration_placement RPC (migration 052); emits registration.completed audit.
620
+ * @stability stable
621
+ * @calls adminDb.rpc('complete_registration_placement'), checkRateLimit, getClientGeo
622
+ * @cil-generated
623
+ */
624
+ // CIL[BEGIN stable_id="~/.framework/functions/invites.ts:completeregistration:constant" type="constant" identity="completeRegistration" parent="~/.framework/functions/invites.ts"]
625
+ // CIL[DOC-MACRO purpose="completeRegistration constant"]
626
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the completeRegistration constant."]
627
+ // CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
628
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
629
+ // CIL[DOC-MICRO logic_summary="Validates app+path, runs blocklist and person/ip-keyed rate limits, then delegates ALL placement work to the complete_registration_placement RPC bound by ctx.principal.id." notable_decisions="Body email is used ONLY for the blocklist check — the request body can no longer steer account placement, role assignment, or join targets; the RPC enforces app policy, role allowlists, and idempotency atomically"]
630
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
631
+ // CIL[DOC-STABILITY stability="stable"]
632
+ // CIL[DOC-NOTE type="security" content="Registration/invitation hardening: previously bound people rows by client-supplied email (identity spoofing), accepted client role_slug/target_account_id, and reported success on zero-row updates. Placement now lives in the service-role-only RPC from migration 052."]
274
633
  export const completeRegistration = createHandler(async (ctx, body) => {
275
- const { path, account_name, email, role_slug, target_account } = body
276
-
277
- if (!path || !email) {
278
- throw new Error('path and email are required')
634
+ if (!ctx.principal || ctx.principal.id === 'anonymous') {
635
+ throw new Error('Authentication required')
279
636
  }
637
+ const { path, account_name, email, app } = body ?? {}
280
638
 
281
- if (path === 'existing_account') {
282
- if (!target_account) throw new Error('target_account is required for existing_account path')
283
-
284
- const { data: targetAccount } = await adminDb
285
- .from('accounts')
286
- .select('id, slug')
287
- .eq('slug', target_account)
288
- .single()
289
-
290
- if (!targetAccount) {
291
- throw new Error(`Target account '${target_account}' not found`)
292
- }
293
-
294
- const { data: personType } = await adminDb
295
- .from('types')
296
- .select('id, design_schema, validation_schema')
297
- .eq('kind', 'person')
298
- .eq('slug', 'person')
299
- .single()
300
-
301
- const targetRoleSlug = role_slug || 'member'
302
-
303
- let { data: roles } = await adminDb
304
- .from('roles')
305
- .select('id, slug')
306
- .eq('slug', targetRoleSlug)
307
- .limit(1)
308
-
309
- let role = roles?.[0]
310
-
311
- if (!role) {
312
- const { data: fallbackRoles } = await adminDb
313
- .from('roles')
314
- .select('id, slug')
315
- .in('slug', ['member_admin', 'member'])
316
- .order('slug')
317
- .limit(1)
318
- role = fallbackRoles?.[0]
319
- }
320
-
321
- const { error: updateErr } = await adminDb
322
- .from('people')
323
- .update({
324
- account_id: targetAccount.id,
325
- role_id: role?.id ?? null,
326
- status: 'active',
327
- type_id: personType.id,
328
- data: {},
329
- updated_at: new Date().toISOString(),
330
- })
331
- .eq('email', email)
332
- .eq('status', 'active')
333
- .is('role_id', null)
334
-
335
- if (updateErr) throw new Error('Failed to assign user to existing account: ' + updateErr.message)
336
-
337
- return {
338
- status: 'active',
339
- account_slug: targetAccount.slug
340
- }
639
+ if (!app || !path) {
640
+ throw new Error('app and path are required')
341
641
  }
342
642
 
643
+ // Registration chokepoint: deny blocklisted email / domain / client IP
644
+ // before any placement work. Fails open pre-049. The body email is used
645
+ // ONLY here — binding below is always ctx.principal.id.
646
+ const clientGeo = getClientGeo(ctx)
647
+ await checkRegistrationBlocklist(ctx, email || ctx.principal.email || '', clientGeo.ip)
648
+
649
+ // Rate limits keyed by the server-resolved person plus the client IP —
650
+ // a client-supplied email can no longer mint fresh limiter buckets.
651
+ await checkRateLimit(ctx, 'registration', { identifier: `p:${ctx.principal.id}` })
652
+ if (clientGeo.ip) {
653
+ await checkRateLimit(ctx, 'registration', { identifier: `ip:${clientGeo.ip}` })
654
+ }
343
655
  if (path === 'new_account') {
344
- if (!account_name) throw new Error('account_name is required for new_account path')
345
-
346
- const { data: personType } = await adminDb
347
- .from('types')
348
- .select('id, design_schema, validation_schema')
349
- .eq('kind', 'person')
350
- .eq('slug', 'person')
351
- .single()
352
-
353
- const { data: accountType } = await adminDb
354
- .from('types')
355
- .select('id')
356
- .eq('kind', 'account')
357
- .eq('slug', 'account')
358
- .single()
359
-
360
- const targetRoleSlug = role_slug || 'member'
361
-
362
- let { data: roles } = await adminDb
363
- .from('roles')
364
- .select('id, slug')
365
- .eq('slug', targetRoleSlug)
366
- .limit(1)
367
-
368
- let role = roles?.[0]
369
-
370
- if (!role) {
371
- const { data: fallbackRoles } = await adminDb
372
- .from('roles')
373
- .select('id, slug')
374
- .in('slug', ['member_admin', 'member'])
375
- .order('slug')
376
- .limit(1)
377
- role = fallbackRoles?.[0]
378
- }
379
-
380
- if (!role && !role_slug) {
381
- const { data: newRole, error: roleErr } = await adminDb
382
- .from('roles')
383
- .insert({
384
- slug: 'member',
385
- name: 'Member',
386
- description: 'Default member role with basic permissions',
387
- permissions: { items: ['read', 'create'], accounts: ['read'] },
388
- is_active: true,
389
- })
390
- .select('id')
391
- .single()
392
-
393
- if (!roleErr && newRole) {
394
- role = newRole
395
- }
656
+ // Preserve the account-creation abuse cap (config: rate_limits.account_create).
657
+ await checkRateLimit(ctx, 'account_create', { identifier: `p:${ctx.principal.id}` })
658
+ if (clientGeo.ip) {
659
+ await checkRateLimit(ctx, 'account_create', { identifier: `ip:${clientGeo.ip}` })
396
660
  }
661
+ }
397
662
 
398
- const slug = generateAccountSlug(account_name)
399
- const { data: account, error: accErr } = await adminDb
400
- .from('accounts')
401
- .insert({
402
- slug,
403
- display_name: account_name,
404
- type_id: accountType?.id,
405
- is_active: true,
406
- })
407
- .select('id')
408
- .single()
409
-
410
- if (accErr || !account) throw new Error('Failed to create account: ' + accErr?.message)
411
-
412
- const { error: pErr } = await adminDb
413
- .from('people')
414
- .update({
415
- account_id: account.id,
416
- role_id: role?.id ?? null,
417
- status: 'active',
418
- updated_at: new Date().toISOString(),
419
- })
420
- .eq('email', email)
421
- .eq('status', 'active')
422
- .is('role_id', null)
423
-
424
- if (pErr) throw new Error('Failed to update person: ' + pErr.message)
663
+ // All placement logic lives in the service-role-only RPC (migration 052):
664
+ // app policy resolution, role allowlist/non-system validation, account
665
+ // creation, domain-join targeting, and the role_id IS NULL idempotency
666
+ // precondition are enforced atomically there.
667
+ const { data: rpcResult, error: rpcError } = await adminDb.rpc('complete_registration_placement', {
668
+ p_person_id: ctx.principal.id,
669
+ p_app_slug: app,
670
+ p_path: path,
671
+ p_account_name: account_name ?? null,
672
+ })
425
673
 
426
- return { status: 'active', account_slug: slug }
674
+ if (rpcError || !rpcResult) {
675
+ const err: any = new Error(rpcError?.message || 'Registration placement failed')
676
+ err.statusCode = 400
677
+ throw err
427
678
  }
428
679
 
429
- if (path === 'join_request') {
430
- const { target_account_id, role_slug } = body
431
- if (!target_account_id) throw new Error('target_account_id is required for join_request')
680
+ const placement = typeof rpcResult === 'string' ? JSON.parse(rpcResult) : rpcResult
432
681
 
433
- const targetRoleSlug = role_slug || 'member'
434
- let { data: roles } = await adminDb
435
- .from('roles')
436
- .select('id')
437
- .eq('slug', targetRoleSlug)
438
- .limit(1)
439
-
440
- let role = roles?.[0]
441
-
442
- if (!role) {
443
- const { data: fallbackRoles } = await adminDb
444
- .from('roles')
445
- .select('id')
446
- .eq('slug', 'member')
447
- .limit(1)
448
- role = fallbackRoles?.[0]
682
+ void recordRegistrationGeo(ctx.principal.id, ctx)
683
+ await emitLog(ctx, 'registration.completed', { type: 'person', id: ctx.principal.id }, {
684
+ after: {
685
+ app,
686
+ path,
687
+ status: placement.status,
688
+ account_slug: placement.account_slug,
689
+ role_slug: placement.role_slug,
449
690
  }
691
+ })
450
692
 
451
- const { data: personType } = await adminDb
452
- .from('types')
453
- .select('id, design_schema, validation_schema')
454
- .eq('kind', 'person')
455
- .eq('slug', 'person')
456
- .single()
457
-
458
- const { error: pErr } = await adminDb
459
- .from('people')
460
- .update({
461
- account_id: target_account_id,
462
- role_id: role?.id ?? null,
463
- status: 'pending',
464
- data: {
465
- invite_kind: 'access_request',
466
- invite_invited_by: null,
467
- invite_token: null,
468
- },
469
- updated_at: new Date().toISOString(),
470
- })
471
- .eq('email', email)
472
- .eq('status', 'active')
473
- .is('role_id', null)
474
-
475
- if (pErr) throw new Error('Failed to create access request: ' + pErr.message)
476
-
477
- return { status: 'pending' }
693
+ // Response shape preserved: {status, account_slug} plus role_slug and a
694
+ // `next` passthrough when the RPC supplies them.
695
+ const result: Record<string, any> = {
696
+ status: placement.status,
697
+ account_slug: placement.account_slug ?? null,
478
698
  }
479
-
480
- throw new Error('Unknown registration path: ' + path)
699
+ if (placement.role_slug !== undefined) result.role_slug = placement.role_slug
700
+ if (placement.next !== undefined) result.next = placement.next
701
+ return result
481
702
  })
482
- // CIL[END chunk_id="invites_completeregistration_1783968140"]
483
- // CIL[BEGIN chunk_id="invites_createinvite_1783968140" type="function" identity="createInvite" parent="none"]
484
- // CIL[DOC-MACRO purpose="Create invite stub with metadata and return shareable URL"]
485
- // CIL[DOC-RESPONSIBILITY responsibility="Generate new invite tokens and create person records"]
486
- // CIL[DOC-CONTEXT module="invites" layer="api-handler" related_files="[]"]
487
- // 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"]
488
- // CIL[DOC-MICRO logic_summary="Validates admin permissions, creates person stub with invite metadata, generates token" notable_decisions="[]"]
489
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="complex_dependencies"]
490
- // CIL[DOC-STABILITY stability="stable"]
491
- // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
492
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
493
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
494
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
703
+ // CIL[END stable_id="~/.framework/functions/invites.ts:completeregistration:constant"]
704
+ // CIL[DOC-VECTOR type="context" content="purpose='The completeRegistration function handles the registration process for users, either linking them to an existing account or creating a new account based on the provided path.'; No verified external callers" status="ready"]
705
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
706
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
707
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
495
708
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
496
709
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
710
+ // CIL[DOC-MACRO purpose="Creates an invite for a user to join an account, ensuring proper authentication and authorization."]
711
+ // CIL[DOC-RESPONSIBILITY responsibility="Creates an invite for a user to join an account, ensuring proper authentication and authorization. It validates input and checks for existing users before generating an invite token."]
712
+ // CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
713
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="Error — Authentication required, Error — email, account_id, and role_slug are required, Error — Insufficient permissions — *_admin role required to create invites, Error — Role '{role_slug}' is not permitted in this context, Error — Role '{role_slug}' not found, Error — A person with this email already exists or has a pending invite for this account, Error — Failed to create invite." side_effects="Writes to the database to create an invite and may read from multiple tables. dependencies="adminDb.from('people'), adminDb.from('roles'), adminDb.from('types'), adminDataCreate, checkRateLimit, generateToken"]
714
+ // CIL[DOC-MICRO logic_summary="Creates an invite for a user to join an account, ensuring proper authentication and authorization. It validates input and checks for existing users before generating an invite token." notable_decisions="none"]
715
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
716
+ // CIL[DOC-STABILITY stability="stable"]
717
+ /**
718
+ * Creates an invite for a user to join an account, ensuring proper authentication and authorization. It validates input and checks for existing users before generating an invite token.
719
+ *
720
+ * @throws Error — Authentication required, Error — email, account_id, and role_slug are required, Error — Insufficient permissions — *_admin role required to create invites, Error — Role '{role_slug}' is not permitted in this context, Error — Role '{role_slug}' not found, Error — A person with this email already exists or has a pending invite for this account, Error — Failed to create invite.
721
+ * @sideEffects Writes to the database to create an invite and may read from multiple tables.
722
+ * @stability stable
723
+ * @calls adminDb.from('people'), adminDb.from('roles'), adminDb.from('types'), adminDataCreate, checkRateLimit, generateToken
724
+ * @cil-generated
725
+ */
726
+ // CIL[BEGIN stable_id="~/.framework/functions/invites.ts:createinvite:constant" type="constant" identity="createInvite" parent="~/.framework/functions/invites.ts"]
727
+ // CIL[DOC-MACRO purpose="createInvite constant"]
728
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the createInvite constant."]
729
+ // CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
730
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
731
+ // CIL[DOC-MICRO logic_summary="Requires an account-scoped admin principal, enforces the calling app's declared role allowlist, rejects system_admin/is_system roles, creates the invited-stub person, and builds the invite link on an origin-allowlisted base URL." notable_decisions="Client-supplied allowed_role_slugs and arbitrary base_url origins are no longer trusted; admin scope is ctx.principal.accountId vs the target account_id"]
732
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
733
+ // CIL[DOC-STABILITY stability="stable"]
734
+ // CIL[DOC-NOTE type="security" content="Registration/invitation hardening: caller admin check is now account-scoped via requireAccountAdmin; role_slug must be non-system and inside the app's server-resolved allowlist; base_url passes the origin allowlist instead of a yourapp.com fallback."]
497
735
  export const createInvite = createHandler(async (ctx, body) => {
498
736
  if (!ctx.principal || ctx.principal.id === 'anonymous') {
499
737
  throw new Error('Authentication required')
500
738
  }
501
739
 
502
- const { full_name, email, account_id, role_slug, allowed_role_slugs, base_url } = body
740
+ const { full_name, email, account_id, role_slug, base_url } = body
503
741
  if (!email || !account_id || !role_slug) {
504
742
  throw new Error('email, account_id, and role_slug are required')
505
743
  }
506
744
 
507
- const { data: caller } = await adminDb
508
- .from('people')
509
- .select('role:roles(slug)')
510
- .eq('id', ctx.principal.id)
511
- .eq('account_id', account_id)
512
- .single()
745
+ // Caller must be system_admin or an *_admin bound to THIS account.
746
+ requireAccountAdmin(ctx, account_id)
513
747
 
514
- const callerRole = Array.isArray(caller?.role) ? caller?.role[0] : caller?.role
515
- const callerRoleSlug = callerRole?.slug
748
+ // Abuse control: cap invites per account (config: rate_limits.invite_create).
749
+ await checkRateLimit(ctx, 'invite_create', { identifier: `acct:${account_id}` })
516
750
 
517
- const isSystemAdmin = ctx.principal.permissions?.includes('system') ||
518
- callerRoleSlug === 'system_admin'
519
- const isAccountAdmin = isAdminRole(callerRoleSlug)
520
-
521
- if (!isSystemAdmin && !isAccountAdmin) {
522
- throw new Error('Insufficient permissions — *_admin role required to create invites')
751
+ // Privileged roles can never be assigned through invites.
752
+ if (role_slug === 'system_admin') {
753
+ throw new Error(`Role 'system_admin' is not permitted in this context`)
523
754
  }
524
755
 
525
- if (allowed_role_slugs && Array.isArray(allowed_role_slugs) && !allowed_role_slugs.includes(role_slug)) {
756
+ // Server-side allowlist: the calling app's config.invitation.allowed_roles
757
+ // when declared, else config.registration.roles. Client-supplied
758
+ // allowed_role_slugs are no longer consulted.
759
+ const allowlist = await resolveAppRoleAllowlist(ctx, body)
760
+ if (allowlist && !allowlist.includes(role_slug)) {
526
761
  throw new Error(`Role '${role_slug}' is not permitted in this context`)
527
762
  }
528
763
 
529
764
  const { data: roleRows } = await adminDb
530
765
  .from('roles')
531
- .select('id, slug')
766
+ .select('id, slug, is_system')
532
767
  .eq('slug', role_slug)
533
768
  .limit(1)
534
769
 
535
770
  const resolvedRole = roleRows?.[0]
536
771
  if (!resolvedRole) throw new Error(`Role '${role_slug}' not found`)
772
+ if (resolvedRole.is_system === true) {
773
+ throw new Error(`Role '${role_slug}' is not permitted in this context`)
774
+ }
537
775
 
538
776
  const { data: existing } = await adminDb
539
777
  .from('people')
@@ -580,30 +818,82 @@ export const createInvite = createHandler(async (ctx, body) => {
580
818
 
581
819
  if (!stub) throw new Error('Failed to create invite')
582
820
 
583
- const appBaseUrl = base_url || process.env.APP_BASE_URL || 'https://yourapp.com'
821
+ const appBaseUrl = resolveInviteBaseUrl(base_url)
584
822
  const inviteUrl = `${appBaseUrl}/register?t=${token}`
585
823
 
824
+ // Email delivery: when an 'invite-email' integration is configured, send the
825
+ // invite through the outbound broker. Always return invite_url so the UI can
826
+ // fall back to a copyable link when no provider is configured.
827
+ let emailSent = false
828
+ try {
829
+ const { data: emailIntegration } = await adminDb
830
+ .from('integrations')
831
+ .select('id')
832
+ .eq('slug', 'invite-email')
833
+ .eq('is_active', true)
834
+ .maybeSingle()
835
+
836
+ if (emailIntegration) {
837
+ const { data: account } = await adminDb
838
+ .from('accounts')
839
+ .select('display_name')
840
+ .eq('id', account_id)
841
+ .maybeSingle()
842
+ await checkRateLimit(ctx, 'invite_email', { identifier: `acct:${account_id}` })
843
+ const result = await executeOutbound(ctx, {
844
+ integration_slug: 'invite-email',
845
+ action_config: {
846
+ to: email,
847
+ to_name: stubName,
848
+ invite_url: inviteUrl,
849
+ account_name: account?.display_name || '',
850
+ invited_by: ctx.principal.id,
851
+ expires_at: expiresAt,
852
+ }
853
+ })
854
+ emailSent = !!result?.success
855
+ }
856
+ } catch (err: any) {
857
+ console.warn('invite email send failed:', err?.message)
858
+ }
859
+
586
860
  await emitLog(ctx, 'invite.created', { type: 'person', id: stub.id }, {
587
- after: { email, full_name: stubName, account_id, role_slug, expires_at: expiresAt }
861
+ after: { email, full_name: stubName, account_id, role_slug, expires_at: expiresAt, email_sent: emailSent }
588
862
  })
589
863
 
590
- return { id: stub.id, token, invite_url: inviteUrl, expires_at: expiresAt }
864
+ return { id: stub.id, token, invite_url: inviteUrl, expires_at: expiresAt, email_sent: emailSent }
591
865
  })
592
- // CIL[END chunk_id="invites_createinvite_1783968140"]
593
- // CIL[BEGIN chunk_id="invites_listinvites_1783968140" type="function" identity="listInvites" parent="none"]
594
- // CIL[DOC-MACRO purpose="List invite records with filtering and role-based access control"]
595
- // CIL[DOC-RESPONSIBILITY responsibility="Query and return scoped invite data"]
596
- // CIL[DOC-CONTEXT module="invites" layer="api-handler" related_files="[]"]
597
- // CIL[DOC-CONTRACT inputs="none" outputs="Array of invite records" errors="Auth required, DB errors" side_effects="DB read operations"]
598
- // CIL[DOC-MICRO logic_summary="Filters by account scope, role slugs, returns mapped invite data" notable_decisions="[]"]
599
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="complex_dependencies"]
600
- // CIL[DOC-STABILITY stability="stable"]
601
- // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
602
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
603
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
604
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
866
+ // CIL[END stable_id="~/.framework/functions/invites.ts:createinvite:constant"]
867
+ // CIL[DOC-VECTOR type="context" content="purpose='Creates an invite for a user to join an account, ensuring proper authentication and authorization.'; No verified external callers" status="ready"]
868
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
869
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
870
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
605
871
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
606
872
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
873
+ // CIL[DOC-MACRO purpose="Lists invites for a user based on their account and roles, ensuring authentication and appropriate permissions."]
874
+ // CIL[DOC-RESPONSIBILITY responsibility="Lists invites for a user based on their account and roles, ensuring authentication and appropriate permissions. It filters invites based on role slugs and returns relevant invite details."]
875
+ // CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
876
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="Error — Authentication required, Error — Account scope required, Error — Database error" side_effects="Database read operations dependencies="adminDb.from('roles'), adminDb.from('people')"]
877
+ // CIL[DOC-MICRO logic_summary="Lists invites for a user based on their account and roles, ensuring authentication and appropriate permissions. It filters invites based on role slugs and returns relevant invite details." notable_decisions="none"]
878
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
879
+ // CIL[DOC-STABILITY stability="stable"]
880
+ /**
881
+ * Lists invites for a user based on their account and roles, ensuring authentication and appropriate permissions. It filters invites based on role slugs and returns relevant invite details.
882
+ *
883
+ * @throws Error — Authentication required, Error — Account scope required, Error — Database error
884
+ * @sideEffects Database read operations
885
+ * @stability stable
886
+ * @calls adminDb.from('roles'), adminDb.from('people')
887
+ * @cil-generated
888
+ */
889
+ // CIL[BEGIN stable_id="~/.framework/functions/invites.ts:listinvites:constant" type="constant" identity="listInvites" parent="~/.framework/functions/invites.ts"]
890
+ // CIL[DOC-MACRO purpose="listInvites constant"]
891
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the listInvites constant."]
892
+ // CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
893
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
894
+ // CIL[DOC-MICRO logic_summary="Implements the listInvites constant." notable_decisions="[]"]
895
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
896
+ // CIL[DOC-STABILITY stability="stable"]
607
897
  export const listInvites = createHandler(async (ctx, _body) => {
608
898
  if (!ctx.principal || ctx.principal.id === 'anonymous') {
609
899
  throw new Error('Authentication required')
@@ -635,7 +925,7 @@ export const listInvites = createHandler(async (ctx, _body) => {
635
925
  id, email, full_name, status, created_at, updated_at,
636
926
  account_id, role_id,
637
927
  account:accounts!people_account_id_fkey(id, slug, display_name),
638
- role:roles(id, slug, name),
928
+ role:roles!people_role_id_fkey(id, slug, name),
639
929
  data
640
930
  `)
641
931
  .not('data->>invite_kind', 'is', null)
@@ -669,21 +959,38 @@ export const listInvites = createHandler(async (ctx, _body) => {
669
959
  role: Array.isArray(row.role) ? row.role[0] : row.role,
670
960
  }))
671
961
  })
672
- // CIL[END chunk_id="invites_listinvites_1783968140"]
673
- // CIL[BEGIN chunk_id="invites_revokeinvite_1783968140" type="function" identity="revokeInvite" parent="none"]
674
- // CIL[DOC-MACRO purpose="Soft-delete invite by setting status to inactive"]
675
- // CIL[DOC-RESPONSIBILITY responsibility="Revoke pending or invited person records"]
676
- // CIL[DOC-CONTEXT module="invites" layer="api-handler" related_files="[]"]
677
- // CIL[DOC-CONTRACT inputs="none" outputs="{success: boolean}" errors="Auth required, person not found, DB errors" side_effects="DB update, audit logging"]
678
- // CIL[DOC-MICRO logic_summary="Sets person status to inactive, clears invite token, logs revocation" notable_decisions="[]"]
679
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
680
- // CIL[DOC-STABILITY stability="stable"]
681
- // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
682
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
683
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
684
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
962
+ // CIL[END stable_id="~/.framework/functions/invites.ts:listinvites:constant"]
963
+ // CIL[DOC-VECTOR type="context" content="purpose='Lists invites for a user based on their account and roles, ensuring authentication and appropriate permissions.'; No verified external callers" status="ready"]
964
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
965
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
966
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
685
967
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
686
968
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
969
+ // CIL[DOC-MACRO purpose="The revokeInvite function revokes an invitation for a person by updating their status in the database."]
970
+ // CIL[DOC-RESPONSIBILITY responsibility="The revokeInvite function revokes an invitation for a person by updating their status in the database. It ensures that only authenticated users can perform this action and that the person exists in a valid state before revocation."]
971
+ // CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
972
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="Error — Authentication required, person_id is required, Invite not found or already processed, error from database update" side_effects="Updates the status of a person in the database, emits a log event dependencies="adminDb.from('people'), emitLog"]
973
+ // CIL[DOC-MICRO logic_summary="The revokeInvite function revokes an invitation for a person by updating their status in the database. It ensures that only authenticated users can perform this action and that the person exists in a valid state before revocation." notable_decisions="none"]
974
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
975
+ // CIL[DOC-STABILITY stability="stable"]
976
+ /**
977
+ * The revokeInvite function revokes an invitation for a person by updating their status in the database. It ensures that only authenticated users can perform this action and that the person exists in a valid state before revocation.
978
+ *
979
+ * @throws Error — Authentication required, person_id is required, Invite not found or already processed, error from database update
980
+ * @sideEffects Updates the status of a person in the database, emits a log event
981
+ * @stability stable
982
+ * @calls adminDb.from('people'), emitLog
983
+ * @cil-generated
984
+ */
985
+ // CIL[BEGIN stable_id="~/.framework/functions/invites.ts:revokeinvite:constant" type="constant" identity="revokeInvite" parent="~/.framework/functions/invites.ts"]
986
+ // CIL[DOC-MACRO purpose="revokeInvite constant"]
987
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the revokeInvite constant."]
988
+ // CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
989
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
990
+ // CIL[DOC-MICRO logic_summary="Implements the revokeInvite constant." notable_decisions="[]"]
991
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
992
+ // CIL[DOC-STABILITY stability="stable"]
993
+ // CIL[DOC-NOTE type="security" content="Registration/invitation hardening: now requires an admin of the target person's account (or system_admin) via requireAccountAdmin — previously any authenticated principal could revoke."]
687
994
  export const revokeInvite = createHandler(async (ctx, body) => {
688
995
  if (!ctx.principal || ctx.principal.id === 'anonymous') throw new Error('Authentication required')
689
996
 
@@ -699,6 +1006,9 @@ export const revokeInvite = createHandler(async (ctx, body) => {
699
1006
 
700
1007
  if (!person) throw new Error('Invite not found or already processed')
701
1008
 
1009
+ // Scope: only an admin of the invite's own account (or system_admin).
1010
+ requireAccountAdmin(ctx, person.account_id)
1011
+
702
1012
  const { error } = await adminDb
703
1013
  .from('people')
704
1014
  .update({
@@ -715,21 +1025,38 @@ export const revokeInvite = createHandler(async (ctx, body) => {
715
1025
 
716
1026
  return { success: true }
717
1027
  })
718
- // CIL[END chunk_id="invites_revokeinvite_1783968140"]
719
- // CIL[BEGIN chunk_id="invites_reinvite_1783968140" type="function" identity="reinvite" parent="none"]
720
- // CIL[DOC-MACRO purpose="Issue new token for existing invite record"]
721
- // CIL[DOC-RESPONSIBILITY responsibility="Regenerate invite tokens for expired invites"]
722
- // CIL[DOC-CONTEXT module="invites" layer="api-handler" related_files="[]"]
723
- // 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"]
724
- // CIL[DOC-MICRO logic_summary="Generates new token, resets attempts, updates expiry, returns new URL" notable_decisions="[]"]
725
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
726
- // CIL[DOC-STABILITY stability="stable"]
727
- // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
728
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
729
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
730
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
1028
+ // CIL[END stable_id="~/.framework/functions/invites.ts:revokeinvite:constant"]
1029
+ // CIL[DOC-VECTOR type="context" content="purpose='The revokeInvite function revokes an invitation for a person by updating their status in the database.'; No verified external callers" status="ready"]
1030
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
1031
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
1032
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
731
1033
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
732
1034
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
1035
+ // CIL[DOC-MACRO purpose="The reinvite constant handles the process of inviting a person by generating a new invite token and updating their status in the database."]
1036
+ // CIL[DOC-RESPONSIBILITY responsibility="The reinvite constant handles the process of inviting a person by generating a new invite token and updating their status in the database. It ensures that only authenticated users can perform this action."]
1037
+ // CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
1038
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="Error — Authentication required, person_id is required, Person not found, error from database update." side_effects="Writes to the 'people' table in the database and emits a log entry. dependencies="adminDb.from('people'), generateToken, emitLog"]
1039
+ // CIL[DOC-MICRO logic_summary="The reinvite constant handles the process of inviting a person by generating a new invite token and updating their status in the database. It ensures that only authenticated users can perform this action." notable_decisions="none"]
1040
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
1041
+ // CIL[DOC-STABILITY stability="stable"]
1042
+ /**
1043
+ * The reinvite constant handles the process of inviting a person by generating a new invite token and updating their status in the database. It ensures that only authenticated users can perform this action.
1044
+ *
1045
+ * @throws Error — Authentication required, person_id is required, Person not found, error from database update.
1046
+ * @sideEffects Writes to the 'people' table in the database and emits a log entry.
1047
+ * @stability stable
1048
+ * @calls adminDb.from('people'), generateToken, emitLog
1049
+ * @cil-generated
1050
+ */
1051
+ // CIL[BEGIN stable_id="~/.framework/functions/invites.ts:reinvite:constant" type="constant" identity="reinvite" parent="~/.framework/functions/invites.ts"]
1052
+ // CIL[DOC-MACRO purpose="reinvite constant"]
1053
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the reinvite constant."]
1054
+ // CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
1055
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
1056
+ // CIL[DOC-MICRO logic_summary="Implements the reinvite constant." notable_decisions="[]"]
1057
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
1058
+ // CIL[DOC-STABILITY stability="stable"]
1059
+ // CIL[DOC-NOTE type="security" content="Registration/invitation hardening: requires an admin of the target person's account via requireAccountAdmin, restricts reinvite to invited/inactive people, and passes base_url through the origin allowlist."]
733
1060
  export const reinvite = createHandler(async (ctx, body) => {
734
1061
  if (!ctx.principal || ctx.principal.id === 'anonymous') throw new Error('Authentication required')
735
1062
 
@@ -744,6 +1071,13 @@ export const reinvite = createHandler(async (ctx, body) => {
744
1071
 
745
1072
  if (!person) throw new Error('Person not found')
746
1073
 
1074
+ // Scope: only an admin of the person's account (or system_admin), and only
1075
+ // invited/inactive people may be re-invited — never an active member.
1076
+ requireAccountAdmin(ctx, person.account_id)
1077
+ if (person.status !== 'invited' && person.status !== 'inactive') {
1078
+ throw new Error('Person is not in a reinvitable state')
1079
+ }
1080
+
747
1081
  const newToken = generateToken()
748
1082
  const expiresAt = new Date(Date.now() + INVITE_EXPIRY_DAYS * 86400 * 1000).toISOString()
749
1083
 
@@ -767,7 +1101,7 @@ export const reinvite = createHandler(async (ctx, body) => {
767
1101
 
768
1102
  if (error) throw error
769
1103
 
770
- const appBaseUrl = base_url || process.env.APP_BASE_URL || 'https://yourapp.com'
1104
+ const appBaseUrl = resolveInviteBaseUrl(base_url)
771
1105
  const inviteUrl = `${appBaseUrl}/register?t=${newToken}`
772
1106
 
773
1107
  await emitLog(ctx, 'invite.reinvited', { type: 'person', id: person_id }, {
@@ -776,21 +1110,38 @@ export const reinvite = createHandler(async (ctx, body) => {
776
1110
 
777
1111
  return { token: newToken, invite_url: inviteUrl, expires_at: expiresAt }
778
1112
  })
779
- // CIL[END chunk_id="invites_reinvite_1783968140"]
780
- // CIL[BEGIN chunk_id="invites_approve_1783968140" type="function" identity="approve" parent="none"]
781
- // CIL[DOC-MACRO purpose="Approve pending access request and activate person"]
782
- // CIL[DOC-RESPONSIBILITY responsibility="Convert pending access requests to active users"]
783
- // CIL[DOC-CONTEXT module="invites" layer="api-handler" related_files="[]"]
784
- // CIL[DOC-CONTRACT inputs="none" outputs="{success: boolean}" errors="Auth required, request not found, DB errors" side_effects="DB update, audit logging"]
785
- // CIL[DOC-MICRO logic_summary="Sets status to active, strips invite metadata, logs approval" notable_decisions="[]"]
786
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
787
- // CIL[DOC-STABILITY stability="stable"]
788
- // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
789
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
790
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
791
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
1113
+ // CIL[END stable_id="~/.framework/functions/invites.ts:reinvite:constant"]
1114
+ // CIL[DOC-VECTOR type="context" content="purpose='The reinvite constant handles the process of inviting a person by generating a new invite token and updating their status in the database.'; No verified external callers" status="ready"]
1115
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
1116
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
1117
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
792
1118
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
793
1119
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
1120
+ // CIL[DOC-MACRO purpose="Approves an access request for a person by updating their status to active."]
1121
+ // CIL[DOC-RESPONSIBILITY responsibility="Approves an access request for a person by updating their status to active. It ensures that the request is authenticated and that the person exists in a pending state before proceeding."]
1122
+ // CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
1123
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="Error — Authentication required, person_id is required, Access request not found or already processed, error from database update" side_effects="Writes to the 'people' table in the database and emits a log event. dependencies="adminDb.from('people'), emitLog"]
1124
+ // CIL[DOC-MICRO logic_summary="Approves an access request for a person by updating their status to active. It ensures that the request is authenticated and that the person exists in a pending state before proceeding." notable_decisions="none"]
1125
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
1126
+ // CIL[DOC-STABILITY stability="stable"]
1127
+ /**
1128
+ * Approves an access request for a person by updating their status to active. It ensures that the request is authenticated and that the person exists in a pending state before proceeding.
1129
+ *
1130
+ * @throws Error — Authentication required, person_id is required, Access request not found or already processed, error from database update
1131
+ * @sideEffects Writes to the 'people' table in the database and emits a log event.
1132
+ * @stability stable
1133
+ * @calls adminDb.from('people'), emitLog
1134
+ * @cil-generated
1135
+ */
1136
+ // CIL[BEGIN stable_id="~/.framework/functions/invites.ts:approve:constant" type="constant" identity="approve" parent="~/.framework/functions/invites.ts"]
1137
+ // CIL[DOC-MACRO purpose="approve constant"]
1138
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the approve constant."]
1139
+ // CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
1140
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
1141
+ // CIL[DOC-MICRO logic_summary="Implements the approve constant." notable_decisions="[]"]
1142
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
1143
+ // CIL[DOC-STABILITY stability="stable"]
1144
+ // CIL[DOC-NOTE type="security" content="Registration/invitation hardening: now requires an admin of the target person's account (or system_admin) via requireAccountAdmin — previously any authenticated principal could approve."]
794
1145
  export const approve = createHandler(async (ctx, body) => {
795
1146
  if (!ctx.principal || ctx.principal.id === 'anonymous') throw new Error('Authentication required')
796
1147
 
@@ -806,6 +1157,9 @@ export const approve = createHandler(async (ctx, body) => {
806
1157
 
807
1158
  if (!person) throw new Error('Access request not found or already processed')
808
1159
 
1160
+ // Scope: only an admin of the requesting account (or system_admin).
1161
+ requireAccountAdmin(ctx, person.account_id)
1162
+
809
1163
  const { invite_token, invite_kind, invite_expires_at, invite_attempts, invite_blocked_at, invite_invited_by, ...remainingData } =
810
1164
  (person.data as any) || {}
811
1165
 
@@ -827,21 +1181,38 @@ export const approve = createHandler(async (ctx, body) => {
827
1181
 
828
1182
  return { success: true }
829
1183
  })
830
- // CIL[END chunk_id="invites_approve_1783968140"]
831
- // CIL[BEGIN chunk_id="invites_blockinvite_1783968140" type="function" identity="blockInvite" parent="none"]
832
- // CIL[DOC-MACRO purpose="Manually block an invite token from being used"]
833
- // CIL[DOC-RESPONSIBILITY responsibility="Flag suspicious or failed invite attempts"]
834
- // CIL[DOC-CONTEXT module="invites" layer="api-handler" related_files="[]"]
835
- // CIL[DOC-CONTRACT inputs="none" outputs="{success: boolean}" errors="Auth required, person not found, DB errors" side_effects="DB update, audit logging"]
836
- // CIL[DOC-MICRO logic_summary="Sets invite_blocked_at timestamp to prevent token validation" notable_decisions="[]"]
837
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
838
- // CIL[DOC-STABILITY stability="stable"]
839
- // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
840
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
841
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
842
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
1184
+ // CIL[END stable_id="~/.framework/functions/invites.ts:approve:constant"]
1185
+ // CIL[DOC-VECTOR type="context" content="purpose='Approves an access request for a person by updating their status to active.'; No verified external callers" status="ready"]
1186
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
1187
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
1188
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
843
1189
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
844
1190
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
1191
+ // CIL[DOC-MACRO purpose="Blocks an invite for a specified person by updating their record in the database."]
1192
+ // CIL[DOC-RESPONSIBILITY responsibility="Blocks an invite for a specified person by updating their record in the database. This function ensures that only authenticated users can perform this action."]
1193
+ // CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
1194
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="Error — Authentication required, person_id is required, Person not found, or database update error." side_effects="Writes to the 'people' table in the database and emits a log event. dependencies="adminDb.from('people'), emitLog"]
1195
+ // CIL[DOC-MICRO logic_summary="Blocks an invite for a specified person by updating their record in the database. This function ensures that only authenticated users can perform this action." notable_decisions="none"]
1196
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
1197
+ // CIL[DOC-STABILITY stability="stable"]
1198
+ /**
1199
+ * Blocks an invite for a specified person by updating their record in the database. This function ensures that only authenticated users can perform this action.
1200
+ *
1201
+ * @throws Error — Authentication required, person_id is required, Person not found, or database update error.
1202
+ * @sideEffects Writes to the 'people' table in the database and emits a log event.
1203
+ * @stability stable
1204
+ * @calls adminDb.from('people'), emitLog
1205
+ * @cil-generated
1206
+ */
1207
+ // CIL[BEGIN stable_id="~/.framework/functions/invites.ts:blockinvite:constant" type="constant" identity="blockInvite" parent="~/.framework/functions/invites.ts"]
1208
+ // CIL[DOC-MACRO purpose="blockInvite constant"]
1209
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the blockInvite constant."]
1210
+ // CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
1211
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
1212
+ // CIL[DOC-MICRO logic_summary="Implements the blockInvite constant." notable_decisions="[]"]
1213
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
1214
+ // CIL[DOC-STABILITY stability="stable"]
1215
+ // CIL[DOC-NOTE type="security" content="Registration/invitation hardening: now requires an admin of the target person's account (or system_admin) via requireAccountAdmin — previously any authenticated principal could block."]
845
1216
  export const blockInvite = createHandler(async (ctx, body) => {
846
1217
  if (!ctx.principal || ctx.principal.id === 'anonymous') throw new Error('Authentication required')
847
1218
 
@@ -856,6 +1227,9 @@ export const blockInvite = createHandler(async (ctx, body) => {
856
1227
 
857
1228
  if (!person) throw new Error('Person not found')
858
1229
 
1230
+ // Scope: only an admin of the person's account (or system_admin).
1231
+ requireAccountAdmin(ctx, person.account_id)
1232
+
859
1233
  const meta = (person.data as any) || {}
860
1234
 
861
1235
  const { error } = await adminDb
@@ -877,21 +1251,38 @@ export const blockInvite = createHandler(async (ctx, body) => {
877
1251
 
878
1252
  return { success: true }
879
1253
  })
880
- // CIL[END chunk_id="invites_blockinvite_1783968140"]
881
- // CIL[BEGIN chunk_id="invites_deny_1783968140" type="function" identity="deny" parent="none"]
882
- // CIL[DOC-MACRO purpose="Deny pending access request and deactivate person"]
883
- // CIL[DOC-RESPONSIBILITY responsibility="Reject access requests and mark users inactive"]
884
- // CIL[DOC-CONTEXT module="invites" layer="api-handler" related_files="[]"]
885
- // CIL[DOC-CONTRACT inputs="none" outputs="{success: boolean}" errors="Auth required, request not found, DB errors" side_effects="DB update, audit logging"]
886
- // CIL[DOC-MICRO logic_summary="Sets status to inactive and is_active false, logs denial" notable_decisions="[]"]
887
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
888
- // CIL[DOC-STABILITY stability="stable"]
889
- // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
890
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
891
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
892
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
1254
+ // CIL[END stable_id="~/.framework/functions/invites.ts:blockinvite:constant"]
1255
+ // CIL[DOC-VECTOR type="context" content="purpose='Blocks an invite for a specified person by updating their record in the database.'; No verified external callers" status="ready"]
1256
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
1257
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
1258
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
893
1259
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
894
1260
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
1261
+ // CIL[DOC-MACRO purpose="The deny function processes an access request by marking a person as inactive and logging the action."]
1262
+ // CIL[DOC-RESPONSIBILITY responsibility="The deny function processes an access request by marking a person as inactive and logging the action. It ensures that only authenticated users can deny requests and that the specified person exists and is pending."]
1263
+ // CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
1264
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="Error — Authentication required, person_id is required, Access request not found or already processed, error from database update" side_effects="Updates the database record for the person and emits a log event. dependencies="adminDb.from('people'), emitLog"]
1265
+ // CIL[DOC-MICRO logic_summary="The deny function processes an access request by marking a person as inactive and logging the action. It ensures that only authenticated users can deny requests and that the specified person exists and is pending." notable_decisions="none"]
1266
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
1267
+ // CIL[DOC-STABILITY stability="stable"]
1268
+ /**
1269
+ * The deny function processes an access request by marking a person as inactive and logging the action. It ensures that only authenticated users can deny requests and that the specified person exists and is pending.
1270
+ *
1271
+ * @throws Error — Authentication required, person_id is required, Access request not found or already processed, error from database update
1272
+ * @sideEffects Updates the database record for the person and emits a log event.
1273
+ * @stability stable
1274
+ * @calls adminDb.from('people'), emitLog
1275
+ * @cil-generated
1276
+ */
1277
+ // CIL[BEGIN stable_id="~/.framework/functions/invites.ts:deny:constant" type="constant" identity="deny" parent="~/.framework/functions/invites.ts"]
1278
+ // CIL[DOC-MACRO purpose="deny constant"]
1279
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the deny constant."]
1280
+ // CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
1281
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
1282
+ // CIL[DOC-MICRO logic_summary="Implements the deny constant." notable_decisions="[]"]
1283
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
1284
+ // CIL[DOC-STABILITY stability="stable"]
1285
+ // CIL[DOC-NOTE type="security" content="Registration/invitation hardening: now requires an admin of the target person's account (or system_admin) via requireAccountAdmin — previously any authenticated principal could deny."]
895
1286
  export const deny = createHandler(async (ctx, body) => {
896
1287
  if (!ctx.principal || ctx.principal.id === 'anonymous') throw new Error('Authentication required')
897
1288
 
@@ -907,6 +1298,9 @@ export const deny = createHandler(async (ctx, body) => {
907
1298
 
908
1299
  if (!person) throw new Error('Access request not found or already processed')
909
1300
 
1301
+ // Scope: only an admin of the requesting account (or system_admin).
1302
+ requireAccountAdmin(ctx, person.account_id)
1303
+
910
1304
  const { error } = await adminDb
911
1305
  .from('people')
912
1306
  .update({
@@ -924,21 +1318,37 @@ export const deny = createHandler(async (ctx, body) => {
924
1318
 
925
1319
  return { success: true }
926
1320
  })
927
- // CIL[END chunk_id="invites_deny_1783968140"]
928
- // CIL[BEGIN chunk_id="invites_handler_1783968140" type="function" identity="handler" parent="none"]
929
- // CIL[DOC-MACRO purpose="Route invite actions to appropriate handler functions"]
930
- // CIL[DOC-RESPONSIBILITY responsibility="Dispatch invite API requests to correct handlers"]
931
- // CIL[DOC-CONTEXT module="invites" layer="api-handler" related_files="[]"]
932
- // CIL[DOC-CONTRACT inputs="none" outputs="Varies by action" errors="Unknown action" side_effects="Delegates to handlers"]
933
- // CIL[DOC-MICRO logic_summary="Switch statement routes action parameter to appropriate handler function" notable_decisions="[]"]
934
- // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
935
- // CIL[DOC-STABILITY stability="stable"]
936
- // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
937
- // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
938
- // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
939
- // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
1321
+ // CIL[END stable_id="~/.framework/functions/invites.ts:deny:constant"]
1322
+ // CIL[DOC-VECTOR type="context" content="purpose='The deny function processes an access request by marking a person as inactive and logging the action.'; No verified external callers" status="ready"]
1323
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
1324
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
1325
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
940
1326
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
941
1327
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
1328
+ // CIL[DOC-MACRO purpose="The handler processes various actions related to user invites and member management based on the provided context and body."]
1329
+ // CIL[DOC-RESPONSIBILITY responsibility="The handler processes various actions related to user invites and member management based on the provided context and body. It serves as a centralized entry point for handling invite-related operations in the application."]
1330
+ // CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
1331
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="Error — Unknown action: {action}" side_effects="none dependencies="checkFirstUser, checkDomain, validateToken, completeRegistration, createInvite, listInvites, revokeInvite, reinvite, blockInvite, approve, deny, listMembers, updateMember, deactivateMember"]
1332
+ // CIL[DOC-MICRO logic_summary="The handler processes various actions related to user invites and member management based on the provided context and body. It serves as a centralized entry point for handling invite-related operations in the application." notable_decisions="none"]
1333
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
1334
+ // CIL[DOC-STABILITY stability="stable"]
1335
+ /**
1336
+ * The handler processes various actions related to user invites and member management based on the provided context and body. It serves as a centralized entry point for handling invite-related operations in the application.
1337
+ *
1338
+ * @throws Error — Unknown action: {action}
1339
+ * @sideEffects none
1340
+ * @stability stable
1341
+ * @calls checkFirstUser, checkDomain, validateToken, completeRegistration, createInvite, listInvites, revokeInvite, reinvite, blockInvite, approve, deny, listMembers, updateMember, deactivateMember
1342
+ * @cil-generated
1343
+ */
1344
+ // CIL[BEGIN stable_id="~/.framework/functions/invites.ts:handler:constant" type="constant" identity="handler" parent="~/.framework/functions/invites.ts"]
1345
+ // CIL[DOC-MACRO purpose="handler constant"]
1346
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the handler constant."]
1347
+ // CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
1348
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
1349
+ // CIL[DOC-MICRO logic_summary="Implements the handler constant." notable_decisions="[]"]
1350
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
1351
+ // CIL[DOC-STABILITY stability="stable"]
942
1352
  export const handler = createPublicHandler(async (ctx, body) => {
943
1353
  const action = ctx.query?.action
944
1354
 
@@ -961,16 +1371,86 @@ export const handler = createPublicHandler(async (ctx, body) => {
961
1371
  throw new Error(`Unknown action: ${action}`)
962
1372
  }
963
1373
  })
964
- // CIL[END chunk_id="invites_handler_1783968140"]
1374
+ // CIL[END stable_id="~/.framework/functions/invites.ts:handler:constant"]
1375
+ // CIL[DOC-VECTOR type="context" content="purpose='The handler processes various actions related to user invites and member management based on the provided context and body.'; No verified external callers" status="ready"]
1376
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
1377
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
1378
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
1379
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
1380
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
965
1381
 
966
1382
  // ─── Member management actions (runtime CRUD via admin-data) ─────────────────
967
1383
 
1384
+ // CIL[DOC-MACRO purpose="Determines if a user has the necessary permissions to manage members based on their principal context."]
1385
+ // CIL[DOC-RESPONSIBILITY responsibility="Determines if a user has the necessary permissions to manage members based on their principal context. This function is essential for enforcing access control in member management operations."]
1386
+ // CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
1387
+ // CIL[DOC-CONTRACT inputs="ctx: any (required) — the context object containing user principal information." outputs="boolean — returns true if the user can manage members, otherwise false." errors="none" side_effects="none dependencies="isAdminRole"]
1388
+ // CIL[DOC-MICRO logic_summary="Determines if a user has the necessary permissions to manage members based on their principal context. This function is essential for enforcing access control in member management operations." notable_decisions="none"]
1389
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
1390
+ // CIL[DOC-STABILITY stability="stable"]
1391
+ /**
1392
+ * Determines if a user has the necessary permissions to manage members based on their principal context. This function is essential for enforcing access control in member management operations.
1393
+ *
1394
+ * @inputSpec
1395
+ * ctx: any (required) — the context object containing user principal information.
1396
+ * @outputSpec boolean — returns true if the user can manage members, otherwise false.
1397
+ * @sideEffects none
1398
+ * @stability stable
1399
+ * @calls isAdminRole
1400
+ * @cil-generated
1401
+ */
1402
+ // CIL[BEGIN stable_id="~/.framework/functions/invites.ts:canmanagemembers:function" type="function" identity="canManageMembers" parent="~/.framework/functions/invites.ts"]
1403
+ // CIL[DOC-MACRO purpose="canManageMembers function"]
1404
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the canManageMembers function."]
1405
+ // CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
1406
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
1407
+ // CIL[DOC-MICRO logic_summary="Implements the canManageMembers function." notable_decisions="[]"]
1408
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
1409
+ // CIL[DOC-STABILITY stability="stable"]
968
1410
  function canManageMembers(ctx: any) {
969
1411
  if (!ctx.principal || ctx.principal.id === 'anonymous') return false
970
1412
  if (ctx.principal.permissions?.includes('system')) return true
971
1413
  return (ctx.principal.roles || []).some((r: string) => isAdminRole(r))
972
1414
  }
1415
+ // CIL[END stable_id="~/.framework/functions/invites.ts:canmanagemembers:function"]
1416
+ // CIL[DOC-VECTOR type="context" content="purpose='Determines if a user has the necessary permissions to manage members based on their principal context.'; No verified external callers" status="ready"]
1417
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
1418
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
1419
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
1420
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
1421
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
973
1422
 
1423
+ // CIL[DOC-MACRO purpose="Async function that retrieves and formats member data for a given account."]
1424
+ // CIL[DOC-RESPONSIBILITY responsibility="Lists members associated with a specific account, requiring admin permissions. It retrieves member data and their roles from the database."]
1425
+ // CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
1426
+ // CIL[DOC-CONTRACT inputs="ctx: any (required) — the context object containing query and principal information.\\nbody: any (required) — the request body containing account_id." outputs="Promise<{ data: Array<any> }> — an object containing an array of member records with their roles." errors="Error — Insufficient permissions — *_admin role required to list members, Error — account_id is required" side_effects="Calls to the database to retrieve member and role information. dependencies="canManageMembers, adminDataList, adminDb.from"]
1427
+ // CIL[DOC-MICRO logic_summary="Utilizes an admin context to fetch member records and their associated roles from the database." notable_decisions="none"]
1428
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
1429
+ // CIL[DOC-STABILITY stability="stable"]
1430
+ /**
1431
+ * Lists members associated with a specific account, requiring admin permissions. It retrieves member data and their roles from the database.
1432
+ *
1433
+ * @macro Async function that retrieves and formats member data for a given account.
1434
+ * @micro Utilizes an admin context to fetch member records and their associated roles from the database.
1435
+ * @inputSpec
1436
+ * ctx: any (required) — the context object containing query and principal information.
1437
+ * body: any (required) — the request body containing account_id.
1438
+ * @outputSpec Promise<{ data: Array<any> }> — an object containing an array of member records with their roles.
1439
+ * @throws Error — Insufficient permissions — *_admin role required to list members, Error — account_id is required
1440
+ * @sideEffects Calls to the database to retrieve member and role information.
1441
+ * @stability stable
1442
+ * @calls canManageMembers, adminDataList, adminDb.from
1443
+ * @cil-generated
1444
+ */
1445
+ // CIL[BEGIN stable_id="~/.framework/functions/invites.ts:listmembers:function" type="function" identity="listMembers" parent="~/.framework/functions/invites.ts"]
1446
+ // CIL[DOC-MACRO purpose="listMembers function"]
1447
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the listMembers function."]
1448
+ // CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
1449
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
1450
+ // CIL[DOC-MICRO logic_summary="Implements the listMembers function." notable_decisions="[]"]
1451
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
1452
+ // CIL[DOC-STABILITY stability="stable"]
1453
+ // CIL[DOC-NOTE type="security" content="Registration/invitation hardening: the queried account_id is now bound to the caller via requireAccountAdmin — previously any *_admin could list members of any account."]
974
1454
  async function listMembers(ctx: any, body: any) {
975
1455
  if (!canManageMembers(ctx)) {
976
1456
  throw new Error('Insufficient permissions — *_admin role required to list members')
@@ -979,6 +1459,9 @@ async function listMembers(ctx: any, body: any) {
979
1459
  const accountId = ctx.query?.account_id || body?.account_id || ctx.principal?.accountId
980
1460
  if (!accountId) throw new Error('account_id is required')
981
1461
 
1462
+ // Scope: only an admin of the queried account (or system_admin).
1463
+ requireAccountAdmin(ctx, accountId)
1464
+
982
1465
  const adminCtx = { ...ctx, query: { ...ctx.query, entity: 'people', type_slug: 'person', account_id: accountId } }
983
1466
  const records = await adminDataList(adminCtx, {})
984
1467
 
@@ -1003,18 +1486,100 @@ async function listMembers(ctx: any, body: any) {
1003
1486
 
1004
1487
  return { data }
1005
1488
  }
1489
+ // CIL[END stable_id="~/.framework/functions/invites.ts:listmembers:function"]
1490
+ // CIL[DOC-VECTOR type="context" content="purpose='Lists members associated with a specific account, requiring admin permissions.'; No verified external callers" status="ready"]
1491
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
1492
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
1493
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
1494
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
1495
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
1006
1496
 
1007
- async function resolveRoleId(slug: string) {
1497
+ // CIL[DOC-MACRO purpose="Resolves the ID of a role based on its slug."]
1498
+ // CIL[DOC-RESPONSIBILITY responsibility="Resolves the ID of a role based on its slug. This function is essential for retrieving role identifiers in the application."]
1499
+ // CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
1500
+ // CIL[DOC-CONTRACT inputs="slug: string (required) — the unique identifier for the role." outputs="Promise<string> — the ID of the resolved role." errors="Error — Role 'slug' not found" side_effects="none dependencies="adminDb.from('roles')"]
1501
+ // CIL[DOC-MICRO logic_summary="Resolves the ID of a role based on its slug. This function is essential for retrieving role identifiers in the application." notable_decisions="none"]
1502
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
1503
+ // CIL[DOC-STABILITY stability="stable"]
1504
+ /**
1505
+ * Resolves the ID of a role based on its slug. This function is essential for retrieving role identifiers in the application.
1506
+ *
1507
+ * @inputSpec
1508
+ * slug: string (required) — the unique identifier for the role.
1509
+ * @outputSpec Promise<string> — the ID of the resolved role.
1510
+ * @throws Error — Role 'slug' not found
1511
+ * @sideEffects none
1512
+ * @stability stable
1513
+ * @calls adminDb.from('roles')
1514
+ * @cil-generated
1515
+ */
1516
+ // CIL[BEGIN stable_id="~/.framework/functions/invites.ts:resolveroleid:function" type="function" identity="resolveRoleId" parent="~/.framework/functions/invites.ts"]
1517
+ // CIL[DOC-MACRO purpose="resolveRoleId function"]
1518
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the resolveRoleId function."]
1519
+ // CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
1520
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
1521
+ // CIL[DOC-MICRO logic_summary="Implements the resolveRoleId function." notable_decisions="[]"]
1522
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
1523
+ // CIL[DOC-STABILITY stability="stable"]
1524
+ // CIL[DOC-NOTE type="security" content="Registration/invitation hardening: rejects system_admin and is_system roles and enforces an optional server-resolved allowlist — arbitrary role_slug (incl. system_admin) could previously be assigned to any member."]
1525
+ async function resolveRoleId(slug: string, allowlist?: string[] | null) {
1526
+ if (slug === 'system_admin') {
1527
+ throw new Error(`Role 'system_admin' is not permitted in this context`)
1528
+ }
1008
1529
  const { data: roleRows } = await adminDb
1009
1530
  .from('roles')
1010
- .select('id, slug')
1531
+ .select('id, slug, is_system')
1011
1532
  .eq('slug', slug)
1012
1533
  .limit(1)
1013
1534
  const resolved = roleRows?.[0]
1014
1535
  if (!resolved) throw new Error(`Role '${slug}' not found`)
1536
+ if (resolved.is_system === true) {
1537
+ throw new Error(`Role '${slug}' is not permitted in this context`)
1538
+ }
1539
+ if (allowlist && !allowlist.includes(slug)) {
1540
+ throw new Error(`Role '${slug}' is not permitted in this context`)
1541
+ }
1015
1542
  return resolved.id
1016
1543
  }
1544
+ // CIL[END stable_id="~/.framework/functions/invites.ts:resolveroleid:function"]
1545
+ // CIL[DOC-VECTOR type="context" content="purpose='Resolves the ID of a role based on its slug.'; No verified external callers" status="ready"]
1546
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
1547
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
1548
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
1549
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
1550
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
1017
1551
 
1552
+ // CIL[DOC-MACRO purpose="Function responsible for updating member information in the system."]
1553
+ // CIL[DOC-RESPONSIBILITY responsibility="Updates the member's details such as full name and role based on the provided input. It ensures that the caller has the necessary permissions before making any changes."]
1554
+ // CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
1555
+ // CIL[DOC-CONTRACT inputs="ctx: any (required) — the context containing user permissions and request details.\\nbody: any (required) — the object containing member update details including person_id, full_name, and role_slug." outputs="Promise<{ success: boolean, data: any }> — indicates success and returns the updated member data." errors="Error — Insufficient permissions, person_id is required, No update fields provided, Failed to update member" side_effects="Database update operation, logging of the update event dependencies="canManageMembers, resolveRoleId, adminDataUpdate, emitLog"]
1556
+ // CIL[DOC-MICRO logic_summary="Validates input fields and constructs an update record before making a database call." notable_decisions="none"]
1557
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
1558
+ // CIL[DOC-STABILITY stability="stable"]
1559
+ /**
1560
+ * Updates the member's details such as full name and role based on the provided input. It ensures that the caller has the necessary permissions before making any changes.
1561
+ *
1562
+ * @macro Function responsible for updating member information in the system.
1563
+ * @micro Validates input fields and constructs an update record before making a database call.
1564
+ * @inputSpec
1565
+ * ctx: any (required) — the context containing user permissions and request details.
1566
+ * body: any (required) — the object containing member update details including person_id, full_name, and role_slug.
1567
+ * @outputSpec Promise<{ success: boolean, data: any }> — indicates success and returns the updated member data.
1568
+ * @throws Error — Insufficient permissions, person_id is required, No update fields provided, Failed to update member
1569
+ * @sideEffects Database update operation, logging of the update event
1570
+ * @stability stable
1571
+ * @calls canManageMembers, resolveRoleId, adminDataUpdate, emitLog
1572
+ * @cil-generated
1573
+ */
1574
+ // CIL[BEGIN stable_id="~/.framework/functions/invites.ts:updatemember:function" type="function" identity="updateMember" parent="~/.framework/functions/invites.ts"]
1575
+ // CIL[DOC-MACRO purpose="updateMember function"]
1576
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the updateMember function."]
1577
+ // CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
1578
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
1579
+ // CIL[DOC-MICRO logic_summary="Implements the updateMember function." notable_decisions="[]"]
1580
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
1581
+ // CIL[DOC-STABILITY stability="stable"]
1582
+ // CIL[DOC-NOTE type="security" content="Registration/invitation hardening: resolves the target person's account first and requires an admin of THAT account (or system_admin) via requireAccountAdmin — previously any *_admin could update members cross-account and assign any role incl. system_admin."]
1018
1583
  async function updateMember(ctx: any, body: any) {
1019
1584
  if (!canManageMembers(ctx)) {
1020
1585
  throw new Error('Insufficient permissions — *_admin role required to update members')
@@ -1023,9 +1588,25 @@ async function updateMember(ctx: any, body: any) {
1023
1588
  const { person_id, full_name, role_slug } = body
1024
1589
  if (!person_id) throw new Error('person_id is required')
1025
1590
 
1591
+ const { data: target } = await adminDb
1592
+ .from('people')
1593
+ .select('id, account_id')
1594
+ .eq('id', person_id)
1595
+ .single()
1596
+
1597
+ if (!target) throw new Error('Person not found')
1598
+
1599
+ // Scope: only an admin of the member's own account (or system_admin).
1600
+ requireAccountAdmin(ctx, target.account_id)
1601
+
1026
1602
  const recordData: any = {}
1027
1603
  if (full_name !== undefined) recordData.full_name = full_name
1028
- if (role_slug !== undefined) recordData.role_id = await resolveRoleId(role_slug)
1604
+ if (role_slug !== undefined) {
1605
+ // Non-system roles only; additionally constrained to the member's
1606
+ // account-app allowlist when one is declared.
1607
+ const allowlist = await roleAllowlistForAccount(target.account_id)
1608
+ recordData.role_id = await resolveRoleId(role_slug, allowlist)
1609
+ }
1029
1610
 
1030
1611
  if (Object.keys(recordData).length === 0) {
1031
1612
  throw new Error('No update fields provided')
@@ -1042,7 +1623,43 @@ async function updateMember(ctx: any, body: any) {
1042
1623
  await emitLog(ctx, 'member.updated', { type: 'person', id: person_id }, { after: recordData })
1043
1624
  return { success: true, data: updated }
1044
1625
  }
1626
+ // CIL[END stable_id="~/.framework/functions/invites.ts:updatemember:function"]
1627
+ // CIL[DOC-VECTOR type="context" content="purpose='Updates the member's details such as full name and role based on the provided input.'; No verified external callers" status="ready"]
1628
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
1629
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
1630
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
1631
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
1632
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
1045
1633
 
1634
+ // CIL[DOC-MACRO purpose="Deactivates a member by updating their status to inactive."]
1635
+ // CIL[DOC-RESPONSIBILITY responsibility="Deactivates a member by updating their status to inactive. This function ensures that only users with the appropriate permissions can perform this action."]
1636
+ // CIL[DOC-CONTEXT module="invites" layer="app-specific" related_files=".framework/functions/_shared/middleware,.framework/functions/_shared/db,.framework/functions/_shared/audit,.framework/functions/_shared/rate-limit,.framework/functions/_shared/geo,.framework/functions/integration-outbound,.framework/functions/admin-data"]
1637
+ // CIL[DOC-CONTRACT inputs="ctx: any (required) — the context object containing user permissions and query parameters.\\nbody: any (required) — the request body containing the person_id of the member to deactivate." outputs="Promise<{ success: boolean, data: any }> — indicates success and returns the updated member data." errors="Error — Insufficient permissions — *_admin role required to deactivate members, Error — person_id is required, Error — Failed to deactivate member" side_effects="Updates the member's status in the database, emits a log event for the deactivation. dependencies="canManageMembers, adminDataUpdate, emitLog"]
1638
+ // CIL[DOC-MICRO logic_summary="Deactivates a member by updating their status to inactive. This function ensures that only users with the appropriate permissions can perform this action." notable_decisions="none"]
1639
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
1640
+ // CIL[DOC-STABILITY stability="stable"]
1641
+ /**
1642
+ * Deactivates a member by updating their status to inactive. This function ensures that only users with the appropriate permissions can perform this action.
1643
+ *
1644
+ * @inputSpec
1645
+ * ctx: any (required) — the context object containing user permissions and query parameters.
1646
+ * body: any (required) — the request body containing the person_id of the member to deactivate.
1647
+ * @outputSpec Promise<{ success: boolean, data: any }> — indicates success and returns the updated member data.
1648
+ * @throws Error — Insufficient permissions — *_admin role required to deactivate members, Error — person_id is required, Error — Failed to deactivate member
1649
+ * @sideEffects Updates the member's status in the database, emits a log event for the deactivation.
1650
+ * @stability stable
1651
+ * @calls canManageMembers, adminDataUpdate, emitLog
1652
+ * @cil-generated
1653
+ */
1654
+ // CIL[BEGIN stable_id="~/.framework/functions/invites.ts:deactivatemember:function" type="function" identity="deactivateMember" parent="~/.framework/functions/invites.ts"]
1655
+ // CIL[DOC-MACRO purpose="deactivateMember function"]
1656
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements the deactivateMember function."]
1657
+ // CIL[DOC-CONTEXT module=".framework/functions/invites.ts" layer="app-specific" related_files=""]
1658
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
1659
+ // CIL[DOC-MICRO logic_summary="Implements the deactivateMember function." notable_decisions="[]"]
1660
+ // CIL[DOC-CONFIDENCE pass="first" score="0.8" flags="none"]
1661
+ // CIL[DOC-STABILITY stability="stable"]
1662
+ // CIL[DOC-NOTE type="security" content="Registration/invitation hardening: resolves the target person's account first and requires an admin of THAT account (or system_admin) via requireAccountAdmin — previously any *_admin could deactivate members cross-account."]
1046
1663
  async function deactivateMember(ctx: any, body: any) {
1047
1664
  if (!canManageMembers(ctx)) {
1048
1665
  throw new Error('Insufficient permissions — *_admin role required to deactivate members')
@@ -1051,6 +1668,17 @@ async function deactivateMember(ctx: any, body: any) {
1051
1668
  const { person_id } = body
1052
1669
  if (!person_id) throw new Error('person_id is required')
1053
1670
 
1671
+ const { data: target } = await adminDb
1672
+ .from('people')
1673
+ .select('id, account_id')
1674
+ .eq('id', person_id)
1675
+ .single()
1676
+
1677
+ if (!target) throw new Error('Person not found')
1678
+
1679
+ // Scope: only an admin of the member's own account (or system_admin).
1680
+ requireAccountAdmin(ctx, target.account_id)
1681
+
1054
1682
  const adminCtx = {
1055
1683
  ...ctx,
1056
1684
  query: { ...ctx.query, entity: 'people', id: person_id }
@@ -1064,4 +1692,11 @@ async function deactivateMember(ctx: any, body: any) {
1064
1692
 
1065
1693
  await emitLog(ctx, 'member.deactivated', { type: 'person', id: person_id }, {})
1066
1694
  return { success: true, data: updated }
1067
- }
1695
+ }
1696
+ // CIL[END stable_id="~/.framework/functions/invites.ts:deactivatemember:function"]
1697
+ // CIL[DOC-VECTOR type="context" content="purpose='Deactivates a member by updating their status to inactive.'; No verified external callers" status="ready"]
1698
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only" status="ready"]
1699
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified" status="ready"]
1700
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented" status="ready"]
1701
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
1702
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]