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,240 @@
1
+ /**
2
+ * Edit an Access key — name, expiry, allowlist, rate, re-attenuated scopes.
3
+ */
4
+
5
+ import { CrownIcon, UserIcon } from "@hugeicons/core-free-icons";
6
+ import { HugeiconsIcon } from "@hugeicons/react";
7
+ import { useEffect, useMemo, useState, type JSX } from "react";
8
+ import type { AccessKeyRow, AccessOperatorRow, AccessUserRow } from "@/client.ts";
9
+ import { Input } from "@/components/ui/input";
10
+ import { cn } from "@/lib/utils.ts";
11
+ import { SHEET_CONTROL, SheetError, SheetFooterButton } from "@/components/ui/sheet-form.tsx";
12
+ import {
13
+ Sheet,
14
+ SheetContent,
15
+ SheetDescription,
16
+ SheetFooter,
17
+ SheetHeader,
18
+ SheetTitle,
19
+ } from "@/components/ui/sheet";
20
+ import {
21
+ accessExpiresAt,
22
+ accessExpiryFromAt,
23
+ parseAccessDurationMs,
24
+ parseAccessRateParts,
25
+ splitAccessRatePer,
26
+ type AccessExpiryChoice,
27
+ type AccessRateUnit,
28
+ } from "../lib/format-when.ts";
29
+ import { AccessAllowFields } from "./access-allow-fields.tsx";
30
+ import { AccessExpiryFields } from "./access-expiry-fields.tsx";
31
+ import { AccessScopeField } from "./access-scope-field.tsx";
32
+
33
+ /** Patch from {@link AccessEditSheet}. */
34
+ export interface AccessEditKeyInput {
35
+ readonly name: string;
36
+ readonly scopes: readonly string[];
37
+ readonly expiresAt: number | null;
38
+ readonly rateLimit: { max: number; per: string } | null;
39
+ readonly ipAllowlist: readonly string[];
40
+ }
41
+
42
+ /** Props for {@link AccessEditSheet}. */
43
+ export interface AccessEditSheetProps {
44
+ readonly open: boolean;
45
+ readonly keyRow: AccessKeyRow;
46
+ readonly users: readonly AccessUserRow[];
47
+ readonly operators: readonly AccessOperatorRow[];
48
+ readonly operatorScopes: readonly string[];
49
+ readonly pending: boolean;
50
+ readonly error: string | null;
51
+ readonly now?: number;
52
+ readonly onOpenChange: (open: boolean) => void;
53
+ readonly onSubmit: (input: AccessEditKeyInput) => void;
54
+ }
55
+
56
+ /**
57
+ * Edit sheet — tighten metadata against the stored issuer ceiling.
58
+ *
59
+ * @param props - Open + key + submit
60
+ */
61
+ export function AccessEditSheet({
62
+ open,
63
+ keyRow,
64
+ users,
65
+ operators,
66
+ operatorScopes,
67
+ pending,
68
+ error,
69
+ now = Date.now(),
70
+ onOpenChange,
71
+ onSubmit,
72
+ }: AccessEditSheetProps): JSX.Element {
73
+ const seeded = accessExpiryFromAt(keyRow.expiresAt, now);
74
+ const [name, setName] = useState(keyRow.name);
75
+ const [scopes, setScopes] = useState<readonly string[]>(keyRow.scopes);
76
+ const [expires, setExpires] = useState<AccessExpiryChoice>(seeded.choice);
77
+ const [customExpires, setCustomExpires] = useState(seeded.custom);
78
+ const [allowEntries, setAllowEntries] = useState<readonly string[]>(keyRow.ipAllowlist);
79
+ const seededRate = splitAccessRatePer(keyRow.rateLimit?.per ?? "");
80
+ const [rateMax, setRateMax] = useState(keyRow.rateLimit ? String(keyRow.rateLimit.max) : "");
81
+ const [rateCount, setRateCount] = useState(seededRate.count);
82
+ const [rateUnit, setRateUnit] = useState<AccessRateUnit>(seededRate.unit);
83
+
84
+ useEffect(() => {
85
+ if (!open) return;
86
+ const next = accessExpiryFromAt(keyRow.expiresAt, now);
87
+ setName(keyRow.name);
88
+ setScopes(keyRow.scopes);
89
+ setExpires(next.choice);
90
+ setCustomExpires(next.custom);
91
+ setAllowEntries(keyRow.ipAllowlist);
92
+ const nextRate = splitAccessRatePer(keyRow.rateLimit?.per ?? "");
93
+ setRateMax(keyRow.rateLimit ? String(keyRow.rateLimit.max) : "");
94
+ setRateCount(nextRate.count);
95
+ setRateUnit(nextRate.unit);
96
+ // Seed once per open / key — list refetch must not wipe in-progress edits.
97
+ }, [open, keyRow.id]);
98
+
99
+ const creatorId = keyRow.creatorId ?? "";
100
+ const issuer = useMemo(() => {
101
+ if (creatorId.length === 0) return null;
102
+ return (
103
+ users.find((row) => row.id === creatorId) ??
104
+ operators.find((row) => row.id === creatorId) ??
105
+ null
106
+ );
107
+ }, [creatorId, operators, users]);
108
+ const issuerCaption = issuer
109
+ ? issuer.name.trim() || issuer.email.trim() || issuer.id
110
+ : creatorId || "Unknown";
111
+ const grantable = useMemo(() => {
112
+ const stored = keyRow.creatorScopes ?? [];
113
+ if (stored.length > 0) return stored;
114
+ if (keyRow.plane === "operator") {
115
+ return issuer && issuer.scopes.length > 0 ? issuer.scopes : operatorScopes;
116
+ }
117
+ return issuer?.scopes ?? [];
118
+ }, [issuer, keyRow.creatorScopes, keyRow.plane, operatorScopes]);
119
+ const catalog = Array.from(new Set([...grantable, ...keyRow.scopes]));
120
+ const customMs = parseAccessDurationMs(customExpires);
121
+ const parsedRate = parseAccessRateParts(rateMax, rateCount, rateUnit);
122
+ const ready =
123
+ name.trim().length > 0 &&
124
+ scopes.length > 0 &&
125
+ !pending &&
126
+ parsedRate !== undefined &&
127
+ (expires !== "custom" || customMs > 0);
128
+
129
+ return (
130
+ <Sheet open={open} modal={false} disablePointerDismissal onOpenChange={onOpenChange}>
131
+ <SheetContent
132
+ side="right"
133
+ showOverlay={false}
134
+ className="gap-0 p-0 data-[side=right]:sm:max-w-md"
135
+ data-slot="access-edit-sheet"
136
+ >
137
+ <SheetHeader className="gap-1 border-b border-border/50">
138
+ <SheetTitle className="text-sm">Edit key</SheetTitle>
139
+ <SheetDescription className="text-[11px]">
140
+ Change name, expiry, allow, rate, and scopes. Issuer stays frozen.
141
+ </SheetDescription>
142
+ </SheetHeader>
143
+ <div className="flex min-h-0 flex-1 flex-col overflow-y-auto">
144
+ <div
145
+ className="flex items-stretch border-b border-border/50"
146
+ data-slot="access-name-field"
147
+ >
148
+ <p className="flex h-8 w-[5.5rem] shrink-0 items-center px-4 text-[10px] font-semibold tracking-[0.12em] text-muted-foreground uppercase">
149
+ Name
150
+ </p>
151
+ <Input
152
+ value={name}
153
+ onChange={(e) => setName(e.target.value)}
154
+ placeholder="CI deploy"
155
+ autoComplete="off"
156
+ aria-label="Key name"
157
+ flat
158
+ className={cn(SHEET_CONTROL, "min-w-0 flex-1")}
159
+ />
160
+ </div>
161
+ <div
162
+ className="flex items-stretch border-b border-border/50"
163
+ data-slot="access-issuer-field"
164
+ >
165
+ <p className="flex h-8 w-[5.5rem] shrink-0 items-center px-4 text-[10px] font-semibold tracking-[0.12em] text-muted-foreground uppercase">
166
+ Issuer
167
+ </p>
168
+ <p
169
+ className="flex min-w-0 flex-1 items-center gap-1.5 px-4 text-[12px] text-muted-foreground"
170
+ aria-label={`Issuer ${issuerCaption}`}
171
+ >
172
+ <HugeiconsIcon
173
+ icon={keyRow.plane === "operator" ? CrownIcon : UserIcon}
174
+ className="size-3 shrink-0"
175
+ aria-hidden
176
+ />
177
+ <span className="min-w-0 truncate text-foreground">{issuerCaption}</span>
178
+ </p>
179
+ {creatorId.length > 0 && creatorId !== issuerCaption ? (
180
+ <span className="flex h-8 shrink-0 items-center pr-4 font-mono text-[10px] text-muted-foreground">
181
+ {creatorId}
182
+ </span>
183
+ ) : null}
184
+ </div>
185
+ <AccessExpiryFields
186
+ expires={expires}
187
+ customExpires={customExpires}
188
+ onExpires={setExpires}
189
+ onCustomExpires={setCustomExpires}
190
+ />
191
+ <AccessAllowFields
192
+ entries={allowEntries}
193
+ rateMax={rateMax}
194
+ rateCount={rateCount}
195
+ rateUnit={rateUnit}
196
+ onEntries={setAllowEntries}
197
+ onRateMax={setRateMax}
198
+ onRateCount={setRateCount}
199
+ onRateUnit={setRateUnit}
200
+ />
201
+ <AccessScopeField
202
+ scopes={catalog}
203
+ selected={scopes}
204
+ empty={
205
+ keyRow.plane === "user" && issuer == null
206
+ ? "Issuer is gone. Only scopes already on this key remain."
207
+ : keyRow.plane === "user"
208
+ ? "This issuer has no grants."
209
+ : "No grantable scopes on this plane."
210
+ }
211
+ onChange={setScopes}
212
+ />
213
+ {error ? <SheetError slot="access-edit-error">{error}</SheetError> : null}
214
+ </div>
215
+ <SheetFooter>
216
+ <SheetFooterButton split onClick={() => onOpenChange(false)} disabled={pending}>
217
+ Cancel
218
+ </SheetFooterButton>
219
+ <SheetFooterButton
220
+ variant="default"
221
+ disabled={!ready}
222
+ data-slot="access-edit-submit"
223
+ onClick={() => {
224
+ if (parsedRate === undefined) return;
225
+ onSubmit({
226
+ name: name.trim(),
227
+ scopes,
228
+ expiresAt: accessExpiresAt(expires, Date.now(), customExpires),
229
+ rateLimit: parsedRate,
230
+ ipAllowlist: allowEntries,
231
+ });
232
+ }}
233
+ >
234
+ {pending ? "Saving…" : "Save"}
235
+ </SheetFooterButton>
236
+ </SheetFooter>
237
+ </SheetContent>
238
+ </Sheet>
239
+ );
240
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Expiry presets shared by Access create / edit / refresh.
3
+ */
4
+
5
+ import type { JSX } from "react";
6
+ import { Input } from "@/components/ui/input";
7
+ import {
8
+ SHEET_CONTROL,
9
+ SheetChoice,
10
+ SheetChoiceRow,
11
+ SheetField,
12
+ } from "@/components/ui/sheet-form.tsx";
13
+ import { cn } from "@/lib/utils.ts";
14
+ import type { AccessExpiryChoice } from "../lib/format-when.ts";
15
+
16
+ /** Props for {@link AccessExpiryFields}. */
17
+ export interface AccessExpiryFieldsProps {
18
+ readonly expires: AccessExpiryChoice;
19
+ readonly customExpires: string;
20
+ readonly onExpires: (choice: AccessExpiryChoice) => void;
21
+ readonly onCustomExpires: (raw: string) => void;
22
+ }
23
+
24
+ /**
25
+ * Never / 30d / 90d / custom duration row.
26
+ *
27
+ * @param props - Choice + custom duration
28
+ */
29
+ export function AccessExpiryFields({
30
+ expires,
31
+ customExpires,
32
+ onExpires,
33
+ onCustomExpires,
34
+ }: AccessExpiryFieldsProps): JSX.Element {
35
+ return (
36
+ <>
37
+ <SheetChoiceRow label="Expires">
38
+ <SheetChoice active={expires === "never"} onClick={() => onExpires("never")}>
39
+ never
40
+ </SheetChoice>
41
+ <SheetChoice active={expires === "30d"} onClick={() => onExpires("30d")}>
42
+ 30d
43
+ </SheetChoice>
44
+ <SheetChoice active={expires === "90d"} onClick={() => onExpires("90d")}>
45
+ 90d
46
+ </SheetChoice>
47
+ <SheetChoice active={expires === "custom"} onClick={() => onExpires("custom")}>
48
+ custom
49
+ </SheetChoice>
50
+ </SheetChoiceRow>
51
+ {expires === "custom" ? (
52
+ <SheetField label="Duration" hint="7d · 12h · 30m">
53
+ <Input
54
+ value={customExpires}
55
+ onChange={(e) => onCustomExpires(e.target.value)}
56
+ placeholder="7d"
57
+ autoComplete="off"
58
+ aria-label="Custom expiry duration"
59
+ flat
60
+ className={cn(SHEET_CONTROL, "font-mono")}
61
+ />
62
+ </SheetField>
63
+ ) : null}
64
+ </>
65
+ );
66
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Refresh an Access key expiry — same secret, new deadline from now.
3
+ */
4
+
5
+ import { useEffect, useState, type JSX } from "react";
6
+ import type { AccessKeyRow } from "@/client.ts";
7
+ import { SheetError, SheetFooterButton } from "@/components/ui/sheet-form.tsx";
8
+ import {
9
+ Sheet,
10
+ SheetContent,
11
+ SheetDescription,
12
+ SheetFooter,
13
+ SheetHeader,
14
+ SheetTitle,
15
+ } from "@/components/ui/sheet";
16
+ import {
17
+ accessExpiresAt,
18
+ accessRefreshExpiry,
19
+ parseAccessDurationMs,
20
+ type AccessExpiryChoice,
21
+ } from "../lib/format-when.ts";
22
+ import { AccessExpiryFields } from "./access-expiry-fields.tsx";
23
+
24
+ /** Props for {@link AccessRefreshSheet}. */
25
+ export interface AccessRefreshSheetProps {
26
+ readonly open: boolean;
27
+ readonly keyRow: AccessKeyRow;
28
+ readonly pending: boolean;
29
+ readonly error: string | null;
30
+ readonly now?: number;
31
+ readonly onOpenChange: (open: boolean) => void;
32
+ readonly onSubmit: (expiresAt: number | null) => void;
33
+ }
34
+
35
+ /**
36
+ * Expiry-only sheet. Does not mint or reveal a secret.
37
+ *
38
+ * @param props - Open + key + submit
39
+ */
40
+ export function AccessRefreshSheet({
41
+ open,
42
+ keyRow,
43
+ pending,
44
+ error,
45
+ now = Date.now(),
46
+ onOpenChange,
47
+ onSubmit,
48
+ }: AccessRefreshSheetProps): JSX.Element {
49
+ const seeded = accessRefreshExpiry(keyRow.expiresAt, now);
50
+ const [expires, setExpires] = useState<AccessExpiryChoice>(seeded.choice);
51
+ const [customExpires, setCustomExpires] = useState(seeded.custom);
52
+
53
+ useEffect(() => {
54
+ if (!open) return;
55
+ const next = accessRefreshExpiry(keyRow.expiresAt, now);
56
+ setExpires(next.choice);
57
+ setCustomExpires(next.custom);
58
+ }, [open, keyRow.id]);
59
+
60
+ const customMs = parseAccessDurationMs(customExpires);
61
+ const ready = !pending && (expires !== "custom" || customMs > 0);
62
+
63
+ return (
64
+ <Sheet open={open} modal={false} disablePointerDismissal onOpenChange={onOpenChange}>
65
+ <SheetContent
66
+ side="right"
67
+ showOverlay={false}
68
+ className="gap-0 p-0 data-[side=right]:sm:max-w-md"
69
+ data-slot="access-refresh-sheet"
70
+ >
71
+ <SheetHeader className="gap-1 border-b border-border/50">
72
+ <SheetTitle className="text-sm">Refresh expiry</SheetTitle>
73
+ <SheetDescription className="text-[11px]">
74
+ Reset the deadline from now. The secret does not change.
75
+ </SheetDescription>
76
+ </SheetHeader>
77
+ <div className="flex min-h-0 flex-1 flex-col overflow-y-auto">
78
+ <AccessExpiryFields
79
+ expires={expires}
80
+ customExpires={customExpires}
81
+ onExpires={setExpires}
82
+ onCustomExpires={setCustomExpires}
83
+ />
84
+ {error ? <SheetError slot="access-refresh-error">{error}</SheetError> : null}
85
+ </div>
86
+ <SheetFooter>
87
+ <SheetFooterButton split onClick={() => onOpenChange(false)} disabled={pending}>
88
+ Cancel
89
+ </SheetFooterButton>
90
+ <SheetFooterButton
91
+ variant="default"
92
+ disabled={!ready}
93
+ data-slot="access-refresh-submit"
94
+ onClick={() => {
95
+ onSubmit(accessExpiresAt(expires, Date.now(), customExpires));
96
+ }}
97
+ >
98
+ {pending ? "Refreshing…" : "Refresh"}
99
+ </SheetFooterButton>
100
+ </SheetFooter>
101
+ </SheetContent>
102
+ </Sheet>
103
+ );
104
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Multi-select grantable scopes — grouped by Module:Action prefix.
3
+ */
4
+
5
+ import { useEffect, useMemo, useState, type JSX } from "react";
6
+ import { cn } from "@/lib/utils.ts";
7
+ import { groupAccessScopes } from "../lib/scope-groups.ts";
8
+ import { AccessSheetSearch } from "./access-sheet-search.tsx";
9
+
10
+ /** Props for {@link AccessScopeField}. */
11
+ export interface AccessScopeFieldProps {
12
+ readonly scopes: readonly string[];
13
+ readonly selected: readonly string[];
14
+ readonly empty: string;
15
+ readonly onChange: (scopes: readonly string[]) => void;
16
+ }
17
+
18
+ /**
19
+ * Toggle chips for grantable scopes, one band per module.
20
+ *
21
+ * @param props - Catalog + selection
22
+ */
23
+ export function AccessScopeField({
24
+ scopes,
25
+ selected,
26
+ empty,
27
+ onChange,
28
+ }: AccessScopeFieldProps): JSX.Element {
29
+ const [query, setQuery] = useState("");
30
+ const chosen = new Set(selected);
31
+ const visible = useMemo(() => filterScopes(scopes, query), [scopes, query]);
32
+ const groups = useMemo(() => groupAccessScopes(visible), [visible]);
33
+
34
+ const catalogKey = scopes.join("\0");
35
+ useEffect(() => {
36
+ setQuery("");
37
+ }, [catalogKey]);
38
+
39
+ return (
40
+ <div className="border-b border-border/50" data-slot="access-scope-field">
41
+ <div className="flex items-stretch">
42
+ <p className="flex h-8 w-[5.5rem] shrink-0 items-center px-4 text-[10px] font-semibold tracking-[0.12em] text-muted-foreground uppercase">
43
+ Scopes
44
+ </p>
45
+ {scopes.length > 0 ? (
46
+ <AccessSheetSearch
47
+ value={query}
48
+ placeholder="Search scopes"
49
+ label="Search scopes"
50
+ onChange={setQuery}
51
+ />
52
+ ) : null}
53
+ </div>
54
+ {scopes.length === 0 ? (
55
+ <p className="border-t border-border/50 px-4 py-3 text-[11px] text-muted-foreground">
56
+ {empty}
57
+ </p>
58
+ ) : visible.length === 0 ? (
59
+ <p className="border-t border-border/50 px-4 py-3 text-[11px] text-muted-foreground">
60
+ No scopes match.
61
+ </p>
62
+ ) : (
63
+ <div className="max-h-56 overflow-y-auto" role="group" aria-label="Scopes">
64
+ {groups.map((band) => (
65
+ <div
66
+ key={band.group}
67
+ className="flex items-stretch border-t border-border/50"
68
+ role="group"
69
+ aria-label={band.group}
70
+ >
71
+ <p className="flex w-[5.5rem] shrink-0 items-center px-4 font-mono text-[10px] font-semibold tracking-[0.08em] text-muted-foreground uppercase">
72
+ {band.group}
73
+ </p>
74
+ <div className="flex min-w-0 flex-1 flex-wrap items-stretch">
75
+ {band.items.map((item) => {
76
+ const on = chosen.has(item.scope);
77
+ return (
78
+ <button
79
+ key={item.scope}
80
+ type="button"
81
+ aria-pressed={on}
82
+ aria-label={item.scope}
83
+ data-slot="access-scope-chip"
84
+ title={item.scope}
85
+ onClick={() => {
86
+ onChange(
87
+ on ? selected.filter((s) => s !== item.scope) : [...selected, item.scope],
88
+ );
89
+ }}
90
+ className={cn(
91
+ "inline-flex h-8 min-w-0 items-center px-2 font-mono text-[10px] font-semibold outline-none select-none",
92
+ "focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-inset",
93
+ on
94
+ ? "bg-muted text-foreground"
95
+ : "text-muted-foreground hover:text-foreground",
96
+ )}
97
+ >
98
+ {item.action}
99
+ </button>
100
+ );
101
+ })}
102
+ </div>
103
+ </div>
104
+ ))}
105
+ </div>
106
+ )}
107
+ </div>
108
+ );
109
+ }
110
+
111
+ function filterScopes(scopes: readonly string[], query: string): readonly string[] {
112
+ const q = query.trim().toLowerCase();
113
+ if (q.length === 0) return scopes;
114
+ return scopes.filter((scope) => scope.toLowerCase().includes(q));
115
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Reveal-once API key secret — same acknowledge pattern as Vault master key.
3
+ */
4
+
5
+ import { useState, type JSX } from "react";
6
+ import {
7
+ Sheet,
8
+ SheetContent,
9
+ SheetDescription,
10
+ SheetFooter,
11
+ SheetHeader,
12
+ SheetTitle,
13
+ } from "@/components/ui/sheet";
14
+ import { SheetFooterButton } from "@/components/ui/sheet-form.tsx";
15
+
16
+ /** Props for {@link AccessSecretSheet}. */
17
+ export interface AccessSecretSheetProps {
18
+ readonly secret: string | null;
19
+ readonly title?: string;
20
+ readonly onDismiss: () => void;
21
+ }
22
+
23
+ /**
24
+ * Shows the generated key secret once. Copy warns; dismiss is acknowledge.
25
+ *
26
+ * @param props - Secret + dismiss
27
+ */
28
+ export function AccessSecretSheet({
29
+ secret,
30
+ title = "New API key",
31
+ onDismiss,
32
+ }: AccessSecretSheetProps): JSX.Element {
33
+ const [copied, setCopied] = useState(false);
34
+ const open = secret !== null;
35
+
36
+ return (
37
+ <Sheet
38
+ open={open}
39
+ onOpenChange={(next) => {
40
+ if (!next) onDismiss();
41
+ }}
42
+ >
43
+ <SheetContent
44
+ side="right"
45
+ showOverlay={false}
46
+ className="gap-0 p-0 data-[side=right]:sm:max-w-md"
47
+ data-slot="access-secret-sheet"
48
+ >
49
+ <SheetHeader className="gap-1 border-b border-border/50">
50
+ <SheetTitle className="text-sm">{title}</SheetTitle>
51
+ <SheetDescription className="text-[11px]">
52
+ Shown once. Store it offline. This Console process does not keep it for you.
53
+ </SheetDescription>
54
+ </SheetHeader>
55
+ <div className="flex min-h-0 flex-1 flex-col overflow-y-auto">
56
+ <p className="border-b border-border/50 px-4 py-3 font-mono text-[11px] break-all">
57
+ {secret}
58
+ </p>
59
+ <p className="px-4 py-3 text-[11px] text-muted-foreground" role="status">
60
+ Copying puts the secret on the clipboard. Anyone with this device can read it until you
61
+ clear it.
62
+ </p>
63
+ </div>
64
+ <SheetFooter>
65
+ <SheetFooterButton
66
+ split
67
+ disabled={!secret}
68
+ onClick={() => {
69
+ if (!secret) return;
70
+ void navigator.clipboard?.writeText(secret);
71
+ setCopied(true);
72
+ }}
73
+ >
74
+ {copied ? "Copied" : "Copy (clipboard warning)"}
75
+ </SheetFooterButton>
76
+ <SheetFooterButton
77
+ variant="destructive"
78
+ onClick={() => {
79
+ setCopied(false);
80
+ onDismiss();
81
+ }}
82
+ >
83
+ I have stored this key
84
+ </SheetFooterButton>
85
+ </SheetFooter>
86
+ </SheetContent>
87
+ </Sheet>
88
+ );
89
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Flush search field for Access sheet catalogs (issuer, scopes).
3
+ */
4
+
5
+ import { Search01Icon } from "@hugeicons/core-free-icons";
6
+ import { HugeiconsIcon } from "@hugeicons/react";
7
+ import type { JSX } from "react";
8
+ import { Input } from "@/components/ui/input";
9
+ import { SHEET_SEARCH } from "@/components/ui/sheet-form.tsx";
10
+ import { cn } from "@/lib/utils.ts";
11
+
12
+ /** Props for {@link AccessSheetSearch}. */
13
+ export interface AccessSheetSearchProps {
14
+ readonly value: string;
15
+ readonly placeholder: string;
16
+ readonly label: string;
17
+ readonly onChange: (value: string) => void;
18
+ }
19
+
20
+ /**
21
+ * Leading-icon search inside a sheet section header.
22
+ *
23
+ * @param props - Query + labels
24
+ */
25
+ export function AccessSheetSearch({
26
+ value,
27
+ placeholder,
28
+ label,
29
+ onChange,
30
+ }: AccessSheetSearchProps): JSX.Element {
31
+ return (
32
+ <label className="relative min-w-0 flex-1">
33
+ <HugeiconsIcon
34
+ icon={Search01Icon}
35
+ className="pointer-events-none absolute top-1/2 left-3 size-3.5 -translate-y-1/2 text-muted-foreground"
36
+ aria-hidden
37
+ />
38
+ <Input
39
+ value={value}
40
+ onChange={(e) => onChange(e.target.value)}
41
+ placeholder={placeholder}
42
+ aria-label={label}
43
+ autoComplete="off"
44
+ flat
45
+ className={cn(SHEET_SEARCH)}
46
+ />
47
+ </label>
48
+ );
49
+ }