spine-framework 0.4.4 → 0.5.0-rc.4

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 (283) hide show
  1. package/.env.example +13 -3
  2. package/.framework/cli/commands/bootstrap.ts +392 -0
  3. package/.framework/cli/commands/dev.ts +91 -218
  4. package/.framework/cli/commands/doctor.ts +132 -26
  5. package/.framework/cli/commands/generate.ts +38 -298
  6. package/.framework/cli/commands/init.ts +20 -1
  7. package/.framework/cli/commands/migrate.ts +36 -30
  8. package/.framework/cli/commands/migrations.ts +16 -56
  9. package/.framework/cli/commands/upgrade.ts +35 -0
  10. package/.framework/cli/commands/validate-schema.ts +147 -0
  11. package/.framework/cli/context.ts +223 -110
  12. package/.framework/cli/index.ts +6 -0
  13. package/.framework/cli/lib/project-files.ts +241 -1
  14. package/.framework/functions/_shared/hash-api-key.ts +183 -79
  15. package/.framework/functions/_shared/logging.ts +562 -224
  16. package/.framework/functions/_shared/middleware.ts +47 -44
  17. package/.framework/functions/admin-data.ts +39 -28
  18. package/.framework/functions/ai-agents.ts +10 -6
  19. package/.framework/functions/api-keys.ts +301 -122
  20. package/.framework/functions/apps.ts +43 -26
  21. package/.framework/functions/cil-ingest.ts +48 -15
  22. package/.framework/functions/debug-auth.ts +14 -8
  23. package/.framework/functions/deliveries.ts +39 -12
  24. package/.framework/functions/embeddings.ts +145 -76
  25. package/.framework/functions/integration-outbound.ts +67 -27
  26. package/.framework/functions/integrations.ts +254 -127
  27. package/.framework/functions/logs.ts +273 -133
  28. package/.framework/functions/observability.ts +277 -147
  29. package/.framework/functions/pipeline-executions.ts +1 -1
  30. package/.framework/functions/pipelines.ts +14 -9
  31. package/.framework/functions/prompt-configs.ts +7 -4
  32. package/.framework/functions/roles.ts +8 -5
  33. package/.framework/functions/security.ts +30 -17
  34. package/.framework/functions/system-cron.ts +240 -127
  35. package/.framework/functions/tests.ts +7 -6
  36. package/.framework/functions/timers.ts +310 -107
  37. package/.framework/functions/triggers.ts +28 -13
  38. package/.framework/functions/types.ts +13 -10
  39. package/.framework/migrations/069_log_stats_window.sql +54 -0
  40. package/.framework/migrations/070_app_type_schema_rpcs.sql +166 -0
  41. package/.framework/migrations/071_timer_interval_keys.sql +96 -0
  42. package/.framework/migrations/072_slug_autofill.sql +77 -0
  43. package/.framework/migrations/073_seed_member_roles.sql +26 -0
  44. package/.framework/src/apps/admin/index.tsx +1141 -775
  45. package/.framework/src/components/ErrorBoundary.tsx +136 -0
  46. package/.framework/src/components/admin/AdminListPage.tsx +81 -73
  47. package/.framework/src/components/admin/AdminSidebar.tsx +88 -79
  48. package/.framework/src/components/admin/EntitySelect.tsx +181 -0
  49. package/.framework/src/components/admin/PageInstructions.tsx +86 -127
  50. package/.framework/src/components/app-shell/GenericDetailPage.tsx +37 -13
  51. package/.framework/src/components/app-shell/GenericListPage.tsx +28 -32
  52. package/.framework/src/components/layout/Sidebar.tsx +300 -177
  53. package/.framework/src/components/runtime/DataDetailPage.tsx +67 -35
  54. package/.framework/src/components/runtime/DataHeader.tsx +96 -48
  55. package/.framework/src/components/runtime/DataListPage.tsx +3 -26
  56. package/.framework/src/components/runtime/DataStats.tsx +10 -10
  57. package/.framework/src/components/runtime/DataTable.tsx +12 -10
  58. package/.framework/src/components/ui/sheet.tsx +24 -26
  59. package/.framework/src/components/ui/textarea.tsx +74 -48
  60. package/.framework/src/hooks/useEntityList.ts +12 -9
  61. package/.framework/src/hooks/useListSchema.ts +108 -58
  62. package/.framework/src/instructions/README.md +57 -0
  63. package/.framework/src/instructions/ai-agents.md +69 -0
  64. package/.framework/src/instructions/alerts.md +80 -0
  65. package/.framework/src/instructions/api-keys.md +87 -0
  66. package/.framework/src/instructions/apps.md +77 -0
  67. package/.framework/src/instructions/concepts/ai-and-prompts.md +150 -0
  68. package/.framework/src/instructions/concepts/apps-and-renderers.md +162 -0
  69. package/.framework/src/instructions/concepts/auth-and-api-keys.md +133 -0
  70. package/.framework/src/instructions/concepts/automation.md +186 -0
  71. package/.framework/src/instructions/concepts/concurrency-and-idempotency.md +125 -0
  72. package/.framework/src/instructions/concepts/core-vs-custom.md +124 -0
  73. package/.framework/src/instructions/concepts/design-schema.md +152 -0
  74. package/.framework/src/instructions/concepts/integrations-and-webhooks.md +127 -0
  75. package/.framework/src/instructions/concepts/observability.md +116 -0
  76. package/.framework/src/instructions/concepts/permissions.md +150 -0
  77. package/.framework/src/instructions/concepts/runtime-records.md +111 -0
  78. package/.framework/src/instructions/concepts/scope-and-accounts.md +112 -0
  79. package/.framework/src/instructions/concepts/spine-way.md +172 -0
  80. package/.framework/src/instructions/deliveries.md +59 -0
  81. package/.framework/src/instructions/guide/00-orientation.md +106 -0
  82. package/.framework/src/instructions/guide/01-install-and-first-run.md +253 -0
  83. package/.framework/src/instructions/guide/02-project-map.md +147 -0
  84. package/.framework/src/instructions/guide/03-domain-model.md +223 -0
  85. package/.framework/src/instructions/guide/04-first-app.md +227 -0
  86. package/.framework/src/instructions/guide/05-glossary.md +183 -0
  87. package/.framework/src/instructions/integrations.md +96 -0
  88. package/.framework/src/instructions/internal/cil-markers.md +83 -0
  89. package/.framework/src/instructions/invites.md +60 -0
  90. package/.framework/src/instructions/kb.md +80 -0
  91. package/.framework/src/instructions/logs.md +77 -0
  92. package/.framework/src/instructions/observability.md +61 -0
  93. package/.framework/src/instructions/operations/deployment.md +115 -0
  94. package/.framework/src/instructions/operations/migrations.md +125 -0
  95. package/.framework/src/instructions/operations/security-checklist.md +148 -0
  96. package/.framework/src/instructions/operations/troubleshooting.md +170 -0
  97. package/.framework/src/instructions/operations/upgrade.md +116 -0
  98. package/.framework/src/instructions/pipeline-executions.md +62 -0
  99. package/.framework/src/instructions/pipelines.md +78 -0
  100. package/.framework/src/instructions/prompts.md +95 -0
  101. package/.framework/src/instructions/reference/api/admin-data.md +162 -0
  102. package/.framework/src/instructions/reference/api/auth.md +113 -0
  103. package/.framework/src/instructions/reference/api/errors-and-envelopes.md +113 -0
  104. package/.framework/src/instructions/reference/app-manifest.md +150 -0
  105. package/.framework/src/instructions/reference/cli.md +124 -0
  106. package/.framework/src/instructions/reference/database.md +132 -0
  107. package/.framework/src/instructions/reference/design-schema.md +280 -0
  108. package/.framework/src/instructions/reference/env-vars.md +133 -0
  109. package/.framework/src/instructions/reference/functions/account-nodes.md +75 -0
  110. package/.framework/src/instructions/reference/functions/admin-data.md +195 -0
  111. package/.framework/src/instructions/reference/functions/admin-funnel.md +90 -0
  112. package/.framework/src/instructions/reference/functions/admin-install-funnel.md +107 -0
  113. package/.framework/src/instructions/reference/functions/admin-signals.md +102 -0
  114. package/.framework/src/instructions/reference/functions/ai-agents.md +148 -0
  115. package/.framework/src/instructions/reference/functions/ai-interactions.md +156 -0
  116. package/.framework/src/instructions/reference/functions/api-keys.md +130 -0
  117. package/.framework/src/instructions/reference/functions/apps.md +140 -0
  118. package/.framework/src/instructions/reference/functions/auth.md +99 -0
  119. package/.framework/src/instructions/reference/functions/debug-auth.md +91 -0
  120. package/.framework/src/instructions/reference/functions/deliveries.md +110 -0
  121. package/.framework/src/instructions/reference/functions/embeddings.md +166 -0
  122. package/.framework/src/instructions/reference/functions/eval-harness.md +149 -0
  123. package/.framework/src/instructions/reference/functions/index.md +103 -0
  124. package/.framework/src/instructions/reference/functions/integration-outbound.md +200 -0
  125. package/.framework/src/instructions/reference/functions/integration-routes.md +151 -0
  126. package/.framework/src/instructions/reference/functions/integrations.md +130 -0
  127. package/.framework/src/instructions/reference/functions/invites.md +182 -0
  128. package/.framework/src/instructions/reference/functions/item-progress.md +107 -0
  129. package/.framework/src/instructions/reference/functions/kb.md +166 -0
  130. package/.framework/src/instructions/reference/functions/logs.md +141 -0
  131. package/.framework/src/instructions/reference/functions/observability.md +105 -0
  132. package/.framework/src/instructions/reference/functions/pipeline-executions.md +132 -0
  133. package/.framework/src/instructions/reference/functions/pipeline-runner-background.md +121 -0
  134. package/.framework/src/instructions/reference/functions/pipelines.md +136 -0
  135. package/.framework/src/instructions/reference/functions/prompt-configs.md +121 -0
  136. package/.framework/src/instructions/reference/functions/roles.md +121 -0
  137. package/.framework/src/instructions/reference/functions/security.md +140 -0
  138. package/.framework/src/instructions/reference/functions/system-cron.md +136 -0
  139. package/.framework/src/instructions/reference/functions/system.md +106 -0
  140. package/.framework/src/instructions/reference/functions/tests.md +97 -0
  141. package/.framework/src/instructions/reference/functions/timers.md +117 -0
  142. package/.framework/src/instructions/reference/functions/trigger-scheduler.md +102 -0
  143. package/.framework/src/instructions/reference/functions/triggers.md +140 -0
  144. package/.framework/src/instructions/reference/functions/types.md +146 -0
  145. package/.framework/src/instructions/reference/seeds.md +126 -0
  146. package/.framework/src/instructions/reference/shared-modules.md +127 -0
  147. package/.framework/src/instructions/reference/spine-config.md +114 -0
  148. package/.framework/src/instructions/roles.md +80 -0
  149. package/.framework/src/instructions/runtime-detail.md +70 -0
  150. package/.framework/src/instructions/runtime-list.md +77 -0
  151. package/.framework/src/instructions/security.md +67 -0
  152. package/.framework/src/instructions/tasks/add-a-pipeline.md +110 -0
  153. package/.framework/src/instructions/tasks/add-a-role.md +96 -0
  154. package/.framework/src/instructions/tasks/add-a-timer.md +114 -0
  155. package/.framework/src/instructions/tasks/add-a-trigger.md +106 -0
  156. package/.framework/src/instructions/tasks/add-a-type.md +151 -0
  157. package/.framework/src/instructions/tasks/add-a-webhook.md +108 -0
  158. package/.framework/src/instructions/tasks/add-an-ai-agent.md +108 -0
  159. package/.framework/src/instructions/tasks/add-an-integration.md +124 -0
  160. package/.framework/src/instructions/tasks/build-a-custom-page.md +118 -0
  161. package/.framework/src/instructions/tasks/package-and-install-an-app.md +127 -0
  162. package/.framework/src/instructions/tasks/test-your-code.md +108 -0
  163. package/.framework/src/instructions/tasks/write-a-function.md +132 -0
  164. package/.framework/src/instructions/tasks/write-a-migration.md +110 -0
  165. package/.framework/src/instructions/testing.md +59 -0
  166. package/.framework/src/instructions/timers.md +82 -0
  167. package/.framework/src/instructions/triggers.md +69 -0
  168. package/.framework/src/instructions/types.md +93 -0
  169. package/.framework/src/pages/NotFoundPage.tsx +62 -31
  170. package/.framework/src/pages/admin/AIAgentDetailPage.tsx +117 -106
  171. package/.framework/src/pages/admin/AIAgentsPage.tsx +86 -48
  172. package/.framework/src/pages/admin/APIKeyDetailPage.tsx +153 -152
  173. package/.framework/src/pages/admin/APIKeysPage.tsx +91 -90
  174. package/.framework/src/pages/admin/AlertsConfigPage.tsx +29 -18
  175. package/.framework/src/pages/admin/AppDetailPage.tsx +6 -60
  176. package/.framework/src/pages/admin/AppsPage.tsx +104 -66
  177. package/.framework/src/pages/admin/DeliveriesPage.tsx +5 -5
  178. package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +5 -23
  179. package/.framework/src/pages/admin/EmbeddingsPage.tsx +5 -26
  180. package/.framework/src/pages/admin/IntegrationDetailPage.tsx +7 -7
  181. package/.framework/src/pages/admin/IntegrationsPage.tsx +85 -43
  182. package/.framework/src/pages/admin/LogsPage.tsx +144 -76
  183. package/.framework/src/pages/admin/ObservabilityDashboard.tsx +223 -140
  184. package/.framework/src/pages/admin/PipelineDetailPage.tsx +134 -113
  185. package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +108 -45
  186. package/.framework/src/pages/admin/PipelinesPage.tsx +160 -115
  187. package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +96 -98
  188. package/.framework/src/pages/admin/PromptConfigsPage.tsx +101 -59
  189. package/.framework/src/pages/admin/RoleDetailPage.tsx +90 -79
  190. package/.framework/src/pages/admin/RolesPage.tsx +86 -43
  191. package/.framework/src/pages/admin/SecurityPage.tsx +1 -1
  192. package/.framework/src/pages/admin/TestingDashboard.tsx +16 -28
  193. package/.framework/src/pages/admin/TimerDetailPage.tsx +117 -100
  194. package/.framework/src/pages/admin/TimersPage.tsx +90 -62
  195. package/.framework/src/pages/admin/TriggerDetailPage.tsx +90 -83
  196. package/.framework/src/pages/admin/TriggersPage.tsx +124 -99
  197. package/.framework/src/pages/admin/TypeDetailPage.tsx +152 -138
  198. package/.framework/src/pages/admin/TypesPage.tsx +90 -48
  199. package/.framework/src/pages/admin/cil/CILAgentPage.tsx +17 -25
  200. package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +17 -27
  201. package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +72 -40
  202. package/.framework/src/pages/admin/cil/CILFileListPage.tsx +321 -129
  203. package/.framework/src/pages/admin/cil/CILFileSelectPage.tsx +18 -29
  204. package/.framework/src/pages/admin/cil/CILGraphPage.tsx +26 -38
  205. package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +34 -46
  206. package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +189 -103
  207. package/.framework/src/pages/admin/cil/CILSearchPage.tsx +246 -136
  208. package/.framework/src/pages/admin/cil/CILSuggestionsPage.tsx +11 -22
  209. package/.framework/src/pages/admin/cil/CILVersionsPage.tsx +9 -20
  210. package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +26 -38
  211. package/.framework/src/pages/admin/kb/KBAdminPage.tsx +7 -26
  212. package/.framework/templates/agent/AGENTS.md +150 -0
  213. package/.framework/templates/agent/skills/spine-add-ai-agent/SKILL.md +58 -0
  214. package/.framework/templates/agent/skills/spine-add-automation/SKILL.md +62 -0
  215. package/.framework/templates/agent/skills/spine-add-integration/SKILL.md +63 -0
  216. package/.framework/templates/agent/skills/spine-architect/SKILL.md +86 -0
  217. package/.framework/templates/agent/skills/spine-install-app/SKILL.md +72 -0
  218. package/.framework/templates/agent/skills/spine-migration/SKILL.md +75 -0
  219. package/.framework/templates/agent/skills/spine-new-app/SKILL.md +67 -0
  220. package/.framework/templates/agent/skills/spine-new-function/SKILL.md +65 -0
  221. package/.framework/templates/agent/skills/spine-new-type/SKILL.md +65 -0
  222. package/.framework/templates/agent/skills/spine-schema/SKILL.md +77 -0
  223. package/.framework/templates/agent/skills/spine-test/SKILL.md +67 -0
  224. package/.framework/templates/agent/skills/spine-troubleshoot/SKILL.md +74 -0
  225. package/.framework/templates/agent/skills/spine-upgrade/SKILL.md +69 -0
  226. package/.framework/templates/project.json +4 -1
  227. package/.framework/tests/unit/account-scoping-gates.test.ts +392 -0
  228. package/.framework/tests/unit/agent-files.test.ts +355 -0
  229. package/.framework/tests/unit/agent-runner.test.ts +479 -44
  230. package/.framework/tests/unit/api-keys-create-scope-gate.test.ts +160 -0
  231. package/.framework/tests/unit/api-keys-omit-secret-fields.test.ts +38 -0
  232. package/.framework/tests/unit/bootstrap.test.ts +242 -0
  233. package/.framework/tests/unit/cli-context-api-key.test.ts +114 -0
  234. package/.framework/tests/unit/debug-auth.test.ts +38 -0
  235. package/.framework/tests/unit/deliveries-account-scoping.test.ts +185 -0
  236. package/.framework/tests/unit/integration-outbound-credentials.test.ts +167 -0
  237. package/.framework/tests/unit/integrations-audit-redaction.test.ts +130 -0
  238. package/.framework/tests/unit/logging-app-id.test.ts +75 -0
  239. package/.framework/tests/unit/system-context-denial.test.ts +145 -0
  240. package/.framework/tests/unit/system-cron.test.ts +240 -0
  241. package/.framework/tests/unit/timers.test.ts +159 -0
  242. package/README.md +125 -166
  243. package/config/tsconfig.cli.json +2 -2
  244. package/package.json +15 -4
  245. package/templates/project/package-lock.json +2 -2
  246. package/.framework/docs/admin-routes.md +0 -76
  247. package/.framework/docs/api-docs-progress.md +0 -38
  248. package/.framework/docs/api-governance.md +0 -146
  249. package/.framework/docs/api-testing-results.md +0 -212
  250. package/.framework/docs/assembly-launch-guide.md +0 -138
  251. package/.framework/docs/audit-results.md +0 -590
  252. package/.framework/docs/db-api-inventory.md +0 -95
  253. package/.framework/docs/gap-remediation-backlog.md +0 -103
  254. package/.framework/docs/ui-adoption-verification.md +0 -111
  255. package/.framework/docs/ui-api-coverage.md +0 -84
  256. package/.framework/docs/v2-compatibility-audit.md +0 -228
  257. package/.framework/mcp/index.ts +0 -133
  258. package/.framework/mcp/load-env.ts +0 -60
  259. package/.framework/mcp/shared/app-manifest.ts +0 -275
  260. package/.framework/mcp/shared/db.ts +0 -477
  261. package/.framework/mcp/shared/kb.ts +0 -118
  262. package/.framework/mcp/tools/architect.ts +0 -431
  263. package/.framework/mcp/tools/cil/context.ts +0 -345
  264. package/.framework/mcp/tools/cil/graph.ts +0 -242
  265. package/.framework/mcp/tools/cil/ingest.ts +0 -1217
  266. package/.framework/mcp/tools/cil/pipeline-orchestrator.ts +0 -693
  267. package/.framework/mcp/tools/cil/pipeline.ts +0 -767
  268. package/.framework/mcp/tools/cil/project.ts +0 -791
  269. package/.framework/mcp/tools/cil/retrieval.ts +0 -336
  270. package/.framework/mcp/tools/cil/search-core.ts +0 -454
  271. package/.framework/mcp/tools/cil/search.ts +0 -97
  272. package/.framework/mcp/tools/cil/suggest.ts +0 -186
  273. package/.framework/mcp/tools/cil/validate.ts +0 -1369
  274. package/.framework/mcp/tools/kb/delete.ts +0 -139
  275. package/.framework/mcp/tools/kb/index.ts +0 -52
  276. package/.framework/mcp/tools/kb/ingest.ts +0 -84
  277. package/.framework/mcp/tools/kb/list-types.ts +0 -102
  278. package/.framework/mcp/tools/kb/publish.ts +0 -100
  279. package/.framework/mcp/tools/kb/reindex.ts +0 -86
  280. package/.framework/mcp/tools/kb/revoke.ts +0 -85
  281. package/.framework/mcp/tools/kb/search.ts +0 -101
  282. package/.framework/mcp/tools/kb/upsert.ts +0 -125
  283. package/.framework/mcp/tools/observability.ts +0 -510
