okengine 0.15.2 → 0.17.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 (256) hide show
  1. package/manifest.v1.schema.json +21 -2
  2. package/package.json +1 -1
  3. package/site/content/docs/elements/clock.mdx +7 -0
  4. package/site/content/docs/elements/flow.mdx +20 -12
  5. package/site/content/docs/elements/gate.mdx +108 -3
  6. package/site/content/docs/elements/signal.mdx +46 -17
  7. package/site/content/docs/elements/store.mdx +36 -32
  8. package/site/content/docs/elements/vault.mdx +26 -0
  9. package/site/content/docs/get-started/project-structure.mdx +4 -4
  10. package/site/content/docs/reference/cli.md +1 -1
  11. package/site/content/docs/reference/client.mdx +75 -17
  12. package/site/content/docs/reference/configuration.mdx +7 -4
  13. package/site/content/docs/reference/errors.mdx +24 -17
  14. package/site/content/docs/reference/fx.mdx +39 -22
  15. package/site/content/docs/reference/security.md +1 -1
  16. package/src/auth/api-key-sql.test.ts +47 -0
  17. package/src/auth/api-key-sql.ts +261 -0
  18. package/src/auth/api-keys.ts +276 -14
  19. package/src/auth/auth.test.ts +56 -0
  20. package/src/auth/config.ts +24 -5
  21. package/src/auth/index.ts +62 -1
  22. package/src/auth/plugin.ts +6 -1
  23. package/src/auth/schema.ts +8 -0
  24. package/src/auth/sessions.ts +18 -0
  25. package/src/auth/tables.ts +32 -0
  26. package/src/auth/tenant-config.ts +74 -0
  27. package/src/auth/tenant-tables.ts +11 -0
  28. package/src/auth/tenants.test.ts +63 -0
  29. package/src/auth/tenants.ts +360 -0
  30. package/src/cli/attach-host-console.test.ts +37 -0
  31. package/src/cli/attach-host-console.ts +6 -0
  32. package/src/cli/build.ts +2 -2
  33. package/src/client/budget.test.ts +1 -1
  34. package/src/client/create.ts +75 -5
  35. package/src/client/index.ts +13 -0
  36. package/src/client/live.test.ts +422 -0
  37. package/src/client/live.ts +389 -0
  38. package/src/client/notes-contract.test.ts +41 -0
  39. package/src/client/types.ts +85 -6
  40. package/src/client-react/index.ts +85 -1
  41. package/src/client-react/use-live.test.ts +129 -0
  42. package/src/compiler/effects-infer.ts +37 -2
  43. package/src/compiler/extract.test.ts +143 -11
  44. package/src/compiler/extract.ts +210 -30
  45. package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +1 -2
  46. package/src/compiler/fixtures/skyport.expected.json +2 -3
  47. package/src/compiler/response.ts +41 -11
  48. package/src/console/server/access.test.ts +116 -8
  49. package/src/console/server/access.ts +70 -23
  50. package/src/console/server/app.ts +45 -11
  51. package/src/console/server/console.test.ts +6 -8
  52. package/src/console/server/dev-identities.test.ts +127 -0
  53. package/src/console/server/dev-identities.ts +229 -0
  54. package/src/console/server/flows-invoke.test.ts +21 -4
  55. package/src/console/server/flows.ts +113 -58
  56. package/src/console/server/gates.test.ts +24 -0
  57. package/src/console/server/gates.ts +10 -30
  58. package/src/console/server/index.ts +1 -0
  59. package/src/console/server/live.test.ts +26 -1
  60. package/src/console/server/live.ts +4 -2
  61. package/src/console/server/operator-db.ts +2 -0
  62. package/src/console/server/state.ts +29 -55
  63. package/src/console/server/store.test.ts +17 -0
  64. package/src/console/server/store.ts +43 -1
  65. package/src/console/ui-next/dist/assets/access-page-CXVWWMmD.js +4 -0
  66. package/src/console/ui-next/dist/assets/{agent-disclosure-C0X1fbWF.js → agent-disclosure-BP0Y0Sux.js} +1 -1
  67. package/src/console/ui-next/dist/assets/cache-glyph-B5X-NM0-.js +1 -0
  68. package/src/console/ui-next/dist/assets/call-pii-button-ChAUiLo9.js +1 -0
  69. package/src/console/ui-next/dist/assets/collapsible-DGnOM2ph.js +1 -0
  70. package/src/console/ui-next/dist/assets/copy-inline-button-DBHhgHKP.js +1 -0
  71. package/src/console/ui-next/dist/assets/detail-header-DhHM1iaZ.js +1 -0
  72. package/src/console/ui-next/dist/assets/dropdown-menu-_NjTEo5_.js +1 -0
  73. package/src/console/ui-next/dist/assets/element-icons-BVXtRyd3.js +1 -0
  74. package/src/console/ui-next/dist/assets/explorer-empty-2uIhBu0_.js +1 -0
  75. package/src/console/ui-next/dist/assets/flows-page-XDpAJO8f.js +1 -0
  76. package/src/console/ui-next/dist/assets/{highlighted-json-CS_O8L-r.js → highlighted-json-Awq7gYdu.js} +1 -1
  77. package/src/console/ui-next/dist/assets/http-method-_UHM2ODJ.js +1 -0
  78. package/src/console/ui-next/dist/assets/index-BBj2QJCu.js +66 -0
  79. package/src/console/ui-next/dist/assets/index-Ck88Jmv8.css +2 -0
  80. package/src/console/ui-next/dist/assets/link-BX6Vqztd.js +1 -0
  81. package/src/console/ui-next/dist/assets/observability-page-BFaay44m.js +4 -0
  82. package/src/console/ui-next/dist/assets/replica-lag-DKRrbvdo.js +18 -0
  83. package/src/console/ui-next/dist/assets/request-meta-DV0ywz7t.js +1 -0
  84. package/src/console/ui-next/dist/assets/shortcut-JQIZlWfm.js +1 -0
  85. package/src/console/ui-next/dist/assets/shortcut-keys-DKxNTe_m.js +1 -0
  86. package/src/console/ui-next/dist/assets/skeleton-D-czQJT6.js +1 -0
  87. package/src/console/ui-next/dist/assets/store-page-CS5-aETQ.js +41 -0
  88. package/src/console/ui-next/dist/assets/trace-detail-sheet-B09O8rA6.js +2 -0
  89. package/src/console/ui-next/dist/assets/tree-expand-toggle-BtyhmWb4.js +55 -0
  90. package/src/console/ui-next/dist/assets/units-page-Ca2Z-E52.js +1 -0
  91. package/src/console/ui-next/dist/assets/{use-vault-list-BLbZhzyF.js → use-vault-list-uk4WVboC.js} +1 -1
  92. package/src/console/ui-next/dist/assets/useMutation-B8EO02Ej.js +1 -0
  93. package/src/console/ui-next/dist/assets/useRender-BE2A9BWC.js +1 -0
  94. package/src/console/ui-next/dist/assets/vault-page-BmOeAFwg.js +2 -0
  95. package/src/console/ui-next/dist/index.html +11 -9
  96. package/src/console/ui-next/index.html +3 -3
  97. package/src/console/ui-next/src/client.ts +167 -0
  98. package/src/console/ui-next/src/components/shell/app-sidebar.tsx +2 -0
  99. package/src/console/ui-next/src/components/shell/command-items.test.ts +1 -0
  100. package/src/console/ui-next/src/components/shell/command-items.ts +8 -0
  101. package/src/console/ui-next/src/components/shell/shell-layout.tsx +2 -1
  102. package/src/console/ui-next/src/components/ui/sheet-form.tsx +8 -3
  103. package/src/console/ui-next/src/features/access/access-page.tsx +347 -0
  104. package/src/console/ui-next/src/features/access/data/use-access.ts +23 -0
  105. package/src/console/ui-next/src/features/access/detail/access-detail.tsx +552 -0
  106. package/src/console/ui-next/src/features/access/explorer/access-list.tsx +125 -0
  107. package/src/console/ui-next/src/features/access/lib/format-when.test.ts +139 -0
  108. package/src/console/ui-next/src/features/access/lib/format-when.ts +296 -0
  109. package/src/console/ui-next/src/features/access/lib/scope-groups.test.ts +34 -0
  110. package/src/console/ui-next/src/features/access/lib/scope-groups.ts +40 -0
  111. package/src/console/ui-next/src/features/access/sheets/access-allow-fields.tsx +236 -0
  112. package/src/console/ui-next/src/features/access/sheets/access-create-sheet.tsx +316 -0
  113. package/src/console/ui-next/src/features/access/sheets/access-edit-sheet.tsx +240 -0
  114. package/src/console/ui-next/src/features/access/sheets/access-expiry-fields.tsx +66 -0
  115. package/src/console/ui-next/src/features/access/sheets/access-refresh-sheet.tsx +104 -0
  116. package/src/console/ui-next/src/features/access/sheets/access-scope-field.tsx +115 -0
  117. package/src/console/ui-next/src/features/access/sheets/access-secret-sheet.tsx +89 -0
  118. package/src/console/ui-next/src/features/access/sheets/access-sheet-search.tsx +49 -0
  119. package/src/console/ui-next/src/features/access/state/access-selection.ts +84 -0
  120. package/src/console/ui-next/src/features/auth/auth-redirect.test.ts +5 -0
  121. package/src/console/ui-next/src/features/auth/auth-redirect.ts +18 -2
  122. package/src/console/ui-next/src/features/flows/fixture.ts +0 -1
  123. package/src/console/ui-next/src/features/not-found/not-found-page.tsx +1 -1
  124. package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +5 -1
  125. package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +15 -4
  126. package/src/console/ui-next/src/lib/document-meta.test.ts +1 -0
  127. package/src/console/ui-next/src/lib/document-meta.ts +2 -1
  128. package/src/console/ui-next/src/lib/last-module-search.test.ts +1 -0
  129. package/src/console/ui-next/src/lib/last-module-search.ts +2 -1
  130. package/src/console/ui-next/src/lib/shortcut.test.ts +2 -0
  131. package/src/console/ui-next/src/lib/shortcut.ts +3 -0
  132. package/src/console/ui-next/src/router.tsx +10 -1
  133. package/src/console/ui-next/ui-next-seed-app-schema.ts +88 -37
  134. package/src/console/ui-next/ui-next-seed-manifest-surface.ts +2 -9
  135. package/src/console/ui-next/ui-next-seed-manifest.ts +0 -1
  136. package/src/console/ui-next/ui-next-seed-store.ts +43 -15
  137. package/src/console/xss.gate.test.ts +1 -0
  138. package/src/drivers/index.ts +2 -0
  139. package/src/drivers/journal-postgres.ts +12 -3
  140. package/src/drivers/pg-rls.ts +26 -2
  141. package/src/drivers/pg-vault-rls.ts +68 -0
  142. package/src/drivers/signal-engine.ts +69 -15
  143. package/src/drivers/signal-live-iter.ts +65 -0
  144. package/src/drivers/signal-nats.ts +2 -1
  145. package/src/drivers/signal-postgres.ts +95 -12
  146. package/src/drivers/signal-redis.ts +2 -1
  147. package/src/drivers/signal-retention.ts +64 -0
  148. package/src/drivers/signal-types.ts +35 -5
  149. package/src/elements/clock/declare.ts +64 -2
  150. package/src/elements/clock/reconcile.ts +98 -25
  151. package/src/elements/clock/runtime.ts +13 -2
  152. package/src/elements/clock.test.ts +28 -0
  153. package/src/elements/clock.ts +9 -1
  154. package/src/elements/gate/declare.ts +2 -0
  155. package/src/elements/gate/permissions.ts +12 -0
  156. package/src/elements/gate.ts +6 -1
  157. package/src/elements/signal/declare.ts +44 -10
  158. package/src/elements/signal/delivery-modes.test.ts +90 -4
  159. package/src/elements/signal/order-lifecycle.test.ts +20 -4
  160. package/src/elements/signal/runtime.ts +42 -0
  161. package/src/elements/signal.test.ts +21 -8
  162. package/src/elements/signal.ts +1 -1
  163. package/src/elements/store/cache.ts +1 -1
  164. package/src/elements/store/declare.ts +7 -0
  165. package/src/elements/store/rls-identity.test.ts +29 -0
  166. package/src/elements/store/rls-identity.ts +3 -0
  167. package/src/elements/store/runtime.ts +14 -0
  168. package/src/elements/store/schema-decl.ts +162 -44
  169. package/src/elements/store/schema-tenant.ts +41 -0
  170. package/src/elements/store/sql-rls-isolation.test.ts +39 -0
  171. package/src/elements/store/sql-select.types.test.ts +71 -0
  172. package/src/elements/store/sql-session.ts +54 -32
  173. package/src/elements/store.ts +5 -0
  174. package/src/elements/vault/builtin-adapter.ts +15 -2
  175. package/src/elements/vault/declare.ts +8 -0
  176. package/src/elements/vault/runtime.ts +7 -0
  177. package/src/elements/vault/sql-rls-isolation.test.ts +145 -0
  178. package/src/elements/vault/storage.ts +9 -0
  179. package/src/elements/vault/test-helpers.ts +2 -1
  180. package/src/i18n/catalogs/ar.ts +19 -0
  181. package/src/i18n/catalogs/en.ts +19 -0
  182. package/src/index.ts +2 -0
  183. package/src/kernel/adopt-routes.ts +56 -8
  184. package/src/kernel/api-key-host-persist.test.ts +143 -0
  185. package/src/kernel/app-tenant.ts +122 -0
  186. package/src/kernel/app.ts +196 -57
  187. package/src/kernel/auth-resolve.ts +13 -5
  188. package/src/kernel/boot-bind/clock.ts +9 -3
  189. package/src/kernel/boot.test.ts +4 -4
  190. package/src/kernel/boot.ts +2 -0
  191. package/src/kernel/budget.test.ts +1 -1
  192. package/src/kernel/clock-durable.ts +8 -0
  193. package/src/kernel/clock-per-tenant-name.ts +5 -0
  194. package/src/kernel/clock-reconcile.ts +8 -0
  195. package/src/kernel/errors-live-resume.ts +15 -0
  196. package/src/kernel/errors-tenant.ts +29 -0
  197. package/src/kernel/errors.registry.test.ts +31 -3
  198. package/src/kernel/errors.ts +43 -3
  199. package/src/kernel/flow.ts +26 -5
  200. package/src/kernel/fx-auth-keys.test.ts +88 -0
  201. package/src/kernel/fx-auth-keys.ts +203 -0
  202. package/src/kernel/fx-auth-tenants.test.ts +87 -0
  203. package/src/kernel/fx-auth-tenants.ts +286 -0
  204. package/src/kernel/fx-live-stream.ts +149 -0
  205. package/src/kernel/fx-live.test.ts +157 -0
  206. package/src/kernel/fx-runtime.ts +15 -0
  207. package/src/kernel/fx-tenant-store.ts +213 -0
  208. package/src/kernel/fx.test.ts +108 -1
  209. package/src/kernel/fx.ts +206 -28
  210. package/src/kernel/hooks.ts +2 -2
  211. package/src/kernel/http-resource.ts +9 -18
  212. package/src/kernel/index.ts +2 -0
  213. package/src/kernel/journal.ts +12 -0
  214. package/src/kernel/live-http.test.ts +78 -0
  215. package/src/kernel/live-http.ts +114 -0
  216. package/src/kernel/live-resume.test.ts +125 -0
  217. package/src/kernel/on.ts +51 -0
  218. package/src/kernel/pipeline-api-key.test.ts +192 -7
  219. package/src/kernel/pipeline-tenant.ts +49 -0
  220. package/src/kernel/pipeline.test.ts +1 -1
  221. package/src/kernel/pipeline.ts +41 -4
  222. package/src/kernel/resource-mount.test.ts +10 -27
  223. package/src/kernel/tenant-resolve.test.ts +101 -0
  224. package/src/kernel/tenant-resolve.ts +124 -0
  225. package/src/kernel/tenant-roles.test.ts +87 -0
  226. package/src/kernel/triggers.ts +59 -21
  227. package/src/manifest/diff.test.ts +11 -2
  228. package/src/manifest/diff.ts +53 -11
  229. package/src/manifest/fixtures/skyport.excerpt.json +1 -1
  230. package/src/manifest/fixtures/skyport.manifest.json +0 -1
  231. package/src/manifest/types.ts +58 -6
  232. package/src/release/build-lib.ts +14 -1
  233. package/src/release/limits.ts +2 -2
  234. package/src/release/measure.ts +55 -1
  235. package/src/client/live-gap.test.ts +0 -35
  236. package/src/console/ui-next/dist/assets/cache-glyph-F1FI122b.js +0 -1
  237. package/src/console/ui-next/dist/assets/call-pii-button-DEDCl_j3.js +0 -1
  238. package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +0 -1
  239. package/src/console/ui-next/dist/assets/confirm-sheet-3ptDzXbz.js +0 -1
  240. package/src/console/ui-next/dist/assets/explorer-empty-BvYrygyO.js +0 -1
  241. package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +0 -1
  242. package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +0 -1
  243. package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +0 -66
  244. package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +0 -2
  245. package/src/console/ui-next/dist/assets/link-COeyggt-.js +0 -1
  246. package/src/console/ui-next/dist/assets/observability-page-HK9-BO8z.js +0 -4
  247. package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +0 -18
  248. package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +0 -1
  249. package/src/console/ui-next/dist/assets/shortcut-Cgzuv4k1.js +0 -1
  250. package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +0 -1
  251. package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +0 -1
  252. package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +0 -41
  253. package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +0 -2
  254. package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +0 -54
  255. package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +0 -1
  256. package/src/console/ui-next/dist/assets/vault-page-B1SbYe10.js +0 -2
