spine-framework 0.4.4 → 0.5.0-rc.5

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 (305) 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/install-app.ts +183 -13
  8. package/.framework/cli/commands/migrate.ts +36 -30
  9. package/.framework/cli/commands/migrations.ts +16 -56
  10. package/.framework/cli/commands/upgrade.ts +35 -0
  11. package/.framework/cli/commands/validate-app.ts +127 -1
  12. package/.framework/cli/commands/validate-schema.ts +147 -0
  13. package/.framework/cli/context.ts +223 -110
  14. package/.framework/cli/index.ts +6 -0
  15. package/.framework/cli/lib/project-files.ts +241 -1
  16. package/.framework/functions/_shared/app-manifest.ts +2 -0
  17. package/.framework/functions/_shared/hash-api-key.ts +183 -79
  18. package/.framework/functions/_shared/logging.ts +562 -224
  19. package/.framework/functions/_shared/middleware.ts +47 -44
  20. package/.framework/functions/_shared/permissions.ts +4 -0
  21. package/.framework/functions/admin-data.ts +51 -28
  22. package/.framework/functions/ai-agents.ts +10 -6
  23. package/.framework/functions/api-keys.ts +301 -122
  24. package/.framework/functions/apps.ts +63 -29
  25. package/.framework/functions/cil-ingest.ts +48 -15
  26. package/.framework/functions/debug-auth.ts +14 -8
  27. package/.framework/functions/deliveries.ts +39 -12
  28. package/.framework/functions/embeddings.ts +149 -78
  29. package/.framework/functions/integration-outbound.ts +67 -27
  30. package/.framework/functions/integrations.ts +254 -127
  31. package/.framework/functions/logs.ts +273 -133
  32. package/.framework/functions/observability.ts +278 -148
  33. package/.framework/functions/pipeline-executions.ts +1 -1
  34. package/.framework/functions/pipelines.ts +14 -9
  35. package/.framework/functions/prompt-configs.ts +7 -4
  36. package/.framework/functions/roles.ts +8 -5
  37. package/.framework/functions/security.ts +30 -17
  38. package/.framework/functions/system-cron.ts +240 -127
  39. package/.framework/functions/tests.ts +7 -6
  40. package/.framework/functions/timers.ts +310 -107
  41. package/.framework/functions/triggers.ts +28 -13
  42. package/.framework/functions/types.ts +13 -10
  43. package/.framework/migrations/069_log_stats_window.sql +54 -0
  44. package/.framework/migrations/070_app_type_schema_rpcs.sql +166 -0
  45. package/.framework/migrations/071_timer_interval_keys.sql +96 -0
  46. package/.framework/migrations/072_slug_autofill.sql +77 -0
  47. package/.framework/migrations/073_seed_member_roles.sql +26 -0
  48. package/.framework/migrations/074_app_route_prefix_domains.sql +168 -0
  49. package/.framework/migrations/075_security_definer_grants.sql +157 -0
  50. package/.framework/src/apps/admin/index.tsx +1141 -775
  51. package/.framework/src/components/ErrorBoundary.tsx +136 -0
  52. package/.framework/src/components/admin/AdminListPage.tsx +81 -73
  53. package/.framework/src/components/admin/AdminSidebar.tsx +88 -79
  54. package/.framework/src/components/admin/EntitySelect.tsx +181 -0
  55. package/.framework/src/components/admin/PageInstructions.tsx +86 -127
  56. package/.framework/src/components/app-shell/GenericDetailPage.tsx +37 -13
  57. package/.framework/src/components/app-shell/GenericListPage.tsx +28 -32
  58. package/.framework/src/components/layout/Sidebar.tsx +300 -177
  59. package/.framework/src/components/runtime/DataDetailPage.tsx +67 -35
  60. package/.framework/src/components/runtime/DataHeader.tsx +96 -48
  61. package/.framework/src/components/runtime/DataListPage.tsx +3 -26
  62. package/.framework/src/components/runtime/DataStats.tsx +10 -10
  63. package/.framework/src/components/runtime/DataTable.tsx +12 -10
  64. package/.framework/src/components/ui/sheet.tsx +24 -26
  65. package/.framework/src/components/ui/textarea.tsx +74 -48
  66. package/.framework/src/contexts/AppContext.tsx +5 -12
  67. package/.framework/src/contexts/AuthContext.tsx +17 -9
  68. package/.framework/src/hooks/useApps.ts +18 -22
  69. package/.framework/src/hooks/useEntityList.ts +12 -9
  70. package/.framework/src/hooks/useListSchema.ts +108 -58
  71. package/.framework/src/instructions/README.md +74 -0
  72. package/.framework/src/instructions/accounts.md +97 -0
  73. package/.framework/src/instructions/ai-agents.md +69 -0
  74. package/.framework/src/instructions/alerts.md +80 -0
  75. package/.framework/src/instructions/api-keys.md +87 -0
  76. package/.framework/src/instructions/apps.md +77 -0
  77. package/.framework/src/instructions/concepts/ai-and-prompts.md +150 -0
  78. package/.framework/src/instructions/concepts/apps-and-renderers.md +228 -0
  79. package/.framework/src/instructions/concepts/auth-and-api-keys.md +133 -0
  80. package/.framework/src/instructions/concepts/automation.md +186 -0
  81. package/.framework/src/instructions/concepts/concurrency-and-idempotency.md +125 -0
  82. package/.framework/src/instructions/concepts/core-vs-custom.md +124 -0
  83. package/.framework/src/instructions/concepts/design-schema.md +152 -0
  84. package/.framework/src/instructions/concepts/integrations-and-webhooks.md +127 -0
  85. package/.framework/src/instructions/concepts/observability.md +116 -0
  86. package/.framework/src/instructions/concepts/permissions.md +150 -0
  87. package/.framework/src/instructions/concepts/runtime-records.md +111 -0
  88. package/.framework/src/instructions/concepts/scope-and-accounts.md +112 -0
  89. package/.framework/src/instructions/concepts/spine-way.md +172 -0
  90. package/.framework/src/instructions/deliveries.md +59 -0
  91. package/.framework/src/instructions/guide/00-orientation.md +106 -0
  92. package/.framework/src/instructions/guide/01-install-and-first-run.md +253 -0
  93. package/.framework/src/instructions/guide/02-project-map.md +147 -0
  94. package/.framework/src/instructions/guide/03-domain-model.md +223 -0
  95. package/.framework/src/instructions/guide/04-first-app.md +227 -0
  96. package/.framework/src/instructions/guide/05-glossary.md +183 -0
  97. package/.framework/src/instructions/integrations.md +96 -0
  98. package/.framework/src/instructions/internal/cil-markers.md +83 -0
  99. package/.framework/src/instructions/internal/docs-contract.md +158 -0
  100. package/.framework/src/instructions/invites.md +60 -0
  101. package/.framework/src/instructions/kb.md +80 -0
  102. package/.framework/src/instructions/logs.md +77 -0
  103. package/.framework/src/instructions/notifications.md +77 -0
  104. package/.framework/src/instructions/observability.md +61 -0
  105. package/.framework/src/instructions/operations/deployment.md +115 -0
  106. package/.framework/src/instructions/operations/migrations.md +125 -0
  107. package/.framework/src/instructions/operations/security-checklist.md +170 -0
  108. package/.framework/src/instructions/operations/troubleshooting.md +170 -0
  109. package/.framework/src/instructions/operations/upgrade.md +116 -0
  110. package/.framework/src/instructions/people.md +91 -0
  111. package/.framework/src/instructions/pipeline-executions.md +62 -0
  112. package/.framework/src/instructions/pipelines.md +78 -0
  113. package/.framework/src/instructions/prompts.md +95 -0
  114. package/.framework/src/instructions/reference/api/admin-data.md +162 -0
  115. package/.framework/src/instructions/reference/api/auth.md +113 -0
  116. package/.framework/src/instructions/reference/api/errors-and-envelopes.md +113 -0
  117. package/.framework/src/instructions/reference/app-manifest.md +184 -0
  118. package/.framework/src/instructions/reference/cli.md +124 -0
  119. package/.framework/src/instructions/reference/database.md +132 -0
  120. package/.framework/src/instructions/reference/design-schema.md +280 -0
  121. package/.framework/src/instructions/reference/env-vars.md +133 -0
  122. package/.framework/src/instructions/reference/frontend-sdk.md +207 -0
  123. package/.framework/src/instructions/reference/functions/account-nodes.md +75 -0
  124. package/.framework/src/instructions/reference/functions/admin-data.md +197 -0
  125. package/.framework/src/instructions/reference/functions/admin-funnel.md +90 -0
  126. package/.framework/src/instructions/reference/functions/admin-install-funnel.md +107 -0
  127. package/.framework/src/instructions/reference/functions/admin-signals.md +102 -0
  128. package/.framework/src/instructions/reference/functions/ai-agents.md +148 -0
  129. package/.framework/src/instructions/reference/functions/ai-interactions.md +156 -0
  130. package/.framework/src/instructions/reference/functions/api-keys.md +130 -0
  131. package/.framework/src/instructions/reference/functions/apps.md +140 -0
  132. package/.framework/src/instructions/reference/functions/auth.md +99 -0
  133. package/.framework/src/instructions/reference/functions/debug-auth.md +91 -0
  134. package/.framework/src/instructions/reference/functions/deliveries.md +110 -0
  135. package/.framework/src/instructions/reference/functions/embeddings.md +166 -0
  136. package/.framework/src/instructions/reference/functions/eval-harness.md +149 -0
  137. package/.framework/src/instructions/reference/functions/index.md +103 -0
  138. package/.framework/src/instructions/reference/functions/integration-outbound.md +200 -0
  139. package/.framework/src/instructions/reference/functions/integration-routes.md +151 -0
  140. package/.framework/src/instructions/reference/functions/integrations.md +130 -0
  141. package/.framework/src/instructions/reference/functions/invites.md +182 -0
  142. package/.framework/src/instructions/reference/functions/item-progress.md +107 -0
  143. package/.framework/src/instructions/reference/functions/kb.md +166 -0
  144. package/.framework/src/instructions/reference/functions/logs.md +141 -0
  145. package/.framework/src/instructions/reference/functions/observability.md +105 -0
  146. package/.framework/src/instructions/reference/functions/pipeline-executions.md +132 -0
  147. package/.framework/src/instructions/reference/functions/pipeline-runner-background.md +121 -0
  148. package/.framework/src/instructions/reference/functions/pipelines.md +136 -0
  149. package/.framework/src/instructions/reference/functions/prompt-configs.md +121 -0
  150. package/.framework/src/instructions/reference/functions/roles.md +121 -0
  151. package/.framework/src/instructions/reference/functions/security.md +140 -0
  152. package/.framework/src/instructions/reference/functions/system-cron.md +136 -0
  153. package/.framework/src/instructions/reference/functions/system.md +106 -0
  154. package/.framework/src/instructions/reference/functions/tests.md +97 -0
  155. package/.framework/src/instructions/reference/functions/timers.md +117 -0
  156. package/.framework/src/instructions/reference/functions/trigger-scheduler.md +102 -0
  157. package/.framework/src/instructions/reference/functions/triggers.md +140 -0
  158. package/.framework/src/instructions/reference/functions/types.md +146 -0
  159. package/.framework/src/instructions/reference/seeds.md +126 -0
  160. package/.framework/src/instructions/reference/shared-modules.md +127 -0
  161. package/.framework/src/instructions/reference/spine-config.md +114 -0
  162. package/.framework/src/instructions/roles.md +80 -0
  163. package/.framework/src/instructions/runtime-collaboration.md +106 -0
  164. package/.framework/src/instructions/runtime-detail.md +70 -0
  165. package/.framework/src/instructions/runtime-list.md +77 -0
  166. package/.framework/src/instructions/security.md +67 -0
  167. package/.framework/src/instructions/tasks/add-a-pipeline.md +110 -0
  168. package/.framework/src/instructions/tasks/add-a-role.md +96 -0
  169. package/.framework/src/instructions/tasks/add-a-timer.md +114 -0
  170. package/.framework/src/instructions/tasks/add-a-trigger.md +106 -0
  171. package/.framework/src/instructions/tasks/add-a-type.md +151 -0
  172. package/.framework/src/instructions/tasks/add-a-webhook.md +108 -0
  173. package/.framework/src/instructions/tasks/add-an-ai-agent.md +108 -0
  174. package/.framework/src/instructions/tasks/add-an-integration.md +124 -0
  175. package/.framework/src/instructions/tasks/build-a-custom-page.md +118 -0
  176. package/.framework/src/instructions/tasks/document-your-app.md +117 -0
  177. package/.framework/src/instructions/tasks/package-and-install-an-app.md +136 -0
  178. package/.framework/src/instructions/tasks/register-an-app.md +137 -0
  179. package/.framework/src/instructions/tasks/test-your-code.md +108 -0
  180. package/.framework/src/instructions/tasks/write-a-function.md +132 -0
  181. package/.framework/src/instructions/tasks/write-a-migration.md +110 -0
  182. package/.framework/src/instructions/testing.md +59 -0
  183. package/.framework/src/instructions/timers.md +84 -0
  184. package/.framework/src/instructions/triggers.md +69 -0
  185. package/.framework/src/instructions/types.md +93 -0
  186. package/.framework/src/lib/app-mount.ts +88 -0
  187. package/.framework/src/pages/NotFoundPage.tsx +62 -31
  188. package/.framework/src/pages/admin/AIAgentDetailPage.tsx +117 -106
  189. package/.framework/src/pages/admin/AIAgentsPage.tsx +86 -48
  190. package/.framework/src/pages/admin/APIKeyDetailPage.tsx +153 -152
  191. package/.framework/src/pages/admin/APIKeysPage.tsx +91 -90
  192. package/.framework/src/pages/admin/AlertsConfigPage.tsx +29 -18
  193. package/.framework/src/pages/admin/AppDetailPage.tsx +6 -60
  194. package/.framework/src/pages/admin/AppsPage.tsx +104 -66
  195. package/.framework/src/pages/admin/DeliveriesPage.tsx +5 -5
  196. package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +5 -23
  197. package/.framework/src/pages/admin/EmbeddingsPage.tsx +5 -26
  198. package/.framework/src/pages/admin/IntegrationDetailPage.tsx +7 -7
  199. package/.framework/src/pages/admin/IntegrationsPage.tsx +85 -43
  200. package/.framework/src/pages/admin/LogsPage.tsx +144 -76
  201. package/.framework/src/pages/admin/ObservabilityDashboard.tsx +223 -140
  202. package/.framework/src/pages/admin/PipelineDetailPage.tsx +134 -113
  203. package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +108 -45
  204. package/.framework/src/pages/admin/PipelinesPage.tsx +160 -115
  205. package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +96 -98
  206. package/.framework/src/pages/admin/PromptConfigsPage.tsx +101 -59
  207. package/.framework/src/pages/admin/RoleDetailPage.tsx +90 -79
  208. package/.framework/src/pages/admin/RolesPage.tsx +86 -43
  209. package/.framework/src/pages/admin/SecurityPage.tsx +1 -1
  210. package/.framework/src/pages/admin/TestingDashboard.tsx +16 -28
  211. package/.framework/src/pages/admin/TimerDetailPage.tsx +117 -100
  212. package/.framework/src/pages/admin/TimersPage.tsx +90 -62
  213. package/.framework/src/pages/admin/TriggerDetailPage.tsx +90 -83
  214. package/.framework/src/pages/admin/TriggersPage.tsx +124 -99
  215. package/.framework/src/pages/admin/TypeDetailPage.tsx +152 -138
  216. package/.framework/src/pages/admin/TypesPage.tsx +90 -48
  217. package/.framework/src/pages/admin/cil/CILAgentPage.tsx +17 -25
  218. package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +17 -27
  219. package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +72 -40
  220. package/.framework/src/pages/admin/cil/CILFileListPage.tsx +321 -129
  221. package/.framework/src/pages/admin/cil/CILFileSelectPage.tsx +18 -29
  222. package/.framework/src/pages/admin/cil/CILGraphPage.tsx +26 -38
  223. package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +34 -46
  224. package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +189 -103
  225. package/.framework/src/pages/admin/cil/CILSearchPage.tsx +246 -136
  226. package/.framework/src/pages/admin/cil/CILSuggestionsPage.tsx +11 -22
  227. package/.framework/src/pages/admin/cil/CILVersionsPage.tsx +9 -20
  228. package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +26 -38
  229. package/.framework/src/pages/admin/kb/KBAdminPage.tsx +7 -26
  230. package/.framework/src/pages/auth/LoginPage.tsx +3 -2
  231. package/.framework/src/pages/auth/RegisterPage.tsx +16 -8
  232. package/.framework/templates/agent/AGENTS.md +167 -0
  233. package/.framework/templates/agent/skills/spine-add-ai-agent/SKILL.md +58 -0
  234. package/.framework/templates/agent/skills/spine-add-automation/SKILL.md +62 -0
  235. package/.framework/templates/agent/skills/spine-add-integration/SKILL.md +63 -0
  236. package/.framework/templates/agent/skills/spine-architect/SKILL.md +86 -0
  237. package/.framework/templates/agent/skills/spine-install-app/SKILL.md +79 -0
  238. package/.framework/templates/agent/skills/spine-migration/SKILL.md +75 -0
  239. package/.framework/templates/agent/skills/spine-new-app/SKILL.md +79 -0
  240. package/.framework/templates/agent/skills/spine-new-function/SKILL.md +71 -0
  241. package/.framework/templates/agent/skills/spine-new-type/SKILL.md +72 -0
  242. package/.framework/templates/agent/skills/spine-schema/SKILL.md +77 -0
  243. package/.framework/templates/agent/skills/spine-test/SKILL.md +67 -0
  244. package/.framework/templates/agent/skills/spine-troubleshoot/SKILL.md +74 -0
  245. package/.framework/templates/agent/skills/spine-upgrade/SKILL.md +69 -0
  246. package/.framework/templates/project.json +4 -1
  247. package/.framework/tests/unit/account-scoping-gates.test.ts +394 -0
  248. package/.framework/tests/unit/agent-files.test.ts +355 -0
  249. package/.framework/tests/unit/agent-runner.test.ts +479 -44
  250. package/.framework/tests/unit/api-keys-create-scope-gate.test.ts +160 -0
  251. package/.framework/tests/unit/api-keys-omit-secret-fields.test.ts +38 -0
  252. package/.framework/tests/unit/app-mount.test.ts +80 -0
  253. package/.framework/tests/unit/bootstrap.test.ts +242 -0
  254. package/.framework/tests/unit/cli-context-api-key.test.ts +114 -0
  255. package/.framework/tests/unit/debug-auth.test.ts +38 -0
  256. package/.framework/tests/unit/deliveries-account-scoping.test.ts +185 -0
  257. package/.framework/tests/unit/integration-outbound-credentials.test.ts +167 -0
  258. package/.framework/tests/unit/integrations-audit-redaction.test.ts +130 -0
  259. package/.framework/tests/unit/logging-app-id.test.ts +75 -0
  260. package/.framework/tests/unit/security-definer-grants.test.ts +154 -0
  261. package/.framework/tests/unit/system-context-denial.test.ts +145 -0
  262. package/.framework/tests/unit/system-cron.test.ts +240 -0
  263. package/.framework/tests/unit/timers.test.ts +159 -0
  264. package/README.md +125 -166
  265. package/config/tsconfig.cli.json +2 -2
  266. package/package.json +15 -4
  267. package/templates/project/package-lock.json +2 -2
  268. package/.framework/docs/admin-routes.md +0 -76
  269. package/.framework/docs/api-docs-progress.md +0 -38
  270. package/.framework/docs/api-governance.md +0 -146
  271. package/.framework/docs/api-testing-results.md +0 -212
  272. package/.framework/docs/assembly-launch-guide.md +0 -138
  273. package/.framework/docs/audit-results.md +0 -590
  274. package/.framework/docs/db-api-inventory.md +0 -95
  275. package/.framework/docs/gap-remediation-backlog.md +0 -103
  276. package/.framework/docs/ui-adoption-verification.md +0 -111
  277. package/.framework/docs/ui-api-coverage.md +0 -84
  278. package/.framework/docs/v2-compatibility-audit.md +0 -228
  279. package/.framework/mcp/index.ts +0 -133
  280. package/.framework/mcp/load-env.ts +0 -60
  281. package/.framework/mcp/shared/app-manifest.ts +0 -275
  282. package/.framework/mcp/shared/db.ts +0 -477
  283. package/.framework/mcp/shared/kb.ts +0 -118
  284. package/.framework/mcp/tools/architect.ts +0 -431
  285. package/.framework/mcp/tools/cil/context.ts +0 -345
  286. package/.framework/mcp/tools/cil/graph.ts +0 -242
  287. package/.framework/mcp/tools/cil/ingest.ts +0 -1217
  288. package/.framework/mcp/tools/cil/pipeline-orchestrator.ts +0 -693
  289. package/.framework/mcp/tools/cil/pipeline.ts +0 -767
  290. package/.framework/mcp/tools/cil/project.ts +0 -791
  291. package/.framework/mcp/tools/cil/retrieval.ts +0 -336
  292. package/.framework/mcp/tools/cil/search-core.ts +0 -454
  293. package/.framework/mcp/tools/cil/search.ts +0 -97
  294. package/.framework/mcp/tools/cil/suggest.ts +0 -186
  295. package/.framework/mcp/tools/cil/validate.ts +0 -1369
  296. package/.framework/mcp/tools/kb/delete.ts +0 -139
  297. package/.framework/mcp/tools/kb/index.ts +0 -52
  298. package/.framework/mcp/tools/kb/ingest.ts +0 -84
  299. package/.framework/mcp/tools/kb/list-types.ts +0 -102
  300. package/.framework/mcp/tools/kb/publish.ts +0 -100
  301. package/.framework/mcp/tools/kb/reindex.ts +0 -86
  302. package/.framework/mcp/tools/kb/revoke.ts +0 -85
  303. package/.framework/mcp/tools/kb/search.ts +0 -101
  304. package/.framework/mcp/tools/kb/upsert.ts +0 -125
  305. package/.framework/mcp/tools/observability.ts +0 -510
