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,552 @@
1
+ /**
2
+ * Access key inspector — grant, bounds, usage, working call.
3
+ */
4
+
5
+ import { CrownIcon, Key01Icon, UserIcon } from "@hugeicons/core-free-icons";
6
+ import { HugeiconsIcon } from "@hugeicons/react";
7
+ import { useMemo, type JSX, type ReactNode } from "react";
8
+ import type { AccessKeyBlastPayload, AccessKeyRow } from "@/client.ts";
9
+ import { CopyInlineButton } from "@/components/explorer/copy-inline-button.tsx";
10
+ import { DetailHeader } from "@/components/explorer/detail-header.tsx";
11
+ import {
12
+ EXPLORER_COUNT_CLASS,
13
+ EXPLORER_ROW_CLASS,
14
+ EXPLORER_STRIP_CLASS,
15
+ EXPLORER_STRIP_TOKEN_CLASS,
16
+ EXPLORER_STRIP_TOKEN_IDLE_CLASS,
17
+ SECTION_HEAD_CLASS,
18
+ explorerIconInk,
19
+ } from "@/components/explorer/explorer-chrome.ts";
20
+ import { Button } from "@/components/ui/button";
21
+ import { ToolbarTip } from "@/components/ui/toolbar-tip.tsx";
22
+ import { httpMethodBadgeClass, httpMethodRailClass } from "@/features/flows/traces/http-method.ts";
23
+ import { cn } from "@/lib/utils.ts";
24
+ import {
25
+ classifyAccessAllowEntry,
26
+ formatAccessExpiry,
27
+ formatAccessWhen,
28
+ } from "../lib/format-when.ts";
29
+ import { groupAccessScopes } from "../lib/scope-groups.ts";
30
+
31
+ const CALL_REQUEST = `GET /secure
32
+ Authorization: Bearer <secret>`;
33
+ const CALL_RESPONSE = `{ "ok": true }`;
34
+
35
+ /** Props for {@link AccessDetail}. */
36
+ export interface AccessDetailProps {
37
+ readonly keyRow: AccessKeyRow;
38
+ readonly blast: AccessKeyBlastPayload | undefined;
39
+ readonly now: number;
40
+ readonly leading?: ReactNode;
41
+ readonly onEdit: () => void;
42
+ readonly onRefresh: () => void;
43
+ readonly onRevoke: () => void;
44
+ readonly onRotate: () => void;
45
+ readonly rotatePending: boolean;
46
+ }
47
+
48
+ /**
49
+ * Right-pane key inspector.
50
+ *
51
+ * @param props - Selected key + actions
52
+ */
53
+ export function AccessDetail({
54
+ keyRow: row,
55
+ blast,
56
+ now,
57
+ leading,
58
+ onEdit,
59
+ onRefresh,
60
+ onRevoke,
61
+ onRotate,
62
+ rotatePending,
63
+ }: AccessDetailProps): JSX.Element {
64
+ const revoked = row.revokedAt !== null;
65
+ const expired = row.expiresAt !== null && row.expiresAt <= now;
66
+ const lastUsed = blast?.lastUsedAt ?? row.lastUsedAt;
67
+ const calls = blast?.callVolume ?? 0;
68
+ const expiry = formatAccessExpiry(row.expiresAt, now);
69
+ const used = formatAccessWhen(lastUsed, now);
70
+ const rate = row.rateLimit ? `${row.rateLimit.max} / ${row.rateLimit.per}` : "none";
71
+ const groups = useMemo(() => groupAccessScopes(row.scopes), [row.scopes]);
72
+ const ceiling = row.creatorScopes.length;
73
+ const attenuated = ceiling > 0 && row.scopes.length < ceiling;
74
+
75
+ return (
76
+ <div
77
+ className="flex h-full min-h-0 flex-col overflow-y-auto"
78
+ data-slot="access-detail"
79
+ aria-label="Access key detail"
80
+ aria-live="polite"
81
+ >
82
+ <DetailHeader
83
+ dataSlot="access-detail-header"
84
+ leading={leading}
85
+ icon={<HugeiconsIcon icon={Key01Icon} className="size-4" />}
86
+ title={row.name}
87
+ badge={
88
+ <>
89
+ <span className="inline-flex items-center gap-1 font-mono text-[10px] font-medium tracking-[0.08em] text-muted-foreground uppercase">
90
+ <HugeiconsIcon
91
+ icon={row.plane === "operator" ? CrownIcon : UserIcon}
92
+ className="size-3"
93
+ aria-hidden
94
+ />
95
+ {row.plane}
96
+ </span>
97
+ {revoked ? (
98
+ <span className="font-mono text-[10px] text-destructive">revoked</span>
99
+ ) : null}
100
+ {expired && !revoked ? (
101
+ <span className="font-mono text-[10px] text-amber-700 dark:text-amber-400">
102
+ expired
103
+ </span>
104
+ ) : null}
105
+ {row.unused90d && !revoked && !expired ? (
106
+ <span className="font-mono text-[10px] text-amber-700 dark:text-amber-400">
107
+ unused
108
+ </span>
109
+ ) : null}
110
+ </>
111
+ }
112
+ subtitle={
113
+ <>
114
+ <code className="min-w-0 truncate font-mono text-[11px] leading-none text-foreground/80">
115
+ {row.id}
116
+ </code>
117
+ <CopyInlineButton value={row.id} label={`Copy ${row.id}`} />
118
+ </>
119
+ }
120
+ actions={
121
+ <>
122
+ <ToolbarTip label="Rename or re-attenuate scopes" className="flex self-stretch">
123
+ <Button
124
+ type="button"
125
+ variant="ghost"
126
+ size="sm"
127
+ className="h-full rounded-none"
128
+ disabled={revoked}
129
+ onClick={onEdit}
130
+ >
131
+ Edit
132
+ </Button>
133
+ </ToolbarTip>
134
+ <ToolbarTip
135
+ label="Reset expiry from now. The secret stays the same."
136
+ className="flex self-stretch"
137
+ >
138
+ <Button
139
+ type="button"
140
+ variant="ghost"
141
+ size="sm"
142
+ className="h-full rounded-none"
143
+ data-slot="access-refresh"
144
+ disabled={revoked}
145
+ onClick={onRefresh}
146
+ >
147
+ Refresh
148
+ </Button>
149
+ </ToolbarTip>
150
+ <ToolbarTip
151
+ label="Mint a new secret. The current secret stops working after residual TTL."
152
+ className="flex self-stretch"
153
+ >
154
+ <Button
155
+ type="button"
156
+ variant="ghost"
157
+ size="sm"
158
+ className="h-full rounded-none"
159
+ disabled={rotatePending || revoked}
160
+ onClick={onRotate}
161
+ >
162
+ Rotate
163
+ </Button>
164
+ </ToolbarTip>
165
+ <ToolbarTip
166
+ label="Irreversible. Residual sessions may continue for the access TTL."
167
+ className="flex self-stretch"
168
+ >
169
+ <Button
170
+ type="button"
171
+ variant="destructive"
172
+ size="sm"
173
+ className="h-full rounded-none"
174
+ disabled={revoked}
175
+ onClick={onRevoke}
176
+ >
177
+ Revoke
178
+ </Button>
179
+ </ToolbarTip>
180
+ </>
181
+ }
182
+ />
183
+
184
+ <PostureBanner
185
+ revoked={revoked}
186
+ expired={expired}
187
+ unused={row.unused90d}
188
+ residual={blast?.residualAccessNote}
189
+ />
190
+
191
+ <div
192
+ className={EXPLORER_STRIP_CLASS}
193
+ data-slot="access-briefing"
194
+ role="group"
195
+ aria-label="Usage"
196
+ >
197
+ <StatusToken label="calls" value={String(calls)} />
198
+ <StatusToken
199
+ label="used"
200
+ value={used}
201
+ hint={lastUsed ? new Date(lastUsed).toISOString() : "No recorded call"}
202
+ warn={lastUsed == null && !revoked}
203
+ />
204
+ <StatusToken
205
+ label="expires"
206
+ value={expiry}
207
+ hint={row.expiresAt ? new Date(row.expiresAt).toISOString() : "No expiry"}
208
+ warn={expired && !revoked}
209
+ />
210
+ <StatusToken label="rate" value={rate} />
211
+ </div>
212
+
213
+ <section className="shrink-0 border-b border-border/60" aria-label="Scopes">
214
+ <SectionStrip
215
+ title="Scopes"
216
+ meta={attenuated ? `${row.scopes.length} of ${ceiling}` : String(row.scopes.length)}
217
+ >
218
+ {row.scopes.length > 0 ? (
219
+ <CopyInlineButton value={row.scopes.join("\n")} label="Copy scopes" />
220
+ ) : null}
221
+ </SectionStrip>
222
+ {groups.length === 0 ? (
223
+ <p className="px-2 py-1.5 text-[11px] text-muted-foreground">No scopes granted.</p>
224
+ ) : (
225
+ <ul data-slot="access-scopes">
226
+ {groups.map((band) => (
227
+ <li key={band.group} className={EXPLORER_ROW_CLASS} aria-label={band.group}>
228
+ <span className={cn(SECTION_HEAD_CLASS, "w-[5.5rem] shrink-0")}>{band.group}</span>
229
+ <div className="flex min-w-0 flex-1 flex-wrap items-center gap-x-2">
230
+ {band.items.map((item) => (
231
+ <span
232
+ key={item.scope}
233
+ title={item.scope}
234
+ className="font-mono text-[11px] text-foreground"
235
+ >
236
+ {item.action}
237
+ </span>
238
+ ))}
239
+ </div>
240
+ <HoverCopy
241
+ value={band.items.map((item) => item.scope).join("\n")}
242
+ label={`Copy ${band.group} scopes`}
243
+ />
244
+ </li>
245
+ ))}
246
+ </ul>
247
+ )}
248
+ </section>
249
+
250
+ <section className="shrink-0 border-b border-border/60" aria-label="Key">
251
+ <SectionStrip title="Key" />
252
+ <dl data-slot="access-key-facts">
253
+ <FactRow
254
+ label="Issuer"
255
+ value={row.creatorId}
256
+ copy={row.creatorId}
257
+ copyLabel={`Copy ${row.creatorId}`}
258
+ />
259
+ <FactRow
260
+ label="Created"
261
+ value={formatAccessWhen(row.createdAt, now)}
262
+ hint={new Date(row.createdAt).toISOString()}
263
+ />
264
+ <FactRow
265
+ label="Expires"
266
+ value={expiry}
267
+ hint={row.expiresAt ? new Date(row.expiresAt).toISOString() : undefined}
268
+ warn={expired && !revoked}
269
+ />
270
+ <FactRow label="Rate" value={rate} />
271
+ </dl>
272
+ </section>
273
+
274
+ <AllowSection entries={row.ipAllowlist} />
275
+
276
+ <UsageSection
277
+ sourceAddresses={blast?.sourceAddresses ?? []}
278
+ residual={blast?.residualAccessNote}
279
+ lastUsed={lastUsed}
280
+ now={now}
281
+ />
282
+
283
+ <CallExample scopes={row.scopes} />
284
+ </div>
285
+ );
286
+ }
287
+
288
+ function PostureBanner({
289
+ revoked,
290
+ expired,
291
+ unused,
292
+ residual,
293
+ }: {
294
+ readonly revoked: boolean;
295
+ readonly expired: boolean;
296
+ readonly unused: boolean;
297
+ readonly residual: string | undefined;
298
+ }): JSX.Element | null {
299
+ if (revoked) {
300
+ return (
301
+ <section
302
+ className={cn(EXPLORER_STRIP_CLASS, "border-destructive/25 bg-destructive/5")}
303
+ aria-label="Revoked"
304
+ role="status"
305
+ >
306
+ <p className="flex min-w-0 flex-1 items-center px-2 text-[12px] text-destructive">
307
+ Revoked
308
+ <span className="text-muted-foreground">
309
+ {" "}
310
+ · {residual ?? "This secret no longer authenticates."}
311
+ </span>
312
+ </p>
313
+ </section>
314
+ );
315
+ }
316
+ if (expired) {
317
+ return (
318
+ <section
319
+ className={cn(EXPLORER_STRIP_CLASS, "border-amber-500/25 bg-amber-500/5")}
320
+ aria-label="Expired"
321
+ role="status"
322
+ >
323
+ <p className="flex min-w-0 flex-1 items-center px-2 text-[12px] text-amber-900 dark:text-amber-300">
324
+ Expired
325
+ {residual ? <span className="text-muted-foreground"> · {residual}</span> : null}
326
+ </p>
327
+ </section>
328
+ );
329
+ }
330
+ if (unused) {
331
+ return (
332
+ <section
333
+ className={cn(EXPLORER_STRIP_CLASS, "border-amber-500/25 bg-amber-500/5")}
334
+ aria-label="Unused"
335
+ role="status"
336
+ >
337
+ <p className="flex min-w-0 flex-1 items-center px-2 text-[12px] text-amber-900 dark:text-amber-300">
338
+ No calls in 90 days
339
+ <span className="text-muted-foreground"> · rotate or revoke</span>
340
+ </p>
341
+ </section>
342
+ );
343
+ }
344
+ return null;
345
+ }
346
+
347
+ function AllowSection({ entries }: { readonly entries: readonly string[] }): JSX.Element {
348
+ return (
349
+ <section className="shrink-0 border-b border-border/60" aria-label="Allow">
350
+ <SectionStrip title="Allow" meta={entries.length > 0 ? String(entries.length) : "any"} />
351
+ {entries.length === 0 ? (
352
+ <p className="px-2 py-1.5 text-[11px] text-muted-foreground">Any origin.</p>
353
+ ) : (
354
+ <ul data-slot="access-allow">
355
+ {entries.map((entry) => {
356
+ const kind = classifyAccessAllowEntry(entry);
357
+ return (
358
+ <li key={entry} className={EXPLORER_ROW_CLASS}>
359
+ <span className={cn(SECTION_HEAD_CLASS, "w-[5.5rem] shrink-0")}>
360
+ {kind === "ip" ? "IP" : kind === "host" ? "Host" : "—"}
361
+ </span>
362
+ <span className="min-w-0 flex-1 truncate font-mono text-[11px]">{entry}</span>
363
+ <HoverCopy value={entry} label={`Copy ${entry}`} />
364
+ </li>
365
+ );
366
+ })}
367
+ </ul>
368
+ )}
369
+ </section>
370
+ );
371
+ }
372
+
373
+ function UsageSection({
374
+ sourceAddresses,
375
+ residual,
376
+ lastUsed,
377
+ now,
378
+ }: {
379
+ readonly sourceAddresses: readonly string[];
380
+ readonly residual: string | undefined;
381
+ readonly lastUsed: number | null;
382
+ readonly now: number;
383
+ }): JSX.Element {
384
+ return (
385
+ <section className="shrink-0 border-b border-border/60" aria-label="Usage">
386
+ <SectionStrip title="Usage" meta={lastUsed ? formatAccessWhen(lastUsed, now) : "never"} />
387
+ {sourceAddresses.length === 0 ? (
388
+ <p className="px-2 py-1.5 text-[11px] text-muted-foreground">No observed source IPs.</p>
389
+ ) : (
390
+ <ul data-slot="access-sources">
391
+ {sourceAddresses.map((ip) => (
392
+ <li key={ip} className={EXPLORER_ROW_CLASS}>
393
+ <span className={cn(SECTION_HEAD_CLASS, "w-[5.5rem] shrink-0")}>IP</span>
394
+ <span className="min-w-0 flex-1 truncate font-mono text-[11px]">{ip}</span>
395
+ <HoverCopy value={ip} label={`Copy ${ip}`} />
396
+ </li>
397
+ ))}
398
+ </ul>
399
+ )}
400
+ {residual ? (
401
+ <p className="border-t border-border/60 px-2 py-1.5 text-[11px] text-muted-foreground">
402
+ {residual}
403
+ </p>
404
+ ) : null}
405
+ </section>
406
+ );
407
+ }
408
+
409
+ function CallExample({ scopes }: { readonly scopes: readonly string[] }): JSX.Element {
410
+ return (
411
+ <section data-slot="access-call-example">
412
+ <SectionStrip title="Request">
413
+ <CopyInlineButton value={CALL_REQUEST} label="Copy request" />
414
+ </SectionStrip>
415
+ <div className="flex min-w-0 border-b border-border/60">
416
+ <div className={cn("w-1 shrink-0 self-stretch", httpMethodRailClass("GET"))} aria-hidden />
417
+ <div className="min-w-0 flex-1">
418
+ <div className="flex min-w-0 items-center gap-2 px-2.5 py-2">
419
+ <span
420
+ className={cn(
421
+ "shrink-0 font-mono text-[10px] font-semibold tracking-[0.08em] uppercase",
422
+ explorerIconInk(httpMethodBadgeClass("GET")),
423
+ )}
424
+ >
425
+ GET
426
+ </span>
427
+ <span className="min-w-0 flex-1 truncate font-mono text-xs text-foreground select-all">
428
+ /secure
429
+ </span>
430
+ </div>
431
+ <div className="flex min-w-0 items-center gap-2 border-t border-border/60 px-2.5 py-2">
432
+ <span className="w-[7.5rem] shrink-0 truncate font-mono text-[11px] font-medium text-sky-600 dark:text-sky-400">
433
+ Authorization
434
+ </span>
435
+ <span className="min-w-0 flex-1 truncate font-mono text-[11px] text-muted-foreground">
436
+ Bearer &lt;secret&gt;
437
+ </span>
438
+ </div>
439
+ </div>
440
+ </div>
441
+
442
+ <SectionStrip title="Response">
443
+ <CopyInlineButton value={CALL_RESPONSE} label="Copy response" />
444
+ </SectionStrip>
445
+ <div className="flex min-w-0 border-b border-border/60">
446
+ <div className="w-1 shrink-0 self-stretch bg-emerald-500" aria-hidden />
447
+ <p className="min-w-0 flex-1 px-2.5 py-2 font-mono text-[11px] text-foreground/80 select-all">
448
+ {CALL_RESPONSE}
449
+ </p>
450
+ </div>
451
+ <p className="px-2 py-1.5 text-[11px] text-muted-foreground">
452
+ Seed the Bearer secret from create / rotate. The key is the issuer; gates see{" "}
453
+ {scopes.join(", ") || "its scopes"}.
454
+ </p>
455
+ </section>
456
+ );
457
+ }
458
+
459
+ function SectionStrip({
460
+ title,
461
+ meta,
462
+ children,
463
+ }: {
464
+ readonly title: string;
465
+ readonly meta?: string;
466
+ readonly children?: ReactNode;
467
+ }): JSX.Element {
468
+ return (
469
+ <header className={cn(EXPLORER_STRIP_CLASS, "items-center gap-2 px-2")}>
470
+ <h3 className={cn(SECTION_HEAD_CLASS, "shrink-0 leading-none")}>{title}</h3>
471
+ {meta ? (
472
+ <span className={cn(EXPLORER_COUNT_CLASS, !children && "ml-auto")}>{meta}</span>
473
+ ) : null}
474
+ {children ? <div className="ml-auto flex items-center">{children}</div> : null}
475
+ </header>
476
+ );
477
+ }
478
+
479
+ function StatusToken({
480
+ label,
481
+ value,
482
+ hint,
483
+ warn = false,
484
+ }: {
485
+ readonly label: string;
486
+ readonly value: string;
487
+ readonly hint?: string;
488
+ readonly warn?: boolean;
489
+ }): JSX.Element {
490
+ return (
491
+ <span
492
+ title={hint}
493
+ className={cn(EXPLORER_STRIP_TOKEN_CLASS, EXPLORER_STRIP_TOKEN_IDLE_CLASS, "gap-1.5")}
494
+ >
495
+ <span className="tracking-[0.08em] uppercase">{label}</span>
496
+ <span
497
+ className={cn(
498
+ "font-mono tabular-nums text-foreground",
499
+ warn && "text-amber-800 dark:text-amber-400",
500
+ )}
501
+ >
502
+ {value}
503
+ </span>
504
+ </span>
505
+ );
506
+ }
507
+
508
+ function HoverCopy({
509
+ value,
510
+ label,
511
+ }: {
512
+ readonly value: string;
513
+ readonly label: string;
514
+ }): JSX.Element {
515
+ return (
516
+ <div className="opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100">
517
+ <CopyInlineButton value={value} label={label} />
518
+ </div>
519
+ );
520
+ }
521
+
522
+ function FactRow({
523
+ label,
524
+ value,
525
+ hint,
526
+ copy,
527
+ copyLabel,
528
+ warn = false,
529
+ }: {
530
+ readonly label: string;
531
+ readonly value: string;
532
+ readonly hint?: string;
533
+ readonly copy?: string;
534
+ readonly copyLabel?: string;
535
+ readonly warn?: boolean;
536
+ }): JSX.Element {
537
+ return (
538
+ <div className={EXPLORER_ROW_CLASS}>
539
+ <dt className={cn(SECTION_HEAD_CLASS, "w-[5.5rem] shrink-0")}>{label}</dt>
540
+ <dd
541
+ className={cn(
542
+ "min-w-0 flex-1 truncate font-mono text-[11px]",
543
+ warn && "text-amber-800 dark:text-amber-400",
544
+ )}
545
+ title={hint}
546
+ >
547
+ {value}
548
+ </dd>
549
+ {copy ? <HoverCopy value={copy} label={copyLabel ?? `Copy ${label}`} /> : null}
550
+ </div>
551
+ );
552
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Access key list — user / operator bands.
3
+ */
4
+
5
+ import { CrownIcon, Key01Icon, UserIcon } from "@hugeicons/core-free-icons";
6
+ import { HugeiconsIcon } from "@hugeicons/react";
7
+ import type { JSX } from "react";
8
+ import type { AccessKeyRow } from "@/client.ts";
9
+ import {
10
+ EXPLORER_BAND_CLASS,
11
+ EXPLORER_BAND_HEADER_CLASS,
12
+ EXPLORER_BAND_LABEL_CLASS,
13
+ EXPLORER_COUNT_CLASS,
14
+ EXPLORER_ICON_CLASS,
15
+ EXPLORER_LIST_EMPTY_CLASS,
16
+ EXPLORER_RAIL_ACTIVE_CLASS,
17
+ EXPLORER_RAIL_CLASS,
18
+ EXPLORER_ROW_CLASS,
19
+ EXPLORER_ROW_SELECTED_CLASS,
20
+ } from "@/components/explorer/explorer-chrome.ts";
21
+ import type { ElementHugeIcon } from "@/lib/element-icons.ts";
22
+ import { cn } from "@/lib/utils.ts";
23
+
24
+ /** Props for {@link AccessList}. */
25
+ export interface AccessListProps {
26
+ readonly rows: readonly AccessKeyRow[];
27
+ readonly selectedKey: string | null;
28
+ readonly query: string;
29
+ readonly loading?: boolean;
30
+ readonly onSelect: (id: string) => void;
31
+ }
32
+
33
+ /**
34
+ * Grouped key list. Empty bands stay visible so both planes remain taught.
35
+ *
36
+ * @param props - Filtered rows + selection
37
+ */
38
+ export function AccessList({
39
+ rows,
40
+ selectedKey,
41
+ query,
42
+ loading = false,
43
+ onSelect,
44
+ }: AccessListProps): JSX.Element {
45
+ const user = rows.filter((row) => row.plane === "user");
46
+ const operator = rows.filter((row) => row.plane === "operator");
47
+
48
+ return (
49
+ <div id="access-list" data-slot="access-list" className="min-h-0 flex-1 overflow-y-auto">
50
+ {loading && rows.length === 0 ? <p className={EXPLORER_LIST_EMPTY_CLASS}>Loading…</p> : null}
51
+ {!loading && rows.length === 0 && query.trim().length > 0 ? (
52
+ <p className={EXPLORER_LIST_EMPTY_CLASS}>No keys match.</p>
53
+ ) : null}
54
+ <AccessBand
55
+ label="User"
56
+ icon={UserIcon}
57
+ rows={user}
58
+ selectedKey={selectedKey}
59
+ onSelect={onSelect}
60
+ />
61
+ <AccessBand
62
+ label="Operator"
63
+ icon={CrownIcon}
64
+ rows={operator}
65
+ selectedKey={selectedKey}
66
+ onSelect={onSelect}
67
+ />
68
+ </div>
69
+ );
70
+ }
71
+
72
+ function AccessBand(props: {
73
+ readonly label: string;
74
+ readonly icon: ElementHugeIcon;
75
+ readonly rows: readonly AccessKeyRow[];
76
+ readonly selectedKey: string | null;
77
+ readonly onSelect: (id: string) => void;
78
+ }): JSX.Element {
79
+ return (
80
+ <section className={EXPLORER_BAND_CLASS} aria-label={props.label}>
81
+ <div className={EXPLORER_BAND_HEADER_CLASS}>
82
+ <HugeiconsIcon
83
+ icon={props.icon}
84
+ className={cn(EXPLORER_ICON_CLASS, "text-muted-foreground")}
85
+ aria-hidden
86
+ />
87
+ <span className={cn(EXPLORER_BAND_LABEL_CLASS, "flex-1")}>{props.label}</span>
88
+ <span className={EXPLORER_COUNT_CLASS}>{props.rows.length}</span>
89
+ </div>
90
+ {props.rows.map((row) => {
91
+ const selected = row.id === props.selectedKey;
92
+ const revoked = row.revokedAt !== null;
93
+ return (
94
+ <button
95
+ key={row.id}
96
+ type="button"
97
+ data-slot="access-key-row"
98
+ className={cn(EXPLORER_ROW_CLASS, selected && EXPLORER_ROW_SELECTED_CLASS)}
99
+ onClick={() => props.onSelect(row.id)}
100
+ >
101
+ <span className={cn(EXPLORER_RAIL_CLASS, selected && EXPLORER_RAIL_ACTIVE_CLASS)} />
102
+ <HugeiconsIcon
103
+ icon={Key01Icon}
104
+ className={cn(EXPLORER_ICON_CLASS, "text-muted-foreground")}
105
+ />
106
+ <span
107
+ className={cn("min-w-0 truncate", revoked && "text-muted-foreground line-through")}
108
+ >
109
+ {row.name}
110
+ </span>
111
+ {revoked ? (
112
+ <span className="ml-auto text-[10px] text-muted-foreground">revoked</span>
113
+ ) : row.unused90d ? (
114
+ <span className="ml-auto text-[10px] text-amber-700 dark:text-amber-400">unused</span>
115
+ ) : (
116
+ <span className={cn("ml-auto tabular-nums text-[10px] text-muted-foreground")}>
117
+ {row.scopes.length}
118
+ </span>
119
+ )}
120
+ </button>
121
+ );
122
+ })}
123
+ </section>
124
+ );
125
+ }