spine-framework 0.3.145 → 0.4.1

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 +5067 -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
@@ -0,0 +1,572 @@
1
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/invites-security.test.ts:module_header:module_header" type="module_header" identity="module_header" parent="none"]
2
+ // CIL[DOC-MACRO purpose="Unit tests for the registration/invitation server-side hardening batch (migration 052 + invites.ts + principal.ts)"]
3
+ // CIL[DOC-RESPONSIBILITY responsibility="Verify principal-bound registration, read-only token preview, account-scoped admin checks, server-side role allowlists, base_url origin allowlist, and invite-stub binding resistance — all with a mocked adminDb."]
4
+ // CIL[DOC-CONTEXT module="tests/unit/invites-security" layer="test-unit" related_files=".framework/functions/invites.ts,.framework/functions/_shared/principal.ts,.framework/migrations/052_registration_security.sql"]
5
+ // CIL[DOC-CONTRACT inputs="none" outputs="vitest assertions" errors="none" side_effects="Mocks db/audit/rate-limit/security/geo/admin-data/integration-outbound modules"]
6
+ // CIL[DOC-MICRO logic_summary="Handlers wrapped by createHandler/createPublicHandler are invoked through the requestId+principal ctx shortcut; adminDb.from returns per-call queued chainable builders; adminDb.rpc is mocked directly." notable_decisions="RPC-internal logic (052 SQL) is exercised via mocked rpc results — these tests cover the endpoint contract, not plpgsql"]
7
+ // CIL[DOC-CONFIDENCE pass="first" score="0.85" flags="none"]
8
+ // CIL[DOC-STABILITY stability="stable"]
9
+ // CIL[DOC-AUDIENCE audience="test-engineers"]
10
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
11
+
12
+ vi.mock('../../functions/_shared/db.ts', () => ({
13
+ adminDb: {
14
+ from: vi.fn(),
15
+ rpc: vi.fn(),
16
+ },
17
+ getUserDb: vi.fn(() => ({})),
18
+ }))
19
+
20
+ vi.mock('../../functions/_shared/audit.ts', () => ({
21
+ emitAudit: vi.fn().mockResolvedValue(undefined),
22
+ emitLog: vi.fn().mockResolvedValue(undefined),
23
+ emitSpan: vi.fn().mockResolvedValue(undefined),
24
+ emitActivity: vi.fn().mockResolvedValue(undefined),
25
+ }))
26
+
27
+ vi.mock('../../functions/_shared/rate-limit.ts', () => ({
28
+ checkRateLimit: vi.fn().mockResolvedValue(undefined),
29
+ RateLimitError: class RateLimitError extends Error {},
30
+ }))
31
+
32
+ vi.mock('../../functions/_shared/security.ts', () => ({
33
+ checkRegistrationBlocklist: vi.fn().mockResolvedValue(undefined),
34
+ recordRegistrationGeo: vi.fn().mockResolvedValue(undefined),
35
+ isBlockedRequest: vi.fn().mockResolvedValue(null),
36
+ isBlockedIp: vi.fn().mockResolvedValue(null),
37
+ touchPersonGeo: vi.fn().mockResolvedValue(undefined),
38
+ }))
39
+
40
+ vi.mock('../../functions/_shared/geo.ts', () => ({
41
+ getClientGeo: vi.fn(() => ({ ip: undefined, country: undefined })),
42
+ }))
43
+
44
+ vi.mock('../../functions/admin-data.ts', () => ({
45
+ create: vi.fn(),
46
+ list: vi.fn(),
47
+ update: vi.fn(),
48
+ }))
49
+
50
+ vi.mock('../../functions/integration-outbound.ts', () => ({
51
+ execute: vi.fn().mockResolvedValue({ success: true }),
52
+ }))
53
+
54
+ import { adminDb } from '../../functions/_shared/db.ts'
55
+ import {
56
+ completeRegistration,
57
+ validateToken,
58
+ createInvite,
59
+ handler,
60
+ } from '../../functions/invites.ts'
61
+ import { resolveInternalPersonId } from '../../functions/_shared/principal.ts'
62
+ import { update as adminDataUpdate, list as adminDataList } from '../../functions/admin-data.ts'
63
+ // CIL[DOC-VECTOR type="context" content="purpose='Unit tests for the registration/invitation server-side hardening batch'; No verified external callers; imports: .framework/functions/invites.ts,.framework/functions/_shared/principal.ts" status="ready"]
64
+ // CIL[DOC-VECTOR type="blast_radius" content="Test file — no production callers; guards the migration-052 endpoint contract" status="ready"]
65
+ // CIL[DOC-VECTOR type="configuration" content="Reads APP_BASE_URL and ALLOWED_ORIGINS env vars via setEnv/afterEach restore" status="ready"]
66
+ // CIL[DOC-VECTOR type="constraint" content="All DB access is mocked — no real Supabase calls; RPC internals (052 SQL) are out of scope" status="ready"]
67
+ // CIL[END stable_id="~/.framework/tests/unit/invites-security.test.ts:module_header:module_header"]
68
+
69
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/invites-security.test.ts:q:function" type="function" identity="q" parent="none"]
70
+ // CIL[DOC-MACRO purpose="Build a chainable adminDb query mock resolving to a fixed result"]
71
+ // CIL[DOC-CONTRACT inputs="result: {data?, error?} — resolved value" outputs="chainable query-builder stub" errors="none" side_effects="none"]
72
+ // CIL[DOC-MICRO logic_summary="Every filter/terminal method returns the builder except single/maybeSingle/await which resolve to the configured {data,error} result"]
73
+ // CIL[DOC-STABILITY stability="stable"]
74
+ function q(result: { data?: any; error?: any }) {
75
+ const b: any = {}
76
+ const chainers = ['select', 'eq', 'in', 'is', 'ilike', 'filter', 'not', 'order',
77
+ 'limit', 'update', 'insert', 'upsert', 'delete', 'neq', 'or', 'contains', 'gte', 'lte']
78
+ for (const m of chainers) b[m] = vi.fn(() => b)
79
+ b.single = vi.fn(async () => result)
80
+ b.maybeSingle = vi.fn(async () => result)
81
+ b.then = (onF: any, onR: any) => Promise.resolve(result).then(onF, onR)
82
+ return b
83
+ }
84
+ // CIL[END stable_id="~/.framework/tests/unit/invites-security.test.ts:q:function"]
85
+
86
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/invites-security.test.ts:db_queue:variable" type="variable" identity="db_queue" parent="none"]
87
+ // CIL[DOC-MACRO purpose="Per-test queue of query builders consumed by adminDb.from"]
88
+ // CIL[DOC-CONTRACT inputs="key: env var name for setEnv, value: env var value or undefined to unset for setEnv, setEnv: env-restore helper, queue entries pushed per test" outputs="mocked adminDb.from/adminDb.rpc behavior" errors="none" side_effects="resets mocks per test; restores APP_BASE_URL/ALLOWED_ORIGINS after each"]
89
+ // CIL[DOC-MICRO logic_summary="Each adminDb.from call shifts the next configured builder; created builders are tracked so tests can assert zero writes"]
90
+ // CIL[DOC-STABILITY stability="stable"]
91
+ let queue: any[] = []
92
+ let built: any[] = []
93
+
94
+ beforeEach(() => {
95
+ queue = []
96
+ built = []
97
+ vi.clearAllMocks()
98
+ vi.mocked(adminDb.from).mockImplementation(() => {
99
+ const b = queue.length ? queue.shift() : q({ data: null, error: null })
100
+ built.push(b)
101
+ return b
102
+ })
103
+ vi.mocked(adminDb.rpc).mockResolvedValue({ data: null, error: null } as any)
104
+ })
105
+
106
+ const savedEnv: Record<string, string | undefined> = {}
107
+ afterEach(() => {
108
+ for (const k of ['APP_BASE_URL', 'ALLOWED_ORIGINS']) {
109
+ if (savedEnv[k] === undefined) delete process.env[k]
110
+ else process.env[k] = savedEnv[k]
111
+ delete savedEnv[k]
112
+ }
113
+ })
114
+
115
+ function setEnv(key: string, value: string | undefined) {
116
+ if (!(key in savedEnv)) savedEnv[key] = process.env[key]
117
+ if (value === undefined) delete process.env[key]
118
+ else process.env[key] = value
119
+ }
120
+ // CIL[END stable_id="~/.framework/tests/unit/invites-security.test.ts:db_queue:variable"]
121
+
122
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/invites-security.test.ts:makeCtx:function" type="function" identity="makeCtx" parent="none"]
123
+ // CIL[DOC-MACRO purpose="Create a RequestContext-shaped object that satisfies the createHandler/createPublicHandler ctx shortcut"]
124
+ // CIL[DOC-CONTRACT inputs="overrides — partial ctx/principal" outputs="ctx object" errors="none" side_effects="none"]
125
+ // CIL[DOC-MICRO logic_summary="requestId + principal present means the middleware wrapper delegates straight to the inner handler with the second arg as body"]
126
+ // CIL[DOC-STABILITY stability="stable"]
127
+ function makeCtx(overrides: any = {}) {
128
+ const principal = {
129
+ id: 'person-1',
130
+ type: 'human',
131
+ accountId: 'acct-1',
132
+ roles: ['member'],
133
+ email: 'user@corp.com',
134
+ provenance: { sourceType: 'jwt', createdBy: 'person-1', invokedAt: new Date().toISOString() },
135
+ ...(overrides.principal || {}),
136
+ }
137
+ return {
138
+ requestId: 'req-test',
139
+ accountId: principal.accountId,
140
+ appId: null,
141
+ query: {},
142
+ headers: {},
143
+ ...overrides,
144
+ principal,
145
+ }
146
+ }
147
+ // CIL[END stable_id="~/.framework/tests/unit/invites-security.test.ts:makeCtx:function"]
148
+
149
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/invites-security.test.ts:expectNoWrites:function" type="function" identity="expectNoWrites" parent="none"]
150
+ // CIL[DOC-MACRO purpose="Assert no query builder issued a write operation"]
151
+ // CIL[DOC-CONTRACT inputs="none" outputs="vitest assertions" errors="throws on any update/insert/delete mock call" side_effects="none"]
152
+ // CIL[DOC-MICRO logic_summary="Iterates every builder handed out by adminDb.from and asserts update/insert/delete were never invoked"]
153
+ // CIL[DOC-STABILITY stability="stable"]
154
+ function expectNoWrites() {
155
+ for (const b of built) {
156
+ expect(b.update?.mock?.calls?.length ?? 0, 'update() calls').toBe(0)
157
+ expect(b.insert?.mock?.calls?.length ?? 0, 'insert() calls').toBe(0)
158
+ expect(b.delete?.mock?.calls?.length ?? 0, 'delete() calls').toBe(0)
159
+ }
160
+ }
161
+ // CIL[END stable_id="~/.framework/tests/unit/invites-security.test.ts:expectNoWrites:function"]
162
+
163
+ // ─── completeRegistration ────────────────────────────────────────────────────
164
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/invites-security.test.ts:describe_completeRegistration:block" type="block" identity="describe_completeRegistration" parent="none"]
165
+ // CIL[DOC-MACRO purpose="completeRegistration binds by principal.id and delegates placement to the RPC"]
166
+ // CIL[DOC-CONTRACT inputs="vitest cases" outputs="assertions" errors="none" side_effects="none"]
167
+ // CIL[DOC-MICRO logic_summary="Covers principal-bound placement, required app/path, RPC error→400 mapping, idempotent retry, and `next` passthrough"]
168
+ // CIL[DOC-STABILITY stability="stable"]
169
+ describe('completeRegistration', () => {
170
+ it('binds placement to ctx.principal.id, never the body email', async () => {
171
+ vi.mocked(adminDb.rpc).mockResolvedValue({
172
+ data: { status: 'active', account_slug: 'acme-a1b2c3', role_slug: 'member' },
173
+ error: null,
174
+ } as any)
175
+
176
+ const res = await completeRegistration(makeCtx() as any, {
177
+ app: 'portal',
178
+ path: 'new_account',
179
+ account_name: 'Acme Co',
180
+ email: 'victim@other.example', // spoofed — must not drive placement
181
+ })
182
+
183
+ expect(adminDb.rpc).toHaveBeenCalledWith('complete_registration_placement', {
184
+ p_person_id: 'person-1',
185
+ p_app_slug: 'portal',
186
+ p_path: 'new_account',
187
+ p_account_name: 'Acme Co',
188
+ })
189
+ expect(res).toMatchObject({
190
+ status: 'active',
191
+ account_slug: 'acme-a1b2c3',
192
+ role_slug: 'member',
193
+ })
194
+ // The endpoint performs no direct people/accounts writes.
195
+ expect(adminDb.from).not.toHaveBeenCalled()
196
+ })
197
+
198
+ it('rejects when app or path is missing', async () => {
199
+ await expect(completeRegistration(makeCtx() as any, { path: 'new_account' }))
200
+ .rejects.toThrow('app and path are required')
201
+ await expect(completeRegistration(makeCtx() as any, { app: 'portal' }))
202
+ .rejects.toThrow('app and path are required')
203
+ expect(adminDb.rpc).not.toHaveBeenCalled()
204
+ })
205
+
206
+ it('maps RPC errors (unknown app, disabled registration, join_request policy) to a 400', async () => {
207
+ const cases = [
208
+ "Unknown or uninstalled app 'ghost' or no registration policy configured",
209
+ "Registration is not enabled for app 'portal'",
210
+ "App 'portal' does not allow domain-based join requests",
211
+ 'No account found for this email domain',
212
+ ]
213
+ for (const message of cases) {
214
+ vi.mocked(adminDb.rpc).mockResolvedValueOnce({ data: null, error: { message } } as any)
215
+ const err = await completeRegistration(makeCtx() as any, {
216
+ app: 'portal', path: 'join_request',
217
+ }).then(() => null).catch((e: any) => e)
218
+ expect(err).toBeTruthy()
219
+ expect(err.statusCode).toBe(400)
220
+ expect(err.message).toBe(message)
221
+ }
222
+ })
223
+
224
+ it('returns the existing placement unchanged on idempotent retry', async () => {
225
+ vi.mocked(adminDb.rpc).mockResolvedValue({
226
+ data: { status: 'active', account_slug: 'acme-a1b2c3', role_slug: 'member_admin' },
227
+ error: null,
228
+ } as any)
229
+ const res = await completeRegistration(makeCtx() as any, {
230
+ app: 'portal', path: 'new_account', account_name: 'Acme Co',
231
+ })
232
+ expect(res).toMatchObject({ status: 'active', account_slug: 'acme-a1b2c3', role_slug: 'member_admin' })
233
+ })
234
+
235
+ it('passes through a `next` field when the RPC supplies one', async () => {
236
+ vi.mocked(adminDb.rpc).mockResolvedValue({
237
+ data: { status: 'pending', account_slug: 'corp-zz99', role_slug: 'member', next: 'pending_approval' },
238
+ error: null,
239
+ } as any)
240
+ const res = await completeRegistration(makeCtx() as any, {
241
+ app: 'portal', path: 'join_request',
242
+ })
243
+ expect(res).toMatchObject({ status: 'pending', next: 'pending_approval' })
244
+ })
245
+ })
246
+ // CIL[END stable_id="~/.framework/tests/unit/invites-security.test.ts:describe_completeRegistration:block"]
247
+
248
+ // ─── validateToken ───────────────────────────────────────────────────────────
249
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/invites-security.test.ts:describe_validateToken:block" type="block" identity="describe_validateToken" parent="none"]
250
+ // CIL[DOC-MACRO purpose="validateToken is read-only: zero writes on preview, expired, blocked, and legacy attempt-cap paths"]
251
+ // CIL[DOC-CONTRACT inputs="data: invite metadata merged onto a stub person fixture (invitePerson); vitest cases" outputs="assertions" errors="none" side_effects="none"]
252
+ // CIL[DOC-MICRO logic_summary="Success/blocked/expired/legacy-attempt-cap previews all assert expectNoWrites"]
253
+ // CIL[DOC-STABILITY stability="stable"]
254
+ describe('validateToken', () => {
255
+ function invitePerson(data: any) {
256
+ return {
257
+ id: 'p-inv', email: 'inv@corp.com', account_id: 'acct-1', role_id: 'r1',
258
+ status: 'invited', data,
259
+ account: { display_name: 'Corp' }, role: { name: 'Member' },
260
+ }
261
+ }
262
+
263
+ it('performs zero writes on a successful preview', async () => {
264
+ queue.push(q({
265
+ data: invitePerson({ invite_token: 'tok', invite_kind: 'invite', invite_attempts: 0 }),
266
+ }))
267
+ const ctx: any = makeCtx()
268
+ ctx.query = { token: 'tok' }
269
+ const res = await validateToken(ctx, undefined)
270
+ expect(res).toMatchObject({ email: 'inv@corp.com', account_name: 'Corp', role_name: 'Member' })
271
+ expectNoWrites()
272
+ })
273
+
274
+ it('rejects blocked and expired invites without writing', async () => {
275
+ queue.push(q({
276
+ data: invitePerson({ invite_token: 'tok', invite_blocked_at: new Date().toISOString() }),
277
+ }))
278
+ const ctx: any = makeCtx()
279
+ ctx.query = { token: 'tok' }
280
+ await expect(validateToken(ctx, undefined)).rejects.toThrow('blocked')
281
+ expectNoWrites()
282
+
283
+ queue.push(q({
284
+ data: invitePerson({ invite_token: 'tok', invite_expires_at: '2000-01-01T00:00:00Z' }),
285
+ }))
286
+ const ctx2: any = makeCtx()
287
+ ctx2.query = { token: 'tok' }
288
+ await expect(validateToken(ctx2, undefined)).rejects.toThrow('expired')
289
+ expectNoWrites()
290
+ })
291
+
292
+ it('surfaces the blocked state for legacy rows at the attempts cap without writing', async () => {
293
+ queue.push(q({
294
+ data: invitePerson({ invite_token: 'tok', invite_attempts: 5 }),
295
+ }))
296
+ const ctx: any = makeCtx()
297
+ ctx.query = { token: 'tok' }
298
+ await expect(validateToken(ctx, undefined)).rejects.toThrow('Too many failed attempts')
299
+ expectNoWrites()
300
+ })
301
+ })
302
+ // CIL[END stable_id="~/.framework/tests/unit/invites-security.test.ts:describe_validateToken:block"]
303
+
304
+ // ─── Account-scoped admin checks ─────────────────────────────────────────────
305
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/invites-security.test.ts:describe_accountScopedAdmin:block" type="block" identity="describe_accountScopedAdmin" parent="none"]
306
+ // CIL[DOC-MACRO purpose="All invite/member mutations require an admin of the TARGET account (or system_admin)"]
307
+ // CIL[DOC-CONTRACT inputs="vitest cases" outputs="assertions" errors="none" side_effects="none"]
308
+ // CIL[DOC-MICRO logic_summary="Matrix: 7 mutations × (cross-account admin rejects / same-account admin accepts / system_admin accepts / member rejects) plus listMembers scope, reinvite status gate, and updateMember role guards"]
309
+ // CIL[DOC-STABILITY stability="stable"]
310
+ describe('account-scoped admin checks', () => {
311
+ const otherAccountPerson = { id: 'p2', email: 't@corp.com', account_id: 'acct-2', status: 'invited', data: {} }
312
+
313
+ const mutationCases: Array<{ action: string; person: any }> = [
314
+ { action: 'revoke-invite', person: { ...otherAccountPerson, status: 'invited' } },
315
+ { action: 'reinvite', person: { ...otherAccountPerson, status: 'invited' } },
316
+ { action: 'approve', person: { ...otherAccountPerson, status: 'pending' } },
317
+ { action: 'block-invite', person: { ...otherAccountPerson } },
318
+ { action: 'deny', person: { ...otherAccountPerson, status: 'pending' } },
319
+ { action: 'update-member', person: { ...otherAccountPerson, status: 'active' } },
320
+ { action: 'deactivate-member', person: { ...otherAccountPerson, status: 'active' } },
321
+ ]
322
+
323
+ for (const { action, person } of mutationCases) {
324
+ it(`${action}: rejects an *_admin of a different account`, async () => {
325
+ queue.push(q({ data: person })) // target person lookup
326
+ const ctx: any = makeCtx({
327
+ principal: { accountId: 'acct-1', roles: ['member_admin'] },
328
+ query: { action },
329
+ })
330
+ await expect(handler(ctx, { person_id: 'p2', full_name: 'X' }))
331
+ .rejects.toThrow('Insufficient permissions')
332
+ })
333
+
334
+ it(`${action}: accepts an *_admin of the target account`, async () => {
335
+ queue.push(q({ data: person })) // target person lookup
336
+ queue.push(q({ data: null, error: null })) // update result / allowlist read
337
+ queue.push(q({ data: [{ id: 'r-member', slug: 'member', is_system: false }] }))
338
+ vi.mocked(adminDataUpdate).mockResolvedValue({ id: 'p2' } as any)
339
+ const ctx: any = makeCtx({
340
+ principal: { accountId: 'acct-2', roles: ['member_admin'] },
341
+ query: { action },
342
+ })
343
+ const res = await handler(ctx, { person_id: 'p2', full_name: 'X' })
344
+ expect(res).toBeTruthy()
345
+ })
346
+
347
+ it(`${action}: accepts system_admin regardless of account`, async () => {
348
+ queue.push(q({ data: person }))
349
+ queue.push(q({ data: null, error: null }))
350
+ queue.push(q({ data: [{ id: 'r-member', slug: 'member', is_system: false }] }))
351
+ vi.mocked(adminDataUpdate).mockResolvedValue({ id: 'p2' } as any)
352
+ const ctx: any = makeCtx({
353
+ principal: { accountId: 'acct-9', roles: ['system_admin'], permissions: ['system'] },
354
+ query: { action },
355
+ })
356
+ const res = await handler(ctx, { person_id: 'p2', full_name: 'X' })
357
+ expect(res).toBeTruthy()
358
+ })
359
+
360
+ it(`${action}: rejects a plain member`, async () => {
361
+ queue.push(q({ data: person }))
362
+ const ctx: any = makeCtx({
363
+ principal: { accountId: 'acct-2', roles: ['member'] },
364
+ query: { action },
365
+ })
366
+ await expect(handler(ctx, { person_id: 'p2' }))
367
+ .rejects.toThrow('Insufficient permissions')
368
+ })
369
+ }
370
+
371
+ it('list-members: rejects an *_admin of a different account', async () => {
372
+ const ctx: any = makeCtx({
373
+ principal: { accountId: 'acct-1', roles: ['member_admin'] },
374
+ query: { action: 'list-members', account_id: 'acct-2' },
375
+ })
376
+ await expect(handler(ctx, {})).rejects.toThrow('Insufficient permissions')
377
+ })
378
+
379
+ it('list-members: accepts an *_admin of the queried account', async () => {
380
+ queue.push(q({ data: [{ id: 'r-member', slug: 'member', name: 'Member' }] }))
381
+ vi.mocked(adminDataList).mockResolvedValue([{ id: 'p2', role_id: 'r-member' }] as any)
382
+ const ctx: any = makeCtx({
383
+ principal: { accountId: 'acct-2', roles: ['member_admin'] },
384
+ query: { action: 'list-members', account_id: 'acct-2' },
385
+ })
386
+ const res = await handler(ctx, {})
387
+ expect(res.data?.[0]?.role).toMatchObject({ slug: 'member' })
388
+ })
389
+
390
+ it('reinvite: rejects when the target person is active', async () => {
391
+ queue.push(q({ data: { ...otherAccountPerson, status: 'active' } }))
392
+ const ctx: any = makeCtx({
393
+ principal: { accountId: 'acct-2', roles: ['member_admin'] },
394
+ query: { action: 'reinvite' },
395
+ })
396
+ await expect(handler(ctx, { person_id: 'p2' }))
397
+ .rejects.toThrow('reinvitable')
398
+ })
399
+
400
+ it('update-member: rejects a system_admin role_slug', async () => {
401
+ queue.push(q({ data: { ...otherAccountPerson, status: 'active' } })) // target lookup
402
+ queue.push(q({ data: [] })) // roleAllowlistForAccount
403
+ const ctx: any = makeCtx({
404
+ principal: { accountId: 'acct-2', roles: ['member_admin'] },
405
+ query: { action: 'update-member' },
406
+ })
407
+ await expect(handler(ctx, { person_id: 'p2', role_slug: 'system_admin' }))
408
+ .rejects.toThrow('not permitted')
409
+ expect(vi.mocked(adminDataUpdate)).not.toHaveBeenCalled()
410
+ })
411
+
412
+ it('update-member: rejects an is_system role_slug', async () => {
413
+ queue.push(q({ data: { ...otherAccountPerson, status: 'active' } }))
414
+ queue.push(q({ data: [] })) // roleAllowlistForAccount → no declared allowlist
415
+ queue.push(q({ data: [{ id: 'r-eng', slug: 'engineering', is_system: true }] }))
416
+ const ctx: any = makeCtx({
417
+ principal: { accountId: 'acct-2', roles: ['member_admin'] },
418
+ query: { action: 'update-member' },
419
+ })
420
+ await expect(handler(ctx, { person_id: 'p2', role_slug: 'engineering' }))
421
+ .rejects.toThrow('not permitted')
422
+ })
423
+ })
424
+ // CIL[END stable_id="~/.framework/tests/unit/invites-security.test.ts:describe_accountScopedAdmin:block"]
425
+
426
+ // ─── createInvite ────────────────────────────────────────────────────────────
427
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/invites-security.test.ts:describe_createInvite:block" type="block" identity="describe_createInvite" parent="none"]
428
+ // CIL[DOC-MACRO purpose="createInvite enforces account admin scope, server role allowlists, and base_url origin allowlist"]
429
+ // CIL[DOC-CONTRACT inputs="opts: optional {appRow} queued for the apps allowlist read (queueHappyPath); vitest cases" outputs="assertions" errors="none" side_effects="none"]
430
+ // CIL[DOC-MICRO logic_summary="Covers system_admin/is_system role rejection, app allowlist enforcement, and resolveInviteBaseUrl origin handling incl. yourapp.com absence"]
431
+ // CIL[DOC-STABILITY stability="stable"]
432
+ describe('createInvite', () => {
433
+ const baseBody = { email: 'new@corp.com', account_id: 'acct-1', role_slug: 'member' }
434
+ const adminCtx = () => makeCtx({ principal: { accountId: 'acct-1', roles: ['member_admin'] } })
435
+
436
+ // Queue order mirrors from() call order in createInvite: optional apps
437
+ // allowlist read (only when body.app/ctx.appId is set) → roles → existing
438
+ // person → person type → integrations.
439
+ function queueHappyPath(opts: { appRow?: any } = {}) {
440
+ if ('appRow' in opts) queue.push(q({ data: opts.appRow }))
441
+ queue.push(q({ data: [{ id: 'r-member', slug: 'member', is_system: false }] })) // roles
442
+ queue.push(q({ data: null })) // existing person
443
+ queue.push(q({ data: { id: 't-person' } })) // person type
444
+ queue.push(q({ data: null })) // integrations
445
+ }
446
+
447
+ it('rejects callers that are not admins of the target account', async () => {
448
+ await expect(createInvite(makeCtx() as any, baseBody))
449
+ .rejects.toThrow('Insufficient permissions')
450
+ const foreignAdmin = makeCtx({ principal: { accountId: 'acct-9', roles: ['member_admin'] } })
451
+ await expect(createInvite(foreignAdmin as any, baseBody))
452
+ .rejects.toThrow('Insufficient permissions')
453
+ })
454
+
455
+ it('rejects a forged system_admin role_slug', async () => {
456
+ await expect(createInvite(adminCtx() as any, { ...baseBody, role_slug: 'system_admin' }))
457
+ .rejects.toThrow('not permitted')
458
+ })
459
+
460
+ it('rejects an is_system role even when the slug exists', async () => {
461
+ queue.push(q({ data: [{ id: 'r-eng', slug: 'engineering', is_system: true }] }))
462
+ await expect(createInvite(adminCtx() as any, { ...baseBody, role_slug: 'engineering' }))
463
+ .rejects.toThrow('not permitted')
464
+ })
465
+
466
+ it('rejects roles outside the app allowlist', async () => {
467
+ queue.push(q({ data: { config: { invitation: { allowed_roles: ['member'] } } } }))
468
+ await expect(createInvite(adminCtx() as any, { ...baseBody, app: 'portal', role_slug: 'member_admin' }))
469
+ .rejects.toThrow('not permitted')
470
+ })
471
+
472
+ it('creates an invite and builds the link on an allowlisted origin', async () => {
473
+ setEnv('APP_BASE_URL', 'https://app.example.com')
474
+ queueHappyPath({ appRow: { config: { registration: { roles: ['member'] } } } })
475
+ const { create: adminDataCreate } = await import('../../functions/admin-data.ts')
476
+ vi.mocked(adminDataCreate).mockResolvedValue({ id: 'stub-1' } as any)
477
+
478
+ const res = await createInvite(adminCtx() as any, {
479
+ ...baseBody, app: 'portal', base_url: 'https://app.example.com/some/path',
480
+ })
481
+ expect(res.invite_url).toMatch(/^https:\/\/app\.example\.com\/register\?t=/)
482
+ expect(res.invite_url).not.toContain('yourapp.com')
483
+ })
484
+
485
+ it('allows localhost base_url for dev', async () => {
486
+ queueHappyPath()
487
+ const { create: adminDataCreate } = await import('../../functions/admin-data.ts')
488
+ vi.mocked(adminDataCreate).mockResolvedValue({ id: 'stub-1' } as any)
489
+ const res = await createInvite(adminCtx() as any, {
490
+ ...baseBody, base_url: 'http://localhost:3000',
491
+ })
492
+ expect(res.invite_url).toMatch(/^http:\/\/localhost:3000\/register\?t=/)
493
+ })
494
+
495
+ it('falls back to APP_BASE_URL for disallowed origins (never yourapp.com)', async () => {
496
+ setEnv('APP_BASE_URL', 'https://app.example.com')
497
+ queueHappyPath()
498
+ const { create: adminDataCreate } = await import('../../functions/admin-data.ts')
499
+ vi.mocked(adminDataCreate).mockResolvedValue({ id: 'stub-1' } as any)
500
+ const res = await createInvite(adminCtx() as any, {
501
+ ...baseBody, base_url: 'https://phish.example.org',
502
+ })
503
+ expect(res.invite_url).toMatch(/^https:\/\/app\.example\.com\/register\?t=/)
504
+ expect(res.invite_url).not.toContain('yourapp.com')
505
+ expect(res.invite_url).not.toContain('phish.example.org')
506
+ })
507
+
508
+ it('falls back to http://localhost:8888 when nothing is configured or allowed', async () => {
509
+ setEnv('APP_BASE_URL', undefined)
510
+ setEnv('ALLOWED_ORIGINS', undefined)
511
+ queueHappyPath()
512
+ const { create: adminDataCreate } = await import('../../functions/admin-data.ts')
513
+ vi.mocked(adminDataCreate).mockResolvedValue({ id: 'stub-1' } as any)
514
+ const res = await createInvite(adminCtx() as any, {
515
+ ...baseBody, base_url: 'not-a-url',
516
+ })
517
+ expect(res.invite_url).toMatch(/^http:\/\/localhost:8888\/register\?t=/)
518
+ })
519
+ })
520
+ // CIL[END stable_id="~/.framework/tests/unit/invites-security.test.ts:describe_createInvite:block"]
521
+
522
+ // ─── resolveInternalPersonId ─────────────────────────────────────────────────
523
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/invites-security.test.ts:describe_resolveInternalPersonId:block" type="block" identity="describe_resolveInternalPersonId" parent="none"]
524
+ // CIL[DOC-MACRO purpose="resolveInternalPersonId resolves auth_uid first and never binds invite stubs by email"]
525
+ // CIL[DOC-CONTRACT inputs="vitest cases" outputs="assertions" errors="none" side_effects="none"]
526
+ // CIL[DOC-MICRO logic_summary="auth_uid hit short-circuits; invited/pending/token-carrying stubs return authUserId unbound; non-stub unbound people still get auth_uid linked in a single write"]
527
+ // CIL[DOC-STABILITY stability="stable"]
528
+ describe('resolveInternalPersonId', () => {
529
+ it('resolves by auth_uid first without touching the email path', async () => {
530
+ queue.push(q({ data: { id: 'p-bound' } }))
531
+ const id = await resolveInternalPersonId('auth-1', 'someone@corp.com')
532
+ expect(id).toBe('p-bound')
533
+ expect(adminDb.from).toHaveBeenCalledTimes(1)
534
+ })
535
+
536
+ it('does not bind an unclaimed invited stub by email', async () => {
537
+ queue.push(q({ data: null })) // auth_uid lookup → miss
538
+ queue.push(q({
539
+ data: { id: 'stub-1', auth_uid: null, status: 'invited', data: { invite_token: 'abc' } },
540
+ }))
541
+ const id = await resolveInternalPersonId('auth-1', 'inv@corp.com')
542
+ expect(id).toBe('auth-1')
543
+ expectNoWrites()
544
+ })
545
+
546
+ it('does not bind an unclaimed pending stub by email', async () => {
547
+ queue.push(q({ data: null }))
548
+ queue.push(q({ data: { id: 'stub-2', auth_uid: null, status: 'pending', data: {} } }))
549
+ const id = await resolveInternalPersonId('auth-1', 'req@corp.com')
550
+ expect(id).toBe('auth-1')
551
+ expectNoWrites()
552
+ })
553
+
554
+ it('still binds a non-stub unbound person by email', async () => {
555
+ queue.push(q({ data: null }))
556
+ queue.push(q({ data: { id: 'p3', auth_uid: null, status: 'active', data: null } }))
557
+ queue.push(q({ data: null, error: null })) // update
558
+ const id = await resolveInternalPersonId('auth-1', 'active@corp.com')
559
+ expect(id).toBe('p3')
560
+ const updateBuilder = built.find(b => b.update?.mock?.calls?.length)
561
+ expect(updateBuilder).toBeTruthy()
562
+ expect(updateBuilder.update).toHaveBeenCalledWith({ auth_uid: 'auth-1' })
563
+ })
564
+
565
+ it('falls back to the auth id when nothing matches', async () => {
566
+ queue.push(q({ data: null }))
567
+ queue.push(q({ data: null }))
568
+ const id = await resolveInternalPersonId('auth-1', 'ghost@corp.com')
569
+ expect(id).toBe('auth-1')
570
+ })
571
+ })
572
+ // CIL[END stable_id="~/.framework/tests/unit/invites-security.test.ts:describe_resolveInternalPersonId:block"]