@@ -0,0 +1,185 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+
3
+ // S-007: deliveries get/retry ran service-role queries (machine principals
4
+ // resolve ctx.db to adminDb) with no account filter. get is now scoped to
5
+ // ctx.accountId for non-system principals; retry verifies the caller's
6
+ // account matches the delivery's before re-executing under SYSTEM_PRINCIPAL.
7
+
8
+ vi.mock('../../functions/_shared/middleware.ts', () => ({
9
+ createHandler: (fn: any) => fn,
10
+ }))
11
+
12
+ vi.mock('../../functions/_shared/db.ts', () => ({
13
+ adminDb: {
14
+ from: vi.fn(),
15
+ },
16
+ getUserDb: vi.fn(() => ({})),
17
+ }))
18
+
19
+ vi.mock('../../functions/_shared/audit.ts', () => ({
20
+ emitAudit: vi.fn().mockResolvedValue(undefined),
21
+ emitLog: vi.fn().mockResolvedValue(undefined),
22
+ emitSpan: vi.fn().mockResolvedValue(undefined),
23
+ emitActivity: vi.fn().mockResolvedValue(undefined),
24
+ }))
25
+
26
+ const mocks = vi.hoisted(() => ({
27
+ executeOutbound: vi.fn(),
28
+ }))
29
+
30
+ vi.mock('../../functions/integration-outbound.ts', () => ({
31
+ execute: mocks.executeOutbound,
32
+ default: vi.fn(),
33
+ }))
34
+
35
+ const executeOutbound = mocks.executeOutbound
36
+
37
+ import { get, retry } from '../../functions/deliveries.ts'
38
+
39
+ function q(result: { data?: any; error?: any }) {
40
+ const b: any = {}
41
+ const chainers = ['select', 'eq', 'in', 'is', 'ilike', 'filter', 'not', 'order',
42
+ 'limit', 'update', 'insert', 'upsert', 'delete', 'neq', 'or', 'contains', 'gte', 'lte', 'range']
43
+ for (const m of chainers) b[m] = vi.fn(() => b)
44
+ b.single = vi.fn(async () => result)
45
+ b.maybeSingle = vi.fn(async () => result)
46
+ b.then = (onF: any, onR: any) => Promise.resolve(result).then(onF, onR)
47
+ return b
48
+ }
49
+
50
+ let queue: any[] = []
51
+ let dbBuilt: any[] = []
52
+
53
+ function stubDb() {
54
+ return {
55
+ from: vi.fn(() => {
56
+ const b = queue.length ? queue.shift() : q({ data: null, error: null })
57
+ dbBuilt.push(b)
58
+ return b
59
+ }),
60
+ }
61
+ }
62
+
63
+ beforeEach(() => {
64
+ queue = []
65
+ dbBuilt = []
66
+ vi.clearAllMocks()
67
+ executeOutbound.mockResolvedValue({ success: true, is_retry: true })
68
+ })
69
+
70
+ const scopedMachine = (accountId: string | null = 'acct-1') => ({
71
+ id: 'key-1',
72
+ type: 'machine',
73
+ accountId,
74
+ roles: [],
75
+ scopes: ['deliveries:read'],
76
+ isInternal: false,
77
+ machineType: 'integration',
78
+ provenance: { sourceType: 'api_key' },
79
+ })
80
+
81
+ const systemAdmin = () => ({
82
+ id: 'user-1',
83
+ type: 'human',
84
+ accountId: 'acct-1',
85
+ roles: ['system_admin'],
86
+ provenance: { sourceType: 'jwt' },
87
+ })
88
+
89
+ const systemPrincipal = () => ({
90
+ id: 'system',
91
+ type: 'machine',
92
+ accountId: null,
93
+ roles: [],
94
+ scopes: ['*:*'],
95
+ isInternal: true,
96
+ machineType: 'internal',
97
+ provenance: { sourceType: 'cron' },
98
+ })
99
+
100
+ function makeCtx(principal: any, overrides: any = {}) {
101
+ return {
102
+ requestId: 'req-1',
103
+ principal,
104
+ accountId: principal?.accountId ?? null,
105
+ db: stubDb(),
106
+ query: {},
107
+ headers: {},
108
+ ...overrides,
109
+ }
110
+ }
111
+
112
+ const delivery = (accountId: string | null) => ({
113
+ id: 'del-1',
114
+ account_id: accountId,
115
+ direction: 'outbound',
116
+ status: 'failed',
117
+ })
118
+
119
+ describe('deliveries get account scoping (S-007)', () => {
120
+ it('scopes the lookup to ctx.accountId for a machine principal', async () => {
121
+ queue.push(q({ data: delivery('acct-1'), error: null }))
122
+ const ctx = makeCtx(scopedMachine())
123
+ await get({ ...ctx, query: { id: 'del-1' } }, {})
124
+ const b = dbBuilt[0]
125
+ expect(b.eq).toHaveBeenCalledWith('id', 'del-1')
126
+ expect(b.eq).toHaveBeenCalledWith('account_id', 'acct-1')
127
+ })
128
+
129
+ it('does not scope for system_admin', async () => {
130
+ queue.push(q({ data: delivery('acct-2'), error: null }))
131
+ const ctx = makeCtx(systemAdmin())
132
+ const res = await get({ ...ctx, query: { id: 'del-1' } }, {})
133
+ expect(res.id).toBe('del-1')
134
+ const accountEq = dbBuilt[0].eq.mock.calls.filter((c: any[]) => c[0] === 'account_id')
135
+ expect(accountEq).toHaveLength(0)
136
+ })
137
+
138
+ it('requires account context for a non-system principal without one', async () => {
139
+ const ctx = makeCtx(scopedMachine(null))
140
+ await expect(get({ ...ctx, query: { id: 'del-1' } }, {})).rejects.toThrow('Account context required')
141
+ })
142
+ })
143
+
144
+ describe('deliveries retry account check (S-007)', () => {
145
+ it('rejects a scoped machine principal retrying another account\u2019s delivery with 403', async () => {
146
+ queue.push(q({ data: delivery('acct-2'), error: null }))
147
+ const ctx = makeCtx(scopedMachine())
148
+ await expect(retry(ctx, { id: 'del-1' })).rejects.toMatchObject({ statusCode: 403 })
149
+ expect(executeOutbound).not.toHaveBeenCalled()
150
+ })
151
+
152
+ it('rejects a non-system principal with no account context', async () => {
153
+ queue.push(q({ data: delivery('acct-2'), error: null }))
154
+ const ctx = makeCtx(scopedMachine(null))
155
+ await expect(retry(ctx, { id: 'del-1' })).rejects.toMatchObject({ statusCode: 403 })
156
+ expect(executeOutbound).not.toHaveBeenCalled()
157
+ })
158
+
159
+ it('allows a scoped caller to retry its own account\u2019s delivery', async () => {
160
+ queue.push(q({ data: delivery('acct-1'), error: null }))
161
+ const ctx = makeCtx(scopedMachine())
162
+ const res = await retry(ctx, { id: 'del-1' })
163
+ expect(res.success).toBe(true)
164
+ expect(executeOutbound).toHaveBeenCalledOnce()
165
+ const retryCtx = executeOutbound.mock.calls[0][0]
166
+ expect(retryCtx.accountId).toBe('acct-1')
167
+ expect(retryCtx.principal.id).toBe('system')
168
+ })
169
+
170
+ it('allows system_admin to retry any account\u2019s delivery', async () => {
171
+ queue.push(q({ data: delivery('acct-2'), error: null }))
172
+ const ctx = makeCtx(systemAdmin())
173
+ const res = await retry(ctx, { id: 'del-1' })
174
+ expect(res.success).toBe(true)
175
+ expect(executeOutbound).toHaveBeenCalledOnce()
176
+ })
177
+
178
+ it('allows the internal SYSTEM_PRINCIPAL (scheduled/manual cron path)', async () => {
179
+ queue.push(q({ data: delivery('acct-2'), error: null }))
180
+ const ctx = makeCtx(systemPrincipal())
181
+ const res = await retry(ctx, { id: 'del-1' })
182
+ expect(res.success).toBe(true)
183
+ expect(executeOutbound).toHaveBeenCalledOnce()
184
+ })
185
+ })
@@ -0,0 +1,167 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
+
3
+ // F-065: the stock `generic-http` seed ships
4
+ // `api_token: { source: 'value', value: '' }`. An absent or explicitly-empty
5
+ // credential must mean "call without auth" — the adapter decides whether a
6
+ // key is required. A configured-but-broken env_var entry still throws.
7
+
8
+ vi.mock('../../functions/_shared/middleware.ts', () => ({
9
+ createHandler: (fn: any) => fn,
10
+ }))
11
+
12
+ vi.mock('../../functions/_shared/db.ts', () => ({
13
+ adminDb: {
14
+ from: vi.fn(),
15
+ },
16
+ getUserDb: vi.fn(() => ({})),
17
+ }))
18
+
19
+ vi.mock('../../functions/_shared/audit.ts', () => ({
20
+ emitAudit: vi.fn().mockResolvedValue(undefined),
21
+ emitLog: vi.fn().mockResolvedValue(undefined),
22
+ emitSpan: vi.fn().mockResolvedValue(undefined),
23
+ emitActivity: vi.fn().mockResolvedValue(undefined),
24
+ }))
25
+
26
+ // admin-data pulls in permissions/trigger-engine — only `get`/`update` are
27
+ // used here, and only when record_id / update_record are set.
28
+ vi.mock('../../functions/admin-data.ts', () => ({
29
+ get: vi.fn(),
30
+ update: vi.fn(),
31
+ }))
32
+
33
+ import { adminDb } from '../../functions/_shared/db.ts'
34
+ import { emitLog } from '../../functions/_shared/audit.ts'
35
+ import { execute } from '../../functions/integration-outbound.ts'
36
+
37
+ // Chainable query-builder stub — every filter/terminal method returns the
38
+ // builder except single/maybeSingle/await, which resolve to the result.
39
+ function q(result: { data?: any; error?: any }) {
40
+ const b: any = {}
41
+ const chainers = ['select', 'eq', 'in', 'is', 'ilike', 'filter', 'not', 'order',
42
+ 'limit', 'update', 'insert', 'upsert', 'delete', 'neq', 'or', 'contains', 'gte', 'lte', 'range']
43
+ for (const m of chainers) b[m] = vi.fn(() => b)
44
+ b.single = vi.fn(async () => result)
45
+ b.maybeSingle = vi.fn(async () => result)
46
+ b.then = (onF: any, onR: any) => Promise.resolve(result).then(onF, onR)
47
+ return b
48
+ }
49
+
50
+ let queue: any[] = []
51
+
52
+ const fetchMock = vi.fn()
53
+
54
+ function makeCtx(overrides: any = {}) {
55
+ return {
56
+ requestId: 'req-1',
57
+ principal: {
58
+ id: 'user-1',
59
+ type: 'human',
60
+ accountId: 'acct-1',
61
+ roles: ['system_admin'],
62
+ },
63
+ accountId: 'acct-1',
64
+ db: adminDb,
65
+ query: {},
66
+ ...overrides,
67
+ }
68
+ }
69
+
70
+ function queueIntegration(credentials: any, config: any = { outbound_adapter: 'adapters/generic-webhook-outbound' }) {
71
+ // execute() looks up the account-scoped row first, then the system row.
72
+ queue.push(q({ data: null, error: null })) // tenant miss
73
+ queue.push(q({ data: { id: 'int-1', slug: 'generic-http', credentials, config, is_active: true }, error: null })) // system hit
74
+ queue.push(q({ data: { id: 'del-1' }, error: null })) // webhook_deliveries insert
75
+ queue.push(q({ data: null, error: null })) // webhook_deliveries update
76
+ }
77
+
78
+ beforeEach(() => {
79
+ queue = []
80
+ vi.clearAllMocks()
81
+ vi.mocked(adminDb.from).mockImplementation(() => (queue.length ? queue.shift() : q({ data: null, error: null })))
82
+ fetchMock.mockResolvedValue({
83
+ ok: true,
84
+ status: 200,
85
+ text: async () => '{"ok":true}',
86
+ })
87
+ vi.stubGlobal('fetch', fetchMock)
88
+ })
89
+
90
+ afterEach(() => {
91
+ vi.unstubAllGlobals()
92
+ })
93
+
94
+ describe('integration-outbound optional credentials (F-065)', () => {
95
+ it('sends the request with no Authorization header when api_token is an empty literal', async () => {
96
+ queueIntegration({ api_token: { source: 'value', value: '' } })
97
+
98
+ const result = await execute(makeCtx(), {
99
+ integration_slug: 'generic-http',
100
+ action_config: { url: 'https://hooks.example.com/in' },
101
+ })
102
+
103
+ expect(result.success).toBe(true)
104
+ expect(fetchMock).toHaveBeenCalledOnce()
105
+ const headers = fetchMock.mock.calls[0][1].headers
106
+ expect(headers.Authorization).toBeUndefined()
107
+
108
+ // The skip is still audited — same event name, marked skipped.
109
+ const events = vi.mocked(emitLog).mock.calls.map(c => c[1])
110
+ expect(events).toContain('integration.credential_empty')
111
+ const emptyCall = vi.mocked(emitLog).mock.calls.find(c => c[1] === 'integration.credential_empty')!
112
+ expect(emptyCall[4]).toMatchObject({ skipped: true, field: 'api_token' })
113
+ })
114
+
115
+ it('proceeds unauthenticated when the credentials map has no entry for the field', async () => {
116
+ queueIntegration({})
117
+
118
+ const result = await execute(makeCtx(), {
119
+ integration_slug: 'generic-http',
120
+ action_config: { url: 'https://hooks.example.com/in' },
121
+ })
122
+
123
+ expect(result.success).toBe(true)
124
+ const headers = fetchMock.mock.calls[0][1].headers
125
+ expect(headers.Authorization).toBeUndefined()
126
+ const events = vi.mocked(emitLog).mock.calls.map(c => c[1])
127
+ expect(events).toContain('integration.credential_missing')
128
+ })
129
+
130
+ it('attaches Authorization: Bearer when a literal token is configured', async () => {
131
+ queueIntegration({ api_token: { source: 'value', value: 'tok-123' } })
132
+
133
+ await execute(makeCtx(), {
134
+ integration_slug: 'generic-http',
135
+ action_config: { url: 'https://hooks.example.com/in' },
136
+ })
137
+
138
+ const headers = fetchMock.mock.calls[0][1].headers
139
+ expect(headers.Authorization).toBe('Bearer tok-123')
140
+ })
141
+
142
+ it('still throws when an env_var credential names an unset variable', async () => {
143
+ queueIntegration({ api_token: { source: 'env_var', value: 'DEFINITELY_UNSET_ENV_VAR' } })
144
+
145
+ await expect(
146
+ execute(makeCtx(), {
147
+ integration_slug: 'generic-http',
148
+ action_config: { url: 'https://hooks.example.com/in' },
149
+ })
150
+ ).rejects.toThrow('Environment variable DEFINITELY_UNSET_ENV_VAR for api_token is not set')
151
+ expect(fetchMock).not.toHaveBeenCalled()
152
+ const events = vi.mocked(emitLog).mock.calls.map(c => c[1])
153
+ expect(events).toContain('integration.credential_env_missing')
154
+ })
155
+
156
+ it('openai adapter still fails when no credential resolved', async () => {
157
+ queueIntegration({}, { outbound_adapter: 'adapters/openai-outbound' })
158
+
159
+ await expect(
160
+ execute(makeCtx(), {
161
+ integration_slug: 'openai',
162
+ action_config: { endpoint: '/chat/completions', body: {} },
163
+ })
164
+ ).rejects.toThrow('OpenAI adapter requires api_token credential')
165
+ expect(fetchMock).not.toHaveBeenCalled()
166
+ })
167
+ })
@@ -0,0 +1,130 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+
3
+ vi.mock('../../functions/_shared/db.ts', () => ({
4
+ adminDb: {
5
+ from: vi.fn(),
6
+ },
7
+ getUserDb: vi.fn(() => ({})),
8
+ joins: {
9
+ app: 'app:apps(id, slug, name)',
10
+ createdBy: 'created_by:people(id, full_name)',
11
+ },
12
+ }))
13
+
14
+ vi.mock('../../functions/_shared/audit.ts', () => ({
15
+ emitAudit: vi.fn().mockResolvedValue(undefined),
16
+ emitLog: vi.fn().mockResolvedValue(undefined),
17
+ emitSpan: vi.fn().mockResolvedValue(undefined),
18
+ emitActivity: vi.fn().mockResolvedValue(undefined),
19
+ }))
20
+
21
+ import { adminDb } from '../../functions/_shared/db.ts'
22
+ import { emitLog } from '../../functions/_shared/audit.ts'
23
+ import { update, create } from '../../functions/integrations.ts'
24
+
25
+ // Chainable query-builder stub — every filter/terminal method returns the
26
+ // builder except single/maybeSingle/await, which resolve to the result.
27
+ function q(result: { data?: any; error?: any }) {
28
+ const b: any = {}
29
+ const chainers = ['select', 'eq', 'in', 'is', 'ilike', 'filter', 'not', 'order',
30
+ 'limit', 'update', 'insert', 'upsert', 'delete', 'neq', 'or', 'contains', 'gte', 'lte', 'range']
31
+ for (const m of chainers) b[m] = vi.fn(() => b)
32
+ b.single = vi.fn(async () => result)
33
+ b.maybeSingle = vi.fn(async () => result)
34
+ b.then = (onF: any, onR: any) => Promise.resolve(result).then(onF, onR)
35
+ return b
36
+ }
37
+
38
+ let queue: any[] = []
39
+ let built: any[] = []
40
+
41
+ beforeEach(() => {
42
+ queue = []
43
+ built = []
44
+ vi.clearAllMocks()
45
+ vi.mocked(adminDb.from).mockImplementation(() => {
46
+ const b = queue.length ? queue.shift() : q({ data: null, error: null })
47
+ built.push(b)
48
+ return b
49
+ })
50
+ })
51
+
52
+ // createHandler-wrapped exports invoke the inner handler directly when the
53
+ // first arg carries requestId + principal (the ctx shortcut used by handler()).
54
+ function makeCtx(overrides: any = {}) {
55
+ return {
56
+ requestId: 'req-1',
57
+ principal: {
58
+ id: 'user-1',
59
+ type: 'human',
60
+ accountId: 'acct-1',
61
+ roles: ['system_admin'],
62
+ },
63
+ accountId: 'acct-1',
64
+ db: adminDb,
65
+ query: {},
66
+ ...overrides,
67
+ }
68
+ }
69
+
70
+ describe('integrations update audit redaction (S-005)', () => {
71
+ it('redacts credentials in the audit after payload while writing them to the DB', async () => {
72
+ const integration = { id: 'int-1', name: 'OpenAI' }
73
+ const builder = q({ data: integration, error: null })
74
+ queue.push(builder)
75
+
76
+ const credentials = {
77
+ api_key: { source: 'value', value: 'sk-super-secret' },
78
+ }
79
+
80
+ const result = await update(makeCtx(), {
81
+ id: 'int-1',
82
+ name: 'OpenAI v2',
83
+ credentials,
84
+ })
85
+
86
+ expect(result).toEqual(integration)
87
+
88
+ // The DB write still receives the real credentials.
89
+ const written = builder.update.mock.calls[0][0]
90
+ expect(written.credentials).toEqual(credentials)
91
+ expect(written.is_configured).toBe(true)
92
+ expect(written.name).toBe('OpenAI v2')
93
+
94
+ // The audit payload carries only a redaction marker.
95
+ expect(emitLog).toHaveBeenCalledOnce()
96
+ const [, eventType, target, changes] = vi.mocked(emitLog).mock.calls[0]
97
+ expect(eventType).toBe('integration.updated')
98
+ expect(target).toEqual({ type: 'integration', id: 'int-1' })
99
+ expect(changes.after.credentials).toEqual({ __redacted: true })
100
+ expect(JSON.stringify(changes.after)).not.toContain('sk-super-secret')
101
+ expect(changes.after.name).toBe('OpenAI v2')
102
+ })
103
+
104
+ it('emits no credentials key in the audit payload when credentials are not updated', async () => {
105
+ const builder = q({ data: { id: 'int-2' }, error: null })
106
+ queue.push(builder)
107
+
108
+ await update(makeCtx(), { id: 'int-2', description: 'new desc' })
109
+
110
+ const [, , , changes] = vi.mocked(emitLog).mock.calls[0]
111
+ expect(changes.after).not.toHaveProperty('credentials')
112
+ expect(changes.after.description).toBe('new desc')
113
+ })
114
+
115
+ it('does not leak credentials in the create audit payload', async () => {
116
+ const builder = q({ data: { id: 'int-3' }, error: null })
117
+ queue.push(builder)
118
+
119
+ await create(makeCtx(), {
120
+ name: 'Resend',
121
+ integration_type: 'api',
122
+ provider: 'resend',
123
+ credentials: { api_key: { source: 'value', value: 're_secret' } },
124
+ })
125
+
126
+ const [, , , changes] = vi.mocked(emitLog).mock.calls[0]
127
+ expect(changes.after).not.toHaveProperty('credentials')
128
+ expect(JSON.stringify(changes.after)).not.toContain('re_secret')
129
+ })
130
+ })
@@ -0,0 +1,75 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+
3
+ const { mockFrom, mockMaybeSingle } = vi.hoisted(() => ({
4
+ mockFrom: vi.fn(),
5
+ mockMaybeSingle: vi.fn(),
6
+ }))
7
+
8
+ vi.mock('../../functions/_shared/db.ts', () => ({
9
+ adminDb: { from: mockFrom },
10
+ getUserDb: vi.fn(),
11
+ }))
12
+
13
+ vi.mock('../../functions/_shared/middleware.ts', () => ({}))
14
+
15
+ vi.mock('../../functions/_shared/principal.ts', () => ({
16
+ formatPrincipalForAudit: vi.fn(() => null),
17
+ }))
18
+
19
+ const APP_UUID = '5f2c1e9a-3b7d-4a8e-9c6f-1d2e3f4a5b6c'
20
+
21
+ async function loadLogging() {
22
+ vi.resetModules()
23
+ return await import('../../functions/_shared/logging.ts')
24
+ }
25
+
26
+ beforeEach(() => {
27
+ vi.clearAllMocks()
28
+ mockFrom.mockReturnValue({
29
+ select: vi.fn(() => ({
30
+ eq: vi.fn(() => ({ maybeSingle: mockMaybeSingle })),
31
+ })),
32
+ })
33
+ })
34
+
35
+ describe('resolveAppUuid', () => {
36
+ it('returns a uuid input unchanged without a db call', async () => {
37
+ const { resolveAppUuid } = await loadLogging()
38
+ await expect(resolveAppUuid(APP_UUID)).resolves.toBe(APP_UUID)
39
+ expect(mockFrom).not.toHaveBeenCalled()
40
+ })
41
+
42
+ it('resolves a slug via the apps lookup and caches the hit', async () => {
43
+ const { resolveAppUuid } = await loadLogging()
44
+ mockMaybeSingle.mockResolvedValue({ data: { id: APP_UUID }, error: null })
45
+ await expect(resolveAppUuid('demo')).resolves.toBe(APP_UUID)
46
+ await expect(resolveAppUuid('demo')).resolves.toBe(APP_UUID)
47
+ expect(mockFrom).toHaveBeenCalledTimes(1)
48
+ expect(mockFrom).toHaveBeenCalledWith('apps')
49
+ })
50
+
51
+ it('returns null for an unknown slug and caches the miss', async () => {
52
+ const { resolveAppUuid } = await loadLogging()
53
+ mockMaybeSingle.mockResolvedValue({ data: null, error: null })
54
+ await expect(resolveAppUuid('missing-app')).resolves.toBeNull()
55
+ await expect(resolveAppUuid('missing-app')).resolves.toBeNull()
56
+ expect(mockFrom).toHaveBeenCalledTimes(1)
57
+ })
58
+
59
+ it('returns null on lookup error and retries instead of caching', async () => {
60
+ const { resolveAppUuid } = await loadLogging()
61
+ mockMaybeSingle.mockResolvedValue({ data: null, error: { message: 'db down' } })
62
+ await expect(resolveAppUuid('demo')).resolves.toBeNull()
63
+ mockMaybeSingle.mockResolvedValue({ data: { id: APP_UUID }, error: null })
64
+ await expect(resolveAppUuid('demo')).resolves.toBe(APP_UUID)
65
+ expect(mockFrom).toHaveBeenCalledTimes(2)
66
+ })
67
+
68
+ it('returns null for null, undefined, and empty input without a db call', async () => {
69
+ const { resolveAppUuid } = await loadLogging()
70
+ await expect(resolveAppUuid(null)).resolves.toBeNull()
71
+ await expect(resolveAppUuid(undefined)).resolves.toBeNull()
72
+ await expect(resolveAppUuid('')).resolves.toBeNull()
73
+ expect(mockFrom).not.toHaveBeenCalled()
74
+ })
75
+ })