@clef-sh/ui 0.1.20 → 0.1.21
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/dist/client/assets/index-DPWHjBbB.js +34 -0
- package/dist/client/assets/index-qsLTYpc9.css +2 -0
- package/dist/client/clef.svg +2 -0
- package/dist/client/index.html +3 -31
- package/dist/client-lib/components/Button.d.ts +1 -1
- package/dist/client-lib/components/Button.d.ts.map +1 -1
- package/dist/client-lib/components/CopyButton.d.ts.map +1 -1
- package/dist/client-lib/components/EnvBadge.d.ts.map +1 -1
- package/dist/client-lib/components/MatrixGrid.d.ts.map +1 -1
- package/dist/client-lib/components/Sidebar.d.ts +1 -1
- package/dist/client-lib/components/Sidebar.d.ts.map +1 -1
- package/dist/client-lib/components/StatusDot.d.ts.map +1 -1
- package/dist/client-lib/components/SyncPanel.d.ts.map +1 -1
- package/dist/client-lib/components/TopBar.d.ts +6 -0
- package/dist/client-lib/components/TopBar.d.ts.map +1 -1
- package/dist/client-lib/primitives/Badge.d.ts +11 -0
- package/dist/client-lib/primitives/Badge.d.ts.map +1 -0
- package/dist/client-lib/primitives/Card.d.ts +28 -0
- package/dist/client-lib/primitives/Card.d.ts.map +1 -0
- package/dist/client-lib/primitives/Dialog.d.ts +30 -0
- package/dist/client-lib/primitives/Dialog.d.ts.map +1 -0
- package/dist/client-lib/primitives/EmptyState.d.ts +10 -0
- package/dist/client-lib/primitives/EmptyState.d.ts.map +1 -0
- package/dist/client-lib/primitives/Field.d.ts +36 -0
- package/dist/client-lib/primitives/Field.d.ts.map +1 -0
- package/dist/client-lib/primitives/Input.d.ts +6 -0
- package/dist/client-lib/primitives/Input.d.ts.map +1 -0
- package/dist/client-lib/primitives/Stat.d.ts +11 -0
- package/dist/client-lib/primitives/Stat.d.ts.map +1 -0
- package/dist/client-lib/primitives/Table.d.ts +37 -0
- package/dist/client-lib/primitives/Table.d.ts.map +1 -0
- package/dist/client-lib/primitives/Tabs.d.ts +29 -0
- package/dist/client-lib/primitives/Tabs.d.ts.map +1 -0
- package/dist/client-lib/primitives/Toast.d.ts +16 -0
- package/dist/client-lib/primitives/Toast.d.ts.map +1 -0
- package/dist/client-lib/primitives/Toolbar.d.ts +29 -0
- package/dist/client-lib/primitives/Toolbar.d.ts.map +1 -0
- package/dist/client-lib/primitives/index.d.ts +23 -0
- package/dist/client-lib/primitives/index.d.ts.map +1 -0
- package/dist/client-lib/theme.d.ts +18 -41
- package/dist/client-lib/theme.d.ts.map +1 -1
- package/dist/server/api.d.ts.map +1 -1
- package/dist/server/api.js +215 -0
- package/dist/server/api.js.map +1 -1
- package/dist/server/envelope.d.ts +15 -0
- package/dist/server/envelope.d.ts.map +1 -0
- package/dist/server/envelope.js +310 -0
- package/dist/server/envelope.js.map +1 -0
- package/package.json +7 -2
- package/src/client/App.tsx +16 -41
- package/src/client/components/Button.tsx +13 -22
- package/src/client/components/CopyButton.tsx +5 -12
- package/src/client/components/EnvBadge.tsx +30 -15
- package/src/client/components/MatrixGrid.tsx +108 -252
- package/src/client/components/Sidebar.tsx +123 -199
- package/src/client/components/StatusDot.tsx +10 -15
- package/src/client/components/SyncPanel.tsx +14 -62
- package/src/client/components/TopBar.tsx +11 -36
- package/src/client/index.html +1 -30
- package/src/client/main.tsx +1 -0
- package/src/client/primitives/Badge.test.tsx +47 -0
- package/src/client/primitives/Badge.tsx +64 -0
- package/src/client/primitives/Card.test.tsx +50 -0
- package/src/client/primitives/Card.tsx +85 -0
- package/src/client/primitives/Dialog.test.tsx +55 -0
- package/src/client/primitives/Dialog.tsx +96 -0
- package/src/client/primitives/EmptyState.test.tsx +25 -0
- package/src/client/primitives/EmptyState.tsx +38 -0
- package/src/client/primitives/Field.test.tsx +46 -0
- package/src/client/primitives/Field.tsx +95 -0
- package/src/client/primitives/Input.tsx +26 -0
- package/src/client/primitives/Stat.test.tsx +32 -0
- package/src/client/primitives/Stat.tsx +52 -0
- package/src/client/primitives/Table.test.tsx +58 -0
- package/src/client/primitives/Table.tsx +113 -0
- package/src/client/primitives/Tabs.test.tsx +44 -0
- package/src/client/primitives/Tabs.tsx +100 -0
- package/src/client/primitives/Toast.test.tsx +77 -0
- package/src/client/primitives/Toast.tsx +89 -0
- package/src/client/primitives/Toolbar.test.tsx +50 -0
- package/src/client/primitives/Toolbar.tsx +86 -0
- package/src/client/primitives/index.ts +43 -0
- package/src/client/public/clef.svg +2 -0
- package/src/client/screens/BackendScreen.tsx +104 -363
- package/src/client/screens/DiffView.tsx +187 -378
- package/src/client/screens/EnvelopeScreen.test.tsx +542 -0
- package/src/client/screens/EnvelopeScreen.tsx +948 -0
- package/src/client/screens/GitLogView.tsx +48 -106
- package/src/client/screens/ImportScreen.tsx +105 -308
- package/src/client/screens/LintView.tsx +184 -379
- package/src/client/screens/ManifestScreen.tsx +283 -445
- package/src/client/screens/MatrixView.tsx +75 -91
- package/src/client/screens/NamespaceEditor.tsx +234 -609
- package/src/client/screens/PolicyView.tsx +183 -453
- package/src/client/screens/RecipientsScreen.tsx +71 -350
- package/src/client/screens/ResetScreen.tsx +67 -237
- package/src/client/screens/ScanScreen.tsx +85 -249
- package/src/client/screens/SchemaEditor.test.tsx +237 -0
- package/src/client/screens/SchemaEditor.tsx +435 -0
- package/src/client/screens/ServiceIdentitiesScreen.tsx +251 -788
- package/src/client/styles.css +77 -0
- package/src/client/theme.ts +27 -48
- package/dist/client/assets/index-Db6WgHgY.js +0 -38
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import React, { useState, useEffect, useCallback } from "react";
|
|
2
|
-
import { theme } from "../theme";
|
|
3
2
|
import { apiFetch } from "../api";
|
|
4
|
-
import { TopBar } from "../components/TopBar";
|
|
5
3
|
import { EnvBadge } from "../components/EnvBadge";
|
|
6
4
|
import { Button } from "../components/Button";
|
|
7
5
|
import { CopyButton } from "../components/CopyButton";
|
|
6
|
+
import { Toolbar } from "../primitives";
|
|
8
7
|
import type { ClefManifest } from "@clef-sh/core";
|
|
9
8
|
|
|
10
9
|
interface EnvInfo {
|
|
@@ -39,38 +38,42 @@ interface ServiceIdentitiesScreenProps {
|
|
|
39
38
|
|
|
40
39
|
type View = "list" | "detail" | "create" | "keys" | "update" | "rotate-keys" | "delete-confirm";
|
|
41
40
|
|
|
41
|
+
const INPUT_BASE =
|
|
42
|
+
"w-full box-border rounded-md border border-edge bg-ink-850 px-3 py-2 font-mono text-[12px] text-bone outline-none focus-visible:border-gold-500 placeholder:text-ash-dim";
|
|
43
|
+
|
|
44
|
+
const SMALL_INPUT_BASE =
|
|
45
|
+
"rounded-md border border-edge bg-ink-850 px-2 py-1.5 font-mono text-[12px] text-bone outline-none focus-visible:border-gold-500";
|
|
46
|
+
|
|
47
|
+
const BACK_BUTTON =
|
|
48
|
+
"cursor-pointer rounded-md border border-edge-strong bg-transparent px-3 py-1 font-sans text-[12px] text-ash hover:bg-ink-800";
|
|
49
|
+
|
|
42
50
|
export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenProps) {
|
|
43
51
|
const [view, setView] = useState<View>("list");
|
|
44
52
|
const [identities, setIdentities] = useState<IdentityInfo[]>([]);
|
|
45
53
|
const [selected, setSelected] = useState<string | null>(null);
|
|
46
54
|
const [error, setError] = useState("");
|
|
47
55
|
|
|
48
|
-
// Create form state
|
|
49
56
|
const [name, setName] = useState("");
|
|
50
57
|
const [description, setDescription] = useState("");
|
|
51
58
|
const [selectedNamespaces, setSelectedNamespaces] = useState<Set<string>>(new Set());
|
|
52
59
|
const [envBackends, setEnvBackends] = useState<Record<string, EnvBackendConfig>>({});
|
|
53
60
|
const [role, setRole] = useState<"ci" | "runtime">("ci");
|
|
54
|
-
const [sharedRecipient, setSharedRecipient] = useState(true);
|
|
61
|
+
const [sharedRecipient, setSharedRecipient] = useState(true);
|
|
55
62
|
const [sharedRecipientOverridden, setSharedRecipientOverridden] = useState(false);
|
|
56
63
|
const [creating, setCreating] = useState(false);
|
|
57
64
|
const [createError, setCreateError] = useState("");
|
|
58
65
|
|
|
59
|
-
// Post-create / post-rotate keys
|
|
60
66
|
const [privateKeys, setPrivateKeys] = useState<Record<string, string>>({});
|
|
61
67
|
const [createdName, setCreatedName] = useState("");
|
|
62
68
|
const [wasSharedRecipient, setWasSharedRecipient] = useState(false);
|
|
63
69
|
|
|
64
|
-
// Update form state
|
|
65
70
|
const [updateEnvBackends, setUpdateEnvBackends] = useState<Record<string, UpdateEnvState>>({});
|
|
66
71
|
const [updating, setUpdating] = useState(false);
|
|
67
72
|
const [updateError, setUpdateError] = useState("");
|
|
68
73
|
|
|
69
|
-
// Rotate state
|
|
70
74
|
const [rotatingEnv, setRotatingEnv] = useState<string | undefined>(undefined);
|
|
71
75
|
const [rotatedKeys, setRotatedKeys] = useState<Record<string, string>>({});
|
|
72
76
|
|
|
73
|
-
// Delete state
|
|
74
77
|
const [deleting, setDeleting] = useState(false);
|
|
75
78
|
const [deleteError, setDeleteError] = useState("");
|
|
76
79
|
|
|
@@ -102,7 +105,7 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
102
105
|
}
|
|
103
106
|
setEnvBackends(defaults);
|
|
104
107
|
setRole("ci");
|
|
105
|
-
setSharedRecipient(true);
|
|
108
|
+
setSharedRecipient(true);
|
|
106
109
|
setSharedRecipientOverridden(false);
|
|
107
110
|
setCreateError("");
|
|
108
111
|
setView("create");
|
|
@@ -137,8 +140,6 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
137
140
|
setView("detail");
|
|
138
141
|
}, []);
|
|
139
142
|
|
|
140
|
-
// ── Handlers ──────────────────────────────────────────────────────────────────
|
|
141
|
-
|
|
142
143
|
async function handleCreate() {
|
|
143
144
|
setCreating(true);
|
|
144
145
|
setCreateError("");
|
|
@@ -154,9 +155,7 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
154
155
|
description: description.trim(),
|
|
155
156
|
namespaces: Array.from(selectedNamespaces),
|
|
156
157
|
};
|
|
157
|
-
if (role === "runtime")
|
|
158
|
-
body.packOnly = true;
|
|
159
|
-
}
|
|
158
|
+
if (role === "runtime") body.packOnly = true;
|
|
160
159
|
if (sharedRecipient) {
|
|
161
160
|
body.sharedRecipient = true;
|
|
162
161
|
} else if (Object.keys(kmsEnvConfigs).length > 0) {
|
|
@@ -269,36 +268,30 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
269
268
|
// ── List view ─────────────────────────────────────────────────────────────────
|
|
270
269
|
if (view === "list") {
|
|
271
270
|
return (
|
|
272
|
-
<div
|
|
273
|
-
<
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
271
|
+
<div className="flex flex-1 flex-col overflow-hidden">
|
|
272
|
+
<Toolbar>
|
|
273
|
+
<div>
|
|
274
|
+
<Toolbar.Title>Service Identities</Toolbar.Title>
|
|
275
|
+
<Toolbar.Subtitle>Per-service cryptographic access scoping</Toolbar.Subtitle>
|
|
276
|
+
</div>
|
|
277
|
+
{manifest && (
|
|
278
|
+
<Toolbar.Actions>
|
|
278
279
|
<Button variant="primary" onClick={openCreate}>
|
|
279
280
|
+ New identity
|
|
280
281
|
</Button>
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
<div
|
|
285
|
-
<div
|
|
282
|
+
</Toolbar.Actions>
|
|
283
|
+
)}
|
|
284
|
+
</Toolbar>
|
|
285
|
+
<div className="flex-1 overflow-auto p-6">
|
|
286
|
+
<div className="mx-auto max-w-[620px]">
|
|
286
287
|
{error && <ErrorBanner>{error}</ErrorBanner>}
|
|
287
288
|
|
|
288
289
|
{identities.length === 0 && (
|
|
289
|
-
<div
|
|
290
|
-
|
|
291
|
-
textAlign: "center",
|
|
292
|
-
padding: "48px 24px",
|
|
293
|
-
color: theme.textMuted,
|
|
294
|
-
fontFamily: theme.sans,
|
|
295
|
-
fontSize: 13,
|
|
296
|
-
}}
|
|
297
|
-
>
|
|
298
|
-
<div style={{ fontSize: 28, marginBottom: 12, opacity: 0.4 }}>{"\uD83D\uDD11"}</div>
|
|
290
|
+
<div className="px-6 py-12 text-center font-sans text-[13px] text-ash">
|
|
291
|
+
<div className="mb-3 text-[28px] opacity-40">{"🔑"}</div>
|
|
299
292
|
No service identities configured.
|
|
300
293
|
{manifest && (
|
|
301
|
-
<div
|
|
294
|
+
<div className="mt-4 flex justify-center">
|
|
302
295
|
<Button variant="primary" onClick={openCreate}>
|
|
303
296
|
Create the first one
|
|
304
297
|
</Button>
|
|
@@ -324,73 +317,28 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
324
317
|
setView("detail");
|
|
325
318
|
}
|
|
326
319
|
}}
|
|
327
|
-
|
|
328
|
-
background: theme.surface,
|
|
329
|
-
border: `1px solid ${theme.border}`,
|
|
330
|
-
borderRadius: 8,
|
|
331
|
-
padding: "16px 20px",
|
|
332
|
-
marginBottom: 8,
|
|
333
|
-
cursor: "pointer",
|
|
334
|
-
transition: "all 0.12s",
|
|
335
|
-
}}
|
|
336
|
-
onMouseEnter={(e) => {
|
|
337
|
-
(e.currentTarget as HTMLDivElement).style.borderColor = theme.borderLight;
|
|
338
|
-
}}
|
|
339
|
-
onMouseLeave={(e) => {
|
|
340
|
-
(e.currentTarget as HTMLDivElement).style.borderColor = theme.border;
|
|
341
|
-
}}
|
|
320
|
+
className="mb-2 cursor-pointer rounded-lg border border-edge bg-ink-850 px-5 py-4 transition-colors hover:border-edge-strong"
|
|
342
321
|
>
|
|
343
|
-
<div
|
|
344
|
-
<span
|
|
345
|
-
style={{
|
|
346
|
-
fontFamily: theme.sans,
|
|
347
|
-
fontWeight: 600,
|
|
348
|
-
fontSize: 14,
|
|
349
|
-
color: theme.text,
|
|
350
|
-
}}
|
|
351
|
-
>
|
|
352
|
-
{si.name}
|
|
353
|
-
</span>
|
|
322
|
+
<div className="mb-2 flex items-center gap-2.5">
|
|
323
|
+
<span className="font-sans text-[14px] font-semibold text-bone">{si.name}</span>
|
|
354
324
|
{si.packOnly && (
|
|
355
325
|
<span
|
|
356
326
|
data-testid={`si-runtime-badge-${si.name}`}
|
|
357
|
-
|
|
358
|
-
fontFamily: theme.mono,
|
|
359
|
-
fontSize: 9,
|
|
360
|
-
color: theme.yellow,
|
|
361
|
-
background: `${theme.yellow}15`,
|
|
362
|
-
border: `1px solid ${theme.yellow}33`,
|
|
363
|
-
borderRadius: 3,
|
|
364
|
-
padding: "1px 6px",
|
|
365
|
-
}}
|
|
327
|
+
className="rounded-sm border border-warn-500/20 bg-warn-500/10 px-1.5 py-px font-mono text-[9px] text-warn-500"
|
|
366
328
|
>
|
|
367
329
|
runtime
|
|
368
330
|
</span>
|
|
369
331
|
)}
|
|
370
332
|
</div>
|
|
371
|
-
<div
|
|
372
|
-
|
|
373
|
-
fontFamily: theme.sans,
|
|
374
|
-
fontSize: 12,
|
|
375
|
-
color: theme.textMuted,
|
|
376
|
-
marginBottom: 10,
|
|
377
|
-
}}
|
|
378
|
-
>
|
|
379
|
-
Scoped to: <span style={{ color: theme.text }}>{si.namespaces.join(", ")}</span>
|
|
333
|
+
<div className="mb-2.5 font-sans text-[12px] text-ash">
|
|
334
|
+
Scoped to: <span className="text-bone">{si.namespaces.join(", ")}</span>
|
|
380
335
|
</div>
|
|
381
|
-
<div
|
|
336
|
+
<div className="flex flex-wrap gap-1.5">
|
|
382
337
|
{Object.entries(si.environments).map(([envName, envInfo]) => (
|
|
383
|
-
<span
|
|
384
|
-
key={envName}
|
|
385
|
-
style={{ display: "inline-flex", alignItems: "center", gap: 4 }}
|
|
386
|
-
>
|
|
338
|
+
<span key={envName} className="inline-flex items-center gap-1">
|
|
387
339
|
<EnvBadge env={envName} small />
|
|
388
340
|
<span
|
|
389
|
-
|
|
390
|
-
fontFamily: theme.mono,
|
|
391
|
-
fontSize: 9,
|
|
392
|
-
color: envInfo.type === "kms" ? theme.purple : theme.textDim,
|
|
393
|
-
}}
|
|
341
|
+
className={`font-mono text-[9px] ${envInfo.type === "kms" ? "text-purple-400" : "text-ash-dim"}`}
|
|
394
342
|
>
|
|
395
343
|
{envInfo.type === "kms" ? "KMS" : "age"}
|
|
396
344
|
</span>
|
|
@@ -408,62 +356,42 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
408
356
|
// ── Detail view ───────────────────────────────────────────────────────────────
|
|
409
357
|
if (view === "detail") {
|
|
410
358
|
return (
|
|
411
|
-
<div
|
|
412
|
-
<
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
)}
|
|
426
|
-
<button
|
|
427
|
-
data-testid="back-button"
|
|
428
|
-
onClick={goList}
|
|
429
|
-
style={{
|
|
430
|
-
background: "none",
|
|
431
|
-
border: `1px solid ${theme.borderLight}`,
|
|
432
|
-
borderRadius: 6,
|
|
433
|
-
padding: "4px 12px",
|
|
434
|
-
cursor: "pointer",
|
|
435
|
-
fontFamily: theme.sans,
|
|
436
|
-
fontSize: 12,
|
|
437
|
-
color: theme.textMuted,
|
|
438
|
-
transition: "all 0.12s",
|
|
439
|
-
}}
|
|
359
|
+
<div className="flex flex-1 flex-col overflow-hidden">
|
|
360
|
+
<Toolbar>
|
|
361
|
+
<div>
|
|
362
|
+
<Toolbar.Title>{selectedIdentity?.name ?? selected ?? ""}</Toolbar.Title>
|
|
363
|
+
{selectedIdentity?.description && (
|
|
364
|
+
<Toolbar.Subtitle>{selectedIdentity.description}</Toolbar.Subtitle>
|
|
365
|
+
)}
|
|
366
|
+
</div>
|
|
367
|
+
<Toolbar.Actions>
|
|
368
|
+
{selectedIdentity && (
|
|
369
|
+
<Button
|
|
370
|
+
data-testid="update-backends-btn"
|
|
371
|
+
variant="ghost"
|
|
372
|
+
onClick={() => openUpdate(selectedIdentity)}
|
|
440
373
|
>
|
|
441
|
-
|
|
442
|
-
</
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
374
|
+
Update backends
|
|
375
|
+
</Button>
|
|
376
|
+
)}
|
|
377
|
+
<button data-testid="back-button" onClick={goList} className={BACK_BUTTON}>
|
|
378
|
+
{"←"} Back
|
|
379
|
+
</button>
|
|
380
|
+
</Toolbar.Actions>
|
|
381
|
+
</Toolbar>
|
|
382
|
+
<div className="flex-1 overflow-auto p-6">
|
|
383
|
+
<div className="mx-auto max-w-[620px]">
|
|
448
384
|
{error && <ErrorBanner>{error}</ErrorBanner>}
|
|
449
385
|
|
|
450
386
|
{selectedIdentity && (
|
|
451
387
|
<>
|
|
452
|
-
<div
|
|
388
|
+
<div className="mb-5">
|
|
453
389
|
<Label>Scoped namespaces</Label>
|
|
454
|
-
<div
|
|
390
|
+
<div className="flex gap-1.5">
|
|
455
391
|
{selectedIdentity.namespaces.map((ns) => (
|
|
456
392
|
<span
|
|
457
393
|
key={ns}
|
|
458
|
-
|
|
459
|
-
fontFamily: theme.mono,
|
|
460
|
-
fontSize: 11,
|
|
461
|
-
color: theme.accent,
|
|
462
|
-
background: theme.accentDim,
|
|
463
|
-
border: `1px solid ${theme.accent}33`,
|
|
464
|
-
borderRadius: 4,
|
|
465
|
-
padding: "2px 8px",
|
|
466
|
-
}}
|
|
394
|
+
className="rounded border border-gold-500/20 bg-gold-500/[0.08] px-2 py-0.5 font-mono text-[11px] text-gold-500"
|
|
467
395
|
>
|
|
468
396
|
{ns}
|
|
469
397
|
</span>
|
|
@@ -474,17 +402,7 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
474
402
|
{selectedIdentity.packOnly && (
|
|
475
403
|
<div
|
|
476
404
|
data-testid="runtime-info-banner"
|
|
477
|
-
|
|
478
|
-
background: `${theme.yellow}10`,
|
|
479
|
-
border: `1px solid ${theme.yellow}33`,
|
|
480
|
-
borderRadius: 8,
|
|
481
|
-
padding: "10px 16px",
|
|
482
|
-
marginBottom: 20,
|
|
483
|
-
fontFamily: theme.sans,
|
|
484
|
-
fontSize: 12,
|
|
485
|
-
color: theme.yellow,
|
|
486
|
-
lineHeight: 1.5,
|
|
487
|
-
}}
|
|
405
|
+
className="mb-5 rounded-lg border border-warn-500/20 bg-warn-500/10 px-4 py-2.5 font-sans text-[12px] leading-relaxed text-warn-500"
|
|
488
406
|
>
|
|
489
407
|
Runtime identity — keys are not registered on encrypted files. This identity can
|
|
490
408
|
only decrypt packed artifacts.
|
|
@@ -503,39 +421,14 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
503
421
|
<div
|
|
504
422
|
key={env.name}
|
|
505
423
|
data-testid={`env-${env.name}`}
|
|
506
|
-
|
|
507
|
-
background: theme.surface,
|
|
508
|
-
border: `1px solid ${theme.border}`,
|
|
509
|
-
borderRadius: 8,
|
|
510
|
-
padding: "16px 20px",
|
|
511
|
-
marginBottom: 10,
|
|
512
|
-
}}
|
|
424
|
+
className="mb-2.5 rounded-lg border border-edge bg-ink-850 px-5 py-4"
|
|
513
425
|
>
|
|
514
|
-
<div
|
|
515
|
-
|
|
516
|
-
display: "flex",
|
|
517
|
-
alignItems: "center",
|
|
518
|
-
justifyContent: "space-between",
|
|
519
|
-
marginBottom: 12,
|
|
520
|
-
}}
|
|
521
|
-
>
|
|
522
|
-
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
|
426
|
+
<div className="mb-3 flex items-center justify-between">
|
|
427
|
+
<div className="flex items-center gap-2.5">
|
|
523
428
|
<EnvBadge env={env.name} />
|
|
524
|
-
{isProtected &&
|
|
525
|
-
<span style={{ fontSize: 12, color: theme.red }}>{"\uD83D\uDD12"}</span>
|
|
526
|
-
)}
|
|
429
|
+
{isProtected && <span className="text-[12px] text-stop-500">{"🔒"}</span>}
|
|
527
430
|
{envInfo.type === "kms" && (
|
|
528
|
-
<span
|
|
529
|
-
style={{
|
|
530
|
-
fontFamily: theme.mono,
|
|
531
|
-
fontSize: 10,
|
|
532
|
-
color: theme.purple,
|
|
533
|
-
background: theme.purpleDim,
|
|
534
|
-
border: `1px solid ${theme.purple}33`,
|
|
535
|
-
borderRadius: 3,
|
|
536
|
-
padding: "1px 6px",
|
|
537
|
-
}}
|
|
538
|
-
>
|
|
431
|
+
<span className="rounded-sm border border-purple-400/20 bg-purple-400/10 px-1.5 py-px font-mono text-[10px] text-purple-400">
|
|
539
432
|
KMS
|
|
540
433
|
</span>
|
|
541
434
|
)}
|
|
@@ -545,17 +438,7 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
545
438
|
data-testid={`rotate-${env.name}`}
|
|
546
439
|
disabled={isRotating}
|
|
547
440
|
onClick={() => handleRotate(env.name)}
|
|
548
|
-
|
|
549
|
-
background: "none",
|
|
550
|
-
border: `1px solid ${theme.borderLight}`,
|
|
551
|
-
borderRadius: 5,
|
|
552
|
-
padding: "3px 10px",
|
|
553
|
-
cursor: isRotating ? "default" : "pointer",
|
|
554
|
-
fontFamily: theme.sans,
|
|
555
|
-
fontSize: 11,
|
|
556
|
-
color: isRotating ? theme.textDim : theme.textMuted,
|
|
557
|
-
opacity: isRotating ? 0.5 : 1,
|
|
558
|
-
}}
|
|
441
|
+
className={`rounded border border-edge-strong px-2.5 py-0.5 font-sans text-[11px] ${isRotating ? "cursor-default text-ash-dim opacity-50" : "cursor-pointer text-ash hover:bg-ink-800"}`}
|
|
559
442
|
>
|
|
560
443
|
{isRotating ? "Rotating…" : "Rotate key"}
|
|
561
444
|
</button>
|
|
@@ -563,34 +446,17 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
563
446
|
</div>
|
|
564
447
|
|
|
565
448
|
{envInfo.type === "kms" && envInfo.kms && (
|
|
566
|
-
<div
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
<div style={{ marginBottom: 8 }}>
|
|
570
|
-
Authentication: <span style={{ color: theme.purple }}>IAM + KMS</span>
|
|
449
|
+
<div className="font-mono text-[11px] text-ash">
|
|
450
|
+
<div className="mb-2">
|
|
451
|
+
Authentication: <span className="text-purple-400">IAM + KMS</span>
|
|
571
452
|
</div>
|
|
572
453
|
<div>
|
|
573
|
-
Provider:
|
|
574
|
-
<span style={{ color: theme.text }}>{envInfo.kms.provider}</span>
|
|
454
|
+
Provider: <span className="text-bone">{envInfo.kms.provider}</span>
|
|
575
455
|
</div>
|
|
576
|
-
<div
|
|
577
|
-
Key ID:
|
|
578
|
-
<span style={{ color: theme.text, wordBreak: "break-all" }}>
|
|
579
|
-
{envInfo.kms.keyId}
|
|
580
|
-
</span>
|
|
456
|
+
<div className="mt-1">
|
|
457
|
+
Key ID: <span className="break-all text-bone">{envInfo.kms.keyId}</span>
|
|
581
458
|
</div>
|
|
582
|
-
<div
|
|
583
|
-
style={{
|
|
584
|
-
marginTop: 10,
|
|
585
|
-
padding: "8px 12px",
|
|
586
|
-
background: theme.purpleDim,
|
|
587
|
-
border: `1px solid ${theme.purple}33`,
|
|
588
|
-
borderRadius: 4,
|
|
589
|
-
fontSize: 11,
|
|
590
|
-
color: theme.purple,
|
|
591
|
-
fontFamily: theme.sans,
|
|
592
|
-
}}
|
|
593
|
-
>
|
|
459
|
+
<div className="mt-2.5 rounded border border-purple-400/20 bg-purple-400/10 px-3 py-2 font-sans text-[11px] text-purple-400">
|
|
594
460
|
No keys to provision. CI and runtime authenticate via IAM role with
|
|
595
461
|
kms:Decrypt permission.
|
|
596
462
|
</div>
|
|
@@ -598,15 +464,13 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
598
464
|
)}
|
|
599
465
|
|
|
600
466
|
{envInfo.type === "age" && (
|
|
601
|
-
<div
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
<div style={{ marginBottom: 8 }}>
|
|
605
|
-
Authentication: <span style={{ color: theme.green }}>age key</span>
|
|
467
|
+
<div className="font-mono text-[11px] text-ash">
|
|
468
|
+
<div className="mb-2">
|
|
469
|
+
Authentication: <span className="text-go-500">age key</span>
|
|
606
470
|
</div>
|
|
607
471
|
<div>
|
|
608
472
|
Public key:{" "}
|
|
609
|
-
<span
|
|
473
|
+
<span className="text-bone">
|
|
610
474
|
{envInfo.publicKey
|
|
611
475
|
? `${envInfo.publicKey.slice(0, 12)}...${envInfo.publicKey.slice(-6)}`
|
|
612
476
|
: "unknown"}
|
|
@@ -618,15 +482,7 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
618
482
|
);
|
|
619
483
|
})}
|
|
620
484
|
|
|
621
|
-
<div
|
|
622
|
-
style={{
|
|
623
|
-
marginTop: 32,
|
|
624
|
-
paddingTop: 20,
|
|
625
|
-
borderTop: `1px solid ${theme.border}`,
|
|
626
|
-
display: "flex",
|
|
627
|
-
justifyContent: "flex-end",
|
|
628
|
-
}}
|
|
629
|
-
>
|
|
485
|
+
<div className="mt-8 flex justify-end border-t border-edge pt-5">
|
|
630
486
|
<Button
|
|
631
487
|
data-testid="delete-identity-btn"
|
|
632
488
|
variant="danger"
|
|
@@ -649,39 +505,29 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
649
505
|
// ── Delete confirm view ───────────────────────────────────────────────────────
|
|
650
506
|
if (view === "delete-confirm") {
|
|
651
507
|
return (
|
|
652
|
-
<div
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
508
|
+
<div data-testid="delete-confirm-view" className="flex flex-1 flex-col overflow-hidden">
|
|
509
|
+
<Toolbar>
|
|
510
|
+
<div>
|
|
511
|
+
<Toolbar.Title>Delete service identity</Toolbar.Title>
|
|
512
|
+
<Toolbar.Subtitle>This action cannot be undone</Toolbar.Subtitle>
|
|
513
|
+
</div>
|
|
514
|
+
</Toolbar>
|
|
515
|
+
<div className="flex-1 overflow-auto p-6">
|
|
516
|
+
<div className="mx-auto max-w-[560px]">
|
|
659
517
|
{deleteError && <ErrorBanner>{deleteError}</ErrorBanner>}
|
|
660
518
|
|
|
661
|
-
<div
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
border: `1px solid ${theme.red}55`,
|
|
665
|
-
borderRadius: 8,
|
|
666
|
-
padding: "16px 20px",
|
|
667
|
-
marginBottom: 24,
|
|
668
|
-
fontFamily: theme.sans,
|
|
669
|
-
fontSize: 13,
|
|
670
|
-
color: theme.red,
|
|
671
|
-
}}
|
|
672
|
-
>
|
|
673
|
-
<div style={{ fontWeight: 600, marginBottom: 8 }}>
|
|
674
|
-
Delete <span style={{ fontFamily: theme.mono }}>{selected}</span>?
|
|
519
|
+
<div className="mb-6 rounded-lg border border-stop-500/40 bg-stop-500/[0.06] px-5 py-4 font-sans text-[13px] text-stop-500">
|
|
520
|
+
<div className="mb-2 font-semibold">
|
|
521
|
+
Delete <span className="font-mono">{selected}</span>?
|
|
675
522
|
</div>
|
|
676
|
-
<div
|
|
677
|
-
This will remove the identity from
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
identity's private key will lose access on the next artifact refresh.
|
|
523
|
+
<div className="text-[12px] leading-relaxed text-ash">
|
|
524
|
+
This will remove the identity from <span className="font-mono">clef.yaml</span> and
|
|
525
|
+
de-register its recipients from all scoped encrypted files. Any runtimes currently
|
|
526
|
+
using this identity's private key will lose access on the next artifact refresh.
|
|
681
527
|
</div>
|
|
682
528
|
</div>
|
|
683
529
|
|
|
684
|
-
<div
|
|
530
|
+
<div className="flex justify-end gap-2">
|
|
685
531
|
<Button
|
|
686
532
|
data-testid="cancel-delete-btn"
|
|
687
533
|
variant="ghost"
|
|
@@ -708,29 +554,17 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
708
554
|
// ── Rotate keys result view ───────────────────────────────────────────────────
|
|
709
555
|
if (view === "rotate-keys") {
|
|
710
556
|
return (
|
|
711
|
-
<div
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
borderRadius: 8,
|
|
723
|
-
padding: "14px 18px",
|
|
724
|
-
marginBottom: 20,
|
|
725
|
-
fontFamily: theme.sans,
|
|
726
|
-
fontSize: 13,
|
|
727
|
-
color: theme.yellow,
|
|
728
|
-
display: "flex",
|
|
729
|
-
gap: 10,
|
|
730
|
-
alignItems: "flex-start",
|
|
731
|
-
}}
|
|
732
|
-
>
|
|
733
|
-
<span style={{ fontSize: 16, flexShrink: 0 }}>⚠</span>
|
|
557
|
+
<div data-testid="rotate-keys-view" className="flex flex-1 flex-col overflow-hidden">
|
|
558
|
+
<Toolbar>
|
|
559
|
+
<div>
|
|
560
|
+
<Toolbar.Title>Key rotated</Toolbar.Title>
|
|
561
|
+
<Toolbar.Subtitle>{`New keys for ${selected}`}</Toolbar.Subtitle>
|
|
562
|
+
</div>
|
|
563
|
+
</Toolbar>
|
|
564
|
+
<div className="flex-1 overflow-auto p-6">
|
|
565
|
+
<div className="mx-auto max-w-[620px]">
|
|
566
|
+
<div className="mb-5 flex items-start gap-2.5 rounded-lg border border-warn-500/40 bg-warn-500/[0.06] px-4 py-3.5 font-sans text-[13px] text-warn-500">
|
|
567
|
+
<span className="shrink-0 text-[16px]">⚠</span>
|
|
734
568
|
<span>
|
|
735
569
|
Copy the new private key now — it will not be shown again. Provision it to the
|
|
736
570
|
runtime and invalidate the old key.
|
|
@@ -741,42 +575,19 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
741
575
|
{Object.entries(rotatedKeys).map(([envName, key]) => (
|
|
742
576
|
<div
|
|
743
577
|
key={envName}
|
|
744
|
-
|
|
745
|
-
background: theme.surface,
|
|
746
|
-
border: `1px solid ${theme.border}`,
|
|
747
|
-
borderRadius: 8,
|
|
748
|
-
padding: "14px 18px",
|
|
749
|
-
marginBottom: 10,
|
|
750
|
-
}}
|
|
578
|
+
className="mb-2.5 rounded-lg border border-edge bg-ink-850 px-4 py-3.5"
|
|
751
579
|
>
|
|
752
|
-
<div
|
|
753
|
-
style={{
|
|
754
|
-
display: "flex",
|
|
755
|
-
alignItems: "center",
|
|
756
|
-
justifyContent: "space-between",
|
|
757
|
-
marginBottom: 10,
|
|
758
|
-
}}
|
|
759
|
-
>
|
|
580
|
+
<div className="mb-2.5 flex items-center justify-between">
|
|
760
581
|
<EnvBadge env={envName} />
|
|
761
582
|
<CopyButton text={key} />
|
|
762
583
|
</div>
|
|
763
|
-
<div
|
|
764
|
-
style={{
|
|
765
|
-
fontFamily: theme.mono,
|
|
766
|
-
fontSize: 11,
|
|
767
|
-
color: theme.textMuted,
|
|
768
|
-
wordBreak: "break-all",
|
|
769
|
-
background: theme.bg,
|
|
770
|
-
borderRadius: 4,
|
|
771
|
-
padding: "8px 10px",
|
|
772
|
-
}}
|
|
773
|
-
>
|
|
584
|
+
<div className="break-all rounded bg-ink-950 px-2.5 py-2 font-mono text-[11px] text-ash">
|
|
774
585
|
{key}
|
|
775
586
|
</div>
|
|
776
587
|
</div>
|
|
777
588
|
))}
|
|
778
589
|
|
|
779
|
-
<div
|
|
590
|
+
<div className="mt-2 flex justify-end">
|
|
780
591
|
<Button data-testid="rotate-done-btn" variant="primary" onClick={goDetail}>
|
|
781
592
|
Done
|
|
782
593
|
</Button>
|
|
@@ -804,77 +615,52 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
804
615
|
});
|
|
805
616
|
|
|
806
617
|
return (
|
|
807
|
-
<div
|
|
808
|
-
<
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
border: `1px solid ${theme.borderLight}`,
|
|
817
|
-
borderRadius: 6,
|
|
818
|
-
padding: "4px 12px",
|
|
819
|
-
cursor: "pointer",
|
|
820
|
-
fontFamily: theme.sans,
|
|
821
|
-
fontSize: 12,
|
|
822
|
-
color: theme.textMuted,
|
|
823
|
-
}}
|
|
824
|
-
>
|
|
825
|
-
{"\u2190"} Cancel
|
|
618
|
+
<div className="flex flex-1 flex-col overflow-hidden">
|
|
619
|
+
<Toolbar>
|
|
620
|
+
<div>
|
|
621
|
+
<Toolbar.Title>Update backends</Toolbar.Title>
|
|
622
|
+
<Toolbar.Subtitle>{`Environment backends for ${selected}`}</Toolbar.Subtitle>
|
|
623
|
+
</div>
|
|
624
|
+
<Toolbar.Actions>
|
|
625
|
+
<button onClick={goDetail} className={BACK_BUTTON}>
|
|
626
|
+
{"←"} Cancel
|
|
826
627
|
</button>
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
<div
|
|
830
|
-
<div
|
|
628
|
+
</Toolbar.Actions>
|
|
629
|
+
</Toolbar>
|
|
630
|
+
<div className="flex-1 overflow-auto p-6">
|
|
631
|
+
<div className="mx-auto max-w-[560px]">
|
|
831
632
|
{updateError && <ErrorBanner>{updateError}</ErrorBanner>}
|
|
832
633
|
|
|
833
|
-
<div
|
|
834
|
-
style={{
|
|
835
|
-
fontFamily: theme.sans,
|
|
836
|
-
fontSize: 12,
|
|
837
|
-
color: theme.textMuted,
|
|
838
|
-
marginBottom: 16,
|
|
839
|
-
lineHeight: 1.6,
|
|
840
|
-
}}
|
|
841
|
-
>
|
|
634
|
+
<div className="mb-4 font-sans text-[12px] leading-relaxed text-ash">
|
|
842
635
|
Switch age environments to KMS, or update an existing KMS key ID. To revert KMS to
|
|
843
636
|
age, delete and recreate the identity.
|
|
844
637
|
</div>
|
|
845
638
|
|
|
846
|
-
<div
|
|
639
|
+
<div className="mb-7 flex flex-col gap-2">
|
|
847
640
|
{environments.map((env) => {
|
|
848
641
|
const state = updateEnvBackends[env.name];
|
|
849
642
|
if (!state) return null;
|
|
850
|
-
|
|
851
643
|
return (
|
|
852
644
|
<div
|
|
853
645
|
key={env.name}
|
|
854
|
-
|
|
855
|
-
background: theme.surface,
|
|
856
|
-
border: `1px solid ${theme.border}`,
|
|
857
|
-
borderRadius: 8,
|
|
858
|
-
padding: "14px 16px",
|
|
859
|
-
}}
|
|
646
|
+
className="rounded-lg border border-edge bg-ink-850 px-4 py-3.5"
|
|
860
647
|
>
|
|
861
648
|
<div
|
|
862
|
-
|
|
863
|
-
display: "flex",
|
|
864
|
-
alignItems: "center",
|
|
865
|
-
justifyContent: "space-between",
|
|
866
|
-
marginBottom: state.type === "kms" ? 12 : 0,
|
|
867
|
-
}}
|
|
649
|
+
className={`flex items-center justify-between ${state.type === "kms" ? "mb-3" : ""}`}
|
|
868
650
|
>
|
|
869
|
-
<div
|
|
651
|
+
<div className="flex items-center gap-2">
|
|
870
652
|
<EnvBadge env={env.name} />
|
|
871
|
-
{env.protected &&
|
|
872
|
-
<span style={{ fontSize: 11, color: theme.red }}>{"\uD83D\uDD12"}</span>
|
|
873
|
-
)}
|
|
653
|
+
{env.protected && <span className="text-[11px] text-stop-500">{"🔒"}</span>}
|
|
874
654
|
</div>
|
|
875
|
-
<div
|
|
655
|
+
<div className="flex gap-1">
|
|
876
656
|
{(["age", "kms"] as const).map((t) => {
|
|
877
657
|
const locked = state.originalType === "kms" && t === "age";
|
|
658
|
+
const isSelected = state.type === t;
|
|
659
|
+
const buttonClass = isSelected
|
|
660
|
+
? t === "kms"
|
|
661
|
+
? "bg-purple-400 border-purple-400 text-ghost"
|
|
662
|
+
: "bg-gold-500 border-gold-500 text-ink-950"
|
|
663
|
+
: "bg-transparent border-edge text-ash";
|
|
878
664
|
return (
|
|
879
665
|
<button
|
|
880
666
|
key={t}
|
|
@@ -890,29 +676,7 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
890
676
|
}));
|
|
891
677
|
}}
|
|
892
678
|
title={locked ? "KMS → age requires delete and recreate" : undefined}
|
|
893
|
-
|
|
894
|
-
background:
|
|
895
|
-
state.type === t
|
|
896
|
-
? t === "kms"
|
|
897
|
-
? theme.purple
|
|
898
|
-
: theme.accent
|
|
899
|
-
: "transparent",
|
|
900
|
-
border: `1px solid ${
|
|
901
|
-
state.type === t
|
|
902
|
-
? t === "kms"
|
|
903
|
-
? theme.purple
|
|
904
|
-
: theme.accent
|
|
905
|
-
: theme.border
|
|
906
|
-
}`,
|
|
907
|
-
borderRadius: 4,
|
|
908
|
-
padding: "3px 10px",
|
|
909
|
-
cursor: locked ? "not-allowed" : "pointer",
|
|
910
|
-
fontFamily: theme.mono,
|
|
911
|
-
fontSize: 11,
|
|
912
|
-
color: state.type === t ? "#fff" : theme.textMuted,
|
|
913
|
-
opacity: locked ? 0.4 : 1,
|
|
914
|
-
transition: "all 0.1s",
|
|
915
|
-
}}
|
|
679
|
+
className={`rounded border px-2.5 py-0.5 font-mono text-[11px] transition-colors ${buttonClass} ${locked ? "cursor-not-allowed opacity-40" : "cursor-pointer"}`}
|
|
916
680
|
>
|
|
917
681
|
{t.toUpperCase()}
|
|
918
682
|
</button>
|
|
@@ -922,7 +686,7 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
922
686
|
</div>
|
|
923
687
|
|
|
924
688
|
{state.type === "kms" && (
|
|
925
|
-
<div
|
|
689
|
+
<div className="flex gap-2">
|
|
926
690
|
<select
|
|
927
691
|
value={state.provider}
|
|
928
692
|
onChange={(e) =>
|
|
@@ -931,12 +695,7 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
931
695
|
[env.name]: { ...state, provider: e.target.value },
|
|
932
696
|
}))
|
|
933
697
|
}
|
|
934
|
-
|
|
935
|
-
...inputStyle,
|
|
936
|
-
width: 90,
|
|
937
|
-
flexShrink: 0,
|
|
938
|
-
padding: "7px 8px",
|
|
939
|
-
}}
|
|
698
|
+
className={`${SMALL_INPUT_BASE} w-[90px] shrink-0`}
|
|
940
699
|
>
|
|
941
700
|
<option value="aws">AWS</option>
|
|
942
701
|
<option value="gcp">GCP</option>
|
|
@@ -952,7 +711,7 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
952
711
|
}))
|
|
953
712
|
}
|
|
954
713
|
placeholder="arn:aws:kms:… or key resource ID"
|
|
955
|
-
|
|
714
|
+
className={`${SMALL_INPUT_BASE} flex-1`}
|
|
956
715
|
/>
|
|
957
716
|
</div>
|
|
958
717
|
)}
|
|
@@ -961,7 +720,7 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
961
720
|
})}
|
|
962
721
|
</div>
|
|
963
722
|
|
|
964
|
-
<div
|
|
723
|
+
<div className="flex justify-end gap-2">
|
|
965
724
|
<Button
|
|
966
725
|
data-testid="update-cancel-btn"
|
|
967
726
|
variant="ghost"
|
|
@@ -988,32 +747,22 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
988
747
|
// ── Keys result view (post-creation) ─────────────────────────────────────────
|
|
989
748
|
if (view === "keys") {
|
|
990
749
|
const hasAgeKeys = Object.keys(privateKeys).length > 0;
|
|
991
|
-
// For shared mode, all entries hold the same key — grab it once
|
|
992
750
|
const sharedKey = wasSharedRecipient ? Object.values(privateKeys)[0] : undefined;
|
|
993
751
|
const sharedEnvNames = wasSharedRecipient ? Object.keys(privateKeys) : [];
|
|
994
752
|
|
|
995
753
|
return (
|
|
996
|
-
<div
|
|
997
|
-
<
|
|
998
|
-
|
|
999
|
-
|
|
754
|
+
<div className="flex flex-1 flex-col overflow-hidden">
|
|
755
|
+
<Toolbar>
|
|
756
|
+
<div>
|
|
757
|
+
<Toolbar.Title>{`${createdName} created`}</Toolbar.Title>
|
|
758
|
+
<Toolbar.Subtitle>Service identity ready</Toolbar.Subtitle>
|
|
759
|
+
</div>
|
|
760
|
+
</Toolbar>
|
|
761
|
+
<div className="flex-1 overflow-auto p-6">
|
|
762
|
+
<div className="mx-auto max-w-[620px]">
|
|
1000
763
|
{hasAgeKeys && (
|
|
1001
|
-
<div
|
|
1002
|
-
|
|
1003
|
-
background: "#1a1200",
|
|
1004
|
-
border: `1px solid ${theme.yellow}55`,
|
|
1005
|
-
borderRadius: 8,
|
|
1006
|
-
padding: "14px 18px",
|
|
1007
|
-
marginBottom: 20,
|
|
1008
|
-
fontFamily: theme.sans,
|
|
1009
|
-
fontSize: 13,
|
|
1010
|
-
color: theme.yellow,
|
|
1011
|
-
display: "flex",
|
|
1012
|
-
gap: 10,
|
|
1013
|
-
alignItems: "flex-start",
|
|
1014
|
-
}}
|
|
1015
|
-
>
|
|
1016
|
-
<span style={{ fontSize: 16, flexShrink: 0 }}>⚠</span>
|
|
764
|
+
<div className="mb-5 flex items-start gap-2.5 rounded-lg border border-warn-500/40 bg-warn-500/[0.06] px-4 py-3.5 font-sans text-[13px] text-warn-500">
|
|
765
|
+
<span className="shrink-0 text-[16px]">⚠</span>
|
|
1017
766
|
<span>
|
|
1018
767
|
{wasSharedRecipient
|
|
1019
768
|
? `Copy this key now — it will not be shown again. Set it as CLEF_AGE_KEY in your CI. It decrypts: ${sharedEnvNames.join(", ")}.`
|
|
@@ -1023,18 +772,7 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
1023
772
|
)}
|
|
1024
773
|
|
|
1025
774
|
{!hasAgeKeys && (
|
|
1026
|
-
<div
|
|
1027
|
-
style={{
|
|
1028
|
-
background: theme.purpleDim,
|
|
1029
|
-
border: `1px solid ${theme.purple}44`,
|
|
1030
|
-
borderRadius: 8,
|
|
1031
|
-
padding: "14px 18px",
|
|
1032
|
-
marginBottom: 20,
|
|
1033
|
-
fontFamily: theme.sans,
|
|
1034
|
-
fontSize: 13,
|
|
1035
|
-
color: theme.purple,
|
|
1036
|
-
}}
|
|
1037
|
-
>
|
|
775
|
+
<div className="mb-5 rounded-lg border border-purple-400/30 bg-purple-400/10 px-4 py-3.5 font-sans text-[13px] text-purple-400">
|
|
1038
776
|
All environments use KMS. No private keys to provision — runtimes authenticate via
|
|
1039
777
|
IAM role.
|
|
1040
778
|
</div>
|
|
@@ -1043,55 +781,20 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
1043
781
|
<Label>Private keys</Label>
|
|
1044
782
|
|
|
1045
783
|
{wasSharedRecipient && sharedKey ? (
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
border: `1px solid ${theme.accent}44`,
|
|
1051
|
-
borderRadius: 8,
|
|
1052
|
-
padding: "14px 18px",
|
|
1053
|
-
marginBottom: 10,
|
|
1054
|
-
}}
|
|
1055
|
-
>
|
|
1056
|
-
<div
|
|
1057
|
-
style={{
|
|
1058
|
-
display: "flex",
|
|
1059
|
-
alignItems: "center",
|
|
1060
|
-
justifyContent: "space-between",
|
|
1061
|
-
marginBottom: 10,
|
|
1062
|
-
}}
|
|
1063
|
-
>
|
|
1064
|
-
<div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
|
|
1065
|
-
<span
|
|
1066
|
-
style={{
|
|
1067
|
-
fontFamily: theme.mono,
|
|
1068
|
-
fontSize: 11,
|
|
1069
|
-
color: theme.accent,
|
|
1070
|
-
fontWeight: 600,
|
|
1071
|
-
}}
|
|
1072
|
-
>
|
|
784
|
+
<div className="mb-2.5 rounded-lg border border-gold-500/30 bg-ink-850 px-4 py-3.5">
|
|
785
|
+
<div className="mb-2.5 flex items-center justify-between">
|
|
786
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
787
|
+
<span className="font-mono text-[11px] font-semibold text-gold-500">
|
|
1073
788
|
CLEF_AGE_KEY
|
|
1074
789
|
</span>
|
|
1075
|
-
<span
|
|
1076
|
-
—
|
|
1077
|
-
</span>
|
|
790
|
+
<span className="font-sans text-[11px] text-ash-dim">—</span>
|
|
1078
791
|
{sharedEnvNames.map((e) => (
|
|
1079
792
|
<EnvBadge key={e} env={e} small />
|
|
1080
793
|
))}
|
|
1081
794
|
</div>
|
|
1082
795
|
<CopyButton text={sharedKey} />
|
|
1083
796
|
</div>
|
|
1084
|
-
<div
|
|
1085
|
-
style={{
|
|
1086
|
-
fontFamily: theme.mono,
|
|
1087
|
-
fontSize: 11,
|
|
1088
|
-
color: theme.textMuted,
|
|
1089
|
-
wordBreak: "break-all",
|
|
1090
|
-
background: theme.bg,
|
|
1091
|
-
borderRadius: 4,
|
|
1092
|
-
padding: "8px 10px",
|
|
1093
|
-
}}
|
|
1094
|
-
>
|
|
797
|
+
<div className="break-all rounded bg-ink-950 px-2.5 py-2 font-mono text-[11px] text-ash">
|
|
1095
798
|
{sharedKey}
|
|
1096
799
|
</div>
|
|
1097
800
|
</div>
|
|
@@ -1099,43 +802,20 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
1099
802
|
Object.entries(privateKeys).map(([envName, key]) => (
|
|
1100
803
|
<div
|
|
1101
804
|
key={envName}
|
|
1102
|
-
|
|
1103
|
-
background: theme.surface,
|
|
1104
|
-
border: `1px solid ${theme.border}`,
|
|
1105
|
-
borderRadius: 8,
|
|
1106
|
-
padding: "14px 18px",
|
|
1107
|
-
marginBottom: 10,
|
|
1108
|
-
}}
|
|
805
|
+
className="mb-2.5 rounded-lg border border-edge bg-ink-850 px-4 py-3.5"
|
|
1109
806
|
>
|
|
1110
|
-
<div
|
|
1111
|
-
style={{
|
|
1112
|
-
display: "flex",
|
|
1113
|
-
alignItems: "center",
|
|
1114
|
-
justifyContent: "space-between",
|
|
1115
|
-
marginBottom: 10,
|
|
1116
|
-
}}
|
|
1117
|
-
>
|
|
807
|
+
<div className="mb-2.5 flex items-center justify-between">
|
|
1118
808
|
<EnvBadge env={envName} />
|
|
1119
809
|
<CopyButton text={key} />
|
|
1120
810
|
</div>
|
|
1121
|
-
<div
|
|
1122
|
-
style={{
|
|
1123
|
-
fontFamily: theme.mono,
|
|
1124
|
-
fontSize: 11,
|
|
1125
|
-
color: theme.textMuted,
|
|
1126
|
-
wordBreak: "break-all",
|
|
1127
|
-
background: theme.bg,
|
|
1128
|
-
borderRadius: 4,
|
|
1129
|
-
padding: "8px 10px",
|
|
1130
|
-
}}
|
|
1131
|
-
>
|
|
811
|
+
<div className="break-all rounded bg-ink-950 px-2.5 py-2 font-mono text-[11px] text-ash">
|
|
1132
812
|
{key}
|
|
1133
813
|
</div>
|
|
1134
814
|
</div>
|
|
1135
815
|
))
|
|
1136
816
|
)}
|
|
1137
817
|
|
|
1138
|
-
<div
|
|
818
|
+
<div className="mt-2 flex justify-end">
|
|
1139
819
|
<Button
|
|
1140
820
|
variant="primary"
|
|
1141
821
|
onClick={() => {
|
|
@@ -1170,158 +850,96 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
1170
850
|
});
|
|
1171
851
|
|
|
1172
852
|
return (
|
|
1173
|
-
<div
|
|
1174
|
-
<
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
border: `1px solid ${theme.borderLight}`,
|
|
1183
|
-
borderRadius: 6,
|
|
1184
|
-
padding: "4px 12px",
|
|
1185
|
-
cursor: "pointer",
|
|
1186
|
-
fontFamily: theme.sans,
|
|
1187
|
-
fontSize: 12,
|
|
1188
|
-
color: theme.textMuted,
|
|
1189
|
-
}}
|
|
1190
|
-
>
|
|
1191
|
-
{"\u2190"} Cancel
|
|
853
|
+
<div className="flex flex-1 flex-col overflow-hidden">
|
|
854
|
+
<Toolbar>
|
|
855
|
+
<div>
|
|
856
|
+
<Toolbar.Title>New service identity</Toolbar.Title>
|
|
857
|
+
<Toolbar.Subtitle>Scope cryptographic access to specific namespaces</Toolbar.Subtitle>
|
|
858
|
+
</div>
|
|
859
|
+
<Toolbar.Actions>
|
|
860
|
+
<button onClick={goList} className={BACK_BUTTON}>
|
|
861
|
+
{"←"} Cancel
|
|
1192
862
|
</button>
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
<div
|
|
1196
|
-
<div
|
|
863
|
+
</Toolbar.Actions>
|
|
864
|
+
</Toolbar>
|
|
865
|
+
<div className="flex-1 overflow-auto p-6">
|
|
866
|
+
<div className="mx-auto max-w-[560px]">
|
|
1197
867
|
{createError && <ErrorBanner>{createError}</ErrorBanner>}
|
|
1198
868
|
|
|
1199
|
-
|
|
1200
|
-
<div style={{ marginBottom: 20 }}>
|
|
869
|
+
<div className="mb-5">
|
|
1201
870
|
<FieldLabel>Name</FieldLabel>
|
|
1202
871
|
<input
|
|
1203
872
|
data-testid="si-name-input"
|
|
1204
873
|
value={name}
|
|
1205
874
|
onChange={(e) => setName(e.target.value)}
|
|
1206
875
|
placeholder="e.g. api-gateway"
|
|
1207
|
-
|
|
876
|
+
className={INPUT_BASE}
|
|
1208
877
|
/>
|
|
1209
878
|
{nameError && (
|
|
1210
|
-
<div
|
|
1211
|
-
style={{
|
|
1212
|
-
fontFamily: theme.sans,
|
|
1213
|
-
fontSize: 12,
|
|
1214
|
-
color: theme.red,
|
|
1215
|
-
marginTop: 6,
|
|
1216
|
-
}}
|
|
1217
|
-
>
|
|
1218
|
-
{nameError}
|
|
1219
|
-
</div>
|
|
879
|
+
<div className="mt-1.5 font-sans text-[12px] text-stop-500">{nameError}</div>
|
|
1220
880
|
)}
|
|
1221
881
|
</div>
|
|
1222
882
|
|
|
1223
|
-
|
|
1224
|
-
<div style={{ marginBottom: 24 }}>
|
|
883
|
+
<div className="mb-6">
|
|
1225
884
|
<FieldLabel>Description (optional)</FieldLabel>
|
|
1226
885
|
<input
|
|
1227
886
|
value={description}
|
|
1228
887
|
onChange={(e) => setDescription(e.target.value)}
|
|
1229
888
|
placeholder="e.g. API gateway service account"
|
|
1230
|
-
|
|
889
|
+
className={INPUT_BASE}
|
|
1231
890
|
/>
|
|
1232
891
|
</div>
|
|
1233
892
|
|
|
1234
|
-
|
|
1235
|
-
<div style={{ marginBottom: 24 }}>
|
|
893
|
+
<div className="mb-6">
|
|
1236
894
|
<FieldLabel>Role</FieldLabel>
|
|
1237
|
-
<div
|
|
1238
|
-
style={{
|
|
1239
|
-
display: "flex",
|
|
1240
|
-
gap: 0,
|
|
1241
|
-
borderRadius: 6,
|
|
1242
|
-
overflow: "hidden",
|
|
1243
|
-
border: `1px solid ${theme.border}`,
|
|
1244
|
-
width: "fit-content",
|
|
1245
|
-
marginBottom: 8,
|
|
1246
|
-
}}
|
|
1247
|
-
>
|
|
895
|
+
<div className="mb-2 flex w-fit overflow-hidden rounded-md border border-edge">
|
|
1248
896
|
{(["ci", "runtime"] as const).map((r) => (
|
|
1249
897
|
<button
|
|
1250
898
|
key={r}
|
|
1251
899
|
data-testid={`role-${r}`}
|
|
1252
900
|
onClick={() => {
|
|
1253
901
|
setRole(r);
|
|
1254
|
-
// Auto-set shared-recipient to the role's natural default
|
|
1255
902
|
const newDefault = r === "ci";
|
|
1256
903
|
setSharedRecipient(newDefault);
|
|
1257
904
|
setSharedRecipientOverridden(false);
|
|
1258
905
|
}}
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
fontFamily: theme.sans,
|
|
1265
|
-
fontSize: 12,
|
|
1266
|
-
fontWeight: role === r ? 600 : 400,
|
|
1267
|
-
color: role === r ? "#fff" : theme.textMuted,
|
|
1268
|
-
transition: "all 0.12s",
|
|
1269
|
-
}}
|
|
906
|
+
className={`cursor-pointer border-none px-4 py-1.5 font-sans text-[12px] transition-colors ${
|
|
907
|
+
role === r
|
|
908
|
+
? "bg-gold-500 font-semibold text-ink-950"
|
|
909
|
+
: "bg-transparent font-normal text-ash"
|
|
910
|
+
}`}
|
|
1270
911
|
>
|
|
1271
912
|
{r === "ci" ? "CI" : "Runtime"}
|
|
1272
913
|
</button>
|
|
1273
914
|
))}
|
|
1274
915
|
</div>
|
|
1275
|
-
<div
|
|
1276
|
-
style={{
|
|
1277
|
-
fontFamily: theme.sans,
|
|
1278
|
-
fontSize: 12,
|
|
1279
|
-
color: theme.textMuted,
|
|
1280
|
-
lineHeight: 1.5,
|
|
1281
|
-
}}
|
|
1282
|
-
>
|
|
916
|
+
<div className="font-sans text-[12px] leading-relaxed text-ash">
|
|
1283
917
|
{role === "ci"
|
|
1284
918
|
? "Decrypts files directly. Keys are registered on encrypted SOPS files. Use for CI pipelines and local tools."
|
|
1285
919
|
: "Decrypts packed artifacts only. Keys are NOT added to encrypted files — smaller blast radius for deployment targets (Lambda, ECS, containers)."}
|
|
1286
920
|
</div>
|
|
1287
921
|
</div>
|
|
1288
922
|
|
|
1289
|
-
|
|
1290
|
-
<div style={{ marginBottom: 24 }}>
|
|
923
|
+
<div className="mb-6">
|
|
1291
924
|
<FieldLabel>Namespaces</FieldLabel>
|
|
1292
|
-
<div
|
|
1293
|
-
style={{
|
|
1294
|
-
fontFamily: theme.sans,
|
|
1295
|
-
fontSize: 12,
|
|
1296
|
-
color: theme.textMuted,
|
|
1297
|
-
marginBottom: 10,
|
|
1298
|
-
}}
|
|
1299
|
-
>
|
|
925
|
+
<div className="mb-2.5 font-sans text-[12px] text-ash">
|
|
1300
926
|
This identity can decrypt secrets only from the selected namespaces.
|
|
1301
927
|
</div>
|
|
1302
928
|
{namespaces.length === 0 && (
|
|
1303
|
-
<div
|
|
929
|
+
<div className="font-sans text-[12px] text-ash-dim">
|
|
1304
930
|
No namespaces defined in manifest.
|
|
1305
931
|
</div>
|
|
1306
932
|
)}
|
|
1307
|
-
<div
|
|
933
|
+
<div className="flex flex-col gap-1.5">
|
|
1308
934
|
{namespaces.map((ns) => {
|
|
1309
935
|
const checked = selectedNamespaces.has(ns.name);
|
|
1310
936
|
return (
|
|
1311
937
|
<label
|
|
1312
938
|
key={ns.name}
|
|
1313
939
|
data-testid={`ns-checkbox-${ns.name}`}
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
gap: 10,
|
|
1318
|
-
padding: "10px 14px",
|
|
1319
|
-
background: checked ? theme.accentDim : theme.surface,
|
|
1320
|
-
border: `1px solid ${checked ? theme.accent + "55" : theme.border}`,
|
|
1321
|
-
borderRadius: 6,
|
|
1322
|
-
cursor: "pointer",
|
|
1323
|
-
transition: "all 0.1s",
|
|
1324
|
-
}}
|
|
940
|
+
className={`flex cursor-pointer items-center gap-2.5 rounded-md border px-3.5 py-2.5 transition-colors ${
|
|
941
|
+
checked ? "border-gold-500/40 bg-gold-500/[0.08]" : "border-edge bg-ink-850"
|
|
942
|
+
}`}
|
|
1325
943
|
>
|
|
1326
944
|
<input
|
|
1327
945
|
type="checkbox"
|
|
@@ -1332,23 +950,15 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
1332
950
|
else next.delete(ns.name);
|
|
1333
951
|
setSelectedNamespaces(next);
|
|
1334
952
|
}}
|
|
1335
|
-
|
|
953
|
+
className="accent-gold-500"
|
|
1336
954
|
/>
|
|
1337
955
|
<span
|
|
1338
|
-
|
|
1339
|
-
fontFamily: theme.mono,
|
|
1340
|
-
fontSize: 12,
|
|
1341
|
-
color: checked ? theme.accent : theme.text,
|
|
1342
|
-
}}
|
|
956
|
+
className={`font-mono text-[12px] ${checked ? "text-gold-500" : "text-bone"}`}
|
|
1343
957
|
>
|
|
1344
958
|
{ns.name}
|
|
1345
959
|
</span>
|
|
1346
960
|
{ns.description && (
|
|
1347
|
-
<span
|
|
1348
|
-
style={{ fontFamily: theme.sans, fontSize: 11, color: theme.textMuted }}
|
|
1349
|
-
>
|
|
1350
|
-
— {ns.description}
|
|
1351
|
-
</span>
|
|
961
|
+
<span className="font-sans text-[11px] text-ash">— {ns.description}</span>
|
|
1352
962
|
)}
|
|
1353
963
|
</label>
|
|
1354
964
|
);
|
|
@@ -1356,98 +966,44 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
1356
966
|
</div>
|
|
1357
967
|
</div>
|
|
1358
968
|
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
<div
|
|
1362
|
-
style={{
|
|
1363
|
-
display: "flex",
|
|
1364
|
-
alignItems: "center",
|
|
1365
|
-
justifyContent: "space-between",
|
|
1366
|
-
marginBottom: 6,
|
|
1367
|
-
}}
|
|
1368
|
-
>
|
|
969
|
+
<div className="mb-7">
|
|
970
|
+
<div className="mb-1.5 flex items-center justify-between">
|
|
1369
971
|
<FieldLabel>Environment backends</FieldLabel>
|
|
1370
|
-
{/* Shared recipient toggle */}
|
|
1371
972
|
<label
|
|
1372
973
|
data-testid="shared-recipient-toggle"
|
|
1373
|
-
|
|
1374
|
-
display: "flex",
|
|
1375
|
-
alignItems: "center",
|
|
1376
|
-
gap: 7,
|
|
1377
|
-
cursor: "pointer",
|
|
1378
|
-
fontFamily: theme.sans,
|
|
1379
|
-
fontSize: 11,
|
|
1380
|
-
color: sharedRecipient ? theme.accent : theme.textMuted,
|
|
1381
|
-
userSelect: "none",
|
|
1382
|
-
}}
|
|
974
|
+
className={`flex cursor-pointer select-none items-center gap-1.5 font-sans text-[11px] ${sharedRecipient ? "text-gold-500" : "text-ash"}`}
|
|
1383
975
|
>
|
|
1384
976
|
<div
|
|
1385
|
-
|
|
1386
|
-
width: 28,
|
|
1387
|
-
height: 16,
|
|
1388
|
-
borderRadius: 8,
|
|
1389
|
-
background: sharedRecipient ? theme.accent : theme.border,
|
|
1390
|
-
position: "relative",
|
|
1391
|
-
transition: "background 0.15s",
|
|
1392
|
-
flexShrink: 0,
|
|
1393
|
-
}}
|
|
977
|
+
className={`relative h-4 w-7 shrink-0 rounded-full transition-colors ${sharedRecipient ? "bg-gold-500" : "bg-edge"}`}
|
|
1394
978
|
>
|
|
1395
979
|
<div
|
|
1396
|
-
|
|
1397
|
-
position: "absolute",
|
|
1398
|
-
top: 2,
|
|
1399
|
-
left: sharedRecipient ? 14 : 2,
|
|
1400
|
-
width: 12,
|
|
1401
|
-
height: 12,
|
|
1402
|
-
borderRadius: "50%",
|
|
1403
|
-
background: "#fff",
|
|
1404
|
-
transition: "left 0.15s",
|
|
1405
|
-
}}
|
|
980
|
+
className={`absolute top-0.5 h-3 w-3 rounded-full bg-white transition-all ${sharedRecipient ? "left-3.5" : "left-0.5"}`}
|
|
1406
981
|
/>
|
|
1407
982
|
<input
|
|
1408
983
|
type="checkbox"
|
|
1409
984
|
checked={sharedRecipient}
|
|
1410
985
|
onChange={(e) => {
|
|
1411
986
|
setSharedRecipient(e.target.checked);
|
|
1412
|
-
// Track whether user manually overrode the role's default
|
|
1413
987
|
const roleDefault = role === "ci";
|
|
1414
988
|
setSharedRecipientOverridden(e.target.checked !== roleDefault);
|
|
1415
989
|
}}
|
|
1416
|
-
|
|
990
|
+
className="absolute h-0 w-0 opacity-0"
|
|
1417
991
|
/>
|
|
1418
992
|
</div>
|
|
1419
993
|
Shared age key
|
|
1420
994
|
</label>
|
|
1421
995
|
</div>
|
|
1422
996
|
|
|
1423
|
-
<div
|
|
1424
|
-
style={{
|
|
1425
|
-
fontFamily: theme.sans,
|
|
1426
|
-
fontSize: 12,
|
|
1427
|
-
color: theme.textMuted,
|
|
1428
|
-
marginBottom: 10,
|
|
1429
|
-
}}
|
|
1430
|
-
>
|
|
997
|
+
<div className="mb-2.5 font-sans text-[12px] text-ash">
|
|
1431
998
|
{sharedRecipient
|
|
1432
999
|
? "One age key pair for all environments — one CI secret, easier to provision."
|
|
1433
1000
|
: "Age generates a key pair per environment. KMS uses your cloud provider — no key material is provisioned."}
|
|
1434
1001
|
</div>
|
|
1435
1002
|
|
|
1436
|
-
{/* Warning when shared-recipient is overridden from role default */}
|
|
1437
1003
|
{sharedRecipientOverridden && (
|
|
1438
1004
|
<div
|
|
1439
1005
|
data-testid="shared-recipient-warning"
|
|
1440
|
-
|
|
1441
|
-
background: "#1a1200",
|
|
1442
|
-
border: `1px solid ${theme.yellow}55`,
|
|
1443
|
-
borderRadius: 6,
|
|
1444
|
-
padding: "10px 14px",
|
|
1445
|
-
marginBottom: 10,
|
|
1446
|
-
fontFamily: theme.sans,
|
|
1447
|
-
fontSize: 12,
|
|
1448
|
-
color: theme.yellow,
|
|
1449
|
-
lineHeight: 1.5,
|
|
1450
|
-
}}
|
|
1006
|
+
className="mb-2.5 rounded-md border border-warn-500/40 bg-warn-500/[0.06] px-3.5 py-2.5 font-sans text-[12px] leading-relaxed text-warn-500"
|
|
1451
1007
|
>
|
|
1452
1008
|
{role === "ci" && !sharedRecipient
|
|
1453
1009
|
? "Most CI pipelines use a single key. Per-environment keys are useful when your CI environments have separate secret access controls (e.g. GitHub environment protection rules)."
|
|
@@ -1455,34 +1011,15 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
1455
1011
|
</div>
|
|
1456
1012
|
)}
|
|
1457
1013
|
|
|
1458
|
-
<div
|
|
1014
|
+
<div className="flex flex-col gap-2">
|
|
1459
1015
|
{sharedRecipient ? (
|
|
1460
|
-
<div
|
|
1461
|
-
|
|
1462
|
-
background: theme.accentDim,
|
|
1463
|
-
border: `1px solid ${theme.accent}44`,
|
|
1464
|
-
borderRadius: 8,
|
|
1465
|
-
padding: "14px 16px",
|
|
1466
|
-
display: "flex",
|
|
1467
|
-
alignItems: "center",
|
|
1468
|
-
gap: 12,
|
|
1469
|
-
}}
|
|
1470
|
-
>
|
|
1471
|
-
<div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
|
|
1016
|
+
<div className="flex items-center gap-3 rounded-lg border border-gold-500/30 bg-gold-500/[0.08] px-4 py-3.5">
|
|
1017
|
+
<div className="flex flex-wrap gap-1.5">
|
|
1472
1018
|
{environments.map((env) => (
|
|
1473
1019
|
<EnvBadge key={env.name} env={env.name} small />
|
|
1474
1020
|
))}
|
|
1475
1021
|
</div>
|
|
1476
|
-
<span
|
|
1477
|
-
style={{
|
|
1478
|
-
fontFamily: theme.mono,
|
|
1479
|
-
fontSize: 11,
|
|
1480
|
-
color: theme.accent,
|
|
1481
|
-
marginLeft: "auto",
|
|
1482
|
-
}}
|
|
1483
|
-
>
|
|
1484
|
-
age (shared)
|
|
1485
|
-
</span>
|
|
1022
|
+
<span className="ml-auto font-mono text-[11px] text-gold-500">age (shared)</span>
|
|
1486
1023
|
</div>
|
|
1487
1024
|
) : (
|
|
1488
1025
|
environments.map((env) => {
|
|
@@ -1490,68 +1027,45 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
1490
1027
|
return (
|
|
1491
1028
|
<div
|
|
1492
1029
|
key={env.name}
|
|
1493
|
-
|
|
1494
|
-
background: theme.surface,
|
|
1495
|
-
border: `1px solid ${theme.border}`,
|
|
1496
|
-
borderRadius: 8,
|
|
1497
|
-
padding: "14px 16px",
|
|
1498
|
-
}}
|
|
1030
|
+
className="rounded-lg border border-edge bg-ink-850 px-4 py-3.5"
|
|
1499
1031
|
>
|
|
1500
1032
|
<div
|
|
1501
|
-
|
|
1502
|
-
display: "flex",
|
|
1503
|
-
alignItems: "center",
|
|
1504
|
-
justifyContent: "space-between",
|
|
1505
|
-
marginBottom: cfg.type === "kms" ? 12 : 0,
|
|
1506
|
-
}}
|
|
1033
|
+
className={`flex items-center justify-between ${cfg.type === "kms" ? "mb-3" : ""}`}
|
|
1507
1034
|
>
|
|
1508
|
-
<div
|
|
1035
|
+
<div className="flex items-center gap-2">
|
|
1509
1036
|
<EnvBadge env={env.name} />
|
|
1510
1037
|
{env.protected && (
|
|
1511
|
-
<span
|
|
1038
|
+
<span className="text-[11px] text-stop-500">{"🔒"}</span>
|
|
1512
1039
|
)}
|
|
1513
1040
|
</div>
|
|
1514
|
-
<div
|
|
1515
|
-
{(["age", "kms"] as const).map((t) =>
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
border
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
}`,
|
|
1538
|
-
borderRadius: 4,
|
|
1539
|
-
padding: "3px 10px",
|
|
1540
|
-
cursor: "pointer",
|
|
1541
|
-
fontFamily: theme.mono,
|
|
1542
|
-
fontSize: 11,
|
|
1543
|
-
color: cfg.type === t ? "#fff" : theme.textMuted,
|
|
1544
|
-
transition: "all 0.1s",
|
|
1545
|
-
}}
|
|
1546
|
-
>
|
|
1547
|
-
{t.toUpperCase()}
|
|
1548
|
-
</button>
|
|
1549
|
-
))}
|
|
1041
|
+
<div className="flex gap-1">
|
|
1042
|
+
{(["age", "kms"] as const).map((t) => {
|
|
1043
|
+
const isSelected = cfg.type === t;
|
|
1044
|
+
const buttonClass = isSelected
|
|
1045
|
+
? t === "kms"
|
|
1046
|
+
? "bg-purple-400 border-purple-400 text-ghost"
|
|
1047
|
+
: "bg-gold-500 border-gold-500 text-ink-950"
|
|
1048
|
+
: "bg-transparent border-edge text-ash";
|
|
1049
|
+
return (
|
|
1050
|
+
<button
|
|
1051
|
+
key={t}
|
|
1052
|
+
onClick={() =>
|
|
1053
|
+
setEnvBackends((prev) => ({
|
|
1054
|
+
...prev,
|
|
1055
|
+
[env.name]: { ...cfg, type: t },
|
|
1056
|
+
}))
|
|
1057
|
+
}
|
|
1058
|
+
className={`cursor-pointer rounded border px-2.5 py-0.5 font-mono text-[11px] transition-colors ${buttonClass}`}
|
|
1059
|
+
>
|
|
1060
|
+
{t.toUpperCase()}
|
|
1061
|
+
</button>
|
|
1062
|
+
);
|
|
1063
|
+
})}
|
|
1550
1064
|
</div>
|
|
1551
1065
|
</div>
|
|
1552
1066
|
|
|
1553
1067
|
{cfg.type === "kms" && (
|
|
1554
|
-
<div
|
|
1068
|
+
<div className="flex gap-2">
|
|
1555
1069
|
<select
|
|
1556
1070
|
value={cfg.provider}
|
|
1557
1071
|
onChange={(e) =>
|
|
@@ -1560,12 +1074,7 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
1560
1074
|
[env.name]: { ...cfg, provider: e.target.value },
|
|
1561
1075
|
}))
|
|
1562
1076
|
}
|
|
1563
|
-
|
|
1564
|
-
...inputStyle,
|
|
1565
|
-
width: 90,
|
|
1566
|
-
flexShrink: 0,
|
|
1567
|
-
padding: "7px 8px",
|
|
1568
|
-
}}
|
|
1077
|
+
className={`${SMALL_INPUT_BASE} w-[90px] shrink-0`}
|
|
1569
1078
|
>
|
|
1570
1079
|
<option value="aws">AWS</option>
|
|
1571
1080
|
<option value="gcp">GCP</option>
|
|
@@ -1580,7 +1089,7 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
1580
1089
|
}))
|
|
1581
1090
|
}
|
|
1582
1091
|
placeholder="arn:aws:kms:… or key resource ID"
|
|
1583
|
-
|
|
1092
|
+
className={`${SMALL_INPUT_BASE} flex-1`}
|
|
1584
1093
|
/>
|
|
1585
1094
|
</div>
|
|
1586
1095
|
)}
|
|
@@ -1591,7 +1100,7 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
1591
1100
|
</div>
|
|
1592
1101
|
</div>
|
|
1593
1102
|
|
|
1594
|
-
<div
|
|
1103
|
+
<div className="flex justify-end gap-2">
|
|
1595
1104
|
<Button variant="ghost" onClick={goList} disabled={creating}>
|
|
1596
1105
|
Cancel
|
|
1597
1106
|
</Button>
|
|
@@ -1614,66 +1123,20 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
1614
1123
|
|
|
1615
1124
|
function Label({ children }: { children: React.ReactNode }) {
|
|
1616
1125
|
return (
|
|
1617
|
-
<div
|
|
1618
|
-
style={{
|
|
1619
|
-
fontFamily: theme.sans,
|
|
1620
|
-
fontSize: 12,
|
|
1621
|
-
fontWeight: 600,
|
|
1622
|
-
color: theme.textMuted,
|
|
1623
|
-
marginBottom: 6,
|
|
1624
|
-
letterSpacing: "0.05em",
|
|
1625
|
-
textTransform: "uppercase",
|
|
1626
|
-
}}
|
|
1627
|
-
>
|
|
1126
|
+
<div className="mb-1.5 font-sans text-[12px] font-semibold uppercase tracking-[0.05em] text-ash">
|
|
1628
1127
|
{children}
|
|
1629
1128
|
</div>
|
|
1630
1129
|
);
|
|
1631
1130
|
}
|
|
1632
1131
|
|
|
1633
1132
|
function FieldLabel({ children }: { children: React.ReactNode }) {
|
|
1634
|
-
return
|
|
1635
|
-
<div
|
|
1636
|
-
style={{
|
|
1637
|
-
fontFamily: theme.sans,
|
|
1638
|
-
fontSize: 12,
|
|
1639
|
-
fontWeight: 600,
|
|
1640
|
-
color: theme.textMuted,
|
|
1641
|
-
marginBottom: 6,
|
|
1642
|
-
}}
|
|
1643
|
-
>
|
|
1644
|
-
{children}
|
|
1645
|
-
</div>
|
|
1646
|
-
);
|
|
1133
|
+
return <div className="mb-1.5 font-sans text-[12px] font-semibold text-ash">{children}</div>;
|
|
1647
1134
|
}
|
|
1648
1135
|
|
|
1649
1136
|
function ErrorBanner({ children }: { children: React.ReactNode }) {
|
|
1650
1137
|
return (
|
|
1651
|
-
<div
|
|
1652
|
-
style={{
|
|
1653
|
-
background: theme.redDim,
|
|
1654
|
-
border: `1px solid ${theme.red}44`,
|
|
1655
|
-
borderRadius: 8,
|
|
1656
|
-
padding: "12px 16px",
|
|
1657
|
-
marginBottom: 16,
|
|
1658
|
-
fontFamily: theme.sans,
|
|
1659
|
-
fontSize: 13,
|
|
1660
|
-
color: theme.red,
|
|
1661
|
-
}}
|
|
1662
|
-
>
|
|
1138
|
+
<div className="mb-4 rounded-lg border border-stop-500/30 bg-stop-500/10 px-4 py-3 font-sans text-[13px] text-stop-500">
|
|
1663
1139
|
{children}
|
|
1664
1140
|
</div>
|
|
1665
1141
|
);
|
|
1666
1142
|
}
|
|
1667
|
-
|
|
1668
|
-
const inputStyle: React.CSSProperties = {
|
|
1669
|
-
width: "100%",
|
|
1670
|
-
background: theme.surface,
|
|
1671
|
-
border: `1px solid ${theme.border}`,
|
|
1672
|
-
borderRadius: 6,
|
|
1673
|
-
padding: "8px 12px",
|
|
1674
|
-
fontFamily: theme.mono,
|
|
1675
|
-
fontSize: 12,
|
|
1676
|
-
color: theme.text,
|
|
1677
|
-
outline: "none",
|
|
1678
|
-
boxSizing: "border-box",
|
|
1679
|
-
};
|