@seekrit/cli 0.27.0 → 0.29.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/dist/index.js +1637 -315
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -39,6 +39,12 @@ const ENTITLEMENT_KEYS = Object.keys({
|
|
|
39
39
|
description: "Single sign-on beyond the built-in providers.",
|
|
40
40
|
default: true
|
|
41
41
|
},
|
|
42
|
+
"feature.sync": {
|
|
43
|
+
kind: "feature",
|
|
44
|
+
label: "Third-party sync",
|
|
45
|
+
description: "Push environment secrets to external platforms like Vercel.",
|
|
46
|
+
default: true
|
|
47
|
+
},
|
|
42
48
|
"apps.max": {
|
|
43
49
|
kind: "limit",
|
|
44
50
|
label: "Applications",
|
|
@@ -87,6 +93,12 @@ const ENTITLEMENT_KEYS = Object.keys({
|
|
|
87
93
|
description: "Maximum registered temporary-access targets.",
|
|
88
94
|
default: null
|
|
89
95
|
},
|
|
96
|
+
"sync.connections.max": {
|
|
97
|
+
kind: "limit",
|
|
98
|
+
label: "Sync connections",
|
|
99
|
+
description: "Maximum registered third-party sync destinations.",
|
|
100
|
+
default: null
|
|
101
|
+
},
|
|
90
102
|
members: {
|
|
91
103
|
kind: "metered",
|
|
92
104
|
label: "Members",
|
|
@@ -135,7 +147,8 @@ const PLAN_FAMILIES = {
|
|
|
135
147
|
}
|
|
136
148
|
};
|
|
137
149
|
const PLAN_FAMILY_IDS = Object.keys(PLAN_FAMILIES);
|
|
138
|
-
|
|
150
|
+
/** Plan families a plan picker should render. Excludes families marked `hidden`. */
|
|
151
|
+
const VISIBLE_PLAN_FAMILY_IDS = PLAN_FAMILY_IDS.filter((family) => !PLAN_FAMILIES[family].hidden);
|
|
139
152
|
//#endregion
|
|
140
153
|
//#region ../../packages/core/src/billing.ts
|
|
141
154
|
/**
|
|
@@ -195,6 +208,55 @@ function parseBranchTtl(input) {
|
|
|
195
208
|
return value * multiplier;
|
|
196
209
|
}
|
|
197
210
|
//#endregion
|
|
211
|
+
//#region ../../packages/core/src/dotenv.ts
|
|
212
|
+
/**
|
|
213
|
+
* Minimal `.env` parser: `KEY=VALUE`, `#` comments, an optional `export`
|
|
214
|
+
* prefix, and single/double-quoted values (double quotes honor `\n \t \r \" \\`
|
|
215
|
+
* escapes; unquoted values drop trailing ` # comments`). Multiline values are
|
|
216
|
+
* not supported — keep those in seekrit itself.
|
|
217
|
+
*/
|
|
218
|
+
function parseDotenv(content) {
|
|
219
|
+
const out = {};
|
|
220
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
221
|
+
let line = raw.trim();
|
|
222
|
+
if (!line || line.startsWith("#")) continue;
|
|
223
|
+
if (line.startsWith("export ")) line = line.slice(7).trimStart();
|
|
224
|
+
const eq = line.indexOf("=");
|
|
225
|
+
if (eq === -1) continue;
|
|
226
|
+
const key = line.slice(0, eq).trim();
|
|
227
|
+
if (!key) continue;
|
|
228
|
+
let value = line.slice(eq + 1).trim();
|
|
229
|
+
const quote = value[0];
|
|
230
|
+
if (value.length >= 2 && (quote === "\"" || quote === "'") && value.at(-1) === quote) {
|
|
231
|
+
value = value.slice(1, -1);
|
|
232
|
+
if (quote === "\"") value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
|
|
233
|
+
} else {
|
|
234
|
+
const comment = value.indexOf(" #");
|
|
235
|
+
if (comment !== -1) value = value.slice(0, comment).trim();
|
|
236
|
+
}
|
|
237
|
+
out[key] = value;
|
|
238
|
+
}
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
//#endregion
|
|
242
|
+
//#region ../../packages/core/src/ids.ts
|
|
243
|
+
const ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
244
|
+
/**
|
|
245
|
+
* Generate a prefixed, URL-safe random ID, e.g. `org_h9K2x…`.
|
|
246
|
+
* Uses rejection sampling so every character is uniformly distributed.
|
|
247
|
+
*/
|
|
248
|
+
function randomId(prefix, length = 24) {
|
|
249
|
+
let out = "";
|
|
250
|
+
while (out.length < length) {
|
|
251
|
+
const bytes = crypto.getRandomValues(new Uint8Array(length - out.length));
|
|
252
|
+
for (const byte of bytes) {
|
|
253
|
+
if (byte < 248) out += ALPHABET[byte % 62];
|
|
254
|
+
if (out.length === length) break;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return `${prefix}_${out}`;
|
|
258
|
+
}
|
|
259
|
+
//#endregion
|
|
198
260
|
//#region ../../packages/core/src/interpolate.ts
|
|
199
261
|
/**
|
|
200
262
|
* Secret references: `${OTHER_SECRET}` inside a secret value.
|
|
@@ -829,6 +891,84 @@ function sshHostSetupInstructions(config) {
|
|
|
829
891
|
}
|
|
830
892
|
//#endregion
|
|
831
893
|
//#region ../../packages/core/src/types.ts
|
|
894
|
+
const AUDIT_ACTIONS = [
|
|
895
|
+
"org.created",
|
|
896
|
+
"org.updated",
|
|
897
|
+
"org.member_added",
|
|
898
|
+
"org.member_removed",
|
|
899
|
+
"org.member_invited",
|
|
900
|
+
"org.invite_revoked",
|
|
901
|
+
"org.mfa_policy_changed",
|
|
902
|
+
"user.keys_updated",
|
|
903
|
+
"user.notification_prefs_updated",
|
|
904
|
+
"app.created",
|
|
905
|
+
"app.updated",
|
|
906
|
+
"app.deleted",
|
|
907
|
+
"group.created",
|
|
908
|
+
"group.updated",
|
|
909
|
+
"group.deleted",
|
|
910
|
+
"env.created",
|
|
911
|
+
"env.updated",
|
|
912
|
+
"env.deleted",
|
|
913
|
+
"env.group_linked",
|
|
914
|
+
"env.group_unlinked",
|
|
915
|
+
"env.resolved",
|
|
916
|
+
"env.resolve_denied",
|
|
917
|
+
"env.key_granted",
|
|
918
|
+
"env.key_revoked",
|
|
919
|
+
"branch.created",
|
|
920
|
+
"branch.deleted",
|
|
921
|
+
"branch.expired",
|
|
922
|
+
"secret.created",
|
|
923
|
+
"secret.updated",
|
|
924
|
+
"secret.deleted",
|
|
925
|
+
"secret.read",
|
|
926
|
+
"secret.restored",
|
|
927
|
+
"token.created",
|
|
928
|
+
"token.revoked",
|
|
929
|
+
"token.deleted",
|
|
930
|
+
"cli_session.approved",
|
|
931
|
+
"cli_session.denied",
|
|
932
|
+
"cli_session.revoked",
|
|
933
|
+
"m2m.client_created",
|
|
934
|
+
"m2m.client_revoked",
|
|
935
|
+
"lease.target_registered",
|
|
936
|
+
"lease.target_deleted",
|
|
937
|
+
"lease.created",
|
|
938
|
+
"lease.revoked",
|
|
939
|
+
"lease.expired",
|
|
940
|
+
"log_sink.configured",
|
|
941
|
+
"log_sink.deleted",
|
|
942
|
+
"log_sink.test",
|
|
943
|
+
"subscription.updated",
|
|
944
|
+
"subscription.canceled",
|
|
945
|
+
"entitlement.override_set",
|
|
946
|
+
"entitlement.override_cleared",
|
|
947
|
+
"billing.checkout_started",
|
|
948
|
+
"billing.portal_opened",
|
|
949
|
+
"kms.key_created",
|
|
950
|
+
"kms.key_granted",
|
|
951
|
+
"kms.key_revoked",
|
|
952
|
+
"kms.key_rotated",
|
|
953
|
+
"kms.key_disabled",
|
|
954
|
+
"kms.key_deleted",
|
|
955
|
+
"recovery.configured",
|
|
956
|
+
"recovery.rotated",
|
|
957
|
+
"recovery.disabled",
|
|
958
|
+
"recovery.env_wrapped",
|
|
959
|
+
"recovery.requested",
|
|
960
|
+
"recovery.share_contributed",
|
|
961
|
+
"recovery.completed",
|
|
962
|
+
"recovery.canceled",
|
|
963
|
+
"sync.connection_created",
|
|
964
|
+
"sync.connection_updated",
|
|
965
|
+
"sync.connection_deleted",
|
|
966
|
+
"sync.binding_created",
|
|
967
|
+
"sync.binding_updated",
|
|
968
|
+
"sync.binding_deleted",
|
|
969
|
+
"sync.run_succeeded",
|
|
970
|
+
"sync.run_failed"
|
|
971
|
+
];
|
|
832
972
|
/**
|
|
833
973
|
* Transactional notification emails seekrit can send. Each id is one
|
|
834
974
|
* user-facing on/off toggle (see `NOTIFICATION_TYPE_META`). These carry only
|
|
@@ -844,8 +984,57 @@ const NOTIFICATION_TYPES = [
|
|
|
844
984
|
"resolve_denied",
|
|
845
985
|
"org_welcome",
|
|
846
986
|
"token_expiring",
|
|
847
|
-
"lease_expired"
|
|
987
|
+
"lease_expired",
|
|
988
|
+
"sync_failed"
|
|
848
989
|
];
|
|
990
|
+
/** UI-facing copy + default for each notification type. */
|
|
991
|
+
const NOTIFICATION_TYPE_META = {
|
|
992
|
+
token_created: {
|
|
993
|
+
label: "Service token created",
|
|
994
|
+
description: "A new service token that can decrypt secrets was created in one of your organizations.",
|
|
995
|
+
defaultEnabled: true
|
|
996
|
+
},
|
|
997
|
+
token_revoked: {
|
|
998
|
+
label: "Service token revoked",
|
|
999
|
+
description: "A service token was revoked in one of your organizations.",
|
|
1000
|
+
defaultEnabled: true
|
|
1001
|
+
},
|
|
1002
|
+
env_access_granted: {
|
|
1003
|
+
label: "Environment access granted",
|
|
1004
|
+
description: "You were granted access to decrypt an environment's secrets.",
|
|
1005
|
+
defaultEnabled: true
|
|
1006
|
+
},
|
|
1007
|
+
env_access_revoked: {
|
|
1008
|
+
label: "Environment access revoked",
|
|
1009
|
+
description: "Your access to an environment's secrets was revoked.",
|
|
1010
|
+
defaultEnabled: true
|
|
1011
|
+
},
|
|
1012
|
+
resolve_denied: {
|
|
1013
|
+
label: "Denied-access alerts",
|
|
1014
|
+
description: "A principal was denied when trying to resolve an environment's secrets (throttled).",
|
|
1015
|
+
defaultEnabled: true
|
|
1016
|
+
},
|
|
1017
|
+
org_welcome: {
|
|
1018
|
+
label: "Welcome & key setup",
|
|
1019
|
+
description: "You joined an organization — a welcome with next steps for finishing encryption key setup.",
|
|
1020
|
+
defaultEnabled: true
|
|
1021
|
+
},
|
|
1022
|
+
token_expiring: {
|
|
1023
|
+
label: "Service token expiring",
|
|
1024
|
+
description: "A service token in one of your organizations is expiring within 7 days.",
|
|
1025
|
+
defaultEnabled: true
|
|
1026
|
+
},
|
|
1027
|
+
lease_expired: {
|
|
1028
|
+
label: "Temporary credential expired",
|
|
1029
|
+
description: "A temporary (leased) credential you own expired and was automatically revoked.",
|
|
1030
|
+
defaultEnabled: true
|
|
1031
|
+
},
|
|
1032
|
+
sync_failed: {
|
|
1033
|
+
label: "Third-party sync failed",
|
|
1034
|
+
description: "A sync to an external destination failed repeatedly and stopped retrying. The destination is now holding stale values.",
|
|
1035
|
+
defaultEnabled: true
|
|
1036
|
+
}
|
|
1037
|
+
};
|
|
849
1038
|
//#endregion
|
|
850
1039
|
//#region ../../packages/core/src/schemas.ts
|
|
851
1040
|
/** URL-safe identifier segment: `my-app`, `production`, … */
|
|
@@ -1081,6 +1270,128 @@ z.object({
|
|
|
1081
1270
|
resourceType: z.string().optional()
|
|
1082
1271
|
});
|
|
1083
1272
|
//#endregion
|
|
1273
|
+
//#region ../../packages/core/src/sync.ts
|
|
1274
|
+
/**
|
|
1275
|
+
* Third-party sync: seekrit as source of truth, pushing an environment's
|
|
1276
|
+
* resolved secrets out to a platform that holds its own copy (Vercel project
|
|
1277
|
+
* env vars, GitHub Actions secrets, a cloud secret manager, …).
|
|
1278
|
+
*
|
|
1279
|
+
* This file is the transport-free vocabulary — provider kinds, connection and
|
|
1280
|
+
* destination shapes, the name-mapping rules, and the request schemas. The
|
|
1281
|
+
* execution side (the per-org `SyncEngine` Durable Object and the connectors)
|
|
1282
|
+
* lives in `apps/api`.
|
|
1283
|
+
*
|
|
1284
|
+
* ## Sync is the one place plaintext exists server-side
|
|
1285
|
+
*
|
|
1286
|
+
* Every other seekrit feature keeps decryption on the caller's machine. A sync
|
|
1287
|
+
* destination needs the plaintext value, and sync runs when nobody is logged
|
|
1288
|
+
* in, so the sync engine must be able to decrypt on its own. That capability is
|
|
1289
|
+
* granted exactly like any other: an `environment_keys` row of principal type
|
|
1290
|
+
* `sync`, whose `principalId` is the **connection id** and whose wrapped DEK is
|
|
1291
|
+
* computed client-side by a key holder. No grant, no sync — the control plane
|
|
1292
|
+
* cannot manufacture one.
|
|
1293
|
+
*
|
|
1294
|
+
* Keying per connection (not per org) is deliberate: one stray grant exposes
|
|
1295
|
+
* one named destination, and deleting a connection destroys its keypair, which
|
|
1296
|
+
* turns every grant to it into dead ciphertext.
|
|
1297
|
+
*
|
|
1298
|
+
* See `docs/third-party-sync.md`.
|
|
1299
|
+
*/
|
|
1300
|
+
/** Platforms seekrit can push to. Append-only — persisted in `sync_connections.provider`. */
|
|
1301
|
+
const SYNC_PROVIDER_KINDS = ["vercel"];
|
|
1302
|
+
z.enum(SYNC_PROVIDER_KINDS);
|
|
1303
|
+
/**
|
|
1304
|
+
* Vercel account scope. The API token itself is never here — it is wrapped to
|
|
1305
|
+
* the connection's public key and stored as ciphertext.
|
|
1306
|
+
*
|
|
1307
|
+
* `teamId` is required for tokens scoped to a Vercel Team; personal-account
|
|
1308
|
+
* tokens omit it. Vercel rejects team-owned project calls that lack it with a
|
|
1309
|
+
* bare 403, so we pass it through as `?teamId=` on every request.
|
|
1310
|
+
*/
|
|
1311
|
+
const vercelConnectionConfigSchema = z.object({
|
|
1312
|
+
provider: z.literal("vercel"),
|
|
1313
|
+
/** Vercel Team id (`team_…`). Omit for a personal account. */
|
|
1314
|
+
teamId: z.string().trim().min(1).max(128).optional()
|
|
1315
|
+
});
|
|
1316
|
+
const syncConnectionConfigSchema = z.discriminatedUnion("provider", [vercelConnectionConfigSchema]);
|
|
1317
|
+
/** Vercel's three deployment targets. A binding writes to one or more. */
|
|
1318
|
+
const VERCEL_TARGETS = [
|
|
1319
|
+
"production",
|
|
1320
|
+
"preview",
|
|
1321
|
+
"development"
|
|
1322
|
+
];
|
|
1323
|
+
const vercelDestinationSchema = z.object({
|
|
1324
|
+
provider: z.literal("vercel"),
|
|
1325
|
+
/** Vercel project id (`prj_…`) or project name. */
|
|
1326
|
+
projectId: z.string().trim().min(1).max(128),
|
|
1327
|
+
/** Which deployment targets receive these values. At least one. */
|
|
1328
|
+
targets: z.array(z.enum(VERCEL_TARGETS)).min(1),
|
|
1329
|
+
/**
|
|
1330
|
+
* Restrict `preview` writes to one git branch. Vercel only honors this when
|
|
1331
|
+
* `targets` includes `preview`; ignored otherwise.
|
|
1332
|
+
*/
|
|
1333
|
+
gitBranch: z.string().trim().min(1).max(255).optional()
|
|
1334
|
+
});
|
|
1335
|
+
const syncDestinationSchema = z.discriminatedUnion("provider", [vercelDestinationSchema]);
|
|
1336
|
+
/**
|
|
1337
|
+
* How seekrit secret names become destination key names. Applied in order:
|
|
1338
|
+
* explicit `rename` (wins outright), then `prefix`/`suffix`, then `case`.
|
|
1339
|
+
*/
|
|
1340
|
+
const nameTransformSchema = z.object({
|
|
1341
|
+
prefix: z.string().max(64).regex(/^[A-Za-z0-9_]*$/, "must be alphanumeric or underscore").optional(),
|
|
1342
|
+
suffix: z.string().max(64).regex(/^[A-Za-z0-9_]*$/, "must be alphanumeric or underscore").optional(),
|
|
1343
|
+
case: z.enum([
|
|
1344
|
+
"preserve",
|
|
1345
|
+
"upper",
|
|
1346
|
+
"lower"
|
|
1347
|
+
]).optional(),
|
|
1348
|
+
/** Exact per-secret overrides, seekrit name → destination name. */
|
|
1349
|
+
rename: z.record(secretNameSchema, secretNameSchema).optional()
|
|
1350
|
+
});
|
|
1351
|
+
z.object({
|
|
1352
|
+
/**
|
|
1353
|
+
* The id the client already fetched a public key for. Connections are a
|
|
1354
|
+
* two-step dance — mint the key, wrap the credential to it, then create the
|
|
1355
|
+
* row — so the id has to be chosen before the row exists. Omit it and the
|
|
1356
|
+
* server generates one (only useful when there is no credential to wrap yet).
|
|
1357
|
+
*/
|
|
1358
|
+
id: z.string().regex(/^syc_[A-Za-z0-9]{24}$/, "must be a connection id from the public-key call").optional(),
|
|
1359
|
+
name: z.string().trim().min(1).max(128),
|
|
1360
|
+
config: syncConnectionConfigSchema,
|
|
1361
|
+
/**
|
|
1362
|
+
* The destination's API credential (a Vercel token), encrypted client-side to
|
|
1363
|
+
* the connection's public key (a `wd1.` wrap). The control plane stores only
|
|
1364
|
+
* this ciphertext; it is unwrapped transiently inside the sync engine DO.
|
|
1365
|
+
*/
|
|
1366
|
+
wrappedCredential: z.string().min(1)
|
|
1367
|
+
});
|
|
1368
|
+
z.object({ destination: syncDestinationSchema });
|
|
1369
|
+
const globListSchema = z.array(z.string().trim().min(1).max(256)).max(100);
|
|
1370
|
+
z.object({
|
|
1371
|
+
connectionId: z.string().min(1),
|
|
1372
|
+
environmentId: z.string().min(1),
|
|
1373
|
+
destination: syncDestinationSchema,
|
|
1374
|
+
nameTransform: nameTransformSchema.optional(),
|
|
1375
|
+
include: globListSchema.optional(),
|
|
1376
|
+
exclude: globListSchema.optional(),
|
|
1377
|
+
onDelete: z.enum(["delete", "retain"]).default("delete"),
|
|
1378
|
+
mode: z.enum(["auto", "manual"]).default("auto"),
|
|
1379
|
+
wrappedDeks: z.array(z.object({
|
|
1380
|
+
environmentId: z.string().min(1),
|
|
1381
|
+
wrappedDek: z.string().min(1)
|
|
1382
|
+
})).min(1),
|
|
1383
|
+
acknowledgedDecryption: z.literal(true)
|
|
1384
|
+
});
|
|
1385
|
+
z.object({
|
|
1386
|
+
destination: syncDestinationSchema.optional(),
|
|
1387
|
+
nameTransform: nameTransformSchema.nullable().optional(),
|
|
1388
|
+
include: globListSchema.nullable().optional(),
|
|
1389
|
+
exclude: globListSchema.nullable().optional(),
|
|
1390
|
+
onDelete: z.enum(["delete", "retain"]).optional(),
|
|
1391
|
+
mode: z.enum(["auto", "manual"]).optional(),
|
|
1392
|
+
enabled: z.boolean().optional()
|
|
1393
|
+
});
|
|
1394
|
+
//#endregion
|
|
1084
1395
|
//#region ../../packages/crypto/src/encoding.ts
|
|
1085
1396
|
const CHUNK = 32768;
|
|
1086
1397
|
/** Base64url (no padding) — portable across browsers, Workers, and Node. */
|
|
@@ -2217,7 +2528,7 @@ function isCliSessionToken(value) {
|
|
|
2217
2528
|
}
|
|
2218
2529
|
//#endregion
|
|
2219
2530
|
//#region package.json
|
|
2220
|
-
var version = "0.
|
|
2531
|
+
var version = "0.29.0";
|
|
2221
2532
|
//#endregion
|
|
2222
2533
|
//#region ../../packages/api-client/src/index.ts
|
|
2223
2534
|
var SeekritApiError = class extends Error {
|
|
@@ -2582,6 +2893,54 @@ var SeekritClient = class {
|
|
|
2582
2893
|
deleteLeaseTarget(orgId, targetId) {
|
|
2583
2894
|
return this.request("DELETE", `/v1/orgs/${orgId}/leases/targets/${targetId}`);
|
|
2584
2895
|
}
|
|
2896
|
+
listSyncConnections(orgId) {
|
|
2897
|
+
return this.request("GET", `/v1/orgs/${orgId}/sync/connections`);
|
|
2898
|
+
}
|
|
2899
|
+
/**
|
|
2900
|
+
* Mint (or re-read) the keypair for a connection id, *before* the connection
|
|
2901
|
+
* exists. Wrap the destination credential and every environment DEK to this
|
|
2902
|
+
* key, then pass the same id to {@link createSyncConnection}.
|
|
2903
|
+
*/
|
|
2904
|
+
getSyncConnectionKey(orgId, connectionId) {
|
|
2905
|
+
return this.request("GET", `/v1/orgs/${orgId}/sync/connections/${connectionId}/public-key`);
|
|
2906
|
+
}
|
|
2907
|
+
createSyncConnection(orgId, input) {
|
|
2908
|
+
return this.request("POST", `/v1/orgs/${orgId}/sync/connections`, input);
|
|
2909
|
+
}
|
|
2910
|
+
/** Test the stored credential against a destination. Never throws on a bad token. */
|
|
2911
|
+
verifySyncConnection(orgId, connectionId, destination) {
|
|
2912
|
+
return this.request("POST", `/v1/orgs/${orgId}/sync/connections/${connectionId}/verify`, { destination });
|
|
2913
|
+
}
|
|
2914
|
+
/** Deletes the connection, its bindings, its key grants, and its keypair. */
|
|
2915
|
+
deleteSyncConnection(orgId, connectionId) {
|
|
2916
|
+
return this.request("DELETE", `/v1/orgs/${orgId}/sync/connections/${connectionId}`);
|
|
2917
|
+
}
|
|
2918
|
+
listSyncBindings(orgId) {
|
|
2919
|
+
return this.request("GET", `/v1/orgs/${orgId}/sync/bindings`);
|
|
2920
|
+
}
|
|
2921
|
+
/**
|
|
2922
|
+
* Enable sync for one environment. `wrappedDeks` must cover the target
|
|
2923
|
+
* environment *and* every group environment it composes, each wrapped to the
|
|
2924
|
+
* connection's public key — the API cannot compute these, which is what keeps
|
|
2925
|
+
* enabling sync a key-holder operation.
|
|
2926
|
+
*/
|
|
2927
|
+
createSyncBinding(orgId, input) {
|
|
2928
|
+
return this.request("POST", `/v1/orgs/${orgId}/sync/bindings`, input);
|
|
2929
|
+
}
|
|
2930
|
+
updateSyncBinding(orgId, bindingId, input) {
|
|
2931
|
+
return this.request("PATCH", `/v1/orgs/${orgId}/sync/bindings/${bindingId}`, input);
|
|
2932
|
+
}
|
|
2933
|
+
deleteSyncBinding(orgId, bindingId) {
|
|
2934
|
+
return this.request("DELETE", `/v1/orgs/${orgId}/sync/bindings/${bindingId}`);
|
|
2935
|
+
}
|
|
2936
|
+
/** Push now, synchronously. */
|
|
2937
|
+
runSyncBinding(orgId, bindingId) {
|
|
2938
|
+
return this.request("POST", `/v1/orgs/${orgId}/sync/bindings/${bindingId}/run`);
|
|
2939
|
+
}
|
|
2940
|
+
listSyncRuns(orgId, bindingId) {
|
|
2941
|
+
const qs = bindingId ? `?bindingId=${encodeURIComponent(bindingId)}` : "";
|
|
2942
|
+
return this.request("GET", `/v1/orgs/${orgId}/sync/runs${qs}`);
|
|
2943
|
+
}
|
|
2585
2944
|
listLeases(orgId) {
|
|
2586
2945
|
return this.request("GET", `/v1/orgs/${orgId}/leases`);
|
|
2587
2946
|
}
|
|
@@ -2788,6 +3147,31 @@ function promptEnter(question) {
|
|
|
2788
3147
|
});
|
|
2789
3148
|
});
|
|
2790
3149
|
}
|
|
3150
|
+
function askYesNo(question) {
|
|
3151
|
+
const rl = createInterface({
|
|
3152
|
+
input: process.stdin,
|
|
3153
|
+
output: process.stderr,
|
|
3154
|
+
terminal: true
|
|
3155
|
+
});
|
|
3156
|
+
return new Promise((resolve) => {
|
|
3157
|
+
rl.question(`${question} [y/N] `, (answer) => {
|
|
3158
|
+
rl.close();
|
|
3159
|
+
resolve(/^y(es)?$/i.test(answer.trim()));
|
|
3160
|
+
});
|
|
3161
|
+
});
|
|
3162
|
+
}
|
|
3163
|
+
/**
|
|
3164
|
+
* Gate an irreversible action behind an explicit yes.
|
|
3165
|
+
*
|
|
3166
|
+
* The flag skips the question outright. Without it, an interactive run has to
|
|
3167
|
+
* answer; a non-interactive one (a script, CI, an agent) is refused and told
|
|
3168
|
+
* about the flag, because a destructive command must never proceed on silence.
|
|
3169
|
+
*/
|
|
3170
|
+
async function confirmDestructive(confirmed, question, flag = "--yes") {
|
|
3171
|
+
if (confirmed) return;
|
|
3172
|
+
if (!process.stdin.isTTY) fail(`${question} Re-run with ${flag} to confirm.`);
|
|
3173
|
+
if (!await askYesNo(question)) fail("aborted");
|
|
3174
|
+
}
|
|
2791
3175
|
/** Read all of stdin (for `seekrit secrets set NAME -` piping). */
|
|
2792
3176
|
async function readStdin() {
|
|
2793
3177
|
const chunks = [];
|
|
@@ -2861,6 +3245,69 @@ async function getDek(ctx, orgId, envId) {
|
|
|
2861
3245
|
return unwrapDek(wrappedDek, privateKey);
|
|
2862
3246
|
}
|
|
2863
3247
|
//#endregion
|
|
3248
|
+
//#region src/output.ts
|
|
3249
|
+
/** Shorthand for a column whose header is fixed and value is one field. */
|
|
3250
|
+
function col(header, value) {
|
|
3251
|
+
return {
|
|
3252
|
+
header,
|
|
3253
|
+
value: (row) => stringify(value(row))
|
|
3254
|
+
};
|
|
3255
|
+
}
|
|
3256
|
+
function stringify(value) {
|
|
3257
|
+
if (value === null || value === void 0) return "-";
|
|
3258
|
+
return String(value);
|
|
3259
|
+
}
|
|
3260
|
+
/**
|
|
3261
|
+
* Print `data` as JSON when `--json` was passed, otherwise run `render`.
|
|
3262
|
+
* Keeps the JSON branch identical across commands (and machine-parseable even
|
|
3263
|
+
* for detail views, which have no table form).
|
|
3264
|
+
*/
|
|
3265
|
+
function emit(options, data, render) {
|
|
3266
|
+
if (options.json) {
|
|
3267
|
+
console.log(JSON.stringify(data, null, 2));
|
|
3268
|
+
return;
|
|
3269
|
+
}
|
|
3270
|
+
render();
|
|
3271
|
+
}
|
|
3272
|
+
/**
|
|
3273
|
+
* Render rows as a table. `empty` is the stderr note shown when there is
|
|
3274
|
+
* nothing to list — an empty list is a normal answer, not an error, so it must
|
|
3275
|
+
* not go to stdout and must not exit non-zero.
|
|
3276
|
+
*/
|
|
3277
|
+
function printTable(rows, columns, empty = "none") {
|
|
3278
|
+
if (rows.length === 0) {
|
|
3279
|
+
console.error(empty);
|
|
3280
|
+
return;
|
|
3281
|
+
}
|
|
3282
|
+
const cells = rows.map((row) => columns.map((c) => c.value(row)));
|
|
3283
|
+
if (!process.stdout.isTTY) {
|
|
3284
|
+
for (const line of cells) console.log(line.join(" "));
|
|
3285
|
+
return;
|
|
3286
|
+
}
|
|
3287
|
+
const widths = columns.map((c, i) => Math.max(c.header.length, ...cells.map((line) => (line[i] ?? "").length)));
|
|
3288
|
+
const format = (line) => line.map((cell, i) => i === line.length - 1 ? cell : cell.padEnd(widths[i] ?? 0)).join(" ");
|
|
3289
|
+
console.log(format(columns.map((c) => c.header.toUpperCase())));
|
|
3290
|
+
for (const line of cells) console.log(format(line));
|
|
3291
|
+
}
|
|
3292
|
+
/**
|
|
3293
|
+
* Render a detail view: one `label value` per line, labels aligned. Null and
|
|
3294
|
+
* undefined values are dropped rather than printed as blanks, so a caller can
|
|
3295
|
+
* build the list unconditionally and let absent fields disappear.
|
|
3296
|
+
*/
|
|
3297
|
+
function printFields(fields) {
|
|
3298
|
+
const present = fields.filter(([, value]) => value !== null && value !== void 0);
|
|
3299
|
+
const width = Math.max(0, ...present.map(([label]) => label.length));
|
|
3300
|
+
for (const [label, value] of present) console.log(`${label.padEnd(width)} ${value}`);
|
|
3301
|
+
}
|
|
3302
|
+
/**
|
|
3303
|
+
* A titled break between the sections of a detail view. Detail views are for
|
|
3304
|
+
* reading — a script wanting to parse one should ask for `--json`.
|
|
3305
|
+
*/
|
|
3306
|
+
function section(title) {
|
|
3307
|
+
console.log("");
|
|
3308
|
+
console.log(title);
|
|
3309
|
+
}
|
|
3310
|
+
//#endregion
|
|
2864
3311
|
//#region src/target.ts
|
|
2865
3312
|
/** Resolve the target org from a flag, the committed config, or a lone org. */
|
|
2866
3313
|
async function resolveOrg(ctx, orgSlug) {
|
|
@@ -2976,144 +3423,149 @@ async function resolveGroup(ctx, opts) {
|
|
|
2976
3423
|
};
|
|
2977
3424
|
}
|
|
2978
3425
|
//#endregion
|
|
2979
|
-
//#region src/
|
|
3426
|
+
//#region src/access.ts
|
|
2980
3427
|
/**
|
|
2981
|
-
*
|
|
2982
|
-
*
|
|
3428
|
+
* Human-readable labels for the principals holding a key grant. `grant list`
|
|
3429
|
+
* would otherwise be a column of opaque ids, which is exactly the question
|
|
3430
|
+
* ("who can read prod?") it exists to answer.
|
|
2983
3431
|
*
|
|
2984
|
-
*
|
|
2985
|
-
*
|
|
2986
|
-
* credential and the broker returns it wrapped to that key, so the control plane
|
|
2987
|
-
* only ever relays ciphertext and only this machine can unwrap it. Registering a
|
|
2988
|
-
* target wraps the base IAM credential to the broker's public key locally, so
|
|
2989
|
-
* the control plane never sees it either — its only needed permission is
|
|
2990
|
-
* `sts:AssumeRole` on the target role.
|
|
3432
|
+
* Both lookups are best-effort: `listMembers` is open to any member but
|
|
3433
|
+
* `listTokens` may not be, and a grant can outlive the principal it names.
|
|
2991
3434
|
*/
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
const
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3435
|
+
async function principalLabels(ctx, orgId) {
|
|
3436
|
+
const labels = /* @__PURE__ */ new Map();
|
|
3437
|
+
const [members, tokens] = await Promise.all([ctx.client.listMembers(orgId).then((r) => r.members).catch(() => []), ctx.client.listTokens(orgId).then((r) => r.tokens).catch(() => [])]);
|
|
3438
|
+
for (const m of members) labels.set(m.userId, m.email);
|
|
3439
|
+
for (const t of tokens) labels.set(t.id, t.name);
|
|
3440
|
+
return labels;
|
|
3441
|
+
}
|
|
3442
|
+
/** Turn `--user email` / `--token skt_…` into the principal to wrap a DEK to. */
|
|
3443
|
+
async function resolvePrincipal(ctx, orgId, options) {
|
|
3444
|
+
if (!options.user === !options.token) fail("pass exactly one of --user or --token");
|
|
3445
|
+
if (options.user) {
|
|
3446
|
+
const { members } = await ctx.client.listMembers(orgId);
|
|
3447
|
+
const member = members.find((m) => m.email === options.user);
|
|
3448
|
+
if (!member) fail(`no member ${options.user}`);
|
|
3449
|
+
if (!member.publicKeyJwk) fail(`${options.user} has not completed key setup`);
|
|
3450
|
+
return {
|
|
3451
|
+
type: "user",
|
|
3452
|
+
id: member.userId,
|
|
3453
|
+
publicKeyJwk: member.publicKeyJwk
|
|
3454
|
+
};
|
|
3455
|
+
}
|
|
3456
|
+
const { tokens } = await ctx.client.listTokens(orgId);
|
|
3457
|
+
const token = tokens.find((t) => t.id === options.token);
|
|
3458
|
+
if (!token) fail(`no service token ${options.token}`);
|
|
3459
|
+
return {
|
|
3460
|
+
type: "service_token",
|
|
3461
|
+
id: token.id,
|
|
3462
|
+
publicKeyJwk: token.publicKeyJwk
|
|
3463
|
+
};
|
|
3464
|
+
}
|
|
3465
|
+
/** The `--org/--app/--group/--env` selection every grant command shares. */
|
|
3466
|
+
function withEnvTarget(cmd) {
|
|
3467
|
+
return cmd.option("--org <slug>").option("--app <slug>", "application slug (defaults to seekrit.json)").option("--group <slug>", "target a group environment instead of an app").requiredOption("--env <slug>");
|
|
3002
3468
|
}
|
|
3003
3469
|
/**
|
|
3004
|
-
*
|
|
3005
|
-
*
|
|
3006
|
-
*
|
|
3470
|
+
* Environment key grants — who can decrypt what.
|
|
3471
|
+
*
|
|
3472
|
+
* `seekrit grant` keeps working as the bare "give access" verb (it is the
|
|
3473
|
+
* default subcommand), with `list` and `rm` alongside it so access can be
|
|
3474
|
+
* audited and taken back from the CLI rather than only the dashboard.
|
|
3007
3475
|
*/
|
|
3008
|
-
function
|
|
3009
|
-
const
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
if (!accessKeyId || !secretAccessKey) fail("provide the base IAM credential via --access-key-id/--secret-access-key or AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY (it needs only sts:AssumeRole on the role)");
|
|
3013
|
-
return JSON.stringify({
|
|
3014
|
-
accessKeyId,
|
|
3015
|
-
secretAccessKey,
|
|
3016
|
-
...sessionToken ? { sessionToken } : {}
|
|
3017
|
-
});
|
|
3018
|
-
}
|
|
3019
|
-
function registerAwsCommands(program) {
|
|
3020
|
-
const aws = program.command("aws").description("temporary AWS credentials (STS AssumeRole, zero-knowledge)");
|
|
3021
|
-
const target = aws.command("target").description("manage AWS role targets");
|
|
3022
|
-
target.command("add").description("register an assumable IAM role to issue temporary credentials from").requiredOption("--name <name>", "display name, e.g. prod-deploy").requiredOption("--role-arn <arn>", "the IAM role to assume, arn:aws:iam::<acct>:role/<name>").requiredOption("--region <region>", "region whose STS endpoint to call, e.g. us-east-1").option("--org <slug>").option("--external-id <id>", "STS ExternalId the role's trust policy requires").option("--session-policy <file>", "path to an inline session policy JSON (further restricts)").option("--max-ttl <duration>", "clamp requested credential lifetime, e.g. 8h").option("--access-key-id <id>", "base IAM access key id (else AWS_ACCESS_KEY_ID)").option("--secret-access-key <secret>", "base IAM secret (else AWS_SECRET_ACCESS_KEY)").action(async (options) => {
|
|
3476
|
+
function registerAccessCommands(program) {
|
|
3477
|
+
const grant = program.command("grant").description("manage who can decrypt an environment (`seekrit grant --help`)");
|
|
3478
|
+
withEnvTarget(grant.command("add", { isDefault: true }).description("give a member or service token access to an environment's key").option("--user <email>", "grant to an org member by email").option("--token <tokenId>", "grant to a service token by id (skt_…)")).action(async (options) => {
|
|
3479
|
+
if (!options.user === !options.token) fail("pass exactly one of --user or --token");
|
|
3023
3480
|
const ctx = buildContext();
|
|
3024
|
-
const
|
|
3025
|
-
const
|
|
3026
|
-
const
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
...options.externalId ? { externalId: options.externalId } : {},
|
|
3032
|
-
...sessionPolicy ? { sessionPolicy } : {},
|
|
3033
|
-
...options.maxTtl ? { maxTtlSeconds: parseTtlSeconds$6(options.maxTtl) } : {}
|
|
3034
|
-
};
|
|
3035
|
-
const baseCredential = resolveBaseCredential(options);
|
|
3036
|
-
const { publicKeyJwk } = await ctx.client.getLeaseBrokerKey(org.id);
|
|
3037
|
-
const wrappedAdminSecret = await wrapDek(new TextEncoder().encode(baseCredential), publicKeyJwk);
|
|
3038
|
-
const { target: created } = await ctx.client.registerLeaseTarget(org.id, {
|
|
3039
|
-
name: options.name,
|
|
3040
|
-
config,
|
|
3041
|
-
wrappedAdminSecret
|
|
3481
|
+
const { orgId, envId, label } = await resolveEnvTarget(ctx, options);
|
|
3482
|
+
const principal = await resolvePrincipal(ctx, orgId, options);
|
|
3483
|
+
const dek = await getDek(ctx, orgId, envId);
|
|
3484
|
+
await ctx.client.grantEnvKey(orgId, envId, {
|
|
3485
|
+
principalType: principal.type,
|
|
3486
|
+
principalId: principal.id,
|
|
3487
|
+
wrappedDek: await wrapDek(dek, principal.publicKeyJwk)
|
|
3042
3488
|
});
|
|
3043
|
-
console.error(`
|
|
3044
|
-
console.error("\nEnsure the role trusts the admin principal, then `seekrit aws lease`:\n");
|
|
3045
|
-
console.log(awsTrustPolicyInstructions(config));
|
|
3489
|
+
console.error(`granted ${label} access to ${principal.id}`);
|
|
3046
3490
|
});
|
|
3047
|
-
|
|
3491
|
+
withEnvTarget(grant.command("list").alias("ls").description("list who holds a key for an environment").option("--json", "print the raw API response")).action(async (options) => {
|
|
3048
3492
|
const ctx = buildContext();
|
|
3049
|
-
const
|
|
3050
|
-
const {
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3493
|
+
const { orgId, envId } = await resolveEnvTarget(ctx, options);
|
|
3494
|
+
const [{ grants }, labels] = await Promise.all([ctx.client.listEnvKeys(orgId, envId), principalLabels(ctx, orgId)]);
|
|
3495
|
+
emit(options, { grants }, () => printTable(grants, [
|
|
3496
|
+
col("principal", (g) => labels.get(g.principalId) ?? g.principalId),
|
|
3497
|
+
col("type", (g) => g.principalType === "service_token" ? "token" : "user"),
|
|
3498
|
+
col("granted", (g) => g.createdAt),
|
|
3499
|
+
col("id", (g) => g.principalId)
|
|
3500
|
+
], "nobody holds a key for this environment"));
|
|
3056
3501
|
});
|
|
3057
|
-
|
|
3502
|
+
withEnvTarget(grant.command("rm").alias("revoke").description("take away a member's or token's access to an environment's key").option("--user <email>", "revoke an org member by email").option("--token <tokenId>", "revoke a service token by id (skt_…)").option("--yes", "skip the confirmation prompt")).action(async (options) => {
|
|
3503
|
+
if (!options.user === !options.token) fail("pass exactly one of --user or --token");
|
|
3058
3504
|
const ctx = buildContext();
|
|
3059
|
-
const
|
|
3060
|
-
const {
|
|
3061
|
-
|
|
3062
|
-
if (
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3505
|
+
const { orgId, envId, label } = await resolveEnvTarget(ctx, options);
|
|
3506
|
+
const { grants } = await ctx.client.listEnvKeys(orgId, envId);
|
|
3507
|
+
let principalId = options.token;
|
|
3508
|
+
if (options.user) {
|
|
3509
|
+
const { members } = await ctx.client.listMembers(orgId);
|
|
3510
|
+
principalId = members.find((m) => m.email === options.user)?.userId;
|
|
3511
|
+
if (!principalId) fail(`no member ${options.user}`);
|
|
3512
|
+
}
|
|
3513
|
+
const grantRow = grants.find((g) => g.principalId === principalId);
|
|
3514
|
+
if (!grantRow) fail(`${options.user ?? options.token} holds no key for ${label}`);
|
|
3515
|
+
await confirmDestructive(options.yes, `Revoke ${options.user ?? options.token}'s key for ${label}? Anything already decrypted stays decrypted.`);
|
|
3516
|
+
await ctx.client.revokeEnvKey(orgId, envId, grantRow.id);
|
|
3517
|
+
console.error(`revoked ${options.user ?? options.token}'s access to ${label}`);
|
|
3066
3518
|
});
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3519
|
+
}
|
|
3520
|
+
//#endregion
|
|
3521
|
+
//#region src/account.ts
|
|
3522
|
+
function sessionStatus(session) {
|
|
3523
|
+
if (session.revokedAt) return "revoked";
|
|
3524
|
+
if (session.expiresAt && Date.parse(session.expiresAt) < Date.now()) return "expired";
|
|
3525
|
+
return "active";
|
|
3526
|
+
}
|
|
3527
|
+
/**
|
|
3528
|
+
* Your own account: the devices signed in as you, and which emails seekrit
|
|
3529
|
+
* sends you. Both are per-user (not per-org), so neither takes `--org`.
|
|
3530
|
+
*/
|
|
3531
|
+
function registerAccountCommands(program) {
|
|
3532
|
+
const session = program.command("session").description("manage the devices authorized by `seekrit login`");
|
|
3533
|
+
session.command("list").alias("ls").description("list CLI sessions authorized for your account").option("--all", "include revoked and expired sessions").option("--json", "print the raw API response").action(async (options) => {
|
|
3534
|
+
const { sessions, currentSessionId } = await buildContext().client.listCliSessions();
|
|
3535
|
+
const rows = options.all ? sessions : sessions.filter((s) => sessionStatus(s) === "active");
|
|
3536
|
+
emit(options, {
|
|
3537
|
+
sessions: rows,
|
|
3538
|
+
currentSessionId
|
|
3539
|
+
}, () => printTable(rows, [
|
|
3540
|
+
col("device", (s) => s.deviceLabel),
|
|
3541
|
+
col("client", (s) => s.client),
|
|
3542
|
+
col("status", (s) => sessionStatus(s)),
|
|
3543
|
+
col("last used", (s) => s.lastUsedAt),
|
|
3544
|
+
col("ip", (s) => s.ipAddress),
|
|
3545
|
+
col("id", (s) => `${s.id}${s.id === currentSessionId ? " (this one)" : ""}`)
|
|
3546
|
+
], options.all ? "no CLI sessions" : "no active CLI sessions (try --all)"));
|
|
3072
3547
|
});
|
|
3073
|
-
|
|
3548
|
+
session.command("revoke <sessionId>").description("sign a device out — its token stops working immediately").option("--yes", "skip the confirmation prompt").action(async (sessionId, options) => {
|
|
3074
3549
|
const ctx = buildContext();
|
|
3075
|
-
const
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
const cfg = t.config;
|
|
3080
|
-
if (cfg.provider !== "aws") fail(`"${t.name}" is not an aws target (see \`seekrit pg\`)`);
|
|
3081
|
-
const ttlSeconds = parseTtlSeconds$6(options.ttl);
|
|
3082
|
-
if (ttlSeconds < 900) fail(`--ttl must be at least ${900 / 60}m (STS minimum)`);
|
|
3083
|
-
const recipient = await generateAwsRecipientKeyPair();
|
|
3084
|
-
const { aws: leased } = await ctx.client.mintLease(org.id, {
|
|
3085
|
-
provider: "aws",
|
|
3086
|
-
targetId: t.id,
|
|
3087
|
-
recipientPublicKey: recipient.publicKeyJwk,
|
|
3088
|
-
ttlSeconds
|
|
3089
|
-
});
|
|
3090
|
-
const cred = await unwrapAwsCredential(leased.wrappedCredential, recipient.privateKeyJwk);
|
|
3091
|
-
console.error(`leased ${cfg.roleArn} in ${cred.region} — expires ${cred.expiration}`);
|
|
3092
|
-
if (options.json) console.log(JSON.stringify({
|
|
3093
|
-
...cred,
|
|
3094
|
-
roleArn: cfg.roleArn
|
|
3095
|
-
}, null, 2));
|
|
3096
|
-
else {
|
|
3097
|
-
console.log(`export AWS_ACCESS_KEY_ID=${cred.accessKeyId}`);
|
|
3098
|
-
console.log(`export AWS_SECRET_ACCESS_KEY=${cred.secretAccessKey}`);
|
|
3099
|
-
console.log(`export AWS_SESSION_TOKEN=${cred.sessionToken}`);
|
|
3100
|
-
console.log(`export AWS_REGION=${cred.region}`);
|
|
3101
|
-
}
|
|
3550
|
+
const self = ctx.auth.type === "bearer" && isCliSessionToken(ctx.auth.token) && parseCliSessionToken(ctx.auth.token).sessionId === sessionId;
|
|
3551
|
+
await confirmDestructive(options.yes, self ? `${sessionId} is the session you are using right now — sign it out?` : `Sign out ${sessionId}?`);
|
|
3552
|
+
await ctx.client.revokeCliSession(sessionId);
|
|
3553
|
+
console.error(`${sessionId} revoked${self ? " — run `seekrit login` to sign back in" : ""}`);
|
|
3102
3554
|
});
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
const
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3555
|
+
const notifications = program.command("notifications").description("manage the emails seekrit sends you");
|
|
3556
|
+
notifications.command("list", { isDefault: true }).alias("ls").description("show your notification preferences").option("--json", "print the raw API response").action(async (options) => {
|
|
3557
|
+
const { prefs } = await buildContext().client.getMyNotificationPrefs();
|
|
3558
|
+
emit(options, { prefs }, () => printTable(NOTIFICATION_TYPES, [
|
|
3559
|
+
col("type", (t) => t),
|
|
3560
|
+
col("email", (t) => prefs[t] ? "on" : "off"),
|
|
3561
|
+
col("about", (t) => NOTIFICATION_TYPE_META[t].label)
|
|
3562
|
+
]));
|
|
3111
3563
|
});
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
await
|
|
3116
|
-
console.error(
|
|
3564
|
+
notifications.command("set <type> <state>").description("turn one notification on or off (`state` is on or off)").action(async (type, state) => {
|
|
3565
|
+
if (state !== "on" && state !== "off") fail("state must be on or off");
|
|
3566
|
+
if (!NOTIFICATION_TYPES.includes(type)) fail(`unknown notification "${type}" — one of: ${NOTIFICATION_TYPES.join(", ")}`);
|
|
3567
|
+
await buildContext().client.setMyNotificationPrefs({ prefs: { [type]: state === "on" } });
|
|
3568
|
+
console.error(`${type} emails ${state}`);
|
|
3117
3569
|
});
|
|
3118
3570
|
}
|
|
3119
3571
|
//#endregion
|
|
@@ -3602,6 +4054,477 @@ function registerRecoveryCommands(program) {
|
|
|
3602
4054
|
});
|
|
3603
4055
|
}
|
|
3604
4056
|
//#endregion
|
|
4057
|
+
//#region src/apps.ts
|
|
4058
|
+
/**
|
|
4059
|
+
* Applications and their environments — the structure everything else hangs
|
|
4060
|
+
* off. Creating an environment is the one command here that does crypto: the
|
|
4061
|
+
* data key is generated locally and only ciphertext leaves the machine.
|
|
4062
|
+
*/
|
|
4063
|
+
function registerAppCommands(program) {
|
|
4064
|
+
const app = program.command("app").description("manage applications");
|
|
4065
|
+
app.command("list").alias("ls").description("list applications in an organization").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
|
|
4066
|
+
const ctx = buildContext();
|
|
4067
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
4068
|
+
const { apps } = await ctx.client.listApps(ref.id);
|
|
4069
|
+
emit(options, { apps }, () => printTable(apps, [
|
|
4070
|
+
col("slug", (a) => a.slug),
|
|
4071
|
+
col("name", (a) => a.name),
|
|
4072
|
+
col("created", (a) => a.createdAt),
|
|
4073
|
+
col("id", (a) => a.id)
|
|
4074
|
+
], "no applications — create one with `seekrit app create`"));
|
|
4075
|
+
});
|
|
4076
|
+
app.command("show [slug]").description("show an application, its environments, and your access to each").option("--org <slug>").option("--app <slug>", "application slug (defaults to seekrit.json)").option("--json", "print the raw API response").action(async (slug, options) => {
|
|
4077
|
+
const ctx = buildContext();
|
|
4078
|
+
const ref = await resolveApp(ctx, {
|
|
4079
|
+
org: options.org,
|
|
4080
|
+
app: slug ?? options.app
|
|
4081
|
+
});
|
|
4082
|
+
const [{ app: row, environments }, { branches }] = await Promise.all([ctx.client.getApp(ref.orgId, ref.id), ctx.client.listAppBranches(ref.orgId, ref.id)]);
|
|
4083
|
+
emit(options, {
|
|
4084
|
+
app: row,
|
|
4085
|
+
environments,
|
|
4086
|
+
branches
|
|
4087
|
+
}, () => {
|
|
4088
|
+
printFields([
|
|
4089
|
+
["slug", row.slug],
|
|
4090
|
+
["name", row.name],
|
|
4091
|
+
["id", row.id],
|
|
4092
|
+
["org", ref.orgSlug],
|
|
4093
|
+
["created", row.createdAt]
|
|
4094
|
+
]);
|
|
4095
|
+
section("environments");
|
|
4096
|
+
printTable(environments, [
|
|
4097
|
+
col("slug", (e) => e.slug),
|
|
4098
|
+
col("name", (e) => e.name),
|
|
4099
|
+
col("access", (e) => e.canDecrypt ? "can decrypt" : "no key"),
|
|
4100
|
+
col("id", (e) => e.id)
|
|
4101
|
+
], "no environments — create one with `seekrit env create`");
|
|
4102
|
+
if (branches.length > 0) {
|
|
4103
|
+
section("branches");
|
|
4104
|
+
printTable(branches, [
|
|
4105
|
+
col("slug", (b) => b.slug),
|
|
4106
|
+
col("expires", (b) => b.expiresAt ?? "never"),
|
|
4107
|
+
col("id", (b) => b.id)
|
|
4108
|
+
]);
|
|
4109
|
+
}
|
|
4110
|
+
});
|
|
4111
|
+
});
|
|
4112
|
+
app.command("create").description("create an application").option("--org <slug>").requiredOption("--name <name>").requiredOption("--slug <slug>").action(async (options) => {
|
|
4113
|
+
const ctx = buildContext();
|
|
4114
|
+
const orgRef = await resolveOrg(ctx, options.org);
|
|
4115
|
+
const created = await ctx.client.createApp(orgRef.id, {
|
|
4116
|
+
name: options.name,
|
|
4117
|
+
slug: options.slug
|
|
4118
|
+
});
|
|
4119
|
+
console.error(`created app ${created.app.slug} (${created.app.id})`);
|
|
4120
|
+
});
|
|
4121
|
+
app.command("rename [slug]").description("change an application's display name (the slug is permanent)").option("--org <slug>").option("--app <slug>", "application slug (defaults to seekrit.json)").requiredOption("--name <name>", "new display name").action(async (slug, options) => {
|
|
4122
|
+
const ctx = buildContext();
|
|
4123
|
+
const ref = await resolveApp(ctx, {
|
|
4124
|
+
org: options.org,
|
|
4125
|
+
app: slug ?? options.app
|
|
4126
|
+
});
|
|
4127
|
+
const { app: row } = await ctx.client.updateApp(ref.orgId, ref.id, { name: options.name });
|
|
4128
|
+
console.error(`renamed ${row.slug} to "${row.name}"`);
|
|
4129
|
+
});
|
|
4130
|
+
app.command("rm <slug>").alias("delete").description("delete an application and every environment and secret in it").option("--org <slug>").option("--yes", "skip the confirmation prompt").action(async (slug, options) => {
|
|
4131
|
+
const ctx = buildContext();
|
|
4132
|
+
const ref = await resolveApp(ctx, {
|
|
4133
|
+
org: options.org,
|
|
4134
|
+
app: slug
|
|
4135
|
+
});
|
|
4136
|
+
const { environments } = await ctx.client.getApp(ref.orgId, ref.id);
|
|
4137
|
+
await confirmDestructive(options.yes, `Delete ${ref.slug} and its ${environments.length} environment(s), with every secret in them?`);
|
|
4138
|
+
await ctx.client.deleteApp(ref.orgId, ref.id);
|
|
4139
|
+
console.error(`deleted app ${ref.slug}`);
|
|
4140
|
+
});
|
|
4141
|
+
const env = program.command("env").description("manage environments");
|
|
4142
|
+
env.command("list").alias("ls").description("list an application's environments").option("--org <slug>").option("--app <slug>", "application slug (defaults to seekrit.json)").option("--json", "print the raw API response").action(async (options) => {
|
|
4143
|
+
const ctx = buildContext();
|
|
4144
|
+
const ref = await resolveApp(ctx, options);
|
|
4145
|
+
const { environments } = await ctx.client.getApp(ref.orgId, ref.id);
|
|
4146
|
+
emit(options, { environments }, () => printTable(environments, [
|
|
4147
|
+
col("slug", (e) => e.slug),
|
|
4148
|
+
col("name", (e) => e.name),
|
|
4149
|
+
col("access", (e) => e.canDecrypt ? "can decrypt" : "no key"),
|
|
4150
|
+
col("id", (e) => e.id)
|
|
4151
|
+
], "no environments — create one with `seekrit env create`"));
|
|
4152
|
+
});
|
|
4153
|
+
env.command("show").description("show one environment: composed groups, who holds a key, secret count").option("--org <slug>").option("--app <slug>", "application slug (defaults to seekrit.json)").requiredOption("--env <slug>").option("--json", "print the raw API response").action(async (options) => {
|
|
4154
|
+
const ctx = buildContext();
|
|
4155
|
+
const target = await resolveAppEnv(ctx, options);
|
|
4156
|
+
const [{ environment }, { groups }, { secrets }, { branches }] = await Promise.all([
|
|
4157
|
+
ctx.client.getEnv(target.orgId, target.envId),
|
|
4158
|
+
ctx.client.listEnvGroups(target.orgId, target.envId),
|
|
4159
|
+
ctx.client.listSecrets(target.orgId, target.envId),
|
|
4160
|
+
ctx.client.listBranches(target.orgId, target.envId)
|
|
4161
|
+
]);
|
|
4162
|
+
const grants = await ctx.client.listEnvKeys(target.orgId, target.envId).then((r) => r.grants).catch(() => null);
|
|
4163
|
+
emit(options, {
|
|
4164
|
+
environment,
|
|
4165
|
+
groups,
|
|
4166
|
+
secretCount: secrets.length,
|
|
4167
|
+
branches,
|
|
4168
|
+
grants
|
|
4169
|
+
}, () => {
|
|
4170
|
+
printFields([
|
|
4171
|
+
["slug", environment.slug],
|
|
4172
|
+
["name", environment.name],
|
|
4173
|
+
["id", environment.id],
|
|
4174
|
+
["app", target.appSlug],
|
|
4175
|
+
["secrets", secrets.length],
|
|
4176
|
+
["created", environment.createdAt]
|
|
4177
|
+
]);
|
|
4178
|
+
section("composed groups (lowest precedence first)");
|
|
4179
|
+
printTable([...groups].sort((a, b) => a.position - b.position), [
|
|
4180
|
+
col("position", (g) => g.position),
|
|
4181
|
+
col("group", (g) => g.slug),
|
|
4182
|
+
col("name", (g) => g.name)
|
|
4183
|
+
], "none — this environment's secrets are all its own");
|
|
4184
|
+
section("key holders");
|
|
4185
|
+
if (grants === null) console.error("(admin only)");
|
|
4186
|
+
else printTable(grants, [col("type", (g) => g.principalType), col("principal", (g) => g.principalId)], "none");
|
|
4187
|
+
if (branches.length > 0) {
|
|
4188
|
+
section("branches");
|
|
4189
|
+
printTable(branches, [col("slug", (b) => b.slug), col("expires", (b) => b.expiresAt ?? "never")]);
|
|
4190
|
+
}
|
|
4191
|
+
});
|
|
4192
|
+
});
|
|
4193
|
+
env.command("create").description("create an application environment (generates its data key locally)").option("--org <slug>").requiredOption("--app <slug>").requiredOption("--name <name>").requiredOption("--slug <slug>").action(async (options) => {
|
|
4194
|
+
const ctx = buildContext();
|
|
4195
|
+
const orgRef = await resolveOrg(ctx, options.org);
|
|
4196
|
+
const { apps } = await ctx.client.listApps(orgRef.id);
|
|
4197
|
+
const appRow = apps.find((a) => a.slug === options.app || a.id === options.app);
|
|
4198
|
+
if (!appRow) fail(`no app "${options.app}" in ${orgRef.slug}`);
|
|
4199
|
+
const { user } = await ctx.client.me();
|
|
4200
|
+
if (!user.publicKeyJwk) fail("run `seekrit keys setup` first");
|
|
4201
|
+
const dek = generateDek();
|
|
4202
|
+
const wrappedDek = await wrapDek(dek, user.publicKeyJwk);
|
|
4203
|
+
const recoveryWrappedDek = await recoveryWrapForNewEnv(ctx, orgRef.id, dek);
|
|
4204
|
+
const created = await ctx.client.createEnv(orgRef.id, appRow.id, {
|
|
4205
|
+
name: options.name,
|
|
4206
|
+
slug: options.slug,
|
|
4207
|
+
wrappedDek,
|
|
4208
|
+
recoveryWrappedDek
|
|
4209
|
+
});
|
|
4210
|
+
console.error(`created environment ${created.environment.slug} (${created.environment.id})`);
|
|
4211
|
+
});
|
|
4212
|
+
env.command("rm").alias("delete").description("delete an environment and every secret in it").option("--org <slug>").option("--app <slug>", "application slug (defaults to seekrit.json)").option("--group <slug>", "delete a group environment instead of an app one").requiredOption("--env <slug>").option("--yes", "skip the confirmation prompt").action(async (options) => {
|
|
4213
|
+
const ctx = buildContext();
|
|
4214
|
+
const target = await resolveEnvTarget(ctx, options);
|
|
4215
|
+
const { secrets } = await ctx.client.listSecrets(target.orgId, target.envId);
|
|
4216
|
+
await confirmDestructive(options.yes, `Delete ${target.label} and the ${secrets.length} secret(s) in it?`);
|
|
4217
|
+
await ctx.client.deleteEnv(target.orgId, target.envId);
|
|
4218
|
+
console.error(`deleted ${target.label}`);
|
|
4219
|
+
});
|
|
4220
|
+
const envGroups = env.command("groups").description("compose shared groups into an application environment");
|
|
4221
|
+
envGroups.command("add").description("compose a group into an app environment").option("--org <slug>").requiredOption("--app <slug>").requiredOption("--env <slug>").requiredOption("--group <slug>").option("--position <n>", "precedence among groups (higher wins)").action(async (options) => {
|
|
4222
|
+
const ctx = buildContext();
|
|
4223
|
+
const target = await resolveAppEnv(ctx, options);
|
|
4224
|
+
const group = await resolveGroup(ctx, {
|
|
4225
|
+
org: options.org,
|
|
4226
|
+
group: options.group
|
|
4227
|
+
});
|
|
4228
|
+
await ctx.client.linkEnvGroup(target.orgId, target.envId, {
|
|
4229
|
+
groupId: group.id,
|
|
4230
|
+
position: options.position === void 0 ? void 0 : Number.parseInt(options.position, 10)
|
|
4231
|
+
});
|
|
4232
|
+
console.error(`composed ${group.slug} into ${target.appSlug}/${target.envSlug}`);
|
|
4233
|
+
});
|
|
4234
|
+
envGroups.command("list").alias("ls").description("list groups composed into an app environment").option("--org <slug>").requiredOption("--app <slug>").requiredOption("--env <slug>").option("--json", "print the raw API response").action(async (options) => {
|
|
4235
|
+
const ctx = buildContext();
|
|
4236
|
+
const target = await resolveAppEnv(ctx, options);
|
|
4237
|
+
const { groups } = await ctx.client.listEnvGroups(target.orgId, target.envId);
|
|
4238
|
+
emit(options, { groups }, () => printTable([...groups].sort((a, b) => a.position - b.position), [
|
|
4239
|
+
col("position", (g) => g.position),
|
|
4240
|
+
col("group", (g) => g.slug),
|
|
4241
|
+
col("name", (g) => g.name)
|
|
4242
|
+
], "no groups composed into this environment"));
|
|
4243
|
+
});
|
|
4244
|
+
envGroups.command("rm").description("remove a group from an app environment").option("--org <slug>").requiredOption("--app <slug>").requiredOption("--env <slug>").requiredOption("--group <slug>").action(async (options) => {
|
|
4245
|
+
const ctx = buildContext();
|
|
4246
|
+
const target = await resolveAppEnv(ctx, options);
|
|
4247
|
+
const group = await resolveGroup(ctx, {
|
|
4248
|
+
org: options.org,
|
|
4249
|
+
group: options.group
|
|
4250
|
+
});
|
|
4251
|
+
await ctx.client.unlinkEnvGroup(target.orgId, target.envId, group.id);
|
|
4252
|
+
console.error(`removed ${group.slug} from ${target.appSlug}/${target.envSlug}`);
|
|
4253
|
+
});
|
|
4254
|
+
}
|
|
4255
|
+
//#endregion
|
|
4256
|
+
//#region src/audit.ts
|
|
4257
|
+
/** The API's per-page ceiling (`auditQuerySchema.limit`). */
|
|
4258
|
+
const MAX_PAGE = 200;
|
|
4259
|
+
function parseLimit(raw) {
|
|
4260
|
+
const n = Number(raw);
|
|
4261
|
+
if (!Number.isInteger(n) || n < 1) fail(`--limit must be a positive whole number, got "${raw}"`);
|
|
4262
|
+
return Math.min(n, MAX_PAGE);
|
|
4263
|
+
}
|
|
4264
|
+
/**
|
|
4265
|
+
* The org audit trail. Append-only and admin-only, so this is the record of
|
|
4266
|
+
* who did what — the CLI needs the same filters the dashboard has, plus
|
|
4267
|
+
* pagination, or "find the grant that shouldn't be there" means scrolling.
|
|
4268
|
+
*/
|
|
4269
|
+
function registerAuditCommands(program) {
|
|
4270
|
+
const audit = program.command("audit").description("read the org audit trail");
|
|
4271
|
+
audit.command("list", { isDefault: true }).alias("ls").description("show the org audit trail").option("--org <slug>").option("--limit <n>", `entries per page (max ${MAX_PAGE})`, "50").option("--action <action>", "only this action, e.g. env.key_granted").option("--resource-type <type>", "only this resource type, e.g. environment").option("--cursor <cursor>", "continue from a previous page's cursor").option("--all", "page through the whole trail, not just the first page").option("--metadata", "include each entry's metadata as JSON").option("--json", "print the raw API response").action(async (options) => {
|
|
4272
|
+
if (options.action && !AUDIT_ACTIONS.includes(options.action)) fail(`unknown action "${options.action}" — see \`seekrit audit actions\``);
|
|
4273
|
+
const ctx = buildContext();
|
|
4274
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
4275
|
+
const limit = parseLimit(options.limit);
|
|
4276
|
+
const entries = [];
|
|
4277
|
+
let cursor = options.cursor;
|
|
4278
|
+
let nextCursor = null;
|
|
4279
|
+
do {
|
|
4280
|
+
const page = await ctx.client.listAudit(ref.id, {
|
|
4281
|
+
limit,
|
|
4282
|
+
cursor,
|
|
4283
|
+
action: options.action,
|
|
4284
|
+
resourceType: options.resourceType
|
|
4285
|
+
});
|
|
4286
|
+
entries.push(...page.entries);
|
|
4287
|
+
nextCursor = page.nextCursor;
|
|
4288
|
+
cursor = page.nextCursor ?? void 0;
|
|
4289
|
+
} while (options.all && cursor);
|
|
4290
|
+
emit(options, {
|
|
4291
|
+
entries,
|
|
4292
|
+
nextCursor
|
|
4293
|
+
}, () => {
|
|
4294
|
+
printTable(entries, [
|
|
4295
|
+
col("when", (e) => e.createdAt),
|
|
4296
|
+
col("action", (e) => e.action),
|
|
4297
|
+
col("actor", (e) => `${e.actorType}:${e.actorId}`),
|
|
4298
|
+
col("resource", (e) => `${e.resourceType}${e.resourceId ? `:${e.resourceId}` : ""}`),
|
|
4299
|
+
...options.metadata ? [col("metadata", (e) => e.metadata ? JSON.stringify(e.metadata) : "-")] : []
|
|
4300
|
+
], "no audit entries match");
|
|
4301
|
+
if (nextCursor && !options.all) console.error(`more entries — continue with --cursor ${nextCursor} (or pass --all)`);
|
|
4302
|
+
});
|
|
4303
|
+
});
|
|
4304
|
+
audit.command("actions").description("list every action the audit trail can record (for `audit --action`)").option("--json", "print the raw API response").action((options) => {
|
|
4305
|
+
emit(options, { actions: AUDIT_ACTIONS }, () => {
|
|
4306
|
+
for (const action of AUDIT_ACTIONS) console.log(action);
|
|
4307
|
+
});
|
|
4308
|
+
});
|
|
4309
|
+
}
|
|
4310
|
+
//#endregion
|
|
4311
|
+
//#region src/aws.ts
|
|
4312
|
+
/**
|
|
4313
|
+
* `seekrit aws` — temporary AWS credentials via STS AssumeRole (Vault-style
|
|
4314
|
+
* dynamic secrets, the tier-2 sibling of `seekrit pg`).
|
|
4315
|
+
*
|
|
4316
|
+
* Zero-knowledge for the leased credential: minting generates an ephemeral P-256
|
|
4317
|
+
* keypair on THIS machine and sends only the public key; STS mints the
|
|
4318
|
+
* credential and the broker returns it wrapped to that key, so the control plane
|
|
4319
|
+
* only ever relays ciphertext and only this machine can unwrap it. Registering a
|
|
4320
|
+
* target wraps the base IAM credential to the broker's public key locally, so
|
|
4321
|
+
* the control plane never sees it either — its only needed permission is
|
|
4322
|
+
* `sts:AssumeRole` on the target role.
|
|
4323
|
+
*/
|
|
4324
|
+
/** Parse a duration like `30m`, `1h`, or a bare seconds count. */
|
|
4325
|
+
function parseTtlSeconds$6(input) {
|
|
4326
|
+
const m = /^(\d+)\s*([smhd]?)$/.exec(input.trim());
|
|
4327
|
+
if (!m) fail(`invalid --ttl "${input}" (try 30m, 1h, 12h)`);
|
|
4328
|
+
return Number(m[1]) * ({
|
|
4329
|
+
s: 1,
|
|
4330
|
+
m: 60,
|
|
4331
|
+
h: 3600,
|
|
4332
|
+
d: 86400
|
|
4333
|
+
}[m[2] || "s"] ?? 1);
|
|
4334
|
+
}
|
|
4335
|
+
/**
|
|
4336
|
+
* The base IAM credential the broker assumes the role with. From flags or the
|
|
4337
|
+
* standard AWS env vars; JSON-serialized so the executor can parse it. Never the
|
|
4338
|
+
* plaintext leaves this machine unwrapped — it is wrapped to the broker key.
|
|
4339
|
+
*/
|
|
4340
|
+
function resolveBaseCredential(opts) {
|
|
4341
|
+
const accessKeyId = opts.accessKeyId ?? process.env.AWS_ACCESS_KEY_ID;
|
|
4342
|
+
const secretAccessKey = opts.secretAccessKey ?? process.env.AWS_SECRET_ACCESS_KEY;
|
|
4343
|
+
const sessionToken = process.env.AWS_SESSION_TOKEN;
|
|
4344
|
+
if (!accessKeyId || !secretAccessKey) fail("provide the base IAM credential via --access-key-id/--secret-access-key or AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY (it needs only sts:AssumeRole on the role)");
|
|
4345
|
+
return JSON.stringify({
|
|
4346
|
+
accessKeyId,
|
|
4347
|
+
secretAccessKey,
|
|
4348
|
+
...sessionToken ? { sessionToken } : {}
|
|
4349
|
+
});
|
|
4350
|
+
}
|
|
4351
|
+
function registerAwsCommands(program) {
|
|
4352
|
+
const aws = program.command("aws").description("temporary AWS credentials (STS AssumeRole, zero-knowledge)");
|
|
4353
|
+
const target = aws.command("target").description("manage AWS role targets");
|
|
4354
|
+
target.command("add").description("register an assumable IAM role to issue temporary credentials from").requiredOption("--name <name>", "display name, e.g. prod-deploy").requiredOption("--role-arn <arn>", "the IAM role to assume, arn:aws:iam::<acct>:role/<name>").requiredOption("--region <region>", "region whose STS endpoint to call, e.g. us-east-1").option("--org <slug>").option("--external-id <id>", "STS ExternalId the role's trust policy requires").option("--session-policy <file>", "path to an inline session policy JSON (further restricts)").option("--max-ttl <duration>", "clamp requested credential lifetime, e.g. 8h").option("--access-key-id <id>", "base IAM access key id (else AWS_ACCESS_KEY_ID)").option("--secret-access-key <secret>", "base IAM secret (else AWS_SECRET_ACCESS_KEY)").action(async (options) => {
|
|
4355
|
+
const ctx = buildContext();
|
|
4356
|
+
const org = await resolveOrg(ctx, options.org);
|
|
4357
|
+
const sessionPolicy = options.sessionPolicy ? readFileSync(options.sessionPolicy, "utf8").trim() : void 0;
|
|
4358
|
+
const config = {
|
|
4359
|
+
provider: "aws",
|
|
4360
|
+
executor: "in_do",
|
|
4361
|
+
roleArn: options.roleArn,
|
|
4362
|
+
region: options.region,
|
|
4363
|
+
...options.externalId ? { externalId: options.externalId } : {},
|
|
4364
|
+
...sessionPolicy ? { sessionPolicy } : {},
|
|
4365
|
+
...options.maxTtl ? { maxTtlSeconds: parseTtlSeconds$6(options.maxTtl) } : {}
|
|
4366
|
+
};
|
|
4367
|
+
const baseCredential = resolveBaseCredential(options);
|
|
4368
|
+
const { publicKeyJwk } = await ctx.client.getLeaseBrokerKey(org.id);
|
|
4369
|
+
const wrappedAdminSecret = await wrapDek(new TextEncoder().encode(baseCredential), publicKeyJwk);
|
|
4370
|
+
const { target: created } = await ctx.client.registerLeaseTarget(org.id, {
|
|
4371
|
+
name: options.name,
|
|
4372
|
+
config,
|
|
4373
|
+
wrappedAdminSecret
|
|
4374
|
+
});
|
|
4375
|
+
console.error(`registered AWS target ${created.name} (${created.id})`);
|
|
4376
|
+
console.error("\nEnsure the role trusts the admin principal, then `seekrit aws lease`:\n");
|
|
4377
|
+
console.log(awsTrustPolicyInstructions(config));
|
|
4378
|
+
});
|
|
4379
|
+
target.command("list").description("list AWS role targets").option("--org <slug>").action(async (options) => {
|
|
4380
|
+
const ctx = buildContext();
|
|
4381
|
+
const org = await resolveOrg(ctx, options.org);
|
|
4382
|
+
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
4383
|
+
for (const t of targets) {
|
|
4384
|
+
const cfg = t.config;
|
|
4385
|
+
if (cfg.provider !== "aws") continue;
|
|
4386
|
+
console.log(`${t.id}\t${t.name}\t${cfg.region}\t${cfg.roleArn}`);
|
|
4387
|
+
}
|
|
4388
|
+
});
|
|
4389
|
+
target.command("trust <targetId>").description("reprint the IAM trust-policy setup for an AWS target").option("--org <slug>").action(async (targetId, options) => {
|
|
4390
|
+
const ctx = buildContext();
|
|
4391
|
+
const org = await resolveOrg(ctx, options.org);
|
|
4392
|
+
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
4393
|
+
const t = targets.find((x) => x.id === targetId || x.name === targetId);
|
|
4394
|
+
if (!t) fail(`no target "${targetId}" in ${org.slug}`);
|
|
4395
|
+
const cfg = t.config;
|
|
4396
|
+
if (cfg.provider !== "aws") fail("not an aws target (see `seekrit pg`/`seekrit ssh`)");
|
|
4397
|
+
console.log(awsTrustPolicyInstructions(cfg));
|
|
4398
|
+
});
|
|
4399
|
+
target.command("rm <targetId>").description("delete an AWS role target").option("--org <slug>").action(async (targetId, options) => {
|
|
4400
|
+
const ctx = buildContext();
|
|
4401
|
+
const org = await resolveOrg(ctx, options.org);
|
|
4402
|
+
await ctx.client.deleteLeaseTarget(org.id, targetId);
|
|
4403
|
+
console.error(`deleted ${targetId}`);
|
|
4404
|
+
});
|
|
4405
|
+
aws.command("lease <target>").description("mint short-lived AWS credentials; prints ready-to-source export lines").option("--org <slug>").option("--ttl <duration>", "credential lifetime, e.g. 15m, 1h, 12h", "1h").option("--json", "print the full credential as JSON").action(async (targetRef, options) => {
|
|
4406
|
+
const ctx = buildContext();
|
|
4407
|
+
const org = await resolveOrg(ctx, options.org);
|
|
4408
|
+
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
4409
|
+
const t = targets.find((x) => x.id === targetRef || x.name === targetRef);
|
|
4410
|
+
if (!t) fail(`no target "${targetRef}" in ${org.slug}`);
|
|
4411
|
+
const cfg = t.config;
|
|
4412
|
+
if (cfg.provider !== "aws") fail(`"${t.name}" is not an aws target (see \`seekrit pg\`)`);
|
|
4413
|
+
const ttlSeconds = parseTtlSeconds$6(options.ttl);
|
|
4414
|
+
if (ttlSeconds < 900) fail(`--ttl must be at least ${900 / 60}m (STS minimum)`);
|
|
4415
|
+
const recipient = await generateAwsRecipientKeyPair();
|
|
4416
|
+
const { aws: leased } = await ctx.client.mintLease(org.id, {
|
|
4417
|
+
provider: "aws",
|
|
4418
|
+
targetId: t.id,
|
|
4419
|
+
recipientPublicKey: recipient.publicKeyJwk,
|
|
4420
|
+
ttlSeconds
|
|
4421
|
+
});
|
|
4422
|
+
const cred = await unwrapAwsCredential(leased.wrappedCredential, recipient.privateKeyJwk);
|
|
4423
|
+
console.error(`leased ${cfg.roleArn} in ${cred.region} — expires ${cred.expiration}`);
|
|
4424
|
+
if (options.json) console.log(JSON.stringify({
|
|
4425
|
+
...cred,
|
|
4426
|
+
roleArn: cfg.roleArn
|
|
4427
|
+
}, null, 2));
|
|
4428
|
+
else {
|
|
4429
|
+
console.log(`export AWS_ACCESS_KEY_ID=${cred.accessKeyId}`);
|
|
4430
|
+
console.log(`export AWS_SECRET_ACCESS_KEY=${cred.secretAccessKey}`);
|
|
4431
|
+
console.log(`export AWS_SESSION_TOKEN=${cred.sessionToken}`);
|
|
4432
|
+
console.log(`export AWS_REGION=${cred.region}`);
|
|
4433
|
+
}
|
|
4434
|
+
});
|
|
4435
|
+
aws.command("leases").description("list AWS leases (the ledger — never secret material)").option("--org <slug>").action(async (options) => {
|
|
4436
|
+
const ctx = buildContext();
|
|
4437
|
+
const org = await resolveOrg(ctx, options.org);
|
|
4438
|
+
const { leases } = await ctx.client.listLeases(org.id);
|
|
4439
|
+
for (const l of leases) {
|
|
4440
|
+
if (l.provider !== "aws") continue;
|
|
4441
|
+
console.log(`${l.id}\t${l.status}\texpires ${l.expiresAt}`);
|
|
4442
|
+
}
|
|
4443
|
+
});
|
|
4444
|
+
aws.command("revoke <leaseId>").description("mark a lease revoked in the ledger (STS credentials stay valid until they expire)").option("--org <slug>").action(async (leaseId, options) => {
|
|
4445
|
+
const ctx = buildContext();
|
|
4446
|
+
const org = await resolveOrg(ctx, options.org);
|
|
4447
|
+
await ctx.client.revokeLease(org.id, leaseId);
|
|
4448
|
+
console.error(`revoked ${leaseId} (issued credentials remain valid until they expire)`);
|
|
4449
|
+
});
|
|
4450
|
+
}
|
|
4451
|
+
//#endregion
|
|
4452
|
+
//#region src/billing.ts
|
|
4453
|
+
/** `included: null` means unlimited; an unavailable meter is unknown, not zero. */
|
|
4454
|
+
function usageLine(usage) {
|
|
4455
|
+
if (!usage.available) return "unknown";
|
|
4456
|
+
return `${usage.quantity} of ${usage.included ?? "unlimited"}`;
|
|
4457
|
+
}
|
|
4458
|
+
/**
|
|
4459
|
+
* Plan, usage, and self-serve billing.
|
|
4460
|
+
*
|
|
4461
|
+
* Checkout and the billing portal are browser flows the biller hosts, so those
|
|
4462
|
+
* commands print a URL rather than pretending to complete a payment in the
|
|
4463
|
+
* terminal.
|
|
4464
|
+
*/
|
|
4465
|
+
function registerBillingCommands(program) {
|
|
4466
|
+
const billing = program.command("billing").description("plan, usage, and subscription");
|
|
4467
|
+
billing.command("show", { isDefault: true }).description("show the org's plan, what it includes, and current usage").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
|
|
4468
|
+
const ctx = buildContext();
|
|
4469
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
4470
|
+
const info = await ctx.client.getBilling(ref.id);
|
|
4471
|
+
emit(options, info, () => {
|
|
4472
|
+
printFields([
|
|
4473
|
+
["plan", `${info.plan.name} (${info.plan.id})`],
|
|
4474
|
+
["status", info.subscription?.status ?? "no subscription (default plan)"],
|
|
4475
|
+
["renews", info.subscription?.currentPeriodEnd ?? null],
|
|
4476
|
+
["cancels", info.subscription?.cancelAt ?? null],
|
|
4477
|
+
["trial ends", info.subscription?.trialEndsAt ?? null],
|
|
4478
|
+
["limits enforced", info.enforced ? "yes" : "no (informational only)"],
|
|
4479
|
+
["self-serve", [info.manage.checkout ? `checkout (${info.manage.checkoutFamilies.join(", ")})` : null, info.manage.portal ? "portal" : null].filter(Boolean).join(", ") || "unavailable"]
|
|
4480
|
+
]);
|
|
4481
|
+
section("usage");
|
|
4482
|
+
printTable(info.usage, [col("metric", (u) => u.label), col("used", (u) => usageLine(u))], "nothing metered yet");
|
|
4483
|
+
if (info.overrides.length > 0) {
|
|
4484
|
+
section("overrides");
|
|
4485
|
+
printTable(info.overrides, [
|
|
4486
|
+
col("key", (o) => o.key),
|
|
4487
|
+
col("value", (o) => String(o.value)),
|
|
4488
|
+
col("note", (o) => o.note),
|
|
4489
|
+
col("expires", (o) => o.expiresAt)
|
|
4490
|
+
]);
|
|
4491
|
+
}
|
|
4492
|
+
});
|
|
4493
|
+
});
|
|
4494
|
+
billing.command("entitlements").description("list every entitlement this org resolves to").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
|
|
4495
|
+
const ctx = buildContext();
|
|
4496
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
4497
|
+
const info = await ctx.client.getBilling(ref.id);
|
|
4498
|
+
emit(options, { entitlements: info.entitlements }, () => printTable(info.entitlements, [
|
|
4499
|
+
col("key", (e) => e.key),
|
|
4500
|
+
col("value", (e) => e.value === null ? "unlimited" : String(e.value)),
|
|
4501
|
+
col("source", (e) => e.source)
|
|
4502
|
+
]));
|
|
4503
|
+
});
|
|
4504
|
+
billing.command("checkout <family>").description(`start a self-serve upgrade (${VISIBLE_PLAN_FAMILY_IDS.join(" | ")}) — prints a URL`).option("--org <slug>").action(async (family, options) => {
|
|
4505
|
+
if (!PLAN_FAMILY_IDS.includes(family)) fail(`unknown plan "${family}" — one of: ${PLAN_FAMILY_IDS.join(", ")}`);
|
|
4506
|
+
const ctx = buildContext();
|
|
4507
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
4508
|
+
const { url } = await ctx.client.startCheckout(ref.id, { family });
|
|
4509
|
+
console.error("open this to complete checkout:");
|
|
4510
|
+
console.log(url);
|
|
4511
|
+
});
|
|
4512
|
+
billing.command("portal").description("open the billing portal (prints a URL) to manage payment and invoices").option("--org <slug>").action(async (options) => {
|
|
4513
|
+
const ctx = buildContext();
|
|
4514
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
4515
|
+
const { url } = await ctx.client.openBillingPortal(ref.id);
|
|
4516
|
+
console.error("open this to manage billing:");
|
|
4517
|
+
console.log(url);
|
|
4518
|
+
});
|
|
4519
|
+
billing.command("cancel").description("cancel the subscription and drop back to the Free plan").option("--org <slug>").option("--yes", "skip the confirmation prompt").action(async (options) => {
|
|
4520
|
+
const ctx = buildContext();
|
|
4521
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
4522
|
+
await confirmDestructive(options.yes, `Cancel ${ref.slug}'s subscription and move it to the Free plan?`);
|
|
4523
|
+
await ctx.client.cancelSubscription(ref.id);
|
|
4524
|
+
console.error(`${ref.slug} moved to the Free plan`);
|
|
4525
|
+
});
|
|
4526
|
+
}
|
|
4527
|
+
//#endregion
|
|
3605
4528
|
//#region src/branches.ts
|
|
3606
4529
|
/**
|
|
3607
4530
|
* Resolve `--from` to the environment being branched.
|
|
@@ -3674,17 +4597,21 @@ function registerBranchCommands(program) {
|
|
|
3674
4597
|
console.error(created.branch.expiresAt ? `expires ${created.branch.expiresAt}` : "no expiry — delete it explicitly when the PR closes");
|
|
3675
4598
|
if (grants.length > 0) console.error(`shared with ${grants.length} other reader(s)`);
|
|
3676
4599
|
});
|
|
3677
|
-
branch.command("list").description("list branches in an application (or of one environment)").option("--org <slug>").option("--app <slug>", "application slug (defaults to seekrit.json)").option("--env <slug>", "only branches of this environment").action(async (options) => {
|
|
4600
|
+
branch.command("list").alias("ls").description("list branches in an application (or of one environment)").option("--org <slug>").option("--app <slug>", "application slug (defaults to seekrit.json)").option("--env <slug>", "only branches of this environment").option("--json", "print the raw API response").action(async (options) => {
|
|
3678
4601
|
const ctx = buildContext();
|
|
3679
|
-
|
|
4602
|
+
const branches = options.env ? await (async () => {
|
|
3680
4603
|
const parent = await resolveAppEnv(ctx, options);
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
4604
|
+
return (await ctx.client.listBranches(parent.orgId, parent.envId)).branches;
|
|
4605
|
+
})() : await (async () => {
|
|
4606
|
+
const app = await resolveApp(ctx, options);
|
|
4607
|
+
return (await ctx.client.listAppBranches(app.orgId, app.id)).branches;
|
|
4608
|
+
})();
|
|
4609
|
+
emit(options, { branches }, () => printTable(branches, [
|
|
4610
|
+
col("slug", (b) => b.slug),
|
|
4611
|
+
col("expires", (b) => b.expiresAt ?? "never"),
|
|
4612
|
+
col("created", (b) => b.createdAt),
|
|
4613
|
+
col("id", (b) => b.id)
|
|
4614
|
+
], "no branches — create one with `seekrit branch create`"));
|
|
3688
4615
|
});
|
|
3689
4616
|
branch.command("delete <slug>").alias("rm").description("tear down a branch and everything it overrode").option("--org <slug>").option("--app <slug>", "application slug (defaults to seekrit.json)").action(async (slug, options) => {
|
|
3690
4617
|
const ctx = buildContext();
|
|
@@ -3695,37 +4622,6 @@ function registerBranchCommands(program) {
|
|
|
3695
4622
|
});
|
|
3696
4623
|
}
|
|
3697
4624
|
//#endregion
|
|
3698
|
-
//#region src/dotenv.ts
|
|
3699
|
-
/**
|
|
3700
|
-
* Minimal `.env` parser: `KEY=VALUE`, `#` comments, an optional `export`
|
|
3701
|
-
* prefix, and single/double-quoted values (double quotes honor `\n \t \r \" \\`
|
|
3702
|
-
* escapes; unquoted values drop trailing ` # comments`). Multiline values are
|
|
3703
|
-
* not supported — keep those in seekrit itself.
|
|
3704
|
-
*/
|
|
3705
|
-
function parseDotenv(content) {
|
|
3706
|
-
const out = {};
|
|
3707
|
-
for (const raw of content.split(/\r?\n/)) {
|
|
3708
|
-
let line = raw.trim();
|
|
3709
|
-
if (!line || line.startsWith("#")) continue;
|
|
3710
|
-
if (line.startsWith("export ")) line = line.slice(7).trimStart();
|
|
3711
|
-
const eq = line.indexOf("=");
|
|
3712
|
-
if (eq === -1) continue;
|
|
3713
|
-
const key = line.slice(0, eq).trim();
|
|
3714
|
-
if (!key) continue;
|
|
3715
|
-
let value = line.slice(eq + 1).trim();
|
|
3716
|
-
const quote = value[0];
|
|
3717
|
-
if (value.length >= 2 && (quote === "\"" || quote === "'") && value.at(-1) === quote) {
|
|
3718
|
-
value = value.slice(1, -1);
|
|
3719
|
-
if (quote === "\"") value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
|
|
3720
|
-
} else {
|
|
3721
|
-
const comment = value.indexOf(" #");
|
|
3722
|
-
if (comment !== -1) value = value.slice(0, comment).trim();
|
|
3723
|
-
}
|
|
3724
|
-
out[key] = value;
|
|
3725
|
-
}
|
|
3726
|
-
return out;
|
|
3727
|
-
}
|
|
3728
|
-
//#endregion
|
|
3729
4625
|
//#region src/format.ts
|
|
3730
4626
|
function needsQuoting(value) {
|
|
3731
4627
|
return /[\s"'`$\\#]/.test(value) || value === "";
|
|
@@ -3887,6 +4783,181 @@ function registerGcpCommands(program) {
|
|
|
3887
4783
|
});
|
|
3888
4784
|
}
|
|
3889
4785
|
//#endregion
|
|
4786
|
+
//#region src/groups.ts
|
|
4787
|
+
/**
|
|
4788
|
+
* Shared groups: reusable secret bags composed into application environments
|
|
4789
|
+
* by matching slug (`seekrit env groups add`). A group holds one environment
|
|
4790
|
+
* per slug — the "variants" of the shared values.
|
|
4791
|
+
*/
|
|
4792
|
+
function registerGroupCommands(program) {
|
|
4793
|
+
const group = program.command("group").description("manage shared groups (reusable secret bags)");
|
|
4794
|
+
group.command("list").alias("ls").description("list shared groups in an organization").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
|
|
4795
|
+
const ctx = buildContext();
|
|
4796
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
4797
|
+
const { groups } = await ctx.client.listGroups(ref.id);
|
|
4798
|
+
emit(options, { groups }, () => printTable(groups, [
|
|
4799
|
+
col("slug", (g) => g.slug),
|
|
4800
|
+
col("name", (g) => g.name),
|
|
4801
|
+
col("created", (g) => g.createdAt),
|
|
4802
|
+
col("id", (g) => g.id)
|
|
4803
|
+
], "no groups — create one with `seekrit group create`"));
|
|
4804
|
+
});
|
|
4805
|
+
group.command("show <slug>").description("show a group and the environments (value sets) it holds").option("--org <slug>").option("--json", "print the raw API response").action(async (slug, options) => {
|
|
4806
|
+
const ctx = buildContext();
|
|
4807
|
+
const ref = await resolveGroup(ctx, {
|
|
4808
|
+
org: options.org,
|
|
4809
|
+
group: slug
|
|
4810
|
+
});
|
|
4811
|
+
const { group: row, environments } = await ctx.client.getGroup(ref.orgId, ref.id);
|
|
4812
|
+
emit(options, {
|
|
4813
|
+
group: row,
|
|
4814
|
+
environments
|
|
4815
|
+
}, () => {
|
|
4816
|
+
printFields([
|
|
4817
|
+
["slug", row.slug],
|
|
4818
|
+
["name", row.name],
|
|
4819
|
+
["id", row.id],
|
|
4820
|
+
["created", row.createdAt]
|
|
4821
|
+
]);
|
|
4822
|
+
section("environments");
|
|
4823
|
+
printTable(environments, [
|
|
4824
|
+
col("slug", (e) => e.slug),
|
|
4825
|
+
col("name", (e) => e.name),
|
|
4826
|
+
col("access", (e) => e.canDecrypt ? "can decrypt" : "no key"),
|
|
4827
|
+
col("id", (e) => e.id)
|
|
4828
|
+
], "no environments — create one with `seekrit group env create`");
|
|
4829
|
+
});
|
|
4830
|
+
});
|
|
4831
|
+
group.command("create").description("create a shared group").option("--org <slug>").requiredOption("--name <name>").requiredOption("--slug <slug>").action(async (options) => {
|
|
4832
|
+
const ctx = buildContext();
|
|
4833
|
+
const orgRef = await resolveOrg(ctx, options.org);
|
|
4834
|
+
const created = await ctx.client.createGroup(orgRef.id, {
|
|
4835
|
+
name: options.name,
|
|
4836
|
+
slug: options.slug
|
|
4837
|
+
});
|
|
4838
|
+
console.error(`created group ${created.group.slug} (${created.group.id})`);
|
|
4839
|
+
});
|
|
4840
|
+
group.command("rename <slug>").description("change a group's display name (the slug is permanent)").option("--org <slug>").requiredOption("--name <name>", "new display name").action(async (slug, options) => {
|
|
4841
|
+
const ctx = buildContext();
|
|
4842
|
+
const ref = await resolveGroup(ctx, {
|
|
4843
|
+
org: options.org,
|
|
4844
|
+
group: slug
|
|
4845
|
+
});
|
|
4846
|
+
const { group: row } = await ctx.client.updateGroup(ref.orgId, ref.id, { name: options.name });
|
|
4847
|
+
console.error(`renamed ${row.slug} to "${row.name}"`);
|
|
4848
|
+
});
|
|
4849
|
+
group.command("rm <slug>").alias("delete").description("delete a group, its environments, and their secrets").option("--org <slug>").option("--yes", "skip the confirmation prompt").action(async (slug, options) => {
|
|
4850
|
+
const ctx = buildContext();
|
|
4851
|
+
const ref = await resolveGroup(ctx, {
|
|
4852
|
+
org: options.org,
|
|
4853
|
+
group: slug
|
|
4854
|
+
});
|
|
4855
|
+
const { environments } = await ctx.client.getGroup(ref.orgId, ref.id);
|
|
4856
|
+
await confirmDestructive(options.yes, `Delete group ${ref.slug} and its ${environments.length} environment(s)? Environments composing it will stop receiving these values.`);
|
|
4857
|
+
await ctx.client.deleteGroup(ref.orgId, ref.id);
|
|
4858
|
+
console.error(`deleted group ${ref.slug}`);
|
|
4859
|
+
});
|
|
4860
|
+
const groupEnv = group.command("env").description("manage a group’s environments (per-slug value sets / variants)");
|
|
4861
|
+
groupEnv.command("list").alias("ls").description("list a group's environments").option("--org <slug>").requiredOption("--group <slug>").option("--json", "print the raw API response").action(async (options) => {
|
|
4862
|
+
const ctx = buildContext();
|
|
4863
|
+
const ref = await resolveGroup(ctx, options);
|
|
4864
|
+
const { environments } = await ctx.client.listGroupEnvs(ref.orgId, ref.id);
|
|
4865
|
+
emit(options, { environments }, () => printTable(environments, [
|
|
4866
|
+
col("slug", (e) => e.slug),
|
|
4867
|
+
col("name", (e) => e.name),
|
|
4868
|
+
col("created", (e) => e.createdAt),
|
|
4869
|
+
col("id", (e) => e.id)
|
|
4870
|
+
], "no environments — create one with `seekrit group env create`"));
|
|
4871
|
+
});
|
|
4872
|
+
groupEnv.command("create").description("create a group environment (generates its data key locally)").option("--org <slug>").requiredOption("--group <slug>").requiredOption("--name <name>").requiredOption("--slug <slug>").action(async (options) => {
|
|
4873
|
+
const ctx = buildContext();
|
|
4874
|
+
const groupRef = await resolveGroup(ctx, {
|
|
4875
|
+
org: options.org,
|
|
4876
|
+
group: options.group
|
|
4877
|
+
});
|
|
4878
|
+
const { user } = await ctx.client.me();
|
|
4879
|
+
if (!user.publicKeyJwk) fail("run `seekrit keys setup` first");
|
|
4880
|
+
const dek = generateDek();
|
|
4881
|
+
const wrappedDek = await wrapDek(dek, user.publicKeyJwk);
|
|
4882
|
+
const recoveryWrappedDek = await recoveryWrapForNewEnv(ctx, groupRef.orgId, dek);
|
|
4883
|
+
const created = await ctx.client.createGroupEnv(groupRef.orgId, groupRef.id, {
|
|
4884
|
+
name: options.name,
|
|
4885
|
+
slug: options.slug,
|
|
4886
|
+
wrappedDek,
|
|
4887
|
+
recoveryWrappedDek
|
|
4888
|
+
});
|
|
4889
|
+
console.error(`created ${groupRef.slug}@${created.environment.slug} (${created.environment.id})`);
|
|
4890
|
+
});
|
|
4891
|
+
}
|
|
4892
|
+
//#endregion
|
|
4893
|
+
//#region src/logsink.ts
|
|
4894
|
+
/** Collect repeated `--header Name: value` flags into a map. */
|
|
4895
|
+
function collectHeader(value, acc = {}) {
|
|
4896
|
+
const sep = value.indexOf(":");
|
|
4897
|
+
if (sep <= 0) fail(`expected "Name: value", got "${value}"`);
|
|
4898
|
+
acc[value.slice(0, sep).trim()] = value.slice(sep + 1).trim();
|
|
4899
|
+
return acc;
|
|
4900
|
+
}
|
|
4901
|
+
/**
|
|
4902
|
+
* The org's OTLP audit-log export — near-real-time shipping of every audit row
|
|
4903
|
+
* to a customer SIEM. Header *values* are write-only: they are encrypted at
|
|
4904
|
+
* rest and never echoed back, so `show` reports only the header names.
|
|
4905
|
+
*/
|
|
4906
|
+
function registerLogSinkCommands(program) {
|
|
4907
|
+
const sink = program.command("log-sink").description("stream the audit trail to your own OTLP collector (SIEM)");
|
|
4908
|
+
sink.command("show", { isDefault: true }).description("show the configured log sink and its delivery health").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
|
|
4909
|
+
const ctx = buildContext();
|
|
4910
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
4911
|
+
const { sink: config } = await ctx.client.getLogSink(ref.id);
|
|
4912
|
+
emit(options, { sink: config }, () => {
|
|
4913
|
+
if (!config) {
|
|
4914
|
+
console.error("no log sink configured — set one with `seekrit log-sink set`");
|
|
4915
|
+
return;
|
|
4916
|
+
}
|
|
4917
|
+
printFields([
|
|
4918
|
+
["endpoint", config.endpoint],
|
|
4919
|
+
["enabled", config.enabled ? "yes" : "no"],
|
|
4920
|
+
["headers", config.headerNames.length > 0 ? config.headerNames.join(", ") : "none"],
|
|
4921
|
+
["last success", config.lastSuccessAt ?? "never"],
|
|
4922
|
+
["last attempt", config.lastAttemptAt ?? "never"],
|
|
4923
|
+
["last error", config.lastError ?? "none"]
|
|
4924
|
+
]);
|
|
4925
|
+
});
|
|
4926
|
+
});
|
|
4927
|
+
sink.command("set <endpoint>").description("point the audit export at an OTLP/HTTP logs endpoint").option("--org <slug>").option("--header <name: value>", "auth header to send (repeatable; values are write-only)", collectHeader).option("--clear-headers", "send no headers at all (drops the stored ones)").option("--disabled", "save the config but stop shipping").action(async (endpoint, options) => {
|
|
4928
|
+
if (options.header && options.clearHeaders) fail("pass either --header or --clear-headers, not both");
|
|
4929
|
+
const ctx = buildContext();
|
|
4930
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
4931
|
+
const headers = options.clearHeaders ? {} : options.header;
|
|
4932
|
+
const { sink: config } = await ctx.client.setLogSink(ref.id, {
|
|
4933
|
+
endpoint,
|
|
4934
|
+
headers,
|
|
4935
|
+
enabled: !options.disabled
|
|
4936
|
+
});
|
|
4937
|
+
console.error(`log sink → ${config.endpoint} (${config.enabled ? "enabled" : "disabled"}) — test it with \`seekrit log-sink test\``);
|
|
4938
|
+
});
|
|
4939
|
+
sink.command("test").description("send a probe to the configured endpoint and report the result").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
|
|
4940
|
+
const ctx = buildContext();
|
|
4941
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
4942
|
+
const result = await ctx.client.testLogSink(ref.id);
|
|
4943
|
+
emit(options, result, () => {
|
|
4944
|
+
printFields([
|
|
4945
|
+
["result", result.ok ? "ok" : "failed"],
|
|
4946
|
+
["status", result.status],
|
|
4947
|
+
["error", result.error]
|
|
4948
|
+
]);
|
|
4949
|
+
});
|
|
4950
|
+
if (!result.ok) process.exitCode = 1;
|
|
4951
|
+
});
|
|
4952
|
+
sink.command("rm").alias("delete").description("stop exporting the audit trail and forget the endpoint").option("--org <slug>").option("--yes", "skip the confirmation prompt").action(async (options) => {
|
|
4953
|
+
const ctx = buildContext();
|
|
4954
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
4955
|
+
await confirmDestructive(options.yes, `Remove ${ref.slug}'s audit log export?`);
|
|
4956
|
+
await ctx.client.deleteLogSink(ref.id);
|
|
4957
|
+
console.error("log sink removed");
|
|
4958
|
+
});
|
|
4959
|
+
}
|
|
4960
|
+
//#endregion
|
|
3890
4961
|
//#region src/m2m.ts
|
|
3891
4962
|
/**
|
|
3892
4963
|
* Resolve M2M client credentials from (in order) the process environment, a
|
|
@@ -4278,6 +5349,164 @@ function collect$3(value, acc) {
|
|
|
4278
5349
|
return acc;
|
|
4279
5350
|
}
|
|
4280
5351
|
//#endregion
|
|
5352
|
+
//#region src/orgs.ts
|
|
5353
|
+
/**
|
|
5354
|
+
* Counts for `org show`. Each list is admin-gated to a different degree, so a
|
|
5355
|
+
* member who can't read one still gets the rest: a failed call reports as
|
|
5356
|
+
* `null`, which `printFields` drops and JSON preserves as "not visible to you".
|
|
5357
|
+
*/
|
|
5358
|
+
async function countOrNull(load) {
|
|
5359
|
+
try {
|
|
5360
|
+
return await load();
|
|
5361
|
+
} catch {
|
|
5362
|
+
return null;
|
|
5363
|
+
}
|
|
5364
|
+
}
|
|
5365
|
+
async function orgOverview(ctx, orgId) {
|
|
5366
|
+
const [apps, groups, members, tokens] = await Promise.all([
|
|
5367
|
+
countOrNull(async () => (await ctx.client.listApps(orgId)).apps.length),
|
|
5368
|
+
countOrNull(async () => (await ctx.client.listGroups(orgId)).groups.length),
|
|
5369
|
+
countOrNull(async () => (await ctx.client.listMembers(orgId)).members.length),
|
|
5370
|
+
countOrNull(async () => (await ctx.client.listTokens(orgId)).tokens.length)
|
|
5371
|
+
]);
|
|
5372
|
+
return {
|
|
5373
|
+
apps,
|
|
5374
|
+
groups,
|
|
5375
|
+
members,
|
|
5376
|
+
tokens
|
|
5377
|
+
};
|
|
5378
|
+
}
|
|
5379
|
+
function registerOrgCommands(program) {
|
|
5380
|
+
const org = program.command("org").description("manage organizations");
|
|
5381
|
+
org.command("list").alias("ls").description("list the organizations you can access").option("--json", "print the raw API response").action(async (options) => {
|
|
5382
|
+
const { orgs } = await buildContext().client.listOrgs();
|
|
5383
|
+
emit(options, { orgs }, () => printTable(orgs, [
|
|
5384
|
+
col("slug", (o) => o.slug),
|
|
5385
|
+
col("name", (o) => o.name),
|
|
5386
|
+
col("role", (o) => o.role),
|
|
5387
|
+
col("id", (o) => o.id)
|
|
5388
|
+
], "no organizations — create one with `seekrit org create`"));
|
|
5389
|
+
});
|
|
5390
|
+
org.command("show [slug]").description("show an organization and what it contains").option("--org <slug>", "organization slug (or pass it as the argument)").option("--json", "print the raw API response").action(async (slug, options) => {
|
|
5391
|
+
const ctx = buildContext();
|
|
5392
|
+
const ref = await resolveOrg(ctx, slug ?? options.org);
|
|
5393
|
+
const { org: row } = await ctx.client.getOrg(ref.id);
|
|
5394
|
+
const counts = await orgOverview(ctx, ref.id);
|
|
5395
|
+
const shown = (n) => n === null ? "—" : n;
|
|
5396
|
+
emit(options, {
|
|
5397
|
+
org: row,
|
|
5398
|
+
counts
|
|
5399
|
+
}, () => {
|
|
5400
|
+
printFields([
|
|
5401
|
+
["slug", row.slug],
|
|
5402
|
+
["name", row.name],
|
|
5403
|
+
["id", row.id],
|
|
5404
|
+
["your role", row.role],
|
|
5405
|
+
["created", row.createdAt],
|
|
5406
|
+
["applications", shown(counts.apps)],
|
|
5407
|
+
["groups", shown(counts.groups)],
|
|
5408
|
+
["members", shown(counts.members)],
|
|
5409
|
+
["service tokens", shown(counts.tokens)]
|
|
5410
|
+
]);
|
|
5411
|
+
});
|
|
5412
|
+
});
|
|
5413
|
+
org.command("create").description("create an organization (you become its owner)").requiredOption("--name <name>").requiredOption("--slug <slug>").action(async (options) => {
|
|
5414
|
+
const created = await buildContext().client.createOrg({
|
|
5415
|
+
name: options.name,
|
|
5416
|
+
slug: options.slug
|
|
5417
|
+
});
|
|
5418
|
+
console.error(`created org ${created.org.slug} (${created.org.id})`);
|
|
5419
|
+
});
|
|
5420
|
+
org.command("rename").description("change an organization's display name (the slug is permanent)").option("--org <slug>").requiredOption("--name <name>", "new display name").action(async (options) => {
|
|
5421
|
+
const ctx = buildContext();
|
|
5422
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
5423
|
+
const { org: row } = await ctx.client.updateOrg(ref.id, { name: options.name });
|
|
5424
|
+
console.error(`renamed ${row.slug} to "${row.name}"`);
|
|
5425
|
+
});
|
|
5426
|
+
org.command("member").description("view organization members").command("list").alias("ls").description("list members, and whether each has finished key setup").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
|
|
5427
|
+
const ctx = buildContext();
|
|
5428
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
5429
|
+
const { members } = await ctx.client.listMembers(ref.id);
|
|
5430
|
+
emit(options, { members }, () => printTable(members, [
|
|
5431
|
+
col("email", (m) => m.email),
|
|
5432
|
+
col("role", (m) => m.role),
|
|
5433
|
+
col("name", (m) => m.name),
|
|
5434
|
+
col("keys", (m) => m.publicKeyJwk ? "ready" : "pending"),
|
|
5435
|
+
col("id", (m) => m.userId)
|
|
5436
|
+
], "no members"));
|
|
5437
|
+
});
|
|
5438
|
+
const invite = org.command("invite").description("manage pending invitations");
|
|
5439
|
+
invite.command("list").alias("ls").description("list outstanding invitations").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
|
|
5440
|
+
const ctx = buildContext();
|
|
5441
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
5442
|
+
const { invites } = await ctx.client.listInvites(ref.id);
|
|
5443
|
+
emit(options, { invites }, () => printTable(invites, [
|
|
5444
|
+
col("email", (i) => i.email),
|
|
5445
|
+
col("role", (i) => i.role),
|
|
5446
|
+
col("invited", (i) => i.createdAt),
|
|
5447
|
+
col("id", (i) => i.id)
|
|
5448
|
+
], "no pending invitations"));
|
|
5449
|
+
});
|
|
5450
|
+
invite.command("add <email>").description("invite someone to the organization").option("--org <slug>").option("--role <role>", "admin | member", "member").action(async (email, options) => {
|
|
5451
|
+
if (options.role !== "admin" && options.role !== "member") fail("--role must be admin or member");
|
|
5452
|
+
const ctx = buildContext();
|
|
5453
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
5454
|
+
const { invite: row } = await ctx.client.createInvite(ref.id, {
|
|
5455
|
+
email,
|
|
5456
|
+
role: options.role
|
|
5457
|
+
});
|
|
5458
|
+
console.error(`invited ${row.email} as ${row.role} (${row.id}) — they join when they first sign in`);
|
|
5459
|
+
});
|
|
5460
|
+
invite.command("rm <inviteId>").description("rescind an invitation").option("--org <slug>").action(async (inviteId, options) => {
|
|
5461
|
+
const ctx = buildContext();
|
|
5462
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
5463
|
+
await ctx.client.revokeInvite(ref.id, inviteId);
|
|
5464
|
+
console.error(`${inviteId} revoked`);
|
|
5465
|
+
});
|
|
5466
|
+
org.command("mfa").description("show or set the org-wide second-factor requirement").option("--org <slug>").option("--set <policy>", "required | optional").option("--json", "print the raw API response").action(async (options) => {
|
|
5467
|
+
const ctx = buildContext();
|
|
5468
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
5469
|
+
if (options.set !== void 0 && options.set !== "required" && options.set !== "optional") fail("--set must be required or optional");
|
|
5470
|
+
const policy = options.set === void 0 ? await ctx.client.getMfaPolicy(ref.id) : await ctx.client.setMfaPolicy(ref.id, { required: options.set === "required" });
|
|
5471
|
+
emit(options, policy, () => {
|
|
5472
|
+
if (!policy.configured) {
|
|
5473
|
+
console.error("no identity provider configured — MFA cannot be enforced here");
|
|
5474
|
+
return;
|
|
5475
|
+
}
|
|
5476
|
+
console.log(policy.required ? "required for all members" : "optional");
|
|
5477
|
+
});
|
|
5478
|
+
});
|
|
5479
|
+
org.command("tree").description("print the org's applications, environments, and groups as a tree").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
|
|
5480
|
+
const ctx = buildContext();
|
|
5481
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
5482
|
+
const [{ apps }, { groups }] = await Promise.all([ctx.client.listApps(ref.id), ctx.client.listGroups(ref.id)]);
|
|
5483
|
+
const appTree = await Promise.all(apps.map(async (a) => ({
|
|
5484
|
+
...a,
|
|
5485
|
+
environments: (await ctx.client.listEnvs(ref.id, a.id)).environments
|
|
5486
|
+
})));
|
|
5487
|
+
const groupTree = await Promise.all(groups.map(async (g) => ({
|
|
5488
|
+
...g,
|
|
5489
|
+
environments: (await ctx.client.listGroupEnvs(ref.id, g.id)).environments
|
|
5490
|
+
})));
|
|
5491
|
+
emit(options, {
|
|
5492
|
+
org: ref.slug,
|
|
5493
|
+
apps: appTree,
|
|
5494
|
+
groups: groupTree
|
|
5495
|
+
}, () => {
|
|
5496
|
+
console.log(ref.slug);
|
|
5497
|
+
for (const a of appTree) {
|
|
5498
|
+
console.log(` app ${a.slug}`);
|
|
5499
|
+
for (const e of a.environments) console.log(` ${e.slug}`);
|
|
5500
|
+
}
|
|
5501
|
+
for (const g of groupTree) {
|
|
5502
|
+
console.log(` group ${g.slug}`);
|
|
5503
|
+
for (const e of g.environments) console.log(` ${e.slug}`);
|
|
5504
|
+
}
|
|
5505
|
+
if (appTree.length === 0 && groupTree.length === 0) console.error("no applications or groups yet");
|
|
5506
|
+
});
|
|
5507
|
+
});
|
|
5508
|
+
}
|
|
5509
|
+
//#endregion
|
|
4281
5510
|
//#region src/pg.ts
|
|
4282
5511
|
/**
|
|
4283
5512
|
* `seekrit pg` — temporary Postgres credentials (Vault-style dynamic secrets).
|
|
@@ -4856,6 +6085,208 @@ function collect(value, acc) {
|
|
|
4856
6085
|
return acc;
|
|
4857
6086
|
}
|
|
4858
6087
|
//#endregion
|
|
6088
|
+
//#region src/sync.ts
|
|
6089
|
+
function assertProvider(value) {
|
|
6090
|
+
if (!SYNC_PROVIDER_KINDS.includes(value)) fail(`unknown provider "${value}" — one of: ${SYNC_PROVIDER_KINDS.join(", ")}`);
|
|
6091
|
+
return value;
|
|
6092
|
+
}
|
|
6093
|
+
/** Account-scope config for a connection (never the credential itself). */
|
|
6094
|
+
function buildConfig(provider, options) {
|
|
6095
|
+
switch (provider) {
|
|
6096
|
+
case "vercel": return {
|
|
6097
|
+
provider: "vercel",
|
|
6098
|
+
...options.teamId ? { teamId: options.teamId } : {}
|
|
6099
|
+
};
|
|
6100
|
+
}
|
|
6101
|
+
}
|
|
6102
|
+
/** Where inside the platform a binding writes. */
|
|
6103
|
+
function buildDestination(provider, options) {
|
|
6104
|
+
switch (provider) {
|
|
6105
|
+
case "vercel": {
|
|
6106
|
+
if (!options.project) fail("--project is required for vercel (a prj_… id or project name)");
|
|
6107
|
+
const targets = (options.target ?? "production").split(",").map((t) => t.trim());
|
|
6108
|
+
const unknown = targets.filter((t) => !VERCEL_TARGETS.includes(t));
|
|
6109
|
+
if (unknown.length > 0) fail(`unknown --target ${unknown.join(", ")} — one of: ${VERCEL_TARGETS.join(", ")}`);
|
|
6110
|
+
return {
|
|
6111
|
+
provider: "vercel",
|
|
6112
|
+
projectId: options.project,
|
|
6113
|
+
targets,
|
|
6114
|
+
...options.gitBranch ? { gitBranch: options.gitBranch } : {}
|
|
6115
|
+
};
|
|
6116
|
+
}
|
|
6117
|
+
}
|
|
6118
|
+
}
|
|
6119
|
+
/** One-line description of a destination, for list output. */
|
|
6120
|
+
function describeDestination(destination) {
|
|
6121
|
+
switch (destination.provider) {
|
|
6122
|
+
case "vercel": return `${destination.projectId} (${destination.targets.join(", ")}${destination.gitBranch ? ` @${destination.gitBranch}` : ""})`;
|
|
6123
|
+
}
|
|
6124
|
+
}
|
|
6125
|
+
/** Find a connection by id or name — nobody keeps `syc_…` ids in their head. */
|
|
6126
|
+
async function resolveConnection(ctx, orgId, ref) {
|
|
6127
|
+
const { connections } = await ctx.client.listSyncConnections(orgId);
|
|
6128
|
+
const matches = connections.filter((c) => c.id === ref || c.name === ref);
|
|
6129
|
+
if (matches.length === 0) fail(`no sync connection "${ref}"`);
|
|
6130
|
+
if (matches.length > 1) fail(`"${ref}" matches ${matches.length} connections — use the id`);
|
|
6131
|
+
return matches[0];
|
|
6132
|
+
}
|
|
6133
|
+
function registerSyncCommands(program) {
|
|
6134
|
+
const sync = program.command("sync").description("push environments to a third-party platform (Vercel, …)");
|
|
6135
|
+
sync.command("connections").alias("conns").description("list destination accounts seekrit can push to").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
|
|
6136
|
+
const ctx = buildContext();
|
|
6137
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
6138
|
+
const { connections } = await ctx.client.listSyncConnections(ref.id);
|
|
6139
|
+
emit(options, { connections }, () => printTable(connections, [
|
|
6140
|
+
col("name", (c) => c.name),
|
|
6141
|
+
col("provider", (c) => c.provider),
|
|
6142
|
+
col("status", (c) => c.status),
|
|
6143
|
+
col("error", (c) => c.lastError),
|
|
6144
|
+
col("id", (c) => c.id)
|
|
6145
|
+
], "no connections — add one with `seekrit sync connect`"));
|
|
6146
|
+
});
|
|
6147
|
+
sync.command("connect").description("register a destination account (reads its API token from stdin)").option("--org <slug>").requiredOption("--name <name>", "what to call this account, e.g. acme-vercel").option("--provider <kind>", SYNC_PROVIDER_KINDS.join(" | "), "vercel").option("--team-id <id>", "vercel: Team id (omit for a personal account)").action(async (options) => {
|
|
6148
|
+
const provider = assertProvider(options.provider);
|
|
6149
|
+
const ctx = buildContext();
|
|
6150
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
6151
|
+
const credential = (process.stdin.isTTY ? await promptHidden(`${provider} API token: `) : await readStdin()).trim();
|
|
6152
|
+
if (!credential) fail("no API token given");
|
|
6153
|
+
const id = randomId("syc");
|
|
6154
|
+
const { publicKeyJwk } = await ctx.client.getSyncConnectionKey(ref.id, id);
|
|
6155
|
+
const created = await ctx.client.createSyncConnection(ref.id, {
|
|
6156
|
+
id,
|
|
6157
|
+
name: options.name,
|
|
6158
|
+
config: buildConfig(provider, options),
|
|
6159
|
+
wrappedCredential: await wrapDek(new TextEncoder().encode(credential), publicKeyJwk)
|
|
6160
|
+
});
|
|
6161
|
+
console.error(`connected ${created.connection.name} (${created.connection.id})`);
|
|
6162
|
+
});
|
|
6163
|
+
sync.command("verify <connection>").description("check a stored credential against a destination").option("--org <slug>").option("--provider <kind>", SYNC_PROVIDER_KINDS.join(" | "), "vercel").option("--project <id>", "vercel: project id or name").option("--target <list>", "vercel: comma-separated targets", "production").option("--git-branch <branch>", "vercel: restrict preview writes to one branch").option("--json", "print the raw API response").action(async (connection, options) => {
|
|
6164
|
+
const provider = assertProvider(options.provider);
|
|
6165
|
+
const ctx = buildContext();
|
|
6166
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
6167
|
+
const conn = await resolveConnection(ctx, ref.id, connection);
|
|
6168
|
+
const result = await ctx.client.verifySyncConnection(ref.id, conn.id, buildDestination(provider, options));
|
|
6169
|
+
emit(options, result, () => printFields([["result", result.ok ? "ok" : "failed"], ["error", result.error ?? null]]));
|
|
6170
|
+
if (!result.ok) process.exitCode = 1;
|
|
6171
|
+
});
|
|
6172
|
+
sync.command("disconnect <connection>").description("delete a destination account, its bindings, and its keypair").option("--org <slug>").option("--yes", "skip the confirmation prompt").action(async (connection, options) => {
|
|
6173
|
+
const ctx = buildContext();
|
|
6174
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
6175
|
+
const conn = await resolveConnection(ctx, ref.id, connection);
|
|
6176
|
+
const { bindings } = await ctx.client.listSyncBindings(ref.id);
|
|
6177
|
+
const mine = bindings.filter((b) => b.connectionId === conn.id);
|
|
6178
|
+
await confirmDestructive(options.yes, `Delete connection ${conn.name} and its ${mine.length} binding(s)? Values already pushed stay on the destination.`);
|
|
6179
|
+
await ctx.client.deleteSyncConnection(ref.id, conn.id);
|
|
6180
|
+
console.error(`disconnected ${conn.name}`);
|
|
6181
|
+
});
|
|
6182
|
+
sync.command("bindings").description("list which environments are syncing where").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
|
|
6183
|
+
const ctx = buildContext();
|
|
6184
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
6185
|
+
const [{ bindings }, { connections }] = await Promise.all([ctx.client.listSyncBindings(ref.id), ctx.client.listSyncConnections(ref.id)]);
|
|
6186
|
+
const names = new Map(connections.map((c) => [c.id, c.name]));
|
|
6187
|
+
emit(options, { bindings }, () => printTable(bindings, [
|
|
6188
|
+
col("connection", (b) => names.get(b.connectionId) ?? b.connectionId),
|
|
6189
|
+
col("destination", (b) => describeDestination(b.destination)),
|
|
6190
|
+
col("env", (b) => b.environmentId),
|
|
6191
|
+
col("mode", (b) => b.enabled ? b.mode : "disabled"),
|
|
6192
|
+
col("last run", (b) => b.lastRunAt),
|
|
6193
|
+
col("error", (b) => b.lastError),
|
|
6194
|
+
col("id", (b) => b.id)
|
|
6195
|
+
], "nothing is syncing — enable it with `seekrit sync enable`"));
|
|
6196
|
+
});
|
|
6197
|
+
sync.command("enable").description("start syncing one environment to a destination (lets seekrit decrypt it)").option("--org <slug>").requiredOption("--connection <name>", "destination account, by name or id").option("--app <slug>", "application slug (defaults to seekrit.json)").requiredOption("--env <slug>", "the environment to push").option("--provider <kind>", SYNC_PROVIDER_KINDS.join(" | "), "vercel").option("--project <id>", "vercel: project id or name").option("--target <list>", "vercel: comma-separated targets", "production").option("--git-branch <branch>", "vercel: restrict preview writes to one branch").option("--prefix <prefix>", "prepend this to every destination key name").option("--include <globs>", "comma-separated name globs to push (default: all)").option("--exclude <globs>", "comma-separated name globs to skip").option("--on-delete <action>", "delete | retain — what happens when a secret is removed", "delete").option("--mode <mode>", "auto (push on write) | manual", "auto").option("--acknowledge-decryption", "confirm that seekrit's servers may decrypt this environment to push it").action(async (options) => {
|
|
6198
|
+
const provider = assertProvider(options.provider);
|
|
6199
|
+
if (options.onDelete !== "delete" && options.onDelete !== "retain") fail("--on-delete must be delete or retain");
|
|
6200
|
+
if (options.mode !== "auto" && options.mode !== "manual") fail("--mode must be auto or manual");
|
|
6201
|
+
const ctx = buildContext();
|
|
6202
|
+
const target = await resolveAppEnv(ctx, options);
|
|
6203
|
+
const conn = await resolveConnection(ctx, target.orgId, options.connection);
|
|
6204
|
+
const destination = buildDestination(provider, options);
|
|
6205
|
+
await confirmDestructive(options.acknowledgeDecryption, `Enabling sync lets seekrit's servers decrypt ${target.appSlug}/${target.envSlug} in memory to push it to ${conn.name}. Continue?`, "--acknowledge-decryption");
|
|
6206
|
+
const resolved = await ctx.client.resolve({ env: target.envId });
|
|
6207
|
+
const privateKey = await getPrivateKey(ctx);
|
|
6208
|
+
const wrappedDeks = await Promise.all(resolved.layers.map(async (layer) => {
|
|
6209
|
+
let dek;
|
|
6210
|
+
try {
|
|
6211
|
+
dek = await unwrapDek(layer.wrappedDek, privateKey);
|
|
6212
|
+
} catch {
|
|
6213
|
+
fail(`you hold no key for ${layer.groupSlug ? `group "${layer.groupSlug}"` : "this environment"}, so sync cannot be granted access to it`);
|
|
6214
|
+
}
|
|
6215
|
+
return {
|
|
6216
|
+
environmentId: layer.environmentId,
|
|
6217
|
+
wrappedDek: await wrapDek(dek, conn.publicKeyJwk)
|
|
6218
|
+
};
|
|
6219
|
+
}));
|
|
6220
|
+
const globs = (raw) => raw ? raw.split(",").map((g) => g.trim()).filter(Boolean) : void 0;
|
|
6221
|
+
const { binding } = await ctx.client.createSyncBinding(target.orgId, {
|
|
6222
|
+
connectionId: conn.id,
|
|
6223
|
+
environmentId: target.envId,
|
|
6224
|
+
destination,
|
|
6225
|
+
...options.prefix ? { nameTransform: { prefix: options.prefix } } : {},
|
|
6226
|
+
...globs(options.include) ? { include: globs(options.include) } : {},
|
|
6227
|
+
...globs(options.exclude) ? { exclude: globs(options.exclude) } : {},
|
|
6228
|
+
onDelete: options.onDelete,
|
|
6229
|
+
mode: options.mode,
|
|
6230
|
+
wrappedDeks,
|
|
6231
|
+
acknowledgedDecryption: true
|
|
6232
|
+
});
|
|
6233
|
+
console.error(`syncing ${target.appSlug}/${target.envSlug} → ${conn.name} ${describeDestination(destination)} (${binding.id})`);
|
|
6234
|
+
if (binding.mode === "manual") console.error("mode is manual — push with `seekrit sync run`");
|
|
6235
|
+
});
|
|
6236
|
+
sync.command("pause <bindingId>").description("stop pushing on this binding without deleting it").option("--org <slug>").action(async (bindingId, options) => {
|
|
6237
|
+
const ctx = buildContext();
|
|
6238
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
6239
|
+
await ctx.client.updateSyncBinding(ref.id, bindingId, { enabled: false });
|
|
6240
|
+
console.error(`${bindingId} paused`);
|
|
6241
|
+
});
|
|
6242
|
+
sync.command("resume <bindingId>").description("start pushing on a paused binding again").option("--org <slug>").action(async (bindingId, options) => {
|
|
6243
|
+
const ctx = buildContext();
|
|
6244
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
6245
|
+
await ctx.client.updateSyncBinding(ref.id, bindingId, { enabled: true });
|
|
6246
|
+
console.error(`${bindingId} resumed`);
|
|
6247
|
+
});
|
|
6248
|
+
sync.command("disable <bindingId>").alias("rm").description("stop syncing an environment and revoke seekrit's key for it").option("--org <slug>").option("--yes", "skip the confirmation prompt").action(async (bindingId, options) => {
|
|
6249
|
+
const ctx = buildContext();
|
|
6250
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
6251
|
+
await confirmDestructive(options.yes, `Delete binding ${bindingId}? Values already pushed stay on the destination.`);
|
|
6252
|
+
await ctx.client.deleteSyncBinding(ref.id, bindingId);
|
|
6253
|
+
console.error(`${bindingId} deleted`);
|
|
6254
|
+
});
|
|
6255
|
+
sync.command("run <bindingId>").description("push now, synchronously, and report what landed").option("--org <slug>").option("--json", "print the raw API response").action(async (bindingId, options) => {
|
|
6256
|
+
const ctx = buildContext();
|
|
6257
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
6258
|
+
const { run } = await ctx.client.runSyncBinding(ref.id, bindingId);
|
|
6259
|
+
emit(options, { run }, () => {
|
|
6260
|
+
printFields([
|
|
6261
|
+
["status", run.status],
|
|
6262
|
+
["pushed", run.pushed.length],
|
|
6263
|
+
["deleted", run.deleted.length],
|
|
6264
|
+
["failed", run.failures.length],
|
|
6265
|
+
["error", run.error]
|
|
6266
|
+
]);
|
|
6267
|
+
if (run.failures.length > 0) {
|
|
6268
|
+
section("failures");
|
|
6269
|
+
printTable(run.failures, [col("name", (f) => f.name), col("error", (f) => f.error)]);
|
|
6270
|
+
}
|
|
6271
|
+
});
|
|
6272
|
+
if (run.status !== "succeeded") process.exitCode = 1;
|
|
6273
|
+
});
|
|
6274
|
+
sync.command("runs").description("show the sync run history").option("--org <slug>").option("--binding <id>", "only runs of this binding").option("--json", "print the raw API response").action(async (options) => {
|
|
6275
|
+
const ctx = buildContext();
|
|
6276
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
6277
|
+
const { runs } = await ctx.client.listSyncRuns(ref.id, options.binding);
|
|
6278
|
+
emit(options, { runs }, () => printTable(runs, [
|
|
6279
|
+
col("started", (r) => r.startedAt),
|
|
6280
|
+
col("status", (r) => r.status),
|
|
6281
|
+
col("trigger", (r) => r.trigger),
|
|
6282
|
+
col("pushed", (r) => r.pushedCount),
|
|
6283
|
+
col("deleted", (r) => r.deletedCount),
|
|
6284
|
+
col("error", (r) => r.error),
|
|
6285
|
+
col("binding", (r) => r.bindingId)
|
|
6286
|
+
], "no sync runs yet"));
|
|
6287
|
+
});
|
|
6288
|
+
}
|
|
6289
|
+
//#endregion
|
|
4859
6290
|
//#region src/web-login.ts
|
|
4860
6291
|
/**
|
|
4861
6292
|
* `seekrit login` — sign in through the browser.
|
|
@@ -5121,101 +6552,9 @@ program.command("init").description("link this directory to an org/app (environm
|
|
|
5121
6552
|
});
|
|
5122
6553
|
console.error(`linked ${org.slug}/${app.slug} → ${path} (environment is selected by the service token at runtime)`);
|
|
5123
6554
|
});
|
|
5124
|
-
program
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
slug: options.slug
|
|
5128
|
-
});
|
|
5129
|
-
console.error(`created org ${created.org.slug} (${created.org.id})`);
|
|
5130
|
-
});
|
|
5131
|
-
program.command("app").description("manage applications").command("create").option("--org <slug>").requiredOption("--name <name>").requiredOption("--slug <slug>").action(async (options) => {
|
|
5132
|
-
const ctx = buildContext();
|
|
5133
|
-
const orgRef = await resolveOrg(ctx, options.org);
|
|
5134
|
-
const created = await ctx.client.createApp(orgRef.id, {
|
|
5135
|
-
name: options.name,
|
|
5136
|
-
slug: options.slug
|
|
5137
|
-
});
|
|
5138
|
-
console.error(`created app ${created.app.slug} (${created.app.id})`);
|
|
5139
|
-
});
|
|
5140
|
-
const env = program.command("env").description("manage environments");
|
|
5141
|
-
env.command("create").description("create an application environment (generates its data key locally)").option("--org <slug>").requiredOption("--app <slug>").requiredOption("--name <name>").requiredOption("--slug <slug>").action(async (options) => {
|
|
5142
|
-
const ctx = buildContext();
|
|
5143
|
-
const orgRef = await resolveOrg(ctx, options.org);
|
|
5144
|
-
const { apps } = await ctx.client.listApps(orgRef.id);
|
|
5145
|
-
const appRow = apps.find((a) => a.slug === options.app || a.id === options.app);
|
|
5146
|
-
if (!appRow) fail(`no app "${options.app}" in ${orgRef.slug}`);
|
|
5147
|
-
const { user } = await ctx.client.me();
|
|
5148
|
-
if (!user.publicKeyJwk) fail("run `seekrit keys setup` first");
|
|
5149
|
-
const dek = generateDek();
|
|
5150
|
-
const wrappedDek = await wrapDek(dek, user.publicKeyJwk);
|
|
5151
|
-
const recoveryWrappedDek = await recoveryWrapForNewEnv(ctx, orgRef.id, dek);
|
|
5152
|
-
const created = await ctx.client.createEnv(orgRef.id, appRow.id, {
|
|
5153
|
-
name: options.name,
|
|
5154
|
-
slug: options.slug,
|
|
5155
|
-
wrappedDek,
|
|
5156
|
-
recoveryWrappedDek
|
|
5157
|
-
});
|
|
5158
|
-
console.error(`created environment ${created.environment.slug} (${created.environment.id})`);
|
|
5159
|
-
});
|
|
5160
|
-
const envGroups = env.command("groups").description("compose shared groups into an application environment");
|
|
5161
|
-
envGroups.command("add").description("compose a group into an app environment").option("--org <slug>").requiredOption("--app <slug>").requiredOption("--env <slug>").requiredOption("--group <slug>").option("--position <n>", "precedence among groups (higher wins)").action(async (options) => {
|
|
5162
|
-
const ctx = buildContext();
|
|
5163
|
-
const target = await resolveAppEnv(ctx, options);
|
|
5164
|
-
const group = await resolveGroup(ctx, {
|
|
5165
|
-
org: options.org,
|
|
5166
|
-
group: options.group
|
|
5167
|
-
});
|
|
5168
|
-
await ctx.client.linkEnvGroup(target.orgId, target.envId, {
|
|
5169
|
-
groupId: group.id,
|
|
5170
|
-
position: options.position === void 0 ? void 0 : Number.parseInt(options.position, 10)
|
|
5171
|
-
});
|
|
5172
|
-
console.error(`composed ${group.slug} into ${target.appSlug}/${target.envSlug}`);
|
|
5173
|
-
});
|
|
5174
|
-
envGroups.command("list").description("list groups composed into an app environment").option("--org <slug>").requiredOption("--app <slug>").requiredOption("--env <slug>").action(async (options) => {
|
|
5175
|
-
const ctx = buildContext();
|
|
5176
|
-
const target = await resolveAppEnv(ctx, options);
|
|
5177
|
-
const { groups } = await ctx.client.listEnvGroups(target.orgId, target.envId);
|
|
5178
|
-
for (const g of groups) console.log(`${g.position}\t${g.slug}\t${g.name}`);
|
|
5179
|
-
});
|
|
5180
|
-
envGroups.command("rm").description("remove a group from an app environment").option("--org <slug>").requiredOption("--app <slug>").requiredOption("--env <slug>").requiredOption("--group <slug>").action(async (options) => {
|
|
5181
|
-
const ctx = buildContext();
|
|
5182
|
-
const target = await resolveAppEnv(ctx, options);
|
|
5183
|
-
const group = await resolveGroup(ctx, {
|
|
5184
|
-
org: options.org,
|
|
5185
|
-
group: options.group
|
|
5186
|
-
});
|
|
5187
|
-
await ctx.client.unlinkEnvGroup(target.orgId, target.envId, group.id);
|
|
5188
|
-
console.error(`removed ${group.slug} from ${target.appSlug}/${target.envSlug}`);
|
|
5189
|
-
});
|
|
5190
|
-
const group = program.command("group").description("manage shared groups (reusable secret bags)");
|
|
5191
|
-
group.command("create").option("--org <slug>").requiredOption("--name <name>").requiredOption("--slug <slug>").action(async (options) => {
|
|
5192
|
-
const ctx = buildContext();
|
|
5193
|
-
const orgRef = await resolveOrg(ctx, options.org);
|
|
5194
|
-
const created = await ctx.client.createGroup(orgRef.id, {
|
|
5195
|
-
name: options.name,
|
|
5196
|
-
slug: options.slug
|
|
5197
|
-
});
|
|
5198
|
-
console.error(`created group ${created.group.slug} (${created.group.id})`);
|
|
5199
|
-
});
|
|
5200
|
-
group.command("env").description("manage a group’s environments (per-slug value sets / variants)").command("create").description("create a group environment (generates its data key locally)").option("--org <slug>").requiredOption("--group <slug>").requiredOption("--name <name>").requiredOption("--slug <slug>").action(async (options) => {
|
|
5201
|
-
const ctx = buildContext();
|
|
5202
|
-
const groupRef = await resolveGroup(ctx, {
|
|
5203
|
-
org: options.org,
|
|
5204
|
-
group: options.group
|
|
5205
|
-
});
|
|
5206
|
-
const { user } = await ctx.client.me();
|
|
5207
|
-
if (!user.publicKeyJwk) fail("run `seekrit keys setup` first");
|
|
5208
|
-
const dek = generateDek();
|
|
5209
|
-
const wrappedDek = await wrapDek(dek, user.publicKeyJwk);
|
|
5210
|
-
const recoveryWrappedDek = await recoveryWrapForNewEnv(ctx, groupRef.orgId, dek);
|
|
5211
|
-
const created = await ctx.client.createGroupEnv(groupRef.orgId, groupRef.id, {
|
|
5212
|
-
name: options.name,
|
|
5213
|
-
slug: options.slug,
|
|
5214
|
-
wrappedDek,
|
|
5215
|
-
recoveryWrappedDek
|
|
5216
|
-
});
|
|
5217
|
-
console.error(`created ${groupRef.slug}@${created.environment.slug} (${created.environment.id})`);
|
|
5218
|
-
});
|
|
6555
|
+
registerOrgCommands(program);
|
|
6556
|
+
registerAppCommands(program);
|
|
6557
|
+
registerGroupCommands(program);
|
|
5219
6558
|
/** Parse a positive integer flag/argument (version numbers, page sizes). */
|
|
5220
6559
|
function parseVersion(raw) {
|
|
5221
6560
|
const n = Number(raw);
|
|
@@ -5227,11 +6566,21 @@ function withTarget(cmd) {
|
|
|
5227
6566
|
return cmd.option("--org <slug>", "organization slug").option("--app <slug>", "application slug (defaults to seekrit.json)").option("--group <slug>", "target a group environment instead of an app").option("--branch <slug>", "operate on a branch of --env").requiredOption("--env <slug>", "environment slug");
|
|
5228
6567
|
}
|
|
5229
6568
|
const secrets = program.command("secrets").description("manage secrets in an application or group environment");
|
|
5230
|
-
withTarget(secrets.command("list").description("list secret names (no values)")).action(async (options) => {
|
|
6569
|
+
withTarget(secrets.command("list").alias("ls").description("list secret names (no values)").option("--json", "print the listing as JSON (metadata only — never values)")).action(async (options) => {
|
|
5231
6570
|
const ctx = buildContext();
|
|
5232
|
-
const { orgId, envId } = await resolveEnvTarget(ctx, options);
|
|
6571
|
+
const { orgId, envId, label } = await resolveEnvTarget(ctx, options);
|
|
5233
6572
|
const { secrets: rows } = await ctx.client.listSecrets(orgId, envId);
|
|
5234
|
-
|
|
6573
|
+
emit(options, { secrets: rows.map(({ id, name, version, createdAt, updatedAt }) => ({
|
|
6574
|
+
id,
|
|
6575
|
+
name,
|
|
6576
|
+
version,
|
|
6577
|
+
createdAt,
|
|
6578
|
+
updatedAt
|
|
6579
|
+
})) }, () => printTable(rows, [
|
|
6580
|
+
col("name", (s) => s.name),
|
|
6581
|
+
col("version", (s) => `v${s.version}`),
|
|
6582
|
+
col("updated", (s) => s.updatedAt)
|
|
6583
|
+
], `no secrets in ${label}`));
|
|
5235
6584
|
});
|
|
5236
6585
|
withTarget(secrets.command("get <name>").description("decrypt and print one secret value").option("--raw", "print the stored value without expanding ${OTHER_SECRET} references").option("--version <n>", "print an earlier version instead of the current one")).action(async (name, options) => {
|
|
5237
6586
|
const ctx = buildContext();
|
|
@@ -5274,15 +6623,19 @@ withTarget(secrets.command("import [file]").description("bulk-import secrets fro
|
|
|
5274
6623
|
const { created, updated } = await importSecrets(ctx, orgId, envId, entries);
|
|
5275
6624
|
console.error(`imported ${created.length + updated.length} secret(s) into ${label} (${created.length} new, ${updated.length} updated)`);
|
|
5276
6625
|
});
|
|
5277
|
-
withTarget(secrets.command("history <name>").description("list a secret's versions (metadata only — no values)").option("--limit <n>", `how many versions to show (max 200)`, "20")).action(async (name, options) => {
|
|
6626
|
+
withTarget(secrets.command("history <name>").description("list a secret's versions (metadata only — no values)").option("--limit <n>", `how many versions to show (max 200)`, "20").option("--json", "print the raw API response")).action(async (name, options) => {
|
|
5278
6627
|
const ctx = buildContext();
|
|
5279
6628
|
const { orgId, envId } = await resolveEnvTarget(ctx, options);
|
|
5280
6629
|
const { versions, currentVersion } = await ctx.client.listSecretVersions(orgId, envId, name, { limit: parseVersion(options.limit) });
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
6630
|
+
emit(options, {
|
|
6631
|
+
versions,
|
|
6632
|
+
currentVersion
|
|
6633
|
+
}, () => printTable(versions, [
|
|
6634
|
+
col("version", (v) => `v${v.version}`),
|
|
6635
|
+
col("when", (v) => v.createdAt),
|
|
6636
|
+
col("by", (v) => `${v.createdByType}:${v.createdById}`),
|
|
6637
|
+
col("note", (v) => [v.version === currentVersion ? "current" : null, v.restoredFromVersion === null ? null : `restored from v${v.restoredFromVersion}`].filter(Boolean).join(", "))
|
|
6638
|
+
], `no versions of ${name}`));
|
|
5286
6639
|
});
|
|
5287
6640
|
withTarget(secrets.command("restore <name> <version>").description("roll a secret back to an earlier version (stored as a new version)")).action(async (name, version, options) => {
|
|
5288
6641
|
const ctx = buildContext();
|
|
@@ -5515,42 +6868,7 @@ program.command("export").description("print decrypted secrets (dotenv, json, or
|
|
|
5515
6868
|
});
|
|
5516
6869
|
console.log(formatSecrets(values, options.format));
|
|
5517
6870
|
});
|
|
5518
|
-
program
|
|
5519
|
-
if (!options.user === !options.token) fail("pass exactly one of --user or --token");
|
|
5520
|
-
const ctx = buildContext();
|
|
5521
|
-
const { orgId, envId, label } = await resolveEnvTarget(ctx, options);
|
|
5522
|
-
const dek = await getDek(ctx, orgId, envId);
|
|
5523
|
-
let principalType;
|
|
5524
|
-
let principalId;
|
|
5525
|
-
let publicKeyJwk;
|
|
5526
|
-
if (options.user) {
|
|
5527
|
-
const { members } = await ctx.client.listMembers(orgId);
|
|
5528
|
-
const member = members.find((m) => m.email === options.user);
|
|
5529
|
-
if (!member) fail(`no member ${options.user}`);
|
|
5530
|
-
if (!member.publicKeyJwk) fail(`${options.user} has not completed key setup`);
|
|
5531
|
-
[principalType, principalId, publicKeyJwk] = [
|
|
5532
|
-
"user",
|
|
5533
|
-
member.userId,
|
|
5534
|
-
member.publicKeyJwk
|
|
5535
|
-
];
|
|
5536
|
-
} else {
|
|
5537
|
-
const { tokens } = await ctx.client.listTokens(orgId);
|
|
5538
|
-
const token = tokens.find((t) => t.id === options.token);
|
|
5539
|
-
if (!token) fail(`no service token ${options.token}`);
|
|
5540
|
-
[principalType, principalId, publicKeyJwk] = [
|
|
5541
|
-
"service_token",
|
|
5542
|
-
token.id,
|
|
5543
|
-
token.publicKeyJwk
|
|
5544
|
-
];
|
|
5545
|
-
}
|
|
5546
|
-
const wrappedDek = await wrapDek(dek, publicKeyJwk);
|
|
5547
|
-
await ctx.client.grantEnvKey(orgId, envId, {
|
|
5548
|
-
principalType,
|
|
5549
|
-
principalId,
|
|
5550
|
-
wrappedDek
|
|
5551
|
-
});
|
|
5552
|
-
console.error(`granted ${label} access to ${principalId}`);
|
|
5553
|
-
});
|
|
6871
|
+
registerAccessCommands(program);
|
|
5554
6872
|
const token = program.command("token").description("manage service tokens (CI, docker, agents)");
|
|
5555
6873
|
token.command("create").description("create a service token (runtime, or --admin for provisioning); prints it once").requiredOption("--name <name>", "display name, e.g. ci-deploy").option("--org <slug>").option("--app <slug>", "application to bind the token to (runtime tokens)").option("--env <slug>", "environment to bind the token to (runtime tokens)").option("--admin", "mint an org-scoped admin token that can provision structure (no env binding required)").option("--allow <group=env>", "also grant an alternate group slice (for `run --with`)", collectKv).option("--no-grant", "skip auto-granting the env + composed group keys").action(async (options) => {
|
|
5556
6874
|
const ctx = buildContext();
|
|
@@ -5601,14 +6919,19 @@ token.command("create").description("create a service token (runtime, or --admin
|
|
|
5601
6919
|
console.error(`${role} token created${granted ? " and granted" : ""} for ${scope} — save it now, it is not stored:`);
|
|
5602
6920
|
console.log(created.token);
|
|
5603
6921
|
});
|
|
5604
|
-
token.command("list").description("list service tokens").option("--org <slug>").action(async (options) => {
|
|
6922
|
+
token.command("list").alias("ls").description("list service tokens").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
|
|
5605
6923
|
const ctx = buildContext();
|
|
5606
6924
|
const orgRef = await resolveOrg(ctx, options.org);
|
|
5607
6925
|
const { tokens } = await ctx.client.listTokens(orgRef.id);
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
6926
|
+
const status = (t) => t.revokedAt ? "revoked" : t.expiresAt && Date.parse(t.expiresAt) < Date.now() ? "expired" : "active";
|
|
6927
|
+
emit(options, { tokens }, () => printTable(tokens, [
|
|
6928
|
+
col("name", (t) => t.name),
|
|
6929
|
+
col("role", (t) => t.role),
|
|
6930
|
+
col("status", (t) => status(t)),
|
|
6931
|
+
col("env", (t) => t.environmentId ?? "org-scoped"),
|
|
6932
|
+
col("last used", (t) => t.lastUsedAt ?? "never"),
|
|
6933
|
+
col("id", (t) => t.id)
|
|
6934
|
+
], "no service tokens — create one with `seekrit token create`"));
|
|
5612
6935
|
});
|
|
5613
6936
|
token.command("revoke <tokenId>").description("revoke a service token").option("--org <slug>").action(async (tokenId, options) => {
|
|
5614
6937
|
const ctx = buildContext();
|
|
@@ -5637,12 +6960,11 @@ program.command("mcp").description("run an MCP server over stdio so AI agents ca
|
|
|
5637
6960
|
const { runMcpServer } = await import("./mcp-DLplPOvz.js");
|
|
5638
6961
|
await runMcpServer();
|
|
5639
6962
|
});
|
|
5640
|
-
program
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
});
|
|
6963
|
+
registerAuditCommands(program);
|
|
6964
|
+
registerAccountCommands(program);
|
|
6965
|
+
registerLogSinkCommands(program);
|
|
6966
|
+
registerSyncCommands(program);
|
|
6967
|
+
registerBillingCommands(program);
|
|
5646
6968
|
const argv = process.argv.map((arg) => arg === "-v" ? "--version" : arg);
|
|
5647
6969
|
program.parseAsync(argv).catch((err) => {
|
|
5648
6970
|
fail(err instanceof Error ? err.message : String(err));
|