spine-framework 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (385) hide show
  1. package/.framework/README.md +129 -0
  2. package/.framework/cli/bin.cjs +14 -0
  3. package/.framework/cli/commands/agents.ts +153 -0
  4. package/.framework/cli/commands/auth.ts +94 -0
  5. package/.framework/cli/commands/create-app.ts +185 -0
  6. package/.framework/cli/commands/dev.ts +295 -0
  7. package/.framework/cli/commands/doctor.ts +442 -0
  8. package/.framework/cli/commands/generate.ts +332 -0
  9. package/.framework/cli/commands/init.ts +272 -0
  10. package/.framework/cli/commands/install-app.ts +391 -0
  11. package/.framework/cli/commands/items.ts +253 -0
  12. package/.framework/cli/commands/migrations.ts +141 -0
  13. package/.framework/cli/commands/pipelines.ts +166 -0
  14. package/.framework/cli/commands/status.ts +197 -0
  15. package/.framework/cli/commands/system.ts +184 -0
  16. package/.framework/cli/commands/test.ts +227 -0
  17. package/.framework/cli/commands/uninstall-app.ts +166 -0
  18. package/.framework/cli/context.ts +268 -0
  19. package/.framework/cli/env-loader.ts +36 -0
  20. package/.framework/cli/index.ts +106 -0
  21. package/.framework/cli/welcome.cjs +45 -0
  22. package/.framework/docs/API.md +384 -0
  23. package/.framework/docs/STABILITY.md +52 -0
  24. package/.framework/docs/admin-routes.md +76 -0
  25. package/.framework/docs/api-docs-progress.md +38 -0
  26. package/.framework/docs/api-governance.md +146 -0
  27. package/.framework/docs/api-testing-results.md +212 -0
  28. package/.framework/docs/apis/admin-configs.md +567 -0
  29. package/.framework/docs/apis/admin-data.md +272 -0
  30. package/.framework/docs/apis/index.md +231 -0
  31. package/.framework/docs/apis/internal.md +295 -0
  32. package/.framework/docs/apis/runtime.md +537 -0
  33. package/.framework/docs/assembly-launch-guide.md +138 -0
  34. package/.framework/docs/audit-results.md +590 -0
  35. package/.framework/docs/authorization-model.md +170 -0
  36. package/.framework/docs/db-api-inventory.md +95 -0
  37. package/.framework/docs/examples/custom-app/README.md +77 -0
  38. package/.framework/docs/examples/custom-function/README.md +27 -0
  39. package/.framework/docs/examples/custom-function/handler.ts +48 -0
  40. package/.framework/docs/examples/custom-webhook/README.md +68 -0
  41. package/.framework/docs/gap-remediation-backlog.md +103 -0
  42. package/.framework/docs/guides/cli-guide.md +224 -0
  43. package/.framework/docs/guides/getting-started.md +103 -0
  44. package/.framework/docs/guides/import-guide.md +193 -0
  45. package/.framework/docs/guides/testing-guide.md +229 -0
  46. package/.framework/docs/permission-examples.md +326 -0
  47. package/.framework/docs/ui-adoption-verification.md +111 -0
  48. package/.framework/docs/ui-api-coverage.md +84 -0
  49. package/.framework/docs/v2-compatibility-audit.md +228 -0
  50. package/.framework/functions/.gitkeep +1 -0
  51. package/.framework/functions/_shared/agent-runner.ts +1097 -0
  52. package/.framework/functions/_shared/app-manifest.ts +184 -0
  53. package/.framework/functions/_shared/audit.ts +150 -0
  54. package/.framework/functions/_shared/db.ts +174 -0
  55. package/.framework/functions/_shared/index.ts +382 -0
  56. package/.framework/functions/_shared/middleware.ts +490 -0
  57. package/.framework/functions/_shared/permissions.ts +1325 -0
  58. package/.framework/functions/_shared/pipeline-runner.ts +731 -0
  59. package/.framework/functions/_shared/principal.ts +760 -0
  60. package/.framework/functions/_shared/schema-utils.ts +967 -0
  61. package/.framework/functions/_shared/testing.ts +258 -0
  62. package/.framework/functions/_shared/trigger-engine.ts +425 -0
  63. package/.framework/functions/_shared/webhook-registration.ts +168 -0
  64. package/.framework/functions/_shared/webhook-registry.ts +129 -0
  65. package/.framework/functions/account-nodes.ts +111 -0
  66. package/.framework/functions/admin-data.ts +606 -0
  67. package/.framework/functions/ai-agents.ts +323 -0
  68. package/.framework/functions/api-keys.ts +376 -0
  69. package/.framework/functions/apps.ts +483 -0
  70. package/.framework/functions/auth.ts +196 -0
  71. package/.framework/functions/debug-auth.ts +107 -0
  72. package/.framework/functions/embeddings.ts +556 -0
  73. package/.framework/functions/integration-routes.ts +523 -0
  74. package/.framework/functions/integrations.ts +319 -0
  75. package/.framework/functions/item-progress.ts +272 -0
  76. package/.framework/functions/logs.ts +438 -0
  77. package/.framework/functions/observability.ts +275 -0
  78. package/.framework/functions/pipeline-executions.ts +494 -0
  79. package/.framework/functions/pipelines.ts +485 -0
  80. package/.framework/functions/prompt-configs.ts +339 -0
  81. package/.framework/functions/roles.ts +387 -0
  82. package/.framework/functions/system-cron.ts +742 -0
  83. package/.framework/functions/system.ts +323 -0
  84. package/.framework/functions/tests.ts +119 -0
  85. package/.framework/functions/timers.ts +357 -0
  86. package/.framework/functions/triggers.ts +563 -0
  87. package/.framework/functions/types.ts +604 -0
  88. package/.framework/migrations/000_foundation.sql +1256 -0
  89. package/.framework/migrations/001_seed.sql +92 -0
  90. package/.framework/migrations/002_seed_constraints.sql +13 -0
  91. package/.framework/migrations/003_auth_user_trigger.sql +59 -0
  92. package/.framework/src/App.tsx +126 -0
  93. package/.framework/src/apps/admin/index.tsx +173 -0
  94. package/.framework/src/components/AppWrapper.tsx +56 -0
  95. package/.framework/src/components/CustomAppLoader.tsx +116 -0
  96. package/.framework/src/components/admin/AdminListPage.tsx +151 -0
  97. package/.framework/src/components/admin/AdminSidebar.tsx +166 -0
  98. package/.framework/src/components/admin/AdminStatsCard.tsx +62 -0
  99. package/.framework/src/components/admin/SortableTableHeader.tsx +42 -0
  100. package/.framework/src/components/app-shell/GenericAppShell.tsx +181 -0
  101. package/.framework/src/components/app-shell/GenericDetailPage.tsx +200 -0
  102. package/.framework/src/components/app-shell/GenericListPage.tsx +116 -0
  103. package/.framework/src/components/app-sidebar.tsx +228 -0
  104. package/.framework/src/components/auth/ProtectedRoute.tsx +88 -0
  105. package/.framework/src/components/layout/AppShell.tsx +91 -0
  106. package/.framework/src/components/layout/Header.tsx +88 -0
  107. package/.framework/src/components/layout/Layout.tsx +95 -0
  108. package/.framework/src/components/layout/Sidebar.tsx +329 -0
  109. package/.framework/src/components/runtime/DataDetailHeader.tsx +77 -0
  110. package/.framework/src/components/runtime/DataDetailPage.tsx +171 -0
  111. package/.framework/src/components/runtime/DataFilters.tsx +91 -0
  112. package/.framework/src/components/runtime/DataHeader.tsx +68 -0
  113. package/.framework/src/components/runtime/DataListPage.tsx +124 -0
  114. package/.framework/src/components/runtime/DataStats.tsx +70 -0
  115. package/.framework/src/components/runtime/DataTable.tsx +174 -0
  116. package/.framework/src/components/runtime/SchemaDetailForm.tsx +134 -0
  117. package/.framework/src/components/runtime/index.ts +18 -0
  118. package/.framework/src/components/search-form.tsx +29 -0
  119. package/.framework/src/components/shared/AgentView.tsx +213 -0
  120. package/.framework/src/components/shared/FieldRenderer.tsx +478 -0
  121. package/.framework/src/components/shared/SchemaFields.tsx +226 -0
  122. package/.framework/src/components/ui/DataTable.tsx +343 -0
  123. package/.framework/src/components/ui/Form.tsx +281 -0
  124. package/.framework/src/components/ui/ItemCard.tsx +296 -0
  125. package/.framework/src/components/ui/ItemListView.tsx +308 -0
  126. package/.framework/src/components/ui/LoadingSpinner.tsx +52 -0
  127. package/.framework/src/components/ui/Modal.tsx +61 -0
  128. package/.framework/src/components/ui/RichTextEditor.tsx +210 -0
  129. package/.framework/src/components/ui/accordion.tsx +82 -0
  130. package/.framework/src/components/ui/alert-dialog.tsx +197 -0
  131. package/.framework/src/components/ui/alert.tsx +76 -0
  132. package/.framework/src/components/ui/aspect-ratio.tsx +11 -0
  133. package/.framework/src/components/ui/avatar.tsx +110 -0
  134. package/.framework/src/components/ui/badge.tsx +49 -0
  135. package/.framework/src/components/ui/breadcrumb.tsx +122 -0
  136. package/.framework/src/components/ui/button-group.tsx +83 -0
  137. package/.framework/src/components/ui/button.tsx +65 -0
  138. package/.framework/src/components/ui/calendar.tsx +222 -0
  139. package/.framework/src/components/ui/card.tsx +100 -0
  140. package/.framework/src/components/ui/carousel.tsx +240 -0
  141. package/.framework/src/components/ui/chart.tsx +373 -0
  142. package/.framework/src/components/ui/checkbox.tsx +31 -0
  143. package/.framework/src/components/ui/collapsible.tsx +33 -0
  144. package/.framework/src/components/ui/combobox.tsx +299 -0
  145. package/.framework/src/components/ui/command.tsx +193 -0
  146. package/.framework/src/components/ui/context-menu.tsx +261 -0
  147. package/.framework/src/components/ui/dialog.tsx +165 -0
  148. package/.framework/src/components/ui/direction.tsx +22 -0
  149. package/.framework/src/components/ui/drawer.tsx +132 -0
  150. package/.framework/src/components/ui/dropdown-menu.tsx +269 -0
  151. package/.framework/src/components/ui/empty.tsx +104 -0
  152. package/.framework/src/components/ui/field.tsx +238 -0
  153. package/.framework/src/components/ui/hover-card.tsx +42 -0
  154. package/.framework/src/components/ui/input-group.tsx +153 -0
  155. package/.framework/src/components/ui/input-otp.tsx +87 -0
  156. package/.framework/src/components/ui/input.tsx +19 -0
  157. package/.framework/src/components/ui/item.tsx +196 -0
  158. package/.framework/src/components/ui/kbd.tsx +26 -0
  159. package/.framework/src/components/ui/label.tsx +22 -0
  160. package/.framework/src/components/ui/menubar.tsx +277 -0
  161. package/.framework/src/components/ui/native-select.tsx +61 -0
  162. package/.framework/src/components/ui/navigation-menu.tsx +164 -0
  163. package/.framework/src/components/ui/pagination.tsx +129 -0
  164. package/.framework/src/components/ui/popover.tsx +87 -0
  165. package/.framework/src/components/ui/progress.tsx +31 -0
  166. package/.framework/src/components/ui/radio-group.tsx +42 -0
  167. package/.framework/src/components/ui/resizable.tsx +50 -0
  168. package/.framework/src/components/ui/scroll-area.tsx +53 -0
  169. package/.framework/src/components/ui/select.tsx +195 -0
  170. package/.framework/src/components/ui/separator.tsx +26 -0
  171. package/.framework/src/components/ui/sheet.tsx +145 -0
  172. package/.framework/src/components/ui/sidebar.tsx +706 -0
  173. package/.framework/src/components/ui/skeleton.tsx +13 -0
  174. package/.framework/src/components/ui/slider.tsx +59 -0
  175. package/.framework/src/components/ui/sonner.tsx +47 -0
  176. package/.framework/src/components/ui/spinner.tsx +10 -0
  177. package/.framework/src/components/ui/switch.tsx +33 -0
  178. package/.framework/src/components/ui/table-primitives.tsx +141 -0
  179. package/.framework/src/components/ui/table.tsx +114 -0
  180. package/.framework/src/components/ui/tabs.tsx +90 -0
  181. package/.framework/src/components/ui/textarea.tsx +18 -0
  182. package/.framework/src/components/ui/toggle-group.tsx +89 -0
  183. package/.framework/src/components/ui/toggle.tsx +45 -0
  184. package/.framework/src/components/ui/tooltip.tsx +57 -0
  185. package/.framework/src/contexts/AppContext.tsx +133 -0
  186. package/.framework/src/contexts/AuthContext.tsx +371 -0
  187. package/.framework/src/hooks/use-mobile.ts +19 -0
  188. package/.framework/src/hooks/useApi.ts +526 -0
  189. package/.framework/src/hooks/useApps.ts +114 -0
  190. package/.framework/src/hooks/useEntityList.ts +190 -0
  191. package/.framework/src/hooks/useEntityRecord.ts +308 -0
  192. package/.framework/src/hooks/useForm.ts +307 -0
  193. package/.framework/src/hooks/useListSchema.ts +264 -0
  194. package/.framework/src/hooks/useSchemaRecord.ts +223 -0
  195. package/.framework/src/index.css +128 -0
  196. package/.framework/src/lib/api.ts +156 -0
  197. package/.framework/src/lib/supabase.ts +94 -0
  198. package/.framework/src/lib/utils.ts +317 -0
  199. package/.framework/src/main.tsx +27 -0
  200. package/.framework/src/pages/DashboardPage.tsx +181 -0
  201. package/.framework/src/pages/NotFoundPage.tsx +39 -0
  202. package/.framework/src/pages/admin/AIAgentDetailPage.tsx +161 -0
  203. package/.framework/src/pages/admin/AIAgentsPage.tsx +318 -0
  204. package/.framework/src/pages/admin/APIKeyDetailPage.tsx +199 -0
  205. package/.framework/src/pages/admin/APIKeysPage.tsx +303 -0
  206. package/.framework/src/pages/admin/AlertsConfigPage.tsx +523 -0
  207. package/.framework/src/pages/admin/AppDetailPage.tsx +493 -0
  208. package/.framework/src/pages/admin/AppsPage.tsx +355 -0
  209. package/.framework/src/pages/admin/DesignedPage.tsx +491 -0
  210. package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +534 -0
  211. package/.framework/src/pages/admin/EmbeddingsPage.tsx +424 -0
  212. package/.framework/src/pages/admin/ExtendedShadcnTestPage.tsx +176 -0
  213. package/.framework/src/pages/admin/IncrementalShadcnTestPage.tsx +109 -0
  214. package/.framework/src/pages/admin/IntegratedDashboard.tsx +402 -0
  215. package/.framework/src/pages/admin/IntegrationDetailPage.tsx +187 -0
  216. package/.framework/src/pages/admin/IntegrationsPage.tsx +301 -0
  217. package/.framework/src/pages/admin/LogsPage.tsx +283 -0
  218. package/.framework/src/pages/admin/MinimalShadcnTestPage.tsx +85 -0
  219. package/.framework/src/pages/admin/ObservabilityDashboard.tsx +470 -0
  220. package/.framework/src/pages/admin/PipelineDetailPage.tsx +183 -0
  221. package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +279 -0
  222. package/.framework/src/pages/admin/PipelinesPage.tsx +390 -0
  223. package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +299 -0
  224. package/.framework/src/pages/admin/PromptConfigsPage.tsx +292 -0
  225. package/.framework/src/pages/admin/ProperlyDesignedPage.tsx +434 -0
  226. package/.framework/src/pages/admin/RoleDetailPage.tsx +273 -0
  227. package/.framework/src/pages/admin/RolesPage.tsx +292 -0
  228. package/.framework/src/pages/admin/SelectTestPage.tsx +61 -0
  229. package/.framework/src/pages/admin/ShadcnTestPage.tsx +588 -0
  230. package/.framework/src/pages/admin/SimpleDashboard.tsx +387 -0
  231. package/.framework/src/pages/admin/TestRunDetailPage.tsx +172 -0
  232. package/.framework/src/pages/admin/TestingDashboard.tsx +257 -0
  233. package/.framework/src/pages/admin/TimerDetailPage.tsx +151 -0
  234. package/.framework/src/pages/admin/TimersPage.tsx +376 -0
  235. package/.framework/src/pages/admin/TriggerDetailPage.tsx +149 -0
  236. package/.framework/src/pages/admin/TriggersPage.tsx +381 -0
  237. package/.framework/src/pages/admin/TypeDetailPage.tsx +694 -0
  238. package/.framework/src/pages/admin/TypesPage.tsx +295 -0
  239. package/.framework/src/pages/auth/LoginPage.tsx +188 -0
  240. package/.framework/src/pages/auth/RegisterPage.tsx +163 -0
  241. package/.framework/src/pages/spine-framework/APIPage.tsx +17 -0
  242. package/.framework/src/pages/spine-framework/CLIPage.tsx +25 -0
  243. package/.framework/src/types/auth.ts +125 -0
  244. package/.framework/src/types/types.ts +407 -0
  245. package/STRUCTURE.md +150 -0
  246. package/config/components.json +25 -0
  247. package/config/deno.lock +108 -0
  248. package/config/package-lock.json +17183 -0
  249. package/config/postcss.config.cjs +10 -0
  250. package/config/tailwind.config.cjs +78 -0
  251. package/config/tsconfig.build.json +32 -0
  252. package/config/tsconfig.cli.json +18 -0
  253. package/config/tsconfig.json +41 -0
  254. package/config/tsconfig.node.json +17 -0
  255. package/config/tsconfig.node.tsbuildinfo +1 -0
  256. package/config/tsconfig.tsbuildinfo +1 -0
  257. package/config/typedoc.json +16 -0
  258. package/config/vite.config.d.ts +2 -0
  259. package/config/vite.config.ts +72 -0
  260. package/dist/cli/commands/agents.d.ts +39 -0
  261. package/dist/cli/commands/agents.d.ts.map +1 -0
  262. package/dist/cli/commands/auth.d.ts +36 -0
  263. package/dist/cli/commands/auth.d.ts.map +1 -0
  264. package/dist/cli/commands/create-app.d.ts +23 -0
  265. package/dist/cli/commands/create-app.d.ts.map +1 -0
  266. package/dist/cli/commands/dev.d.ts +39 -0
  267. package/dist/cli/commands/dev.d.ts.map +1 -0
  268. package/dist/cli/commands/doctor.d.ts +42 -0
  269. package/dist/cli/commands/doctor.d.ts.map +1 -0
  270. package/dist/cli/commands/generate.d.ts +36 -0
  271. package/dist/cli/commands/generate.d.ts.map +1 -0
  272. package/dist/cli/commands/init.d.ts +30 -0
  273. package/dist/cli/commands/init.d.ts.map +1 -0
  274. package/dist/cli/commands/install-app.d.ts +30 -0
  275. package/dist/cli/commands/install-app.d.ts.map +1 -0
  276. package/dist/cli/commands/items.d.ts +45 -0
  277. package/dist/cli/commands/items.d.ts.map +1 -0
  278. package/dist/cli/commands/migrations.d.ts +41 -0
  279. package/dist/cli/commands/migrations.d.ts.map +1 -0
  280. package/dist/cli/commands/pipelines.d.ts +40 -0
  281. package/dist/cli/commands/pipelines.d.ts.map +1 -0
  282. package/dist/cli/commands/status.d.ts +23 -0
  283. package/dist/cli/commands/status.d.ts.map +1 -0
  284. package/dist/cli/commands/system.d.ts +29 -0
  285. package/dist/cli/commands/system.d.ts.map +1 -0
  286. package/dist/cli/commands/test.d.ts +46 -0
  287. package/dist/cli/commands/test.d.ts.map +1 -0
  288. package/dist/cli/commands/uninstall-app.d.ts +23 -0
  289. package/dist/cli/commands/uninstall-app.d.ts.map +1 -0
  290. package/dist/cli/context.d.ts +88 -0
  291. package/dist/cli/context.d.ts.map +1 -0
  292. package/dist/cli/env-loader.d.ts +14 -0
  293. package/dist/cli/env-loader.d.ts.map +1 -0
  294. package/dist/cli/index.d.ts +41 -0
  295. package/dist/cli/index.d.ts.map +1 -0
  296. package/dist/functions/_shared/agent-runner.d.ts +156 -0
  297. package/dist/functions/_shared/agent-runner.d.ts.map +1 -0
  298. package/dist/functions/_shared/app-manifest.d.ts +68 -0
  299. package/dist/functions/_shared/app-manifest.d.ts.map +1 -0
  300. package/dist/functions/_shared/audit.d.ts +91 -0
  301. package/dist/functions/_shared/audit.d.ts.map +1 -0
  302. package/dist/functions/_shared/db.d.ts +125 -0
  303. package/dist/functions/_shared/db.d.ts.map +1 -0
  304. package/dist/functions/_shared/index.d.ts +298 -0
  305. package/dist/functions/_shared/index.d.ts.map +1 -0
  306. package/dist/functions/_shared/middleware.d.ts +315 -0
  307. package/dist/functions/_shared/middleware.d.ts.map +1 -0
  308. package/dist/functions/_shared/permissions.d.ts +626 -0
  309. package/dist/functions/_shared/permissions.d.ts.map +1 -0
  310. package/dist/functions/_shared/pipeline-runner.d.ts +124 -0
  311. package/dist/functions/_shared/pipeline-runner.d.ts.map +1 -0
  312. package/dist/functions/_shared/principal.d.ts +284 -0
  313. package/dist/functions/_shared/principal.d.ts.map +1 -0
  314. package/dist/functions/_shared/schema-utils.d.ts +181 -0
  315. package/dist/functions/_shared/schema-utils.d.ts.map +1 -0
  316. package/dist/functions/_shared/testing.d.ts +172 -0
  317. package/dist/functions/_shared/testing.d.ts.map +1 -0
  318. package/dist/functions/_shared/trigger-engine.d.ts +140 -0
  319. package/dist/functions/_shared/trigger-engine.d.ts.map +1 -0
  320. package/dist/functions/_shared/webhook-registration.d.ts +81 -0
  321. package/dist/functions/_shared/webhook-registration.d.ts.map +1 -0
  322. package/dist/functions/_shared/webhook-registry.d.ts +57 -0
  323. package/dist/functions/_shared/webhook-registry.d.ts.map +1 -0
  324. package/dist/functions/account-nodes.d.ts +48 -0
  325. package/dist/functions/account-nodes.d.ts.map +1 -0
  326. package/dist/functions/admin-data.d.ts +178 -0
  327. package/dist/functions/admin-data.d.ts.map +1 -0
  328. package/dist/functions/ai-agents.d.ts +125 -0
  329. package/dist/functions/ai-agents.d.ts.map +1 -0
  330. package/dist/functions/api-keys.d.ts +140 -0
  331. package/dist/functions/api-keys.d.ts.map +1 -0
  332. package/dist/functions/apps.d.ts +163 -0
  333. package/dist/functions/apps.d.ts.map +1 -0
  334. package/dist/functions/auth.d.ts +74 -0
  335. package/dist/functions/auth.d.ts.map +1 -0
  336. package/dist/functions/debug-auth.d.ts +33 -0
  337. package/dist/functions/debug-auth.d.ts.map +1 -0
  338. package/dist/functions/embeddings.d.ts +205 -0
  339. package/dist/functions/embeddings.d.ts.map +1 -0
  340. package/dist/functions/integration-routes.d.ts +45 -0
  341. package/dist/functions/integration-routes.d.ts.map +1 -0
  342. package/dist/functions/integrations.d.ts +124 -0
  343. package/dist/functions/integrations.d.ts.map +1 -0
  344. package/dist/functions/item-progress.d.ts +41 -0
  345. package/dist/functions/item-progress.d.ts.map +1 -0
  346. package/dist/functions/logs.d.ts +162 -0
  347. package/dist/functions/logs.d.ts.map +1 -0
  348. package/dist/functions/observability.d.ts +123 -0
  349. package/dist/functions/observability.d.ts.map +1 -0
  350. package/dist/functions/pipeline-executions.d.ts +190 -0
  351. package/dist/functions/pipeline-executions.d.ts.map +1 -0
  352. package/dist/functions/pipelines.d.ts +171 -0
  353. package/dist/functions/pipelines.d.ts.map +1 -0
  354. package/dist/functions/prompt-configs.d.ts +125 -0
  355. package/dist/functions/prompt-configs.d.ts.map +1 -0
  356. package/dist/functions/roles.d.ts +118 -0
  357. package/dist/functions/roles.d.ts.map +1 -0
  358. package/dist/functions/system-cron.d.ts +65 -0
  359. package/dist/functions/system-cron.d.ts.map +1 -0
  360. package/dist/functions/system.d.ts +29 -0
  361. package/dist/functions/system.d.ts.map +1 -0
  362. package/dist/functions/tests.d.ts +28 -0
  363. package/dist/functions/tests.d.ts.map +1 -0
  364. package/dist/functions/timers.d.ts +139 -0
  365. package/dist/functions/timers.d.ts.map +1 -0
  366. package/dist/functions/triggers.d.ts +203 -0
  367. package/dist/functions/triggers.d.ts.map +1 -0
  368. package/dist/functions/types.d.ts +151 -0
  369. package/dist/functions/types.d.ts.map +1 -0
  370. package/dist/src/types/types.d.ts +364 -0
  371. package/dist/src/types/types.d.ts.map +1 -0
  372. package/package.json +192 -0
  373. package/scripts/app-install-cli.ts +286 -0
  374. package/scripts/assemble-frontend.sh +79 -0
  375. package/scripts/assemble-functions.sh +62 -0
  376. package/scripts/assemble.sh +35 -0
  377. package/scripts/boundary-check.sh +106 -0
  378. package/scripts/build-manifest.sh +80 -0
  379. package/scripts/check-core-integrity.sh +82 -0
  380. package/scripts/ingest-chunks.cjs +202 -0
  381. package/scripts/kb-chunk-parser.cjs +312 -0
  382. package/scripts/kb-chunk-parser.ts +330 -0
  383. package/scripts/load-test-app-install.ts +484 -0
  384. package/scripts/netlify-dev-wrapper.sh +22 -0
  385. package/scripts/verify-integrity.sh +69 -0