@@ -0,0 +1,347 @@
1
+ /**
2
+ * Access page — API keys as issuer credentials, not a second permission system.
3
+ */
4
+
5
+ import { PlusSignIcon } from "@hugeicons/core-free-icons";
6
+ import { HugeiconsIcon } from "@hugeicons/react";
7
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
8
+ import { useEffect, useMemo, useState, type JSX } from "react";
9
+ import {
10
+ accessCreateKey,
11
+ accessKeyBlast,
12
+ accessRevokeKey,
13
+ accessRotateKey,
14
+ accessUpdateKey,
15
+ clientErrorText,
16
+ } from "@/client.ts";
17
+ import {
18
+ EXPLORER_ICON_BUTTON_CLASS,
19
+ EXPLORER_PAGE_CLASS,
20
+ EXPLORER_SPLIT,
21
+ EXPLORER_TOOLBAR_CLASS,
22
+ } from "@/components/explorer/explorer-chrome.ts";
23
+ import { ExplorerEmpty } from "@/components/explorer/explorer-empty.tsx";
24
+ import { ExplorerSearch } from "@/components/explorer/explorer-search.tsx";
25
+ import { ExplorerStartToggle } from "@/components/explorer/explorer-start-toggle.tsx";
26
+ import { useExplorerStartPanel } from "@/components/explorer/use-explorer-start-panel.ts";
27
+ import { ConfirmSheet } from "@/components/ui/confirm-sheet.tsx";
28
+ import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
29
+ import { ToolbarTip } from "@/components/ui/toolbar-tip.tsx";
30
+ import { ELEMENT_ICONS } from "@/lib/element-icons.ts";
31
+ import { AccessDetail } from "./detail/access-detail.tsx";
32
+ import { ACCESS_LIST_QUERY_KEY, useAccessList } from "./data/use-access.ts";
33
+ import { AccessList } from "./explorer/access-list.tsx";
34
+ import { AccessCreateSheet } from "./sheets/access-create-sheet.tsx";
35
+ import { AccessEditSheet } from "./sheets/access-edit-sheet.tsx";
36
+ import { AccessRefreshSheet } from "./sheets/access-refresh-sheet.tsx";
37
+ import { AccessSecretSheet } from "./sheets/access-secret-sheet.tsx";
38
+ import { useAccessSelection } from "./state/access-selection.ts";
39
+
40
+ /**
41
+ * Access explorer — create, edit, refresh expiry, revoke, rotate, usage, working call.
42
+ */
43
+ export function AccessPage(): JSX.Element {
44
+ const list = useAccessList();
45
+ const qc = useQueryClient();
46
+ const { query, selectedKey, action, setQuery, setSelectedKey, setAction } = useAccessSelection();
47
+ const start = useExplorerStartPanel();
48
+ const [now] = useState(() => Date.now());
49
+ const [secret, setSecret] = useState<string | null>(null);
50
+ const [secretTitle, setSecretTitle] = useState("New API key");
51
+
52
+ const keys = useMemo(() => {
53
+ const user = list.data?.userPlane.keys ?? [];
54
+ const ops = list.data?.operatorPlane.keys ?? [];
55
+ return [...user, ...ops];
56
+ }, [list.data]);
57
+
58
+ const filtered = useMemo(() => {
59
+ const q = query.trim().toLowerCase();
60
+ if (!q) return keys;
61
+ return keys.filter(
62
+ (row) =>
63
+ row.name.toLowerCase().includes(q) ||
64
+ row.id.toLowerCase().includes(q) ||
65
+ row.scopes.some((s) => s.toLowerCase().includes(q)),
66
+ );
67
+ }, [keys, query]);
68
+
69
+ const selected =
70
+ filtered.find((row) => row.id === selectedKey) ?? keys.find((row) => row.id === selectedKey);
71
+
72
+ const blast = useQuery({
73
+ queryKey: ["console.access.blast", selected?.id],
74
+ queryFn: async () => {
75
+ if (!selected) throw new Error("no key");
76
+ const res = await accessKeyBlast(selected.id);
77
+ if (res.error || !res.data) throw new Error(res.error?.message ?? "blast failed");
78
+ return res.data;
79
+ },
80
+ enabled: selected !== undefined,
81
+ });
82
+
83
+ useEffect(() => {
84
+ const onKey = (event: KeyboardEvent) => {
85
+ if (event.key !== "/" || event.metaKey || event.ctrlKey || event.altKey) return;
86
+ const target = event.target;
87
+ if (target instanceof HTMLElement) {
88
+ const tag = target.tagName;
89
+ if (tag === "INPUT" || tag === "TEXTAREA" || target.isContentEditable) return;
90
+ }
91
+ event.preventDefault();
92
+ document.querySelector<HTMLInputElement>("[data-slot='access-search-input']")?.focus();
93
+ };
94
+ window.addEventListener("keydown", onKey);
95
+ return () => window.removeEventListener("keydown", onKey);
96
+ }, []);
97
+
98
+ const invalidate = () => {
99
+ void qc.invalidateQueries({ queryKey: ACCESS_LIST_QUERY_KEY });
100
+ };
101
+
102
+ const create = useMutation({
103
+ mutationFn: accessCreateKey,
104
+ onSuccess: (res) => {
105
+ if (res.error || !res.data)
106
+ throw new Error(res.error ? clientErrorText(res.error) : "create failed");
107
+ setSecretTitle("New API key");
108
+ setSecret(res.data.secret);
109
+ setSelectedKey(res.data.key.id);
110
+ setAction(null);
111
+ invalidate();
112
+ },
113
+ });
114
+
115
+ const rotate = useMutation({
116
+ mutationFn: (input: { readonly confirmation: "ROTATE"; readonly reason: string }) =>
117
+ accessRotateKey({
118
+ keyId: selected!.id,
119
+ confirmation: input.confirmation,
120
+ reason: input.reason,
121
+ }),
122
+ onSuccess: (res) => {
123
+ if (res.error || !res.data)
124
+ throw new Error(res.error ? clientErrorText(res.error) : "rotate failed");
125
+ setSecretTitle("Rotated API key");
126
+ setSecret(res.data.secret);
127
+ setAction(null);
128
+ invalidate();
129
+ },
130
+ });
131
+
132
+ const revoke = useMutation({
133
+ mutationFn: (input: { readonly confirmation: "REVOKE"; readonly reason: string }) =>
134
+ accessRevokeKey({
135
+ keyId: selected!.id,
136
+ confirmation: input.confirmation,
137
+ reason: input.reason,
138
+ }),
139
+ onSuccess: (res) => {
140
+ if (res.error || !res.data)
141
+ throw new Error(res.error ? clientErrorText(res.error) : "revoke failed");
142
+ setAction(null);
143
+ invalidate();
144
+ },
145
+ });
146
+
147
+ const update = useMutation({
148
+ mutationFn: accessUpdateKey,
149
+ onSuccess: (res) => {
150
+ if (res.error || !res.data)
151
+ throw new Error(res.error ? clientErrorText(res.error) : "update failed");
152
+ setAction(null);
153
+ invalidate();
154
+ },
155
+ });
156
+
157
+ const refresh = useMutation({
158
+ mutationFn: (expiresAt: number | null) =>
159
+ accessUpdateKey({
160
+ keyId: selected!.id,
161
+ expiresAt,
162
+ }),
163
+ onSuccess: (res) => {
164
+ if (res.error || !res.data)
165
+ throw new Error(res.error ? clientErrorText(res.error) : "refresh failed");
166
+ setAction(null);
167
+ invalidate();
168
+ },
169
+ });
170
+
171
+ const startToggle = (
172
+ <ExplorerStartToggle
173
+ open={start.open}
174
+ onToggle={start.toggle}
175
+ noun="keys"
176
+ controlsId="access-list"
177
+ dataSlot="access-list-toggle"
178
+ />
179
+ );
180
+
181
+ return (
182
+ <div className={EXPLORER_PAGE_CLASS} data-slot="access-page">
183
+ <ResizablePanelGroup orientation="horizontal" className="min-h-0 flex-1">
184
+ <ResizablePanel
185
+ panelRef={start.panelRef}
186
+ collapsible
187
+ collapsedSize={0}
188
+ defaultSize={EXPLORER_SPLIT.start.defaultSize}
189
+ minSize={EXPLORER_SPLIT.start.minSize}
190
+ onResize={start.onResize}
191
+ className="min-h-0 overflow-hidden"
192
+ >
193
+ <div className="flex h-full min-h-0 flex-col overflow-hidden">
194
+ <div className={EXPLORER_TOOLBAR_CLASS}>
195
+ <ExplorerSearch
196
+ data-slot="access-search-input"
197
+ placeholder="Search keys"
198
+ aria-label="Search keys"
199
+ value={query}
200
+ onChange={(e) => setQuery(e.target.value)}
201
+ />
202
+ <ToolbarTip label="Create key" className="flex self-stretch">
203
+ <button
204
+ type="button"
205
+ className={EXPLORER_ICON_BUTTON_CLASS}
206
+ data-slot="access-create"
207
+ aria-label="Create key"
208
+ onClick={() => setAction("create")}
209
+ >
210
+ <HugeiconsIcon icon={PlusSignIcon} className="size-3.5" />
211
+ </button>
212
+ </ToolbarTip>
213
+ </div>
214
+ <AccessList
215
+ rows={filtered}
216
+ selectedKey={selectedKey}
217
+ query={query}
218
+ loading={list.isLoading}
219
+ onSelect={setSelectedKey}
220
+ />
221
+ </div>
222
+ </ResizablePanel>
223
+ {start.open ? <ResizableHandle withHandle /> : null}
224
+ <ResizablePanel
225
+ defaultSize={EXPLORER_SPLIT.end.defaultSize}
226
+ minSize={EXPLORER_SPLIT.end.minSize}
227
+ className="min-h-0 overflow-hidden"
228
+ >
229
+ {selected ? (
230
+ <AccessDetail
231
+ keyRow={selected}
232
+ blast={blast.data}
233
+ now={now}
234
+ leading={startToggle}
235
+ onEdit={() => setAction("edit")}
236
+ onRefresh={() => setAction("refresh")}
237
+ onRevoke={() => setAction("revoke")}
238
+ onRotate={() => setAction("rotate")}
239
+ rotatePending={rotate.isPending}
240
+ />
241
+ ) : (
242
+ <ExplorerEmpty
243
+ icon={ELEMENT_ICONS.gate.icon}
244
+ iconClassName="border-border/70"
245
+ title={
246
+ list.isLoading ? "Loading keys…" : keys.length > 0 ? "Select a key" : "No keys yet"
247
+ }
248
+ leading={startToggle}
249
+ description={
250
+ list.isLoading ? (
251
+ "Reading planes and grantable scopes."
252
+ ) : keys.length > 0 && query.trim().length > 0 ? (
253
+ <>
254
+ Nothing matches <span className="font-mono">{query}</span>.{" "}
255
+ <button
256
+ type="button"
257
+ className="underline underline-offset-2 hover:text-foreground"
258
+ onClick={() => setQuery("")}
259
+ >
260
+ Clear search
261
+ </button>
262
+ </>
263
+ ) : (
264
+ "Create one to mint a secret once."
265
+ )
266
+ }
267
+ />
268
+ )}
269
+ </ResizablePanel>
270
+ </ResizablePanelGroup>
271
+
272
+ <AccessCreateSheet
273
+ open={action === "create"}
274
+ users={list.data?.userPlane.users ?? []}
275
+ userScopes={list.data?.userPlane.grantableScopes ?? []}
276
+ operatorScopes={list.data?.operatorPlane.grantableScopes ?? []}
277
+ pending={create.isPending}
278
+ error={create.error instanceof Error ? create.error.message : null}
279
+ onOpenChange={(open) => {
280
+ if (!open) setAction(null);
281
+ }}
282
+ onSubmit={(input) => create.mutate(input)}
283
+ />
284
+ {selected ? (
285
+ <AccessEditSheet
286
+ open={action === "edit"}
287
+ keyRow={selected}
288
+ users={list.data?.userPlane.users ?? []}
289
+ operators={list.data?.operatorPlane.operators ?? []}
290
+ operatorScopes={list.data?.operatorPlane.grantableScopes ?? []}
291
+ pending={update.isPending}
292
+ error={update.error instanceof Error ? update.error.message : null}
293
+ onOpenChange={(open) => {
294
+ if (!open) setAction(null);
295
+ }}
296
+ onSubmit={(input) => update.mutate({ keyId: selected.id, ...input })}
297
+ />
298
+ ) : null}
299
+ {selected ? (
300
+ <AccessRefreshSheet
301
+ open={action === "refresh"}
302
+ keyRow={selected}
303
+ pending={refresh.isPending}
304
+ error={refresh.error instanceof Error ? refresh.error.message : null}
305
+ onOpenChange={(open) => {
306
+ if (!open) setAction(null);
307
+ }}
308
+ onSubmit={(expiresAt) => refresh.mutate(expiresAt)}
309
+ />
310
+ ) : null}
311
+ {selected ? (
312
+ <ConfirmSheet
313
+ open={action === "revoke"}
314
+ onOpenChange={(open) => {
315
+ if (!open) setAction(null);
316
+ }}
317
+ phrase="REVOKE"
318
+ title={`Revoke ${selected.name}`}
319
+ description="Irreversible. Residual sessions may continue for the access TTL."
320
+ pending={revoke.isPending}
321
+ error={revoke.error instanceof Error ? revoke.error.message : null}
322
+ confirmLabel="Revoke key"
323
+ slot="access-revoke-sheet"
324
+ onConfirm={(input) => revoke.mutate({ confirmation: "REVOKE", reason: input.reason })}
325
+ />
326
+ ) : null}
327
+ {selected ? (
328
+ <ConfirmSheet
329
+ open={action === "rotate"}
330
+ onOpenChange={(open) => {
331
+ if (!open) setAction(null);
332
+ }}
333
+ phrase="ROTATE"
334
+ title={`Rotate ${selected.name}`}
335
+ description="Mints a new secret once. The current secret stops after residual TTL."
336
+ pending={rotate.isPending}
337
+ error={rotate.error instanceof Error ? rotate.error.message : null}
338
+ confirmLabel="Rotate key"
339
+ confirmVariant="ghost"
340
+ slot="access-rotate-sheet"
341
+ onConfirm={(input) => rotate.mutate({ confirmation: "ROTATE", reason: input.reason })}
342
+ />
343
+ ) : null}
344
+ <AccessSecretSheet secret={secret} title={secretTitle} onDismiss={() => setSecret(null)} />
345
+ </div>
346
+ );
347
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * React Query wrapper for `GET /console/access`.
3
+ */
4
+
5
+ import { useQuery } from "@tanstack/react-query";
6
+ import { accessList, type AccessListPayload } from "@/client.ts";
7
+
8
+ /** React Query key for the Access list. */
9
+ export const ACCESS_LIST_QUERY_KEY = ["console.access.list"] as const;
10
+
11
+ /**
12
+ * Fetch Access keys, users, and grantable scopes.
13
+ */
14
+ export function useAccessList() {
15
+ return useQuery({
16
+ queryKey: ACCESS_LIST_QUERY_KEY,
17
+ queryFn: async (): Promise<AccessListPayload> => {
18
+ const res = await accessList();
19
+ if (res.error || !res.data) throw new Error(res.error?.message ?? "Empty access list");
20
+ return res.data;
21
+ },
22
+ });
23
+ }