@@ -0,0 +1,392 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+
3
+ // S-007: machine principals resolve to the service-role adminDb via
4
+ // getPrincipalDb, so HTTP handlers that query on ctx.db or adminDb without an
5
+ // account filter were reachable cross-account by any API key. These tests pin
6
+ // the gates: non-system principals are scoped to ctx.accountId (or denied
7
+ // outright for platform-wide actions); system_admin and the internal
8
+ // SYSTEM_PRINCIPAL keep working.
9
+
10
+ vi.mock('../../functions/_shared/middleware.ts', () => ({
11
+ createHandler: (fn: any) => fn,
12
+ json: vi.fn(),
13
+ error: vi.fn(),
14
+ parseBody: vi.fn(),
15
+ }))
16
+
17
+ vi.mock('../../functions/_shared/db.ts', () => ({
18
+ adminDb: {
19
+ from: vi.fn(),
20
+ rpc: vi.fn(),
21
+ },
22
+ getUserDb: vi.fn(() => ({})),
23
+ }))
24
+
25
+ vi.mock('../../functions/_shared/audit.ts', () => ({
26
+ emitAudit: vi.fn().mockResolvedValue(undefined),
27
+ emitLog: vi.fn().mockResolvedValue(undefined),
28
+ emitSpan: vi.fn().mockResolvedValue(undefined),
29
+ emitActivity: vi.fn().mockResolvedValue(undefined),
30
+ }))
31
+
32
+ vi.mock('../../functions/_shared/permissions.ts', () => ({
33
+ sanitizeRecordData: vi.fn(async (_ctx: any, record: any) => record),
34
+ }))
35
+
36
+ vi.mock('../../functions/_shared/llm.ts', () => ({
37
+ callOpenAIEmbeddings: vi.fn(async (_ctx: any, inputs: string[]) => ({
38
+ data: inputs.map((_t, i) => ({ index: i, embedding: [0.1, 0.2, 0.3] })),
39
+ })),
40
+ callOpenAIChat: vi.fn(async () => ({ choices: [{ message: { content: 'summary' } }] })),
41
+ callOpenAI: vi.fn(async () => ({})),
42
+ callEmbeddingApi: vi.fn(async (inputs: string[]) => inputs.map(() => '[0.1,0.2]')),
43
+ }))
44
+
45
+ vi.mock('../../functions/_shared/logging.ts', () => ({
46
+ resolveAppUuid: vi.fn(async (ref: any) => ref ?? null),
47
+ }))
48
+
49
+ // admin-data pulls in permissions/trigger-engine — integration-outbound only
50
+ // calls get/update when record_id / update_record are set.
51
+ vi.mock('../../functions/admin-data.ts', () => ({
52
+ get: vi.fn(),
53
+ update: vi.fn(),
54
+ }))
55
+
56
+ import { adminDb } from '../../functions/_shared/db.ts'
57
+ import { cleanup as logsCleanup } from '../../functions/logs.ts'
58
+ import {
59
+ list as apiKeysList,
60
+ get as apiKeysGet,
61
+ validate as apiKeysValidate,
62
+ revoke as apiKeysRevoke,
63
+ listUsageLogs,
64
+ } from '../../functions/api-keys.ts'
65
+ import {
66
+ generate as embeddingsGenerate,
67
+ revoke as embeddingsRevoke,
68
+ approve as embeddingsApprove,
69
+ list as embeddingsList,
70
+ searchSimilar,
71
+ } from '../../functions/embeddings.ts'
72
+ import { execute as executeOutbound } from '../../functions/integration-outbound.ts'
73
+
74
+ // Chainable query-builder stub — every filter/terminal method returns the
75
+ // builder except single/maybeSingle/await, which resolve to the result.
76
+ function q(result: { data?: any; error?: any; count?: any }) {
77
+ const b: any = {}
78
+ const chainers = ['select', 'eq', 'in', 'is', 'ilike', 'filter', 'not', 'order',
79
+ 'limit', 'update', 'insert', 'upsert', 'delete', 'neq', 'or', 'contains',
80
+ 'gte', 'lte', 'lt', 'range', 'textSearch', 'or']
81
+ for (const m of chainers) b[m] = vi.fn(() => b)
82
+ b.single = vi.fn(async () => result)
83
+ b.maybeSingle = vi.fn(async () => result)
84
+ b.then = (onF: any, onR: any) => Promise.resolve(result).then(onF, onR)
85
+ return b
86
+ }
87
+
88
+ let queue: any[] = []
89
+ let built: any[] = []
90
+ let dbBuilt: any[] = []
91
+
92
+ beforeEach(() => {
93
+ queue = []
94
+ built = []
95
+ dbBuilt = []
96
+ vi.clearAllMocks()
97
+ vi.mocked(adminDb.from).mockImplementation(() => {
98
+ const b = queue.length ? queue.shift() : q({ data: null, error: null })
99
+ built.push(b)
100
+ return b
101
+ })
102
+ })
103
+
104
+ // ctx.db for non-system callers. In production a machine principal's ctx.db IS
105
+ // adminDb — the tests assert the .eq('account_id', ...) filter lands on it.
106
+ function stubDb() {
107
+ return {
108
+ from: vi.fn((table: string) => {
109
+ const b = queue.length ? queue.shift() : q({ data: null, error: null })
110
+ ;(b as any).__table = table
111
+ dbBuilt.push(b)
112
+ return b
113
+ }),
114
+ rpc: vi.fn(async () => ({ data: [], error: null })),
115
+ }
116
+ }
117
+
118
+ const scopedMachine = (accountId: string | null = 'acct-1', extra: any = {}) => ({
119
+ id: 'key-1',
120
+ type: 'machine',
121
+ accountId,
122
+ roles: [],
123
+ scopes: ['logs:read'],
124
+ isInternal: false,
125
+ machineType: 'integration',
126
+ provenance: { sourceType: 'api_key' },
127
+ ...extra,
128
+ })
129
+
130
+ const internalScopedMachine = (accountId: string | null = 'acct-1') => ({
131
+ ...scopedMachine(accountId),
132
+ isInternal: true,
133
+ })
134
+
135
+ const systemAdmin = () => ({
136
+ id: 'user-1',
137
+ type: 'human',
138
+ accountId: 'acct-1',
139
+ roles: ['system_admin'],
140
+ provenance: { sourceType: 'jwt' },
141
+ })
142
+
143
+ const systemPrincipal = () => ({
144
+ id: 'system',
145
+ type: 'machine',
146
+ accountId: null,
147
+ roles: [],
148
+ scopes: ['*:*'],
149
+ isInternal: true,
150
+ machineType: 'internal',
151
+ provenance: { sourceType: 'cron' },
152
+ })
153
+
154
+ function makeCtx(principal: any, overrides: any = {}) {
155
+ return {
156
+ requestId: 'req-1',
157
+ principal,
158
+ accountId: principal?.accountId ?? null,
159
+ db: stubDb(),
160
+ query: {},
161
+ headers: {},
162
+ ...overrides,
163
+ }
164
+ }
165
+
166
+ describe('logs cleanup gate (S-007)', () => {
167
+ it('denies a scoped machine principal with statusCode 403', async () => {
168
+ const ctx = makeCtx(scopedMachine())
169
+ await expect(logsCleanup(ctx, { days_to_keep: 30 })).rejects.toMatchObject({ statusCode: 403 })
170
+ expect(ctx.db.from).not.toHaveBeenCalled()
171
+ })
172
+
173
+ it('allows a system_admin principal', async () => {
174
+ queue.push(q({ data: [{ id: 'l1' }], error: null }))
175
+ const ctx = makeCtx(systemAdmin())
176
+ const res = await logsCleanup(ctx, { days_to_keep: 30 })
177
+ expect(res.deleted_count).toBe(1)
178
+ })
179
+
180
+ it('allows the internal SYSTEM_PRINCIPAL (no account binding)', async () => {
181
+ queue.push(q({ data: [{ id: 'l1' }], error: null }))
182
+ const ctx = makeCtx(systemPrincipal())
183
+ const res = await logsCleanup(ctx, {})
184
+ expect(res.deleted_count).toBe(1)
185
+ })
186
+ })
187
+
188
+ describe('api-keys account scoping (S-007)', () => {
189
+ it('get scopes the lookup to ctx.accountId for a machine principal', async () => {
190
+ const ctx = makeCtx(scopedMachine())
191
+ await expect(apiKeysGet({ ...ctx, query: { id: 'key-9' } }, {})).resolves.toBeDefined()
192
+ const b = dbBuilt[0]
193
+ expect(b.eq).toHaveBeenCalledWith('id', 'key-9')
194
+ expect(b.eq).toHaveBeenCalledWith('account_id', 'acct-1')
195
+ })
196
+
197
+ it('get does not scope for system_admin', async () => {
198
+ queue.push(q({ data: { id: 'key-9', name: 'k' }, error: null }))
199
+ await apiKeysGet({ ...makeCtx(systemAdmin()), query: { id: 'key-9' } }, {})
200
+ const b = built[0]
201
+ const accountEq = b.eq.mock.calls.filter((c: any[]) => c[0] === 'account_id')
202
+ expect(accountEq).toHaveLength(0)
203
+ })
204
+
205
+ it('get rejects a non-system principal without account context', async () => {
206
+ const ctx = makeCtx(scopedMachine(null))
207
+ await expect(apiKeysGet({ ...ctx, query: { id: 'key-9' } }, {})).rejects.toThrow('Account context required')
208
+ })
209
+
210
+ it('validate scopes the key lookup to ctx.accountId', async () => {
211
+ const ctx = makeCtx(scopedMachine())
212
+ const res = await apiKeysValidate(ctx, { key_value: 'sk_abcdef123456' })
213
+ expect(res.is_valid).toBe(false)
214
+ const b = dbBuilt[0]
215
+ expect(b.eq).toHaveBeenCalledWith('account_id', 'acct-1')
216
+ })
217
+
218
+ it('validate requires account context for non-system principals', async () => {
219
+ const ctx = makeCtx(scopedMachine(null))
220
+ await expect(apiKeysValidate(ctx, { key_value: 'sk_abcdef123456' })).rejects.toThrow('Account context required')
221
+ })
222
+
223
+ it('revoke scopes the update to ctx.accountId', async () => {
224
+ queue.push(q({ data: { id: 'key-9' }, error: null }))
225
+ const ctx = makeCtx(scopedMachine())
226
+ await apiKeysRevoke(ctx, { id: 'key-9' })
227
+ const b = dbBuilt[0]
228
+ expect(b.eq).toHaveBeenCalledWith('id', 'key-9')
229
+ expect(b.eq).toHaveBeenCalledWith('account_id', 'acct-1')
230
+ })
231
+
232
+ it('usage-logs scopes the query to ctx.accountId', async () => {
233
+ const ctx = makeCtx(scopedMachine())
234
+ await listUsageLogs(ctx, {})
235
+ const b = dbBuilt[0]
236
+ expect(b.eq).toHaveBeenCalledWith('account_id', 'acct-1')
237
+ })
238
+
239
+ it('list scopes the query to ctx.accountId for machine principals', async () => {
240
+ const ctx = makeCtx(scopedMachine())
241
+ await apiKeysList(ctx, {})
242
+ const b = dbBuilt[0]
243
+ expect(b.eq).toHaveBeenCalledWith('account_id', 'acct-1')
244
+ })
245
+ })
246
+
247
+ describe('embeddings account scoping (S-007)', () => {
248
+ const item = (accountId: string | null) => ({
249
+ id: 'item-1',
250
+ title: 'Doc',
251
+ description: 'body',
252
+ account_id: accountId,
253
+ data: {},
254
+ type: { slug: 'kb_article', design_schema: {} },
255
+ })
256
+
257
+ it('generate rejects a scoped caller on another account\u2019s item with 403', async () => {
258
+ queue.push(q({ data: item('acct-2'), error: null }))
259
+ const ctx = makeCtx(scopedMachine())
260
+ await expect(embeddingsGenerate(ctx, { item_id: 'item-1' })).rejects.toMatchObject({ statusCode: 403 })
261
+ })
262
+
263
+ it('generate writes vectors under ctx.accountId and ignores account_id_override for scoped callers', async () => {
264
+ queue.push(q({ data: item('acct-1'), error: null })) // items fetch
265
+ queue.push(q({ data: [], error: null })) // existing embeddings
266
+ const ctx = makeCtx(scopedMachine())
267
+ const res = await embeddingsGenerate(ctx, { item_id: 'item-1', account_id_override: 'acct-9' })
268
+ expect(res.embeddings_created).toBeGreaterThan(0)
269
+ const insertCalls = built.flatMap(b => b.insert?.mock.calls ?? [])
270
+ expect(insertCalls.length).toBeGreaterThan(0)
271
+ for (const [row] of insertCalls) {
272
+ expect(row.account_id).toBe('acct-1')
273
+ }
274
+ })
275
+
276
+ it('generate honors account_id_override for system_admin', async () => {
277
+ queue.push(q({ data: item('acct-1'), error: null }))
278
+ queue.push(q({ data: [], error: null }))
279
+ const ctx = makeCtx(systemAdmin())
280
+ await embeddingsGenerate(ctx, { item_id: 'item-1', account_id_override: 'acct-9' })
281
+ const insertCalls = built.flatMap(b => b.insert?.mock.calls ?? [])
282
+ expect(insertCalls.length).toBeGreaterThan(0)
283
+ for (const [row] of insertCalls) {
284
+ expect(row.account_id).toBe('acct-9')
285
+ }
286
+ })
287
+
288
+ it('revoke scopes the update to ctx.accountId', async () => {
289
+ queue.push(q({ data: [{ id: 'e1' }], error: null }))
290
+ const ctx = makeCtx(scopedMachine())
291
+ const res = await embeddingsRevoke(ctx, { document_id: 'doc-1' })
292
+ expect(res.revoked_count).toBe(1)
293
+ const b = built[0]
294
+ expect(b.eq).toHaveBeenCalledWith('document_id', 'doc-1')
295
+ expect(b.eq).toHaveBeenCalledWith('account_id', 'acct-1')
296
+ })
297
+
298
+ it('revoke does not scope for system_admin', async () => {
299
+ queue.push(q({ data: [{ id: 'e1' }], error: null }))
300
+ const ctx = makeCtx(systemAdmin())
301
+ await embeddingsRevoke(ctx, { document_id: 'doc-1' })
302
+ const accountEq = built[0].eq.mock.calls.filter((c: any[]) => c[0] === 'account_id')
303
+ expect(accountEq).toHaveLength(0)
304
+ })
305
+
306
+ it('approve scopes the update to ctx.accountId', async () => {
307
+ queue.push(q({ data: [{ id: 'e1' }], error: null }))
308
+ const ctx = makeCtx(scopedMachine())
309
+ const res = await embeddingsApprove(ctx, { document_id: 'doc-1' })
310
+ expect(res.approved_count).toBe(1)
311
+ expect(built[0].eq).toHaveBeenCalledWith('account_id', 'acct-1')
312
+ })
313
+
314
+ it('list scopes embeddings to ctx.accountId for machine principals', async () => {
315
+ const ctx = makeCtx(scopedMachine())
316
+ await embeddingsList(ctx, {})
317
+ expect(dbBuilt[0].eq).toHaveBeenCalledWith('account_id', 'acct-1')
318
+ })
319
+
320
+ it('searchSimilar forces filter_account_ids to ctx.accountId for scoped callers', async () => {
321
+ const ctx = makeCtx(scopedMachine())
322
+ await searchSimilar(ctx, {
323
+ query_embedding: [0.1],
324
+ filter_account_ids: ['acct-2'],
325
+ })
326
+ const rpcArgs = vi.mocked(ctx.db.rpc).mock.calls[0][1] as any
327
+ expect(rpcArgs.filter_account_ids).toEqual(['acct-1'])
328
+ })
329
+ })
330
+
331
+ describe('integration-outbound delivery_id account check (S-007)', () => {
332
+ const delivery = (accountId: string | null) => ({
333
+ id: 'del-1',
334
+ account_id: accountId,
335
+ direction: 'outbound',
336
+ status: 'pending',
337
+ attempt_count: 0,
338
+ max_attempts: 3,
339
+ payload: {
340
+ integration_slug: 'generic-http',
341
+ action_config: { url: 'https://hooks.example.com/in' },
342
+ },
343
+ })
344
+
345
+ it('rejects a scoped internal key replaying another account\u2019s delivery with 403', async () => {
346
+ queue.push(q({ data: delivery('acct-2'), error: null }))
347
+ const ctx = makeCtx(internalScopedMachine())
348
+ await expect(executeOutbound(ctx, { delivery_id: 'del-1' })).rejects.toMatchObject({ statusCode: 403 })
349
+ // Nothing beyond the delivery lookup should have run.
350
+ expect(vi.mocked(adminDb.from)).toHaveBeenCalledTimes(1)
351
+ })
352
+
353
+ it('rejects a non-internal scoped principal touching another account\u2019s delivery with 403', async () => {
354
+ queue.push(q({ data: delivery('acct-2'), error: null }))
355
+ const ctx = makeCtx(scopedMachine())
356
+ await expect(
357
+ executeOutbound(ctx, { delivery_id: 'del-1', integration_slug: 'generic-http', action_config: {} })
358
+ ).rejects.toMatchObject({ statusCode: 403 })
359
+ })
360
+
361
+ it('lets a scoped internal key replay a delivery in its own account', async () => {
362
+ queue.push(q({ data: delivery('acct-1'), error: null })) // delivery select
363
+ queue.push(q({ data: null, error: null })) // processing update
364
+ queue.push(q({ data: { id: 'int-1', slug: 'generic-http', credentials: {}, config: { outbound_adapter: 'adapters/generic-webhook-outbound' }, is_active: true }, error: null })) // tenant integration
365
+ queue.push(q({ data: null, error: null })) // result update
366
+
367
+ const fetchMock = vi.fn().mockResolvedValue({ ok: true, status: 200, text: async () => '{}' })
368
+ vi.stubGlobal('fetch', fetchMock)
369
+
370
+ const ctx = makeCtx(internalScopedMachine())
371
+ const res = await executeOutbound(ctx, { delivery_id: 'del-1' })
372
+ expect(res.success).toBe(true)
373
+ expect(res.is_retry).toBe(true)
374
+ vi.unstubAllGlobals()
375
+ })
376
+
377
+ it('lets the SYSTEM_PRINCIPAL replay any account\u2019s delivery (cron retry path)', async () => {
378
+ queue.push(q({ data: delivery('acct-2'), error: null })) // delivery select
379
+ queue.push(q({ data: null, error: null })) // processing update
380
+ queue.push(q({ data: { id: 'int-1', slug: 'generic-http', credentials: {}, config: { outbound_adapter: 'adapters/generic-webhook-outbound' }, is_active: true }, error: null })) // system integration
381
+ queue.push(q({ data: null, error: null })) // result update
382
+
383
+ const fetchMock = vi.fn().mockResolvedValue({ ok: true, status: 200, text: async () => '{}' })
384
+ vi.stubGlobal('fetch', fetchMock)
385
+
386
+ const ctx = makeCtx(systemPrincipal(), { accountId: null })
387
+ const res = await executeOutbound(ctx, { delivery_id: 'del-1' })
388
+ expect(res.success).toBe(true)
389
+ expect(res.is_retry).toBe(true)
390
+ vi.unstubAllGlobals()
391
+ })
392
+ })