@@ -0,0 +1,199 @@
1
+ /**
2
+ * @module src/pages/admin/APIKeyDetailPage
3
+ */
4
+ import { useState, useEffect } from 'react'
5
+ import { useParams, useNavigate } from 'react-router-dom'
6
+ import { Button } from '../../components/ui/button'
7
+ import { Input } from '../../components/ui/input'
8
+ import { Textarea } from '../../components/ui/textarea'
9
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../components/ui/select'
10
+ import { Checkbox } from '../../components/ui/checkbox'
11
+ import { Label } from '../../components/ui/label'
12
+ import { Card, CardContent, CardHeader, CardTitle } from '../../components/ui/card'
13
+ import { Skeleton } from '../../components/ui/skeleton'
14
+ import { Alert, AlertDescription, AlertTitle } from '../../components/ui/alert'
15
+ import { ArrowLeft, Key, RefreshCw, Trash2, AlertCircle, Copy, Check } from 'lucide-react'
16
+ import { useApi } from '../../hooks/useApi'
17
+ import { apiFetch } from '../../lib/api'
18
+ import { formatDateTime } from '../../lib/utils'
19
+
20
+ interface APIKey {
21
+ id: string
22
+ name: string
23
+ key_type: 'public' | 'private'
24
+ key_value?: string
25
+ key_prefix: string
26
+ is_active: boolean
27
+ permissions?: Record<string, any>
28
+ rate_limit: number
29
+ expires_at?: string
30
+ last_used_at?: string
31
+ usage_count: number
32
+ integration_id?: string
33
+ metadata?: Record<string, any>
34
+ created_by_person?: { id: string; full_name: string; email: string }
35
+ created_at: string
36
+ updated_at: string
37
+ }
38
+
39
+ const KEY_TYPE_OPTIONS = [
40
+ { value: 'private', label: 'Private (Server-side)' },
41
+ { value: 'public', label: 'Public (Client-side)' }
42
+ ]
43
+
44
+ export function APIKeyDetailPage() {
45
+ const { id } = useParams<{ id: string }>()
46
+ const navigate = useNavigate()
47
+ const isCreateMode = !id || id === 'new'
48
+
49
+ const [formData, setFormData] = useState({
50
+ name: '',
51
+ key_type: 'private' as 'public' | 'private',
52
+ key_prefix: 'sk_',
53
+ rate_limit: 1000,
54
+ expires_at: '',
55
+ integration_id: '',
56
+ permissions: {},
57
+ metadata: {},
58
+ is_active: true
59
+ })
60
+ const [permissionsJson, setPermissionsJson] = useState('{}')
61
+ const [metadataJson, setMetadataJson] = useState('{}')
62
+ const [isSubmitting, setIsSubmitting] = useState(false)
63
+ const [error, setError] = useState<string | null>(null)
64
+ const [generatedKey, setGeneratedKey] = useState<string | null>(null)
65
+ const [copied, setCopied] = useState(false)
66
+
67
+ const { data: apiKey, loading, error: fetchError } = useApi<APIKey>(
68
+ async () => {
69
+ if (isCreateMode) return null
70
+ const response = await apiFetch(`/api/api-keys?action=get&id=${id}`)
71
+ if (!response.ok) throw new Error('Failed to fetch API key')
72
+ const result = await response.json()
73
+ return result.data || result
74
+ },
75
+ { immediate: !isCreateMode }
76
+ )
77
+
78
+ useEffect(() => {
79
+ if (apiKey) {
80
+ setFormData({
81
+ name: apiKey.name || '',
82
+ key_type: apiKey.key_type || 'private',
83
+ key_prefix: apiKey.key_prefix || 'sk_',
84
+ rate_limit: apiKey.rate_limit || 1000,
85
+ expires_at: apiKey.expires_at ? apiKey.expires_at.split('T')[0] : '',
86
+ integration_id: apiKey.integration_id || '',
87
+ permissions: apiKey.permissions || {},
88
+ metadata: apiKey.metadata || {},
89
+ is_active: apiKey.is_active
90
+ })
91
+ setPermissionsJson(JSON.stringify(apiKey.permissions || {}, null, 2))
92
+ setMetadataJson(JSON.stringify(apiKey.metadata || {}, null, 2))
93
+ }
94
+ }, [apiKey])
95
+
96
+ const handleSubmit = async (e: React.FormEvent) => {
97
+ e.preventDefault()
98
+ setIsSubmitting(true)
99
+ try {
100
+ let permissions = {}
101
+ let metadata = {}
102
+ try { permissions = JSON.parse(permissionsJson) } catch {}
103
+ try { metadata = JSON.parse(metadataJson) } catch {}
104
+ const body = { ...formData, permissions, metadata }
105
+ const response = await apiFetch('/api/api-keys', {
106
+ method: 'POST',
107
+ headers: { 'Content-Type': 'application/json' },
108
+ body: JSON.stringify(body)
109
+ })
110
+ if (!response.ok) throw new Error('Failed to create API key')
111
+ const result = await response.json()
112
+ setGeneratedKey(result.key_value || result.data?.key_value)
113
+ setTimeout(() => navigate(`/spine-framework/admin/configs/api-keys/${result.id || result.data?.id}`), 3000)
114
+ } catch (err) {
115
+ setError(err instanceof Error ? err.message : 'Failed to create API key')
116
+ } finally {
117
+ setIsSubmitting(false)
118
+ }
119
+ }
120
+
121
+ const handleCopy = () => {
122
+ if (generatedKey) {
123
+ navigator.clipboard.writeText(generatedKey)
124
+ setCopied(true)
125
+ setTimeout(() => setCopied(false), 2000)
126
+ }
127
+ }
128
+
129
+ if (loading) return (
130
+ <div className="space-y-6">
131
+ <Skeleton className="h-8 w-48" />
132
+ <Card><CardHeader><Skeleton className="h-6 w-32" /></CardHeader><CardContent className="space-y-4"><Skeleton className="h-4 w-full" /><Skeleton className="h-4 w-2/3" /></CardContent></Card>
133
+ </div>
134
+ )
135
+
136
+ if (fetchError) return (
137
+ <Alert variant="destructive"><AlertCircle className="h-4 w-4" /><AlertTitle>Failed to load</AlertTitle><AlertDescription>{fetchError}</AlertDescription></Alert>
138
+ )
139
+
140
+ if (!isCreateMode && apiKey) {
141
+ return (
142
+ <div className="space-y-6">
143
+ <div className="flex items-center gap-4">
144
+ <Button variant="ghost" onClick={() => navigate(-1)}><ArrowLeft className="h-5 w-5" /></Button>
145
+ <h1 className="text-2xl font-bold">{apiKey.name}</h1>
146
+ </div>
147
+ <Card><CardContent className="p-6">
148
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
149
+ <div className="flex justify-between"><span className="text-muted-foreground">Key Type</span><span>{apiKey.key_type}</span></div>
150
+ <div className="flex justify-between"><span className="text-muted-foreground">Prefix</span><span>{apiKey.key_prefix}</span></div>
151
+ <div className="flex justify-between"><span className="text-muted-foreground">Rate Limit</span><span>{apiKey.rate_limit}/min</span></div>
152
+ <div className="flex justify-between"><span className="text-muted-foreground">Status</span><span>{apiKey.is_active ? 'Active' : 'Inactive'}</span></div>
153
+ <div className="flex justify-between"><span className="text-muted-foreground">Usage</span><span>{apiKey.usage_count} requests</span></div>
154
+ <div className="flex justify-between"><span className="text-muted-foreground">Created</span><span>{formatDateTime(apiKey.created_at)}</span></div>
155
+ </div>
156
+ <div className="mt-4"><span className="text-muted-foreground">Key Value</span><div className="mt-1 font-mono text-sm bg-muted p-3 rounded">{apiKey.key_value || '***'}</div></div>
157
+ </CardContent></Card>
158
+ </div>
159
+ )
160
+ }
161
+
162
+ if (generatedKey) {
163
+ return (
164
+ <div className="space-y-6">
165
+ <h1 className="text-2xl font-bold">API Key Created</h1>
166
+ <Alert><AlertCircle className="h-4 w-4" /><AlertTitle>Save this key now</AlertTitle><AlertDescription>You won't be able to see it again. Copy it now and store it securely.</AlertDescription></Alert>
167
+ <Card><CardContent className="p-6">
168
+ <div className="flex items-center gap-2"><code className="flex-1 bg-muted p-4 rounded font-mono text-lg">{generatedKey}</code><Button size="icon" onClick={handleCopy}>{copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}</Button></div>
169
+ <Button className="mt-4" onClick={() => navigate('/spine-framework/admin/configs/api-keys')}>Done</Button>
170
+ </CardContent></Card>
171
+ </div>
172
+ )
173
+ }
174
+
175
+ return (
176
+ <div className="space-y-6">
177
+ <div className="flex items-center gap-4">
178
+ <Button variant="ghost" onClick={() => navigate(-1)}><ArrowLeft className="h-5 w-5" /></Button>
179
+ <h1 className="text-2xl font-bold">Create API Key</h1>
180
+ </div>
181
+ {error && <Alert variant="destructive"><AlertCircle className="h-4 w-4" /><AlertTitle>Error</AlertTitle><AlertDescription>{error}</AlertDescription></Alert>}
182
+ <form onSubmit={handleSubmit} className="space-y-6">
183
+ <Card><CardContent className="p-6 space-y-4">
184
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
185
+ <div className="space-y-2"><Label>Name *</Label><Input value={formData.name} onChange={e => setFormData({...formData, name: e.target.value})} required placeholder="e.g., Production API Key" /></div>
186
+ <div className="space-y-2"><Label>Key Type</Label><Select value={formData.key_type} onValueChange={v => setFormData({...formData, key_type: v as 'public' | 'private'})}><SelectTrigger><SelectValue /></SelectTrigger><SelectContent>{KEY_TYPE_OPTIONS.map(opt => <SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>)}</SelectContent></Select></div>
187
+ <div className="space-y-2"><Label>Prefix</Label><Input value={formData.key_prefix} onChange={e => setFormData({...formData, key_prefix: e.target.value})} placeholder="e.g., sk_" /></div>
188
+ <div className="space-y-2"><Label>Rate Limit (requests/min)</Label><Input type="number" value={formData.rate_limit} onChange={e => setFormData({...formData, rate_limit: parseInt(e.target.value)})} min={1} max={10000} /></div>
189
+ <div className="space-y-2"><Label>Expires At</Label><Input type="date" value={formData.expires_at} onChange={e => setFormData({...formData, expires_at: e.target.value})} /></div>
190
+ </div>
191
+ <div className="flex items-center gap-2"><Checkbox id="active" checked={formData.is_active} onCheckedChange={c => setFormData({...formData, is_active: c === true})} /><Label htmlFor="active">Active</Label></div>
192
+ <div className="space-y-2"><Label>Permissions (JSON)</Label><Textarea value={permissionsJson} onChange={e => setPermissionsJson(e.target.value)} rows={4} className="font-mono text-sm" placeholder='{"resources": ["read", "write"]}' /></div>
193
+ <div className="space-y-2"><Label>Metadata (JSON)</Label><Textarea value={metadataJson} onChange={e => setMetadataJson(e.target.value)} rows={3} className="font-mono text-sm" placeholder='{"env": "production"}' /></div>
194
+ </CardContent></Card>
195
+ <div className="flex justify-end gap-3"><Button type="button" variant="outline" onClick={() => navigate(-1)}>Cancel</Button><Button type="submit" disabled={isSubmitting}>{isSubmitting ? 'Creating...' : 'Create API Key'}</Button></div>
196
+ </form>
197
+ </div>
198
+ )
199
+ }
@@ -0,0 +1,303 @@
1
+ /**
2
+ * @module src/pages/admin/APIKeysPage
3
+ * @audience installer
4
+ * @layer frontend-page
5
+ * @stability stable
6
+ *
7
+ * Admin list page for API keys. Fetches all keys via
8
+ * `/api/api-keys?action=list`, applies client-side search, type filter
9
+ * (`public` | `private`), and sort. Key values are never shown in the
10
+ * list — only the prefix. Renders inside `AdminListPage`. Row clicks
11
+ * navigate to `/spine-framework/admin/configs/api-keys/:id`.
12
+ *
13
+ * @seeAlso src/pages/admin/APIKeyDetailPage.tsx
14
+ */
15
+
16
+ import React, { useState } from 'react'
17
+ import { Plus, Key, CheckCircle, Clock, ShieldCheck, RefreshCw } from 'lucide-react';
18
+ import { LoadingSpinner } from '../../components/ui/LoadingSpinner'
19
+ import { Button } from '../../components/ui/button'
20
+ import { AdminListPage } from '../../components/admin/AdminListPage'
21
+ import { SortableTableHeader } from '../../components/admin/SortableTableHeader'
22
+ import { formatDateTime } from '../../lib/utils'
23
+ import { useApi } from '../../hooks/useApi'
24
+ import { apiFetch } from '../../lib/api'
25
+
26
+ interface APIKey {
27
+ id: string
28
+ name: string
29
+ key_type: 'public' | 'private'
30
+ is_active: boolean
31
+ last_used_at?: string
32
+ expires_at?: string
33
+ rate_limit?: number
34
+ usage_count?: number
35
+ integration?: {
36
+ id: string
37
+ name: string
38
+ provider: string
39
+ }
40
+ created_by_person?: {
41
+ id: string
42
+ full_name: string
43
+ email: string
44
+ }
45
+ created_at: string
46
+ }
47
+
48
+ export function APIKeysPage() {
49
+ const [searchTerm, setSearchTerm] = useState('')
50
+ const [selectedStatus, setSelectedStatus] = useState('all')
51
+ const [selectedType, setSelectedType] = useState('all')
52
+ const [sortKey, setSortKey] = useState('created_at')
53
+ const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc')
54
+
55
+ const { data: apiKeys, loading, error, refetch } = useApi<APIKey[]>(
56
+ async () => {
57
+ const response = await apiFetch('/api/api-keys?action=list')
58
+ if (!response.ok) throw new Error('Failed to fetch API keys')
59
+ const result = await response.json()
60
+ return (result.data || result) as APIKey[]
61
+ },
62
+ { immediate: true }
63
+ )
64
+
65
+ const filteredKeys = (apiKeys || []).filter(key => {
66
+ const matchesSearch = key.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
67
+ (key.integration?.name?.toLowerCase().includes(searchTerm.toLowerCase()) || false)
68
+ const matchesStatus = selectedStatus === 'all' ||
69
+ (selectedStatus === 'active' && key.is_active) ||
70
+ (selectedStatus === 'inactive' && !key.is_active)
71
+ const matchesType = selectedType === 'all' || key.key_type === selectedType
72
+ return matchesSearch && matchesStatus && matchesType
73
+ })
74
+
75
+ const handleSort = (key: string) => {
76
+ if (sortKey === key) {
77
+ setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc')
78
+ } else {
79
+ setSortKey(key)
80
+ setSortDirection('asc')
81
+ }
82
+ }
83
+
84
+ const handleRowClick = (key: APIKey) => {
85
+ window.location.href = `/spine-framework/admin/configs/api-keys/${key.id}`
86
+ }
87
+
88
+ const sortedKeys = [...(filteredKeys || [])].sort((a, b) => {
89
+ let aValue: any = a[sortKey as keyof APIKey]
90
+ let bValue: any = b[sortKey as keyof APIKey]
91
+
92
+ if (typeof aValue === 'string') {
93
+ return sortDirection === 'asc'
94
+ ? aValue.localeCompare(bValue)
95
+ : bValue.localeCompare(aValue)
96
+ }
97
+
98
+ if (typeof aValue === 'boolean') {
99
+ return sortDirection === 'asc' ? (aValue ? 1 : 0) : (bValue ? 1 : 0)
100
+ }
101
+
102
+ return 0
103
+ })
104
+
105
+ const isExpired = (key: APIKey) => {
106
+ if (!key.expires_at) return false
107
+ return new Date(key.expires_at) < new Date()
108
+ }
109
+
110
+ const statsCards = [
111
+ {
112
+ title: 'Total Keys',
113
+ value: (apiKeys || []).length,
114
+ icon: Key,
115
+ iconColor: 'text-blue-500'
116
+ },
117
+ {
118
+ title: 'Active',
119
+ value: (apiKeys || []).filter(k => k.is_active).length,
120
+ icon: CheckCircle,
121
+ iconColor: 'text-green-500'
122
+ },
123
+ {
124
+ title: 'Private Keys',
125
+ value: (apiKeys || []).filter(k => k.key_type === 'private').length,
126
+ icon: ShieldCheck,
127
+ iconColor: 'text-purple-500'
128
+ },
129
+ {
130
+ title: 'Expiring Soon',
131
+ value: (apiKeys || []).filter(k => {
132
+ if (!k.expires_at) return false
133
+ const days = Math.ceil((new Date(k.expires_at).getTime() - Date.now()) / (1000 * 60 * 60 * 24))
134
+ return days > 0 && days <= 7
135
+ }).length,
136
+ icon: Clock,
137
+ iconColor: 'text-orange-500'
138
+ }
139
+ ]
140
+
141
+ const statusOptions = [
142
+ { value: 'all', label: 'All Status' },
143
+ { value: 'active', label: 'Active' },
144
+ { value: 'inactive', label: 'Inactive' }
145
+ ]
146
+
147
+ const typeOptions = [
148
+ { value: 'all', label: 'All Types' },
149
+ { value: 'public', label: 'Public' },
150
+ { value: 'private', label: 'Private' }
151
+ ]
152
+
153
+ const filters = [
154
+ {
155
+ label: 'Status',
156
+ value: selectedStatus,
157
+ options: statusOptions,
158
+ onChange: setSelectedStatus
159
+ },
160
+ {
161
+ label: 'Type',
162
+ value: selectedType,
163
+ options: typeOptions,
164
+ onChange: setSelectedType
165
+ }
166
+ ]
167
+
168
+ return (
169
+ <AdminListPage
170
+ title="API Keys"
171
+ description="Manage API keys and access tokens"
172
+ newButtonText="Generate API Key"
173
+ newButtonHref="/spine-framework/admin/configs/api-keys/new"
174
+ statsCards={statsCards}
175
+ searchPlaceholder="Search API keys..."
176
+ searchValue={searchTerm}
177
+ onSearchChange={setSearchTerm}
178
+ filters={filters}
179
+ loading={loading}
180
+ error={error}
181
+ emptyMessage="No API keys found"
182
+ emptyIcon={Key}
183
+ >
184
+ {sortedKeys.length === 0 ? (
185
+ <div className="p-8 text-center">
186
+ <Key className="mx-auto h-12 w-12 text-slate-400" />
187
+ <h3 className="mt-2 text-sm font-medium text-slate-900">No API keys found</h3>
188
+ <p className="mt-1 text-sm text-slate-500">
189
+ Try adjusting your search or filters
190
+ </p>
191
+ </div>
192
+ ) : (
193
+ <table className="min-w-full divide-y divide-slate-200">
194
+ <thead className="bg-slate-50">
195
+ <tr>
196
+ <SortableTableHeader
197
+ title="Name"
198
+ sortKey="name"
199
+ currentSortKey={sortKey}
200
+ currentSortDirection={sortDirection}
201
+ onSort={handleSort}
202
+ />
203
+ <SortableTableHeader
204
+ title="Type"
205
+ sortKey="key_type"
206
+ currentSortKey={sortKey}
207
+ currentSortDirection={sortDirection}
208
+ onSort={handleSort}
209
+ />
210
+ <SortableTableHeader
211
+ title="Status"
212
+ sortKey="is_active"
213
+ currentSortKey={sortKey}
214
+ currentSortDirection={sortDirection}
215
+ onSort={handleSort}
216
+ />
217
+ <SortableTableHeader
218
+ title="Integration"
219
+ sortKey="integration"
220
+ currentSortKey={sortKey}
221
+ currentSortDirection={sortDirection}
222
+ onSort={handleSort}
223
+ />
224
+ <SortableTableHeader
225
+ title="Usage"
226
+ sortKey="usage_count"
227
+ currentSortKey={sortKey}
228
+ currentSortDirection={sortDirection}
229
+ onSort={handleSort}
230
+ />
231
+ <SortableTableHeader
232
+ title="Expires"
233
+ sortKey="expires_at"
234
+ currentSortKey={sortKey}
235
+ currentSortDirection={sortDirection}
236
+ onSort={handleSort}
237
+ />
238
+ <th className="relative px-6 py-3">
239
+ <span className="sr-only">Actions</span>
240
+ </th>
241
+ </tr>
242
+ </thead>
243
+ <tbody className="bg-white divide-y divide-slate-200">
244
+ {sortedKeys.map((key) => (
245
+ <tr
246
+ key={key.id}
247
+ className="hover:bg-slate-50 cursor-pointer transition-colors"
248
+ onClick={() => handleRowClick(key)}
249
+ >
250
+ <td className="px-6 py-4 whitespace-nowrap">
251
+ <div className="font-medium text-slate-900">
252
+ <span className="text-accent-blue hover:text-navy">
253
+ {key.name}
254
+ </span>
255
+ </div>
256
+ <div className="text-xs text-slate-500">
257
+ {key.created_by_person?.full_name || key.created_by_person?.email || 'System'}
258
+ </div>
259
+ </td>
260
+ <td className="px-6 py-4 whitespace-nowrap">
261
+ <span className={`inline-flex px-2 py-0.5 text-xs font-medium rounded-md ${
262
+ key.key_type === 'private'
263
+ ? 'bg-purple-100 text-purple-700'
264
+ : 'bg-blue-100 text-blue-700'
265
+ }`}>
266
+ {key.key_type}
267
+ </span>
268
+ </td>
269
+ <td className="px-6 py-4 whitespace-nowrap">
270
+ <span className={`inline-flex px-2 py-0.5 text-xs font-medium rounded-md ${
271
+ key.is_active && !isExpired(key)
272
+ ? 'bg-green-100 text-green-700'
273
+ : 'bg-slate-100 text-slate-600'
274
+ }`}>
275
+ {key.is_active && !isExpired(key) ? 'Active' : isExpired(key) ? 'Expired' : 'Inactive'}
276
+ </span>
277
+ </td>
278
+ <td className="px-6 py-4 whitespace-nowrap text-sm text-slate-600">
279
+ {key.integration?.name || '—'}
280
+ </td>
281
+ <td className="px-6 py-4 whitespace-nowrap text-sm text-slate-500">
282
+ {key.usage_count?.toLocaleString() || '0'}
283
+ </td>
284
+ <td className="px-6 py-4 whitespace-nowrap text-sm text-slate-500">
285
+ {key.expires_at ? (
286
+ <span className={isExpired(key) ? 'text-red-600' : ''}>
287
+ {formatDateTime(key.expires_at)}
288
+ </span>
289
+ ) : (
290
+ 'Never'
291
+ )}
292
+ </td>
293
+ <td className="px-6 py-4 whitespace-nowrap text-right">
294
+ <span className="text-slate-400">→</span>
295
+ </td>
296
+ </tr>
297
+ ))}
298
+ </tbody>
299
+ </table>
300
+ )}
301
+ </AdminListPage>
302
+ )
303
+ }