@seekrit/cli 0.32.0 → 0.34.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 +236 -10
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -16,6 +16,12 @@ const ENTITLEMENT_KEYS = Object.keys({
|
|
|
16
16
|
description: "Client-side managed keys for application-layer encryption and signing.",
|
|
17
17
|
default: true
|
|
18
18
|
},
|
|
19
|
+
"feature.honey_tokens": {
|
|
20
|
+
kind: "feature",
|
|
21
|
+
label: "Honey tokens",
|
|
22
|
+
description: "Decoy credentials that alert the moment anyone tries to use them.",
|
|
23
|
+
default: true
|
|
24
|
+
},
|
|
19
25
|
"feature.leases": {
|
|
20
26
|
kind: "feature",
|
|
21
27
|
label: "Temporary access",
|
|
@@ -122,7 +128,7 @@ const PLAN_FAMILIES = {
|
|
|
122
128
|
id: "free",
|
|
123
129
|
name: "Free",
|
|
124
130
|
description: "Get started with the essentials.",
|
|
125
|
-
current:
|
|
131
|
+
current: 3,
|
|
126
132
|
hidden: false
|
|
127
133
|
},
|
|
128
134
|
team: {
|
|
@@ -1119,6 +1125,9 @@ const AUDIT_ACTIONS = [
|
|
|
1119
1125
|
"token.created",
|
|
1120
1126
|
"token.revoked",
|
|
1121
1127
|
"token.deleted",
|
|
1128
|
+
"honey_token.created",
|
|
1129
|
+
"honey_token.deleted",
|
|
1130
|
+
"honey_token.tripped",
|
|
1122
1131
|
"cli_session.approved",
|
|
1123
1132
|
"cli_session.denied",
|
|
1124
1133
|
"cli_session.revoked",
|
|
@@ -1177,7 +1186,8 @@ const NOTIFICATION_TYPES = [
|
|
|
1177
1186
|
"org_welcome",
|
|
1178
1187
|
"token_expiring",
|
|
1179
1188
|
"lease_expired",
|
|
1180
|
-
"sync_failed"
|
|
1189
|
+
"sync_failed",
|
|
1190
|
+
"honey_token_tripped"
|
|
1181
1191
|
];
|
|
1182
1192
|
/** UI-facing copy + default for each notification type. */
|
|
1183
1193
|
const NOTIFICATION_TYPE_META = {
|
|
@@ -1225,6 +1235,11 @@ const NOTIFICATION_TYPE_META = {
|
|
|
1225
1235
|
label: "Third-party sync failed",
|
|
1226
1236
|
description: "A sync to an external destination failed repeatedly and stopped retrying. The destination is now holding stale values.",
|
|
1227
1237
|
defaultEnabled: true
|
|
1238
|
+
},
|
|
1239
|
+
honey_token_tripped: {
|
|
1240
|
+
label: "Honey token tripped",
|
|
1241
|
+
description: "Someone tried to use one of your organization's decoy credentials (throttled per token). Nothing legitimate holds one, so every trip is worth reading.",
|
|
1242
|
+
defaultEnabled: true
|
|
1228
1243
|
}
|
|
1229
1244
|
};
|
|
1230
1245
|
//#endregion
|
|
@@ -1332,6 +1347,14 @@ z.object({
|
|
|
1332
1347
|
environmentId: z.string().min(1).nullish(),
|
|
1333
1348
|
expiresAt: z.iso.datetime().nullish()
|
|
1334
1349
|
});
|
|
1350
|
+
z.object({
|
|
1351
|
+
name: nameSchema,
|
|
1352
|
+
tokenId: z.string().regex(/^skt_[0-9A-Za-z]+$/),
|
|
1353
|
+
/** SHA-256 hash (base64url) of the full token string. */
|
|
1354
|
+
tokenHash: z.string().min(1),
|
|
1355
|
+
/** Where the decoy was planted, as a reminder for whoever reads the alert. */
|
|
1356
|
+
placement: z.string().max(200).nullish()
|
|
1357
|
+
});
|
|
1335
1358
|
const kmsKeyPurposeSchema = z.enum(["encrypt", "sign"]);
|
|
1336
1359
|
const kmsKeySpecSchema = z.enum(["aes-256-gcm", "ecdsa-p256"]);
|
|
1337
1360
|
/** A wrapped key grant supplied by the client (server never sees plaintext material). */
|
|
@@ -1490,7 +1513,12 @@ z.object({
|
|
|
1490
1513
|
* See `docs/third-party-sync.md`.
|
|
1491
1514
|
*/
|
|
1492
1515
|
/** Platforms seekrit can push to. Append-only — persisted in `sync_connections.provider`. */
|
|
1493
|
-
const SYNC_PROVIDER_KINDS = [
|
|
1516
|
+
const SYNC_PROVIDER_KINDS = [
|
|
1517
|
+
"vercel",
|
|
1518
|
+
"cloudflare-workers",
|
|
1519
|
+
"cloudflare-pages",
|
|
1520
|
+
"cloudflare-secrets-store"
|
|
1521
|
+
];
|
|
1494
1522
|
z.enum(SYNC_PROVIDER_KINDS);
|
|
1495
1523
|
/**
|
|
1496
1524
|
* Vercel account scope. The API token itself is never here — it is wrapped to
|
|
@@ -1505,7 +1533,44 @@ const vercelConnectionConfigSchema = z.object({
|
|
|
1505
1533
|
/** Vercel Team id (`team_…`). Omit for a personal account. */
|
|
1506
1534
|
teamId: z.string().trim().min(1).max(128).optional()
|
|
1507
1535
|
});
|
|
1508
|
-
|
|
1536
|
+
/**
|
|
1537
|
+
* A Cloudflare account id — 32 lowercase hex characters, found in the sidebar
|
|
1538
|
+
* of any account's dashboard. Every Cloudflare endpoint seekrit calls is
|
|
1539
|
+
* account-scoped, so this is the account half of "which account, which thing".
|
|
1540
|
+
*
|
|
1541
|
+
* Validated by shape because the alternative is a bare 400 from Cloudflare
|
|
1542
|
+
* hours later inside an alarm, with nobody watching. It does not catch pasting
|
|
1543
|
+
* a *zone* id, which has the same shape — only the API can tell those apart.
|
|
1544
|
+
*/
|
|
1545
|
+
const cloudflareAccountIdSchema = z.string().trim().regex(/^[0-9a-f]{32}$/, "must be a 32-character Cloudflare account ID (lowercase hex)");
|
|
1546
|
+
/**
|
|
1547
|
+
* Cloudflare account scope, shared by all three Cloudflare providers. The API
|
|
1548
|
+
* token is never here — it is wrapped to the connection's public key and
|
|
1549
|
+
* stored as ciphertext, exactly as Vercel's is.
|
|
1550
|
+
*
|
|
1551
|
+
* The three providers are deliberately separate kinds rather than one
|
|
1552
|
+
* `cloudflare` with a mode field: they target different APIs, take different
|
|
1553
|
+
* destinations, and fail in different ways. Splitting them keeps the
|
|
1554
|
+
* exhaustiveness guard in `connectorFor` meaningful.
|
|
1555
|
+
*/
|
|
1556
|
+
const cloudflareWorkersConnectionConfigSchema = z.object({
|
|
1557
|
+
provider: z.literal("cloudflare-workers"),
|
|
1558
|
+
accountId: cloudflareAccountIdSchema
|
|
1559
|
+
});
|
|
1560
|
+
const cloudflarePagesConnectionConfigSchema = z.object({
|
|
1561
|
+
provider: z.literal("cloudflare-pages"),
|
|
1562
|
+
accountId: cloudflareAccountIdSchema
|
|
1563
|
+
});
|
|
1564
|
+
const cloudflareSecretsStoreConnectionConfigSchema = z.object({
|
|
1565
|
+
provider: z.literal("cloudflare-secrets-store"),
|
|
1566
|
+
accountId: cloudflareAccountIdSchema
|
|
1567
|
+
});
|
|
1568
|
+
const syncConnectionConfigSchema = z.discriminatedUnion("provider", [
|
|
1569
|
+
vercelConnectionConfigSchema,
|
|
1570
|
+
cloudflareWorkersConnectionConfigSchema,
|
|
1571
|
+
cloudflarePagesConnectionConfigSchema,
|
|
1572
|
+
cloudflareSecretsStoreConnectionConfigSchema
|
|
1573
|
+
]);
|
|
1509
1574
|
/** Vercel's three deployment targets. A binding writes to one or more. */
|
|
1510
1575
|
const VERCEL_TARGETS = [
|
|
1511
1576
|
"production",
|
|
@@ -1524,7 +1589,51 @@ const vercelDestinationSchema = z.object({
|
|
|
1524
1589
|
*/
|
|
1525
1590
|
gitBranch: z.string().trim().min(1).max(255).optional()
|
|
1526
1591
|
});
|
|
1527
|
-
|
|
1592
|
+
/**
|
|
1593
|
+
* The Worker whose secrets a binding owns. Wrangler *environments* are not a
|
|
1594
|
+
* separate field because they are not a separate concept at the API: deploying
|
|
1595
|
+
* `my-api` with `--env staging` creates a Worker literally named
|
|
1596
|
+
* `my-api-staging`, so pointing at an environment means naming that script.
|
|
1597
|
+
*/
|
|
1598
|
+
const cloudflareWorkersDestinationSchema = z.object({
|
|
1599
|
+
provider: z.literal("cloudflare-workers"),
|
|
1600
|
+
/** Worker script name, as shown in the dashboard (`my-api`). */
|
|
1601
|
+
scriptName: z.string().trim().min(1).max(63).regex(/^[A-Za-z0-9_][A-Za-z0-9_-]*$/, "must be a Worker script name")
|
|
1602
|
+
});
|
|
1603
|
+
/** The two deployment configs a Pages project keeps env vars under. */
|
|
1604
|
+
const CLOUDFLARE_PAGES_ENVIRONMENTS = ["production", "preview"];
|
|
1605
|
+
const cloudflarePagesDestinationSchema = z.object({
|
|
1606
|
+
provider: z.literal("cloudflare-pages"),
|
|
1607
|
+
/** Pages project name (`my-site`) — Pages has no separate project id. */
|
|
1608
|
+
projectName: z.string().trim().min(1).max(58).regex(/^[A-Za-z0-9][A-Za-z0-9-]*$/, "must be a Pages project name"),
|
|
1609
|
+
/** Which deployment configs receive these values. At least one. */
|
|
1610
|
+
environments: z.array(z.enum(CLOUDFLARE_PAGES_ENVIRONMENTS)).min(1)
|
|
1611
|
+
});
|
|
1612
|
+
/**
|
|
1613
|
+
* Scopes a Secrets Store secret may be used by. Cloudflare requires at least
|
|
1614
|
+
* one at creation and they cannot be inferred, so a binding states them.
|
|
1615
|
+
*/
|
|
1616
|
+
const CLOUDFLARE_SECRETS_STORE_SCOPES = [
|
|
1617
|
+
"workers",
|
|
1618
|
+
"ai_gateway",
|
|
1619
|
+
"dex",
|
|
1620
|
+
"access",
|
|
1621
|
+
"containers",
|
|
1622
|
+
"websearch"
|
|
1623
|
+
];
|
|
1624
|
+
const cloudflareSecretsStoreDestinationSchema = z.object({
|
|
1625
|
+
provider: z.literal("cloudflare-secrets-store"),
|
|
1626
|
+
/** Store id (32 hex). An account has exactly one store today. */
|
|
1627
|
+
storeId: z.string().trim().regex(/^[0-9a-f]{32}$/, "must be a 32-character Secrets Store ID (lowercase hex)"),
|
|
1628
|
+
/** Scopes applied to secrets this binding creates. At least one. */
|
|
1629
|
+
scopes: z.array(z.enum(CLOUDFLARE_SECRETS_STORE_SCOPES)).min(1)
|
|
1630
|
+
});
|
|
1631
|
+
const syncDestinationSchema = z.discriminatedUnion("provider", [
|
|
1632
|
+
vercelDestinationSchema,
|
|
1633
|
+
cloudflareWorkersDestinationSchema,
|
|
1634
|
+
cloudflarePagesDestinationSchema,
|
|
1635
|
+
cloudflareSecretsStoreDestinationSchema
|
|
1636
|
+
]);
|
|
1528
1637
|
/**
|
|
1529
1638
|
* How seekrit secret names become destination key names. Applied in order:
|
|
1530
1639
|
* explicit `rename` (wins outright), then `prefix`/`suffix`, then `case`.
|
|
@@ -2684,6 +2793,14 @@ async function createServiceToken() {
|
|
|
2684
2793
|
publicKeyJwk
|
|
2685
2794
|
};
|
|
2686
2795
|
}
|
|
2796
|
+
async function createHoneyToken() {
|
|
2797
|
+
const { token, tokenId, tokenHash } = await createServiceToken();
|
|
2798
|
+
return {
|
|
2799
|
+
token,
|
|
2800
|
+
tokenId,
|
|
2801
|
+
tokenHash
|
|
2802
|
+
};
|
|
2803
|
+
}
|
|
2687
2804
|
async function parseServiceToken(token) {
|
|
2688
2805
|
const match = /^(skt_[0-9A-Za-z]+)_([A-Za-z0-9_-]+)$/.exec(token);
|
|
2689
2806
|
if (!match) throw new SeekritCryptoError("MALFORMED_TOKEN", "not a valid seekrit service token");
|
|
@@ -2720,7 +2837,7 @@ function isCliSessionToken(value) {
|
|
|
2720
2837
|
}
|
|
2721
2838
|
//#endregion
|
|
2722
2839
|
//#region package.json
|
|
2723
|
-
var version = "0.
|
|
2840
|
+
var version = "0.34.0";
|
|
2724
2841
|
//#endregion
|
|
2725
2842
|
//#region ../../packages/api-client/src/index.ts
|
|
2726
2843
|
var SeekritApiError = class extends Error {
|
|
@@ -3032,6 +3149,16 @@ var SeekritClient = class {
|
|
|
3032
3149
|
deleteToken(orgId, tokenId) {
|
|
3033
3150
|
return this.request("DELETE", `/v1/orgs/${orgId}/tokens/${tokenId}/permanent`);
|
|
3034
3151
|
}
|
|
3152
|
+
listHoneyTokens(orgId) {
|
|
3153
|
+
return this.request("GET", `/v1/orgs/${orgId}/honey-tokens`);
|
|
3154
|
+
}
|
|
3155
|
+
createHoneyToken(orgId, input) {
|
|
3156
|
+
return this.request("POST", `/v1/orgs/${orgId}/honey-tokens`, input);
|
|
3157
|
+
}
|
|
3158
|
+
/** Delete a decoy outright — there is no access to revoke first. */
|
|
3159
|
+
deleteHoneyToken(orgId, honeyTokenId) {
|
|
3160
|
+
return this.request("DELETE", `/v1/orgs/${orgId}/honey-tokens/${honeyTokenId}`);
|
|
3161
|
+
}
|
|
3035
3162
|
/** Keys the caller can see: all org keys for admins, granted keys otherwise. */
|
|
3036
3163
|
listKmsKeys(orgId) {
|
|
3037
3164
|
return this.request("GET", `/v1/orgs/${orgId}/kms/keys`);
|
|
@@ -5233,6 +5360,52 @@ function registerGroupCommands(program) {
|
|
|
5233
5360
|
});
|
|
5234
5361
|
}
|
|
5235
5362
|
//#endregion
|
|
5363
|
+
//#region src/honey.ts
|
|
5364
|
+
/**
|
|
5365
|
+
* Honey tokens — decoy credentials that unlock nothing and alert when used.
|
|
5366
|
+
*
|
|
5367
|
+
* Minted client-side like real tokens (the API only ever sees a hash), printed
|
|
5368
|
+
* once, and then planted wherever a thief would rummage. The CLI is the natural
|
|
5369
|
+
* home for this: `seekrit honey-token create` piped straight into the file,
|
|
5370
|
+
* commit, config, or CI variable you want watched.
|
|
5371
|
+
*/
|
|
5372
|
+
function registerHoneyTokenCommands(program) {
|
|
5373
|
+
const honey = program.command("honey-token").description("plant decoy credentials that alert when anyone tries to use them");
|
|
5374
|
+
honey.command("create").description("mint a decoy credential; prints it once").requiredOption("--name <name>", "display name, e.g. legacy-ci-bait").option("--org <slug>").option("--placement <note>", "where you're planting it (echoed in the alert email)").action(async (options) => {
|
|
5375
|
+
const ctx = buildContext();
|
|
5376
|
+
const orgRef = await resolveOrg(ctx, options.org);
|
|
5377
|
+
const created = await createHoneyToken();
|
|
5378
|
+
await ctx.client.createHoneyToken(orgRef.id, {
|
|
5379
|
+
name: options.name,
|
|
5380
|
+
tokenId: created.tokenId,
|
|
5381
|
+
tokenHash: created.tokenHash,
|
|
5382
|
+
placement: options.placement ?? null
|
|
5383
|
+
});
|
|
5384
|
+
console.error("decoy created — save it now, it is not stored. Plant it somewhere a thief would look, NOT anywhere your own tooling reads: a deploy script that tries it by mistake trips the alarm just as loudly. It grants nothing.");
|
|
5385
|
+
console.log(created.token);
|
|
5386
|
+
});
|
|
5387
|
+
honey.command("list").alias("ls").description("list decoy credentials and whether any have been tripped").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
|
|
5388
|
+
const ctx = buildContext();
|
|
5389
|
+
const orgRef = await resolveOrg(ctx, options.org);
|
|
5390
|
+
const { honeyTokens } = await ctx.client.listHoneyTokens(orgRef.id);
|
|
5391
|
+
emit(options, { honeyTokens }, () => printTable(honeyTokens, [
|
|
5392
|
+
col("name", (t) => t.name),
|
|
5393
|
+
col("status", (t) => t.tripCount > 0 ? `TRIPPED ${t.tripCount}×` : "untouched"),
|
|
5394
|
+
col("planted in", (t) => t.placement ?? "—"),
|
|
5395
|
+
col("last tripped", (t) => t.lastTrippedAt ?? "never"),
|
|
5396
|
+
col("last source", (t) => t.lastTripIp ?? "—"),
|
|
5397
|
+
col("id", (t) => t.id)
|
|
5398
|
+
], "no decoys planted — create one with `seekrit honey-token create`"));
|
|
5399
|
+
});
|
|
5400
|
+
honey.command("delete <honeyTokenId>").alias("rm").description("delete a decoy (stops it alerting)").option("--org <slug>").option("--yes", "skip the confirmation prompt").action(async (honeyTokenId, options) => {
|
|
5401
|
+
const ctx = buildContext();
|
|
5402
|
+
const orgRef = await resolveOrg(ctx, options.org);
|
|
5403
|
+
await confirmDestructive(options.yes, `Delete ${honeyTokenId}? Wherever you planted it goes back to being unwatched — pull the bait too.`);
|
|
5404
|
+
await ctx.client.deleteHoneyToken(orgRef.id, honeyTokenId);
|
|
5405
|
+
console.error(`${honeyTokenId} deleted`);
|
|
5406
|
+
});
|
|
5407
|
+
}
|
|
5408
|
+
//#endregion
|
|
5236
5409
|
//#region src/logsink.ts
|
|
5237
5410
|
/** Collect repeated `--header Name: value` flags into a map. */
|
|
5238
5411
|
function collectHeader(value, acc = {}) {
|
|
@@ -6469,6 +6642,16 @@ function collect(value, acc) {
|
|
|
6469
6642
|
}
|
|
6470
6643
|
//#endregion
|
|
6471
6644
|
//#region src/sync.ts
|
|
6645
|
+
/** Split a `--flag a,b` list into trimmed, non-empty members. */
|
|
6646
|
+
function list(raw, fallback) {
|
|
6647
|
+
return (raw ?? fallback).split(",").map((item) => item.trim()).filter(Boolean);
|
|
6648
|
+
}
|
|
6649
|
+
/** Reject list members outside a known set, naming both the strays and the choices. */
|
|
6650
|
+
function assertMembers(values, allowed, flag) {
|
|
6651
|
+
const unknown = values.filter((v) => !allowed.includes(v));
|
|
6652
|
+
if (unknown.length > 0) fail(`unknown ${flag} ${unknown.join(", ")} — one of: ${allowed.join(", ")}`);
|
|
6653
|
+
return values;
|
|
6654
|
+
}
|
|
6472
6655
|
function assertProvider(value) {
|
|
6473
6656
|
if (!SYNC_PROVIDER_KINDS.includes(value)) fail(`unknown provider "${value}" — one of: ${SYNC_PROVIDER_KINDS.join(", ")}`);
|
|
6474
6657
|
return value;
|
|
@@ -6480,6 +6663,14 @@ function buildConfig(provider, options) {
|
|
|
6480
6663
|
provider: "vercel",
|
|
6481
6664
|
...options.teamId ? { teamId: options.teamId } : {}
|
|
6482
6665
|
};
|
|
6666
|
+
case "cloudflare-workers":
|
|
6667
|
+
case "cloudflare-pages":
|
|
6668
|
+
case "cloudflare-secrets-store":
|
|
6669
|
+
if (!options.accountId) fail(`--account-id is required for ${provider} — 32 hex characters, in the sidebar of any Cloudflare dashboard page`);
|
|
6670
|
+
return {
|
|
6671
|
+
provider,
|
|
6672
|
+
accountId: options.accountId
|
|
6673
|
+
};
|
|
6483
6674
|
}
|
|
6484
6675
|
}
|
|
6485
6676
|
/** Where inside the platform a binding writes. */
|
|
@@ -6497,14 +6688,48 @@ function buildDestination(provider, options) {
|
|
|
6497
6688
|
...options.gitBranch ? { gitBranch: options.gitBranch } : {}
|
|
6498
6689
|
};
|
|
6499
6690
|
}
|
|
6691
|
+
case "cloudflare-workers":
|
|
6692
|
+
if (!options.script) fail("--script is required for cloudflare-workers (the Worker's name, e.g. my-api)");
|
|
6693
|
+
return {
|
|
6694
|
+
provider: "cloudflare-workers",
|
|
6695
|
+
scriptName: options.script
|
|
6696
|
+
};
|
|
6697
|
+
case "cloudflare-pages": {
|
|
6698
|
+
if (!options.project) fail("--project is required for cloudflare-pages (the project name)");
|
|
6699
|
+
const environments = assertMembers(list(options.target, "production"), CLOUDFLARE_PAGES_ENVIRONMENTS, "--target");
|
|
6700
|
+
return {
|
|
6701
|
+
provider: "cloudflare-pages",
|
|
6702
|
+
projectName: options.project,
|
|
6703
|
+
environments
|
|
6704
|
+
};
|
|
6705
|
+
}
|
|
6706
|
+
case "cloudflare-secrets-store": {
|
|
6707
|
+
if (!options.storeId) fail("--store-id is required for cloudflare-secrets-store (32 hex characters)");
|
|
6708
|
+
const scopes = assertMembers(list(options.scopes, "workers"), CLOUDFLARE_SECRETS_STORE_SCOPES, "--scopes");
|
|
6709
|
+
return {
|
|
6710
|
+
provider: "cloudflare-secrets-store",
|
|
6711
|
+
storeId: options.storeId,
|
|
6712
|
+
scopes
|
|
6713
|
+
};
|
|
6714
|
+
}
|
|
6500
6715
|
}
|
|
6501
6716
|
}
|
|
6502
6717
|
/** One-line description of a destination, for list output. */
|
|
6503
6718
|
function describeDestination(destination) {
|
|
6504
6719
|
switch (destination.provider) {
|
|
6505
6720
|
case "vercel": return `${destination.projectId} (${destination.targets.join(", ")}${destination.gitBranch ? ` @${destination.gitBranch}` : ""})`;
|
|
6721
|
+
case "cloudflare-workers": return destination.scriptName;
|
|
6722
|
+
case "cloudflare-pages": return `${destination.projectName} (${destination.environments.join(", ")})`;
|
|
6723
|
+
case "cloudflare-secrets-store": return `store ${destination.storeId} (${destination.scopes.join(", ")})`;
|
|
6506
6724
|
}
|
|
6507
6725
|
}
|
|
6726
|
+
/**
|
|
6727
|
+
* Destination flags, shared by `verify` and `enable` so the two can never drift
|
|
6728
|
+
* into accepting different ways of naming the same destination.
|
|
6729
|
+
*/
|
|
6730
|
+
function destinationOptions(command) {
|
|
6731
|
+
return command.option("--project <id>", "vercel: project id or name · cloudflare-pages: project name").option("--target <list>", "vercel / cloudflare-pages: comma-separated deployment targets", "production").option("--git-branch <branch>", "vercel: restrict preview writes to one branch").option("--script <name>", "cloudflare-workers: Worker script name").option("--store-id <id>", "cloudflare-secrets-store: store ID (32 hex)").option("--scopes <list>", "cloudflare-secrets-store: comma-separated scopes", "workers");
|
|
6732
|
+
}
|
|
6508
6733
|
/** Find a connection by id or name — nobody keeps `syc_…` ids in their head. */
|
|
6509
6734
|
async function resolveConnection(ctx, orgId, ref) {
|
|
6510
6735
|
const { connections } = await ctx.client.listSyncConnections(orgId);
|
|
@@ -6514,7 +6739,7 @@ async function resolveConnection(ctx, orgId, ref) {
|
|
|
6514
6739
|
return matches[0];
|
|
6515
6740
|
}
|
|
6516
6741
|
function registerSyncCommands(program) {
|
|
6517
|
-
const sync = program.command("sync").description("push environments to a third-party platform (Vercel, …)");
|
|
6742
|
+
const sync = program.command("sync").description("push environments to a third-party platform (Vercel, Cloudflare, …)");
|
|
6518
6743
|
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) => {
|
|
6519
6744
|
const ctx = buildContext();
|
|
6520
6745
|
const ref = await resolveOrg(ctx, options.org);
|
|
@@ -6527,7 +6752,7 @@ function registerSyncCommands(program) {
|
|
|
6527
6752
|
col("id", (c) => c.id)
|
|
6528
6753
|
], "no connections — add one with `seekrit sync connect`"));
|
|
6529
6754
|
});
|
|
6530
|
-
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) => {
|
|
6755
|
+
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)").option("--account-id <id>", "cloudflare: account ID (32 hex, from the dashboard sidebar)").action(async (options) => {
|
|
6531
6756
|
const provider = assertProvider(options.provider);
|
|
6532
6757
|
const ctx = buildContext();
|
|
6533
6758
|
const ref = await resolveOrg(ctx, options.org);
|
|
@@ -6543,7 +6768,7 @@ function registerSyncCommands(program) {
|
|
|
6543
6768
|
});
|
|
6544
6769
|
console.error(`connected ${created.connection.name} (${created.connection.id})`);
|
|
6545
6770
|
});
|
|
6546
|
-
sync.command("verify <connection>").description("check a stored credential against a destination").option("--org <slug>").option("--provider <kind>", SYNC_PROVIDER_KINDS.join(" | "), "vercel")
|
|
6771
|
+
destinationOptions(sync.command("verify <connection>").description("check a stored credential against a destination").option("--org <slug>").option("--provider <kind>", SYNC_PROVIDER_KINDS.join(" | "), "vercel")).option("--json", "print the raw API response").action(async (connection, options) => {
|
|
6547
6772
|
const provider = assertProvider(options.provider);
|
|
6548
6773
|
const ctx = buildContext();
|
|
6549
6774
|
const ref = await resolveOrg(ctx, options.org);
|
|
@@ -6577,7 +6802,7 @@ function registerSyncCommands(program) {
|
|
|
6577
6802
|
col("id", (b) => b.id)
|
|
6578
6803
|
], "nothing is syncing — enable it with `seekrit sync enable`"));
|
|
6579
6804
|
});
|
|
6580
|
-
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")
|
|
6805
|
+
destinationOptions(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("--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) => {
|
|
6581
6806
|
const provider = assertProvider(options.provider);
|
|
6582
6807
|
if (options.onDelete !== "delete" && options.onDelete !== "retain") fail("--on-delete must be delete or retain");
|
|
6583
6808
|
if (options.mode !== "auto" && options.mode !== "manual") fail("--mode must be auto or manual");
|
|
@@ -7307,6 +7532,7 @@ program.command("export").description("print decrypted secrets (dotenv, json, or
|
|
|
7307
7532
|
console.log(formatSecrets(values, options.format));
|
|
7308
7533
|
});
|
|
7309
7534
|
registerAccessCommands(program);
|
|
7535
|
+
registerHoneyTokenCommands(program);
|
|
7310
7536
|
const token = program.command("token").description("manage service tokens (CI, docker, agents)");
|
|
7311
7537
|
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) => {
|
|
7312
7538
|
const ctx = buildContext();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seekrit/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"description": "End-to-end encrypted secrets manager CLI — inject decrypted secrets into any command.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"@types/node": "^26.1.0",
|
|
28
28
|
"tsdown": "^0.22.3",
|
|
29
29
|
"vitest": "^4.1.9",
|
|
30
|
-
"@seekrit/api-client": "0.0.1",
|
|
31
30
|
"@seekrit/core": "0.0.1",
|
|
32
|
-
"@seekrit/crypto": "0.0.1"
|
|
31
|
+
"@seekrit/crypto": "0.0.1",
|
|
32
|
+
"@seekrit/api-client": "0.0.1"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
35
|
"build": "tsdown",
|