okengine 0.15.2 → 0.16.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 (141) hide show
  1. package/package.json +1 -1
  2. package/site/content/docs/elements/flow.mdx +6 -0
  3. package/site/content/docs/elements/gate.mdx +55 -0
  4. package/site/content/docs/elements/store.mdx +13 -15
  5. package/site/content/docs/elements/vault.mdx +3 -0
  6. package/site/content/docs/reference/client.mdx +3 -0
  7. package/site/content/docs/reference/fx.mdx +29 -18
  8. package/site/content/docs/reference/security.md +1 -1
  9. package/src/auth/api-key-sql.test.ts +47 -0
  10. package/src/auth/api-key-sql.ts +254 -0
  11. package/src/auth/api-keys.ts +273 -14
  12. package/src/auth/auth.test.ts +56 -0
  13. package/src/auth/config.ts +5 -5
  14. package/src/auth/index.ts +25 -1
  15. package/src/auth/schema.ts +8 -0
  16. package/src/cli/attach-host-console.test.ts +37 -0
  17. package/src/cli/attach-host-console.ts +6 -0
  18. package/src/compiler/effects-infer.ts +17 -2
  19. package/src/console/server/access.test.ts +116 -8
  20. package/src/console/server/access.ts +70 -23
  21. package/src/console/server/app.ts +1 -0
  22. package/src/console/server/dev-identities.test.ts +127 -0
  23. package/src/console/server/dev-identities.ts +229 -0
  24. package/src/console/server/flows-invoke.test.ts +21 -4
  25. package/src/console/server/flows.ts +113 -58
  26. package/src/console/server/gates.test.ts +24 -0
  27. package/src/console/server/gates.ts +8 -21
  28. package/src/console/server/index.ts +1 -0
  29. package/src/console/server/live.test.ts +26 -1
  30. package/src/console/server/live.ts +4 -2
  31. package/src/console/server/operator-db.ts +2 -0
  32. package/src/console/server/state.ts +29 -55
  33. package/src/console/ui-next/dist/assets/access-page-CAGHrA9H.js +4 -0
  34. package/src/console/ui-next/dist/assets/{agent-disclosure-C0X1fbWF.js → agent-disclosure-BP0Y0Sux.js} +1 -1
  35. package/src/console/ui-next/dist/assets/cache-glyph-B5X-NM0-.js +1 -0
  36. package/src/console/ui-next/dist/assets/call-pii-button-ChAUiLo9.js +1 -0
  37. package/src/console/ui-next/dist/assets/collapsible-DGnOM2ph.js +1 -0
  38. package/src/console/ui-next/dist/assets/copy-inline-button-DBHhgHKP.js +1 -0
  39. package/src/console/ui-next/dist/assets/detail-header-DhHM1iaZ.js +1 -0
  40. package/src/console/ui-next/dist/assets/dropdown-menu-_NjTEo5_.js +1 -0
  41. package/src/console/ui-next/dist/assets/element-icons-BVXtRyd3.js +1 -0
  42. package/src/console/ui-next/dist/assets/explorer-empty-2uIhBu0_.js +1 -0
  43. package/src/console/ui-next/dist/assets/flows-page-B-OUtiAu.js +1 -0
  44. package/src/console/ui-next/dist/assets/{highlighted-json-CS_O8L-r.js → highlighted-json-Awq7gYdu.js} +1 -1
  45. package/src/console/ui-next/dist/assets/http-method-_UHM2ODJ.js +1 -0
  46. package/src/console/ui-next/dist/assets/index-CGoZkILK.js +66 -0
  47. package/src/console/ui-next/dist/assets/index-Ck88Jmv8.css +2 -0
  48. package/src/console/ui-next/dist/assets/link-BX6Vqztd.js +1 -0
  49. package/src/console/ui-next/dist/assets/observability-page-BDyXalNR.js +4 -0
  50. package/src/console/ui-next/dist/assets/replica-lag-DKRrbvdo.js +18 -0
  51. package/src/console/ui-next/dist/assets/request-meta-DV0ywz7t.js +1 -0
  52. package/src/console/ui-next/dist/assets/shortcut-JQIZlWfm.js +1 -0
  53. package/src/console/ui-next/dist/assets/shortcut-keys-DKxNTe_m.js +1 -0
  54. package/src/console/ui-next/dist/assets/skeleton-D-czQJT6.js +1 -0
  55. package/src/console/ui-next/dist/assets/store-page-Xh8Kn3rx.js +41 -0
  56. package/src/console/ui-next/dist/assets/trace-detail-sheet-B09O8rA6.js +2 -0
  57. package/src/console/ui-next/dist/assets/tree-expand-toggle-DkOXA12R.js +54 -0
  58. package/src/console/ui-next/dist/assets/units-page-Dpk40kOQ.js +1 -0
  59. package/src/console/ui-next/dist/assets/{use-vault-list-BLbZhzyF.js → use-vault-list-uk4WVboC.js} +1 -1
  60. package/src/console/ui-next/dist/assets/useMutation-B8EO02Ej.js +1 -0
  61. package/src/console/ui-next/dist/assets/useRender-BE2A9BWC.js +1 -0
  62. package/src/console/ui-next/dist/assets/vault-page-B1dB9Ft0.js +2 -0
  63. package/src/console/ui-next/dist/index.html +11 -9
  64. package/src/console/ui-next/index.html +3 -3
  65. package/src/console/ui-next/src/client.ts +167 -0
  66. package/src/console/ui-next/src/components/shell/app-sidebar.tsx +2 -0
  67. package/src/console/ui-next/src/components/shell/command-items.test.ts +1 -0
  68. package/src/console/ui-next/src/components/shell/command-items.ts +8 -0
  69. package/src/console/ui-next/src/components/shell/shell-layout.tsx +2 -1
  70. package/src/console/ui-next/src/components/ui/sheet-form.tsx +8 -3
  71. package/src/console/ui-next/src/features/access/access-page.tsx +347 -0
  72. package/src/console/ui-next/src/features/access/data/use-access.ts +23 -0
  73. package/src/console/ui-next/src/features/access/detail/access-detail.tsx +552 -0
  74. package/src/console/ui-next/src/features/access/explorer/access-list.tsx +125 -0
  75. package/src/console/ui-next/src/features/access/lib/format-when.test.ts +139 -0
  76. package/src/console/ui-next/src/features/access/lib/format-when.ts +296 -0
  77. package/src/console/ui-next/src/features/access/lib/scope-groups.test.ts +34 -0
  78. package/src/console/ui-next/src/features/access/lib/scope-groups.ts +40 -0
  79. package/src/console/ui-next/src/features/access/sheets/access-allow-fields.tsx +236 -0
  80. package/src/console/ui-next/src/features/access/sheets/access-create-sheet.tsx +316 -0
  81. package/src/console/ui-next/src/features/access/sheets/access-edit-sheet.tsx +240 -0
  82. package/src/console/ui-next/src/features/access/sheets/access-expiry-fields.tsx +66 -0
  83. package/src/console/ui-next/src/features/access/sheets/access-refresh-sheet.tsx +104 -0
  84. package/src/console/ui-next/src/features/access/sheets/access-scope-field.tsx +115 -0
  85. package/src/console/ui-next/src/features/access/sheets/access-secret-sheet.tsx +89 -0
  86. package/src/console/ui-next/src/features/access/sheets/access-sheet-search.tsx +49 -0
  87. package/src/console/ui-next/src/features/access/state/access-selection.ts +84 -0
  88. package/src/console/ui-next/src/features/auth/auth-redirect.test.ts +5 -0
  89. package/src/console/ui-next/src/features/auth/auth-redirect.ts +18 -2
  90. package/src/console/ui-next/src/features/not-found/not-found-page.tsx +1 -1
  91. package/src/console/ui-next/src/lib/document-meta.test.ts +1 -0
  92. package/src/console/ui-next/src/lib/document-meta.ts +2 -1
  93. package/src/console/ui-next/src/lib/last-module-search.test.ts +1 -0
  94. package/src/console/ui-next/src/lib/last-module-search.ts +2 -1
  95. package/src/console/ui-next/src/lib/shortcut.test.ts +2 -0
  96. package/src/console/ui-next/src/lib/shortcut.ts +3 -0
  97. package/src/console/ui-next/src/router.tsx +10 -1
  98. package/src/console/ui-next/ui-next-seed-app-schema.ts +88 -37
  99. package/src/console/ui-next/ui-next-seed-store.ts +43 -15
  100. package/src/console/xss.gate.test.ts +1 -0
  101. package/src/elements/gate/declare.ts +2 -0
  102. package/src/elements/store/cache.ts +1 -1
  103. package/src/elements/store/runtime.ts +14 -0
  104. package/src/elements/store/schema-decl.ts +99 -41
  105. package/src/elements/store/sql-select.types.test.ts +71 -0
  106. package/src/elements/store/sql-session.ts +54 -32
  107. package/src/elements/store.ts +3 -0
  108. package/src/index.ts +1 -0
  109. package/src/kernel/api-key-host-persist.test.ts +143 -0
  110. package/src/kernel/app.ts +14 -2
  111. package/src/kernel/auth-resolve.ts +10 -5
  112. package/src/kernel/boot.test.ts +4 -4
  113. package/src/kernel/fx-auth-keys.test.ts +88 -0
  114. package/src/kernel/fx-auth-keys.ts +198 -0
  115. package/src/kernel/fx.test.ts +17 -1
  116. package/src/kernel/fx.ts +36 -18
  117. package/src/kernel/pipeline-api-key.test.ts +192 -7
  118. package/src/kernel/pipeline.ts +4 -2
  119. package/src/manifest/types.ts +9 -3
  120. package/src/release/build-lib.ts +1 -0
  121. package/src/console/ui-next/dist/assets/cache-glyph-F1FI122b.js +0 -1
  122. package/src/console/ui-next/dist/assets/call-pii-button-DEDCl_j3.js +0 -1
  123. package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +0 -1
  124. package/src/console/ui-next/dist/assets/confirm-sheet-3ptDzXbz.js +0 -1
  125. package/src/console/ui-next/dist/assets/explorer-empty-BvYrygyO.js +0 -1
  126. package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +0 -1
  127. package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +0 -1
  128. package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +0 -66
  129. package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +0 -2
  130. package/src/console/ui-next/dist/assets/link-COeyggt-.js +0 -1
  131. package/src/console/ui-next/dist/assets/observability-page-HK9-BO8z.js +0 -4
  132. package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +0 -18
  133. package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +0 -1
  134. package/src/console/ui-next/dist/assets/shortcut-Cgzuv4k1.js +0 -1
  135. package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +0 -1
  136. package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +0 -1
  137. package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +0 -41
  138. package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +0 -2
  139. package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +0 -54
  140. package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +0 -1
  141. 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
+ }