@clef-sh/ui 0.1.15 → 0.1.16
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-CogUSGa_.js +26 -0
- package/dist/client/index.html +1 -1
- package/dist/client-lib/components/MatrixGrid.d.ts +1 -1
- package/dist/client-lib/components/MatrixGrid.d.ts.map +1 -1
- package/dist/server/api.d.ts.map +1 -1
- package/dist/server/api.js +13 -3
- package/dist/server/api.js.map +1 -1
- package/package.json +1 -1
- package/src/client/App.tsx +5 -1
- package/src/client/components/MatrixGrid.tsx +6 -1
- package/src/client/screens/MatrixView.tsx +4 -1
- package/src/client/screens/NamespaceEditor.tsx +6 -3
- package/src/client/screens/ServiceIdentitiesScreen.tsx +381 -100
- package/dist/client/assets/index-C4tsbWst.js +0 -26
|
@@ -19,6 +19,7 @@ interface IdentityInfo {
|
|
|
19
19
|
description: string;
|
|
20
20
|
namespaces: string[];
|
|
21
21
|
environments: Record<string, EnvInfo>;
|
|
22
|
+
packOnly?: boolean;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
interface EnvBackendConfig {
|
|
@@ -49,12 +50,16 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
49
50
|
const [description, setDescription] = useState("");
|
|
50
51
|
const [selectedNamespaces, setSelectedNamespaces] = useState<Set<string>>(new Set());
|
|
51
52
|
const [envBackends, setEnvBackends] = useState<Record<string, EnvBackendConfig>>({});
|
|
53
|
+
const [role, setRole] = useState<"ci" | "runtime">("ci");
|
|
54
|
+
const [sharedRecipient, setSharedRecipient] = useState(true); // CI default
|
|
55
|
+
const [sharedRecipientOverridden, setSharedRecipientOverridden] = useState(false);
|
|
52
56
|
const [creating, setCreating] = useState(false);
|
|
53
57
|
const [createError, setCreateError] = useState("");
|
|
54
58
|
|
|
55
59
|
// Post-create / post-rotate keys
|
|
56
60
|
const [privateKeys, setPrivateKeys] = useState<Record<string, string>>({});
|
|
57
61
|
const [createdName, setCreatedName] = useState("");
|
|
62
|
+
const [wasSharedRecipient, setWasSharedRecipient] = useState(false);
|
|
58
63
|
|
|
59
64
|
// Update form state
|
|
60
65
|
const [updateEnvBackends, setUpdateEnvBackends] = useState<Record<string, UpdateEnvState>>({});
|
|
@@ -96,6 +101,9 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
96
101
|
defaults[env.name] = { type: "age", provider: "aws", keyId: "" };
|
|
97
102
|
}
|
|
98
103
|
setEnvBackends(defaults);
|
|
104
|
+
setRole("ci");
|
|
105
|
+
setSharedRecipient(true); // CI default
|
|
106
|
+
setSharedRecipientOverridden(false);
|
|
99
107
|
setCreateError("");
|
|
100
108
|
setView("create");
|
|
101
109
|
}, [manifest]);
|
|
@@ -146,7 +154,12 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
146
154
|
description: description.trim(),
|
|
147
155
|
namespaces: Array.from(selectedNamespaces),
|
|
148
156
|
};
|
|
149
|
-
if (
|
|
157
|
+
if (role === "runtime") {
|
|
158
|
+
body.packOnly = true;
|
|
159
|
+
}
|
|
160
|
+
if (sharedRecipient) {
|
|
161
|
+
body.sharedRecipient = true;
|
|
162
|
+
} else if (Object.keys(kmsEnvConfigs).length > 0) {
|
|
150
163
|
body.kmsEnvConfigs = kmsEnvConfigs;
|
|
151
164
|
}
|
|
152
165
|
const res = await apiFetch("/api/service-identities", {
|
|
@@ -161,6 +174,7 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
161
174
|
}
|
|
162
175
|
setCreatedName(data.identity.name);
|
|
163
176
|
setPrivateKeys(data.privateKeys ?? {});
|
|
177
|
+
setWasSharedRecipient(data.sharedRecipient === true);
|
|
164
178
|
setView("keys");
|
|
165
179
|
} catch {
|
|
166
180
|
setCreateError("Network error. Check that the UI server is running.");
|
|
@@ -284,7 +298,7 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
284
298
|
<div style={{ fontSize: 28, marginBottom: 12, opacity: 0.4 }}>{"\uD83D\uDD11"}</div>
|
|
285
299
|
No service identities configured.
|
|
286
300
|
{manifest && (
|
|
287
|
-
<div style={{ marginTop: 16 }}>
|
|
301
|
+
<div style={{ marginTop: 16, display: "flex", justifyContent: "center" }}>
|
|
288
302
|
<Button variant="primary" onClick={openCreate}>
|
|
289
303
|
Create the first one
|
|
290
304
|
</Button>
|
|
@@ -337,6 +351,22 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
337
351
|
>
|
|
338
352
|
{si.name}
|
|
339
353
|
</span>
|
|
354
|
+
{si.packOnly && (
|
|
355
|
+
<span
|
|
356
|
+
data-testid={`si-runtime-badge-${si.name}`}
|
|
357
|
+
style={{
|
|
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
|
+
}}
|
|
366
|
+
>
|
|
367
|
+
runtime
|
|
368
|
+
</span>
|
|
369
|
+
)}
|
|
340
370
|
</div>
|
|
341
371
|
<div
|
|
342
372
|
style={{
|
|
@@ -441,6 +471,26 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
441
471
|
</div>
|
|
442
472
|
</div>
|
|
443
473
|
|
|
474
|
+
{selectedIdentity.packOnly && (
|
|
475
|
+
<div
|
|
476
|
+
data-testid="runtime-info-banner"
|
|
477
|
+
style={{
|
|
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
|
+
}}
|
|
488
|
+
>
|
|
489
|
+
Runtime identity — keys are not registered on encrypted files. This identity can
|
|
490
|
+
only decrypt packed artifacts.
|
|
491
|
+
</div>
|
|
492
|
+
)}
|
|
493
|
+
|
|
444
494
|
<Label>Environment keys</Label>
|
|
445
495
|
|
|
446
496
|
{manifest?.environments.map((env) => {
|
|
@@ -938,6 +988,10 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
938
988
|
// ── Keys result view (post-creation) ─────────────────────────────────────────
|
|
939
989
|
if (view === "keys") {
|
|
940
990
|
const hasAgeKeys = Object.keys(privateKeys).length > 0;
|
|
991
|
+
// For shared mode, all entries hold the same key — grab it once
|
|
992
|
+
const sharedKey = wasSharedRecipient ? Object.values(privateKeys)[0] : undefined;
|
|
993
|
+
const sharedEnvNames = wasSharedRecipient ? Object.keys(privateKeys) : [];
|
|
994
|
+
|
|
941
995
|
return (
|
|
942
996
|
<div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
|
|
943
997
|
<TopBar title={`${createdName} created`} subtitle="Service identity ready" />
|
|
@@ -961,8 +1015,9 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
961
1015
|
>
|
|
962
1016
|
<span style={{ fontSize: 16, flexShrink: 0 }}>⚠</span>
|
|
963
1017
|
<span>
|
|
964
|
-
|
|
965
|
-
|
|
1018
|
+
{wasSharedRecipient
|
|
1019
|
+
? `Copy this key now — it will not be shown again. Set it as CLEF_AGE_KEY in your CI. It decrypts: ${sharedEnvNames.join(", ")}.`
|
|
1020
|
+
: "Copy these private keys now — they will not be shown again. Store each key securely and provision it to the relevant runtime."}
|
|
966
1021
|
</span>
|
|
967
1022
|
</div>
|
|
968
1023
|
)}
|
|
@@ -986,12 +1041,13 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
986
1041
|
)}
|
|
987
1042
|
|
|
988
1043
|
<Label>Private keys</Label>
|
|
989
|
-
|
|
1044
|
+
|
|
1045
|
+
{wasSharedRecipient && sharedKey ? (
|
|
1046
|
+
// Shared mode: one block, all env badges, labeled CLEF_AGE_KEY
|
|
990
1047
|
<div
|
|
991
|
-
key={envName}
|
|
992
1048
|
style={{
|
|
993
1049
|
background: theme.surface,
|
|
994
|
-
border: `1px solid ${theme.
|
|
1050
|
+
border: `1px solid ${theme.accent}44`,
|
|
995
1051
|
borderRadius: 8,
|
|
996
1052
|
padding: "14px 18px",
|
|
997
1053
|
marginBottom: 10,
|
|
@@ -1005,8 +1061,25 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
1005
1061
|
marginBottom: 10,
|
|
1006
1062
|
}}
|
|
1007
1063
|
>
|
|
1008
|
-
<
|
|
1009
|
-
|
|
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
|
+
>
|
|
1073
|
+
CLEF_AGE_KEY
|
|
1074
|
+
</span>
|
|
1075
|
+
<span style={{ fontFamily: theme.sans, fontSize: 11, color: theme.textDim }}>
|
|
1076
|
+
—
|
|
1077
|
+
</span>
|
|
1078
|
+
{sharedEnvNames.map((e) => (
|
|
1079
|
+
<EnvBadge key={e} env={e} small />
|
|
1080
|
+
))}
|
|
1081
|
+
</div>
|
|
1082
|
+
<CopyButton text={sharedKey} />
|
|
1010
1083
|
</div>
|
|
1011
1084
|
<div
|
|
1012
1085
|
style={{
|
|
@@ -1019,10 +1092,48 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
1019
1092
|
padding: "8px 10px",
|
|
1020
1093
|
}}
|
|
1021
1094
|
>
|
|
1022
|
-
{
|
|
1095
|
+
{sharedKey}
|
|
1023
1096
|
</div>
|
|
1024
1097
|
</div>
|
|
1025
|
-
)
|
|
1098
|
+
) : (
|
|
1099
|
+
Object.entries(privateKeys).map(([envName, key]) => (
|
|
1100
|
+
<div
|
|
1101
|
+
key={envName}
|
|
1102
|
+
style={{
|
|
1103
|
+
background: theme.surface,
|
|
1104
|
+
border: `1px solid ${theme.border}`,
|
|
1105
|
+
borderRadius: 8,
|
|
1106
|
+
padding: "14px 18px",
|
|
1107
|
+
marginBottom: 10,
|
|
1108
|
+
}}
|
|
1109
|
+
>
|
|
1110
|
+
<div
|
|
1111
|
+
style={{
|
|
1112
|
+
display: "flex",
|
|
1113
|
+
alignItems: "center",
|
|
1114
|
+
justifyContent: "space-between",
|
|
1115
|
+
marginBottom: 10,
|
|
1116
|
+
}}
|
|
1117
|
+
>
|
|
1118
|
+
<EnvBadge env={envName} />
|
|
1119
|
+
<CopyButton text={key} />
|
|
1120
|
+
</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
|
+
>
|
|
1132
|
+
{key}
|
|
1133
|
+
</div>
|
|
1134
|
+
</div>
|
|
1135
|
+
))
|
|
1136
|
+
)}
|
|
1026
1137
|
|
|
1027
1138
|
<div style={{ marginTop: 8, display: "flex", justifyContent: "flex-end" }}>
|
|
1028
1139
|
<Button
|
|
@@ -1120,6 +1231,61 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
1120
1231
|
/>
|
|
1121
1232
|
</div>
|
|
1122
1233
|
|
|
1234
|
+
{/* Role */}
|
|
1235
|
+
<div style={{ marginBottom: 24 }}>
|
|
1236
|
+
<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
|
+
>
|
|
1248
|
+
{(["ci", "runtime"] as const).map((r) => (
|
|
1249
|
+
<button
|
|
1250
|
+
key={r}
|
|
1251
|
+
data-testid={`role-${r}`}
|
|
1252
|
+
onClick={() => {
|
|
1253
|
+
setRole(r);
|
|
1254
|
+
// Auto-set shared-recipient to the role's natural default
|
|
1255
|
+
const newDefault = r === "ci";
|
|
1256
|
+
setSharedRecipient(newDefault);
|
|
1257
|
+
setSharedRecipientOverridden(false);
|
|
1258
|
+
}}
|
|
1259
|
+
style={{
|
|
1260
|
+
background: role === r ? theme.accent : "transparent",
|
|
1261
|
+
border: "none",
|
|
1262
|
+
padding: "7px 18px",
|
|
1263
|
+
cursor: "pointer",
|
|
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
|
+
}}
|
|
1270
|
+
>
|
|
1271
|
+
{r === "ci" ? "CI" : "Runtime"}
|
|
1272
|
+
</button>
|
|
1273
|
+
))}
|
|
1274
|
+
</div>
|
|
1275
|
+
<div
|
|
1276
|
+
style={{
|
|
1277
|
+
fontFamily: theme.sans,
|
|
1278
|
+
fontSize: 12,
|
|
1279
|
+
color: theme.textMuted,
|
|
1280
|
+
lineHeight: 1.5,
|
|
1281
|
+
}}
|
|
1282
|
+
>
|
|
1283
|
+
{role === "ci"
|
|
1284
|
+
? "Decrypts files directly. Keys are registered on encrypted SOPS files. Use for CI pipelines and local tools."
|
|
1285
|
+
: "Decrypts packed artifacts only. Keys are NOT added to encrypted files — smaller blast radius for deployment targets (Lambda, ECS, containers)."}
|
|
1286
|
+
</div>
|
|
1287
|
+
</div>
|
|
1288
|
+
|
|
1123
1289
|
{/* Namespaces */}
|
|
1124
1290
|
<div style={{ marginBottom: 24 }}>
|
|
1125
1291
|
<FieldLabel>Namespaces</FieldLabel>
|
|
@@ -1192,7 +1358,68 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
1192
1358
|
|
|
1193
1359
|
{/* Per-environment backend */}
|
|
1194
1360
|
<div style={{ marginBottom: 28 }}>
|
|
1195
|
-
<
|
|
1361
|
+
<div
|
|
1362
|
+
style={{
|
|
1363
|
+
display: "flex",
|
|
1364
|
+
alignItems: "center",
|
|
1365
|
+
justifyContent: "space-between",
|
|
1366
|
+
marginBottom: 6,
|
|
1367
|
+
}}
|
|
1368
|
+
>
|
|
1369
|
+
<FieldLabel>Environment backends</FieldLabel>
|
|
1370
|
+
{/* Shared recipient toggle */}
|
|
1371
|
+
<label
|
|
1372
|
+
data-testid="shared-recipient-toggle"
|
|
1373
|
+
style={{
|
|
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
|
+
}}
|
|
1383
|
+
>
|
|
1384
|
+
<div
|
|
1385
|
+
style={{
|
|
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
|
+
}}
|
|
1394
|
+
>
|
|
1395
|
+
<div
|
|
1396
|
+
style={{
|
|
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
|
+
}}
|
|
1406
|
+
/>
|
|
1407
|
+
<input
|
|
1408
|
+
type="checkbox"
|
|
1409
|
+
checked={sharedRecipient}
|
|
1410
|
+
onChange={(e) => {
|
|
1411
|
+
setSharedRecipient(e.target.checked);
|
|
1412
|
+
// Track whether user manually overrode the role's default
|
|
1413
|
+
const roleDefault = role === "ci";
|
|
1414
|
+
setSharedRecipientOverridden(e.target.checked !== roleDefault);
|
|
1415
|
+
}}
|
|
1416
|
+
style={{ position: "absolute", opacity: 0, width: 0, height: 0 }}
|
|
1417
|
+
/>
|
|
1418
|
+
</div>
|
|
1419
|
+
Shared age key
|
|
1420
|
+
</label>
|
|
1421
|
+
</div>
|
|
1422
|
+
|
|
1196
1423
|
<div
|
|
1197
1424
|
style={{
|
|
1198
1425
|
fontFamily: theme.sans,
|
|
@@ -1201,112 +1428,166 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
|
|
|
1201
1428
|
marginBottom: 10,
|
|
1202
1429
|
}}
|
|
1203
1430
|
>
|
|
1204
|
-
|
|
1205
|
-
|
|
1431
|
+
{sharedRecipient
|
|
1432
|
+
? "One age key pair for all environments — one CI secret, easier to provision."
|
|
1433
|
+
: "Age generates a key pair per environment. KMS uses your cloud provider — no key material is provisioned."}
|
|
1206
1434
|
</div>
|
|
1435
|
+
|
|
1436
|
+
{/* Warning when shared-recipient is overridden from role default */}
|
|
1437
|
+
{sharedRecipientOverridden && (
|
|
1438
|
+
<div
|
|
1439
|
+
data-testid="shared-recipient-warning"
|
|
1440
|
+
style={{
|
|
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
|
+
}}
|
|
1451
|
+
>
|
|
1452
|
+
{role === "ci" && !sharedRecipient
|
|
1453
|
+
? "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)."
|
|
1454
|
+
: "Runtime workloads typically use per-environment keys for isolation. A shared key means a compromised key in any environment decrypts artifacts for all environments."}
|
|
1455
|
+
</div>
|
|
1456
|
+
)}
|
|
1457
|
+
|
|
1207
1458
|
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
|
1208
|
-
{
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1459
|
+
{sharedRecipient ? (
|
|
1460
|
+
<div
|
|
1461
|
+
style={{
|
|
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" }}>
|
|
1472
|
+
{environments.map((env) => (
|
|
1473
|
+
<EnvBadge key={env.name} env={env.name} small />
|
|
1474
|
+
))}
|
|
1475
|
+
</div>
|
|
1476
|
+
<span
|
|
1213
1477
|
style={{
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1478
|
+
fontFamily: theme.mono,
|
|
1479
|
+
fontSize: 11,
|
|
1480
|
+
color: theme.accent,
|
|
1481
|
+
marginLeft: "auto",
|
|
1218
1482
|
}}
|
|
1219
1483
|
>
|
|
1484
|
+
age (shared)
|
|
1485
|
+
</span>
|
|
1486
|
+
</div>
|
|
1487
|
+
) : (
|
|
1488
|
+
environments.map((env) => {
|
|
1489
|
+
const cfg = envBackends[env.name] ?? { type: "age", provider: "aws", keyId: "" };
|
|
1490
|
+
return (
|
|
1220
1491
|
<div
|
|
1492
|
+
key={env.name}
|
|
1221
1493
|
style={{
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1494
|
+
background: theme.surface,
|
|
1495
|
+
border: `1px solid ${theme.border}`,
|
|
1496
|
+
borderRadius: 8,
|
|
1497
|
+
padding: "14px 16px",
|
|
1226
1498
|
}}
|
|
1227
1499
|
>
|
|
1228
|
-
<div
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1500
|
+
<div
|
|
1501
|
+
style={{
|
|
1502
|
+
display: "flex",
|
|
1503
|
+
alignItems: "center",
|
|
1504
|
+
justifyContent: "space-between",
|
|
1505
|
+
marginBottom: cfg.type === "kms" ? 12 : 0,
|
|
1506
|
+
}}
|
|
1507
|
+
>
|
|
1508
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
1509
|
+
<EnvBadge env={env.name} />
|
|
1510
|
+
{env.protected && (
|
|
1511
|
+
<span style={{ fontSize: 11, color: theme.red }}>{"\uD83D\uDD12"}</span>
|
|
1512
|
+
)}
|
|
1513
|
+
</div>
|
|
1514
|
+
<div style={{ display: "flex", gap: 4 }}>
|
|
1515
|
+
{(["age", "kms"] as const).map((t) => (
|
|
1516
|
+
<button
|
|
1517
|
+
key={t}
|
|
1518
|
+
onClick={() =>
|
|
1519
|
+
setEnvBackends((prev) => ({
|
|
1520
|
+
...prev,
|
|
1521
|
+
[env.name]: { ...cfg, type: t },
|
|
1522
|
+
}))
|
|
1523
|
+
}
|
|
1524
|
+
style={{
|
|
1525
|
+
background:
|
|
1526
|
+
cfg.type === t
|
|
1527
|
+
? t === "kms"
|
|
1528
|
+
? theme.purple
|
|
1529
|
+
: theme.accent
|
|
1530
|
+
: "transparent",
|
|
1531
|
+
border: `1px solid ${
|
|
1532
|
+
cfg.type === t
|
|
1533
|
+
? t === "kms"
|
|
1534
|
+
? theme.purple
|
|
1535
|
+
: theme.accent
|
|
1536
|
+
: theme.border
|
|
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
|
+
))}
|
|
1550
|
+
</div>
|
|
1233
1551
|
</div>
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1552
|
+
|
|
1553
|
+
{cfg.type === "kms" && (
|
|
1554
|
+
<div style={{ display: "flex", gap: 8 }}>
|
|
1555
|
+
<select
|
|
1556
|
+
value={cfg.provider}
|
|
1557
|
+
onChange={(e) =>
|
|
1239
1558
|
setEnvBackends((prev) => ({
|
|
1240
1559
|
...prev,
|
|
1241
|
-
[env.name]: { ...cfg,
|
|
1560
|
+
[env.name]: { ...cfg, provider: e.target.value },
|
|
1242
1561
|
}))
|
|
1243
1562
|
}
|
|
1244
1563
|
style={{
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
: theme.accent
|
|
1250
|
-
: "transparent",
|
|
1251
|
-
border: `1px solid ${
|
|
1252
|
-
cfg.type === t
|
|
1253
|
-
? t === "kms"
|
|
1254
|
-
? theme.purple
|
|
1255
|
-
: theme.accent
|
|
1256
|
-
: theme.border
|
|
1257
|
-
}`,
|
|
1258
|
-
borderRadius: 4,
|
|
1259
|
-
padding: "3px 10px",
|
|
1260
|
-
cursor: "pointer",
|
|
1261
|
-
fontFamily: theme.mono,
|
|
1262
|
-
fontSize: 11,
|
|
1263
|
-
color: cfg.type === t ? "#fff" : theme.textMuted,
|
|
1264
|
-
transition: "all 0.1s",
|
|
1564
|
+
...inputStyle,
|
|
1565
|
+
width: 90,
|
|
1566
|
+
flexShrink: 0,
|
|
1567
|
+
padding: "7px 8px",
|
|
1265
1568
|
}}
|
|
1266
1569
|
>
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1570
|
+
<option value="aws">AWS</option>
|
|
1571
|
+
<option value="gcp">GCP</option>
|
|
1572
|
+
<option value="azure">Azure</option>
|
|
1573
|
+
</select>
|
|
1574
|
+
<input
|
|
1575
|
+
value={cfg.keyId}
|
|
1576
|
+
onChange={(e) =>
|
|
1577
|
+
setEnvBackends((prev) => ({
|
|
1578
|
+
...prev,
|
|
1579
|
+
[env.name]: { ...cfg, keyId: e.target.value },
|
|
1580
|
+
}))
|
|
1581
|
+
}
|
|
1582
|
+
placeholder="arn:aws:kms:… or key resource ID"
|
|
1583
|
+
style={{ ...inputStyle, flex: 1 }}
|
|
1584
|
+
/>
|
|
1585
|
+
</div>
|
|
1586
|
+
)}
|
|
1271
1587
|
</div>
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
<select
|
|
1276
|
-
value={cfg.provider}
|
|
1277
|
-
onChange={(e) =>
|
|
1278
|
-
setEnvBackends((prev) => ({
|
|
1279
|
-
...prev,
|
|
1280
|
-
[env.name]: { ...cfg, provider: e.target.value },
|
|
1281
|
-
}))
|
|
1282
|
-
}
|
|
1283
|
-
style={{
|
|
1284
|
-
...inputStyle,
|
|
1285
|
-
width: 90,
|
|
1286
|
-
flexShrink: 0,
|
|
1287
|
-
padding: "7px 8px",
|
|
1288
|
-
}}
|
|
1289
|
-
>
|
|
1290
|
-
<option value="aws">AWS</option>
|
|
1291
|
-
<option value="gcp">GCP</option>
|
|
1292
|
-
<option value="azure">Azure</option>
|
|
1293
|
-
</select>
|
|
1294
|
-
<input
|
|
1295
|
-
value={cfg.keyId}
|
|
1296
|
-
onChange={(e) =>
|
|
1297
|
-
setEnvBackends((prev) => ({
|
|
1298
|
-
...prev,
|
|
1299
|
-
[env.name]: { ...cfg, keyId: e.target.value },
|
|
1300
|
-
}))
|
|
1301
|
-
}
|
|
1302
|
-
placeholder="arn:aws:kms:… or key resource ID"
|
|
1303
|
-
style={{ ...inputStyle, flex: 1 }}
|
|
1304
|
-
/>
|
|
1305
|
-
</div>
|
|
1306
|
-
)}
|
|
1307
|
-
</div>
|
|
1308
|
-
);
|
|
1309
|
-
})}
|
|
1588
|
+
);
|
|
1589
|
+
})
|
|
1590
|
+
)}
|
|
1310
1591
|
</div>
|
|
1311
1592
|
</div>
|
|
1312
1593
|
|