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.
- package/package.json +1 -1
- package/site/content/docs/elements/flow.mdx +6 -0
- package/site/content/docs/elements/gate.mdx +55 -0
- package/site/content/docs/elements/store.mdx +13 -15
- package/site/content/docs/elements/vault.mdx +3 -0
- package/site/content/docs/reference/client.mdx +3 -0
- package/site/content/docs/reference/fx.mdx +29 -18
- package/site/content/docs/reference/security.md +1 -1
- package/src/auth/api-key-sql.test.ts +47 -0
- package/src/auth/api-key-sql.ts +254 -0
- package/src/auth/api-keys.ts +273 -14
- package/src/auth/auth.test.ts +56 -0
- package/src/auth/config.ts +5 -5
- package/src/auth/index.ts +25 -1
- package/src/auth/schema.ts +8 -0
- package/src/cli/attach-host-console.test.ts +37 -0
- package/src/cli/attach-host-console.ts +6 -0
- package/src/compiler/effects-infer.ts +17 -2
- package/src/console/server/access.test.ts +116 -8
- package/src/console/server/access.ts +70 -23
- package/src/console/server/app.ts +1 -0
- package/src/console/server/dev-identities.test.ts +127 -0
- package/src/console/server/dev-identities.ts +229 -0
- package/src/console/server/flows-invoke.test.ts +21 -4
- package/src/console/server/flows.ts +113 -58
- package/src/console/server/gates.test.ts +24 -0
- package/src/console/server/gates.ts +8 -21
- package/src/console/server/index.ts +1 -0
- package/src/console/server/live.test.ts +26 -1
- package/src/console/server/live.ts +4 -2
- package/src/console/server/operator-db.ts +2 -0
- package/src/console/server/state.ts +29 -55
- package/src/console/ui-next/dist/assets/access-page-CAGHrA9H.js +4 -0
- package/src/console/ui-next/dist/assets/{agent-disclosure-C0X1fbWF.js → agent-disclosure-BP0Y0Sux.js} +1 -1
- package/src/console/ui-next/dist/assets/cache-glyph-B5X-NM0-.js +1 -0
- package/src/console/ui-next/dist/assets/call-pii-button-ChAUiLo9.js +1 -0
- package/src/console/ui-next/dist/assets/collapsible-DGnOM2ph.js +1 -0
- package/src/console/ui-next/dist/assets/copy-inline-button-DBHhgHKP.js +1 -0
- package/src/console/ui-next/dist/assets/detail-header-DhHM1iaZ.js +1 -0
- package/src/console/ui-next/dist/assets/dropdown-menu-_NjTEo5_.js +1 -0
- package/src/console/ui-next/dist/assets/element-icons-BVXtRyd3.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-empty-2uIhBu0_.js +1 -0
- package/src/console/ui-next/dist/assets/flows-page-B-OUtiAu.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CS_O8L-r.js → highlighted-json-Awq7gYdu.js} +1 -1
- package/src/console/ui-next/dist/assets/http-method-_UHM2ODJ.js +1 -0
- package/src/console/ui-next/dist/assets/index-CGoZkILK.js +66 -0
- package/src/console/ui-next/dist/assets/index-Ck88Jmv8.css +2 -0
- package/src/console/ui-next/dist/assets/link-BX6Vqztd.js +1 -0
- package/src/console/ui-next/dist/assets/observability-page-BDyXalNR.js +4 -0
- package/src/console/ui-next/dist/assets/replica-lag-DKRrbvdo.js +18 -0
- package/src/console/ui-next/dist/assets/request-meta-DV0ywz7t.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-JQIZlWfm.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-DKxNTe_m.js +1 -0
- package/src/console/ui-next/dist/assets/skeleton-D-czQJT6.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-Xh8Kn3rx.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-B09O8rA6.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-DkOXA12R.js +54 -0
- package/src/console/ui-next/dist/assets/units-page-Dpk40kOQ.js +1 -0
- package/src/console/ui-next/dist/assets/{use-vault-list-BLbZhzyF.js → use-vault-list-uk4WVboC.js} +1 -1
- package/src/console/ui-next/dist/assets/useMutation-B8EO02Ej.js +1 -0
- package/src/console/ui-next/dist/assets/useRender-BE2A9BWC.js +1 -0
- package/src/console/ui-next/dist/assets/vault-page-B1dB9Ft0.js +2 -0
- package/src/console/ui-next/dist/index.html +11 -9
- package/src/console/ui-next/index.html +3 -3
- package/src/console/ui-next/src/client.ts +167 -0
- package/src/console/ui-next/src/components/shell/app-sidebar.tsx +2 -0
- package/src/console/ui-next/src/components/shell/command-items.test.ts +1 -0
- package/src/console/ui-next/src/components/shell/command-items.ts +8 -0
- package/src/console/ui-next/src/components/shell/shell-layout.tsx +2 -1
- package/src/console/ui-next/src/components/ui/sheet-form.tsx +8 -3
- package/src/console/ui-next/src/features/access/access-page.tsx +347 -0
- package/src/console/ui-next/src/features/access/data/use-access.ts +23 -0
- package/src/console/ui-next/src/features/access/detail/access-detail.tsx +552 -0
- package/src/console/ui-next/src/features/access/explorer/access-list.tsx +125 -0
- package/src/console/ui-next/src/features/access/lib/format-when.test.ts +139 -0
- package/src/console/ui-next/src/features/access/lib/format-when.ts +296 -0
- package/src/console/ui-next/src/features/access/lib/scope-groups.test.ts +34 -0
- package/src/console/ui-next/src/features/access/lib/scope-groups.ts +40 -0
- package/src/console/ui-next/src/features/access/sheets/access-allow-fields.tsx +236 -0
- package/src/console/ui-next/src/features/access/sheets/access-create-sheet.tsx +316 -0
- package/src/console/ui-next/src/features/access/sheets/access-edit-sheet.tsx +240 -0
- package/src/console/ui-next/src/features/access/sheets/access-expiry-fields.tsx +66 -0
- package/src/console/ui-next/src/features/access/sheets/access-refresh-sheet.tsx +104 -0
- package/src/console/ui-next/src/features/access/sheets/access-scope-field.tsx +115 -0
- package/src/console/ui-next/src/features/access/sheets/access-secret-sheet.tsx +89 -0
- package/src/console/ui-next/src/features/access/sheets/access-sheet-search.tsx +49 -0
- package/src/console/ui-next/src/features/access/state/access-selection.ts +84 -0
- package/src/console/ui-next/src/features/auth/auth-redirect.test.ts +5 -0
- package/src/console/ui-next/src/features/auth/auth-redirect.ts +18 -2
- package/src/console/ui-next/src/features/not-found/not-found-page.tsx +1 -1
- package/src/console/ui-next/src/lib/document-meta.test.ts +1 -0
- package/src/console/ui-next/src/lib/document-meta.ts +2 -1
- package/src/console/ui-next/src/lib/last-module-search.test.ts +1 -0
- package/src/console/ui-next/src/lib/last-module-search.ts +2 -1
- package/src/console/ui-next/src/lib/shortcut.test.ts +2 -0
- package/src/console/ui-next/src/lib/shortcut.ts +3 -0
- package/src/console/ui-next/src/router.tsx +10 -1
- package/src/console/ui-next/ui-next-seed-app-schema.ts +88 -37
- package/src/console/ui-next/ui-next-seed-store.ts +43 -15
- package/src/console/xss.gate.test.ts +1 -0
- package/src/elements/gate/declare.ts +2 -0
- package/src/elements/store/cache.ts +1 -1
- package/src/elements/store/runtime.ts +14 -0
- package/src/elements/store/schema-decl.ts +99 -41
- package/src/elements/store/sql-select.types.test.ts +71 -0
- package/src/elements/store/sql-session.ts +54 -32
- package/src/elements/store.ts +3 -0
- package/src/index.ts +1 -0
- package/src/kernel/api-key-host-persist.test.ts +143 -0
- package/src/kernel/app.ts +14 -2
- package/src/kernel/auth-resolve.ts +10 -5
- package/src/kernel/boot.test.ts +4 -4
- package/src/kernel/fx-auth-keys.test.ts +88 -0
- package/src/kernel/fx-auth-keys.ts +198 -0
- package/src/kernel/fx.test.ts +17 -1
- package/src/kernel/fx.ts +36 -18
- package/src/kernel/pipeline-api-key.test.ts +192 -7
- package/src/kernel/pipeline.ts +4 -2
- package/src/manifest/types.ts +9 -3
- package/src/release/build-lib.ts +1 -0
- package/src/console/ui-next/dist/assets/cache-glyph-F1FI122b.js +0 -1
- package/src/console/ui-next/dist/assets/call-pii-button-DEDCl_j3.js +0 -1
- package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +0 -1
- package/src/console/ui-next/dist/assets/confirm-sheet-3ptDzXbz.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-BvYrygyO.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +0 -1
- package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +0 -1
- package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +0 -66
- package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +0 -2
- package/src/console/ui-next/dist/assets/link-COeyggt-.js +0 -1
- package/src/console/ui-next/dist/assets/observability-page-HK9-BO8z.js +0 -4
- package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +0 -18
- package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-Cgzuv4k1.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +0 -1
- package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +0 -54
- package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +0 -1
- 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
|
+
}
|