@seekrit/cli 0.40.0 → 0.42.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 +602 -9
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -8,6 +8,43 @@ import { dirname, join, parse } from "node:path";
|
|
|
8
8
|
import { createInterface } from "node:readline";
|
|
9
9
|
import { Writable } from "node:stream";
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
11
|
+
//#region ../../packages/core/src/agent-policy.ts
|
|
12
|
+
/** A bare hostname: no scheme, no port, no path, no wildcard. */
|
|
13
|
+
const policyHostSchema = z.string().trim().min(1).max(253).toLowerCase().refine((h) => !/[:/\s*]/.test(h), { message: "host must be a bare hostname (no scheme, port, path, or wildcard)" }).refine((h) => /^[a-z0-9.-]+$/.test(h), { message: "host contains invalid characters" });
|
|
14
|
+
const policyMethodSchema = z.string().trim().toUpperCase().regex(/^[A-Z]{3,10}$/, "method must be an HTTP method name");
|
|
15
|
+
/**
|
|
16
|
+
* A path pattern. Must be absolute, because it is matched against a request
|
|
17
|
+
* path — a relative pattern is a mistake that would silently match nothing.
|
|
18
|
+
*/
|
|
19
|
+
const policyPathSchema = z.string().trim().min(1).max(512).startsWith("/", "path pattern must start with /").refine((p) => !p.includes("?"), { message: "path patterns match the path only, not the query" });
|
|
20
|
+
const policySecretNameSchema = z.string().trim().regex(/^[A-Za-z0-9_]+$/, "secret names are letters, digits, and underscores");
|
|
21
|
+
z.object({
|
|
22
|
+
host: policyHostSchema,
|
|
23
|
+
methods: z.array(policyMethodSchema).max(16).default([]),
|
|
24
|
+
paths: z.array(policyPathSchema).max(64).default([]),
|
|
25
|
+
allow: z.array(policySecretNameSchema).max(64).default([]),
|
|
26
|
+
label: z.string().trim().max(120).optional()
|
|
27
|
+
});
|
|
28
|
+
z.object({
|
|
29
|
+
/** `ap1.<body>.<signature>` — opaque to the server. */
|
|
30
|
+
bundle: z.string().min(16).max(256 * 1024) });
|
|
31
|
+
z.object({
|
|
32
|
+
name: z.string().trim().min(1).max(120),
|
|
33
|
+
slug: z.string().trim().min(1).max(64).regex(/^[a-z0-9][a-z0-9-]*$/, "slug is lowercase letters, digits, and dashes"),
|
|
34
|
+
/** The environment whose secrets this agent's policy may name. Optional. */
|
|
35
|
+
environmentId: z.string().trim().min(1).max(64).optional()
|
|
36
|
+
});
|
|
37
|
+
z.object({
|
|
38
|
+
name: z.string().trim().min(1).max(120).optional(),
|
|
39
|
+
enabled: z.boolean().optional(),
|
|
40
|
+
environmentId: z.string().trim().min(1).max(64).nullish()
|
|
41
|
+
});
|
|
42
|
+
z.object({
|
|
43
|
+
host: policyHostSchema,
|
|
44
|
+
method: policyMethodSchema,
|
|
45
|
+
path: z.string().trim().min(1).max(2048),
|
|
46
|
+
secret: policySecretNameSchema.optional()
|
|
47
|
+
});
|
|
11
48
|
/** All catalog keys as a runtime array (for iteration / zod enums). */
|
|
12
49
|
const ENTITLEMENT_KEYS = Object.keys({
|
|
13
50
|
"feature.kms": {
|
|
@@ -1188,7 +1225,12 @@ const AUDIT_ACTIONS = [
|
|
|
1188
1225
|
"sync.binding_updated",
|
|
1189
1226
|
"sync.binding_deleted",
|
|
1190
1227
|
"sync.run_succeeded",
|
|
1191
|
-
"sync.run_failed"
|
|
1228
|
+
"sync.run_failed",
|
|
1229
|
+
"agent.created",
|
|
1230
|
+
"agent.updated",
|
|
1231
|
+
"agent.deleted",
|
|
1232
|
+
"agent.policy_published",
|
|
1233
|
+
"agent.policy_rolled_back"
|
|
1192
1234
|
];
|
|
1193
1235
|
/**
|
|
1194
1236
|
* Transactional notification emails seekrit can send. Each id is one
|
|
@@ -1664,7 +1706,10 @@ const SYNC_PROVIDER_KINDS = [
|
|
|
1664
1706
|
"northflank",
|
|
1665
1707
|
"digitalocean",
|
|
1666
1708
|
"heroku",
|
|
1667
|
-
"netlify"
|
|
1709
|
+
"netlify",
|
|
1710
|
+
"bunnyshell",
|
|
1711
|
+
"github-actions",
|
|
1712
|
+
"gcp-secret-manager"
|
|
1668
1713
|
];
|
|
1669
1714
|
z.enum(SYNC_PROVIDER_KINDS);
|
|
1670
1715
|
/**
|
|
@@ -1869,6 +1914,82 @@ const netlifyConnectionConfigSchema = z.object({
|
|
|
1869
1914
|
/** Netlify team slug (`acme`) or account id — `{account_id}` accepts either. */
|
|
1870
1915
|
accountId: z.string().trim().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "must be a Netlify team slug or account ID — no slashes or spaces")
|
|
1871
1916
|
});
|
|
1917
|
+
/**
|
|
1918
|
+
* Bunnyshell account scope — empty, as Fly's, Heroku's, and Northflank's are.
|
|
1919
|
+
*
|
|
1920
|
+
* A Bunnyshell access token (from `environments.bunnyshell.com/access-token`)
|
|
1921
|
+
* belongs to a *user* and carries their access to every organization they are
|
|
1922
|
+
* in, exactly as a Heroku token does. Unlike Netlify's, that does not force an
|
|
1923
|
+
* organization onto the connection, because nothing here is addressed *through*
|
|
1924
|
+
* one: both variable collections name their parent by an opaque, globally
|
|
1925
|
+
* unique id (`environment` or `project`), so the token plus the destination's
|
|
1926
|
+
* id is the whole address. The API offers an `organization` filter, but it
|
|
1927
|
+
* narrows a listing — it is not part of an address.
|
|
1928
|
+
*/
|
|
1929
|
+
const bunnyshellConnectionConfigSchema = z.object({ provider: z.literal("bunnyshell") });
|
|
1930
|
+
/**
|
|
1931
|
+
* GitHub account scope — empty for github.com, which is the whole point.
|
|
1932
|
+
*
|
|
1933
|
+
* A GitHub token addresses everything by `{owner}/{repo}` or `{org}`, and those
|
|
1934
|
+
* are the destination's business, so there is no account half to state the way
|
|
1935
|
+
* Cloudflare and Netlify need one. `baseUrl` is the single exception, and it is
|
|
1936
|
+
* not an account scope at all: it names a **GitHub Enterprise Server** install,
|
|
1937
|
+
* whose API lives on the customer's own host rather than on `api.github.com`.
|
|
1938
|
+
*
|
|
1939
|
+
* Left unset for github.com and for Enterprise Cloud (which is `api.github.com`
|
|
1940
|
+
* with a different plan behind it). Set only for a self-hosted GHES appliance,
|
|
1941
|
+
* where the REST API is at `https://<host>/api/v3`.
|
|
1942
|
+
*/
|
|
1943
|
+
const githubActionsConnectionConfigSchema = z.object({
|
|
1944
|
+
provider: z.literal("github-actions"),
|
|
1945
|
+
/**
|
|
1946
|
+
* GitHub Enterprise Server API root, e.g. `https://github.acme.com/api/v3`.
|
|
1947
|
+
* Omit for github.com. Must be `https:` — this URL carries the token.
|
|
1948
|
+
*/
|
|
1949
|
+
baseUrl: z.string().trim().max(300).refine((value) => {
|
|
1950
|
+
let parsed;
|
|
1951
|
+
try {
|
|
1952
|
+
parsed = new URL(value);
|
|
1953
|
+
} catch {
|
|
1954
|
+
return false;
|
|
1955
|
+
}
|
|
1956
|
+
return parsed.protocol === "https:" && !parsed.username && !parsed.password;
|
|
1957
|
+
}, "must be an https:// URL — the GitHub Enterprise Server API root, e.g. https://github.acme.com/api/v3").optional()
|
|
1958
|
+
});
|
|
1959
|
+
/**
|
|
1960
|
+
* A Google Cloud project, as `projects/{project}` accepts one: either the
|
|
1961
|
+
* project **ID** (`acme-prod`, 6–30 characters, what the console shows) or the
|
|
1962
|
+
* project **number** (all digits). Both are accepted because both work, and
|
|
1963
|
+
* the id is the one an operator can read off their own dashboard.
|
|
1964
|
+
*
|
|
1965
|
+
* Validated by shape for the reason Cloudflare's account id is: every Secret
|
|
1966
|
+
* Manager URL is built from this string, and a typo would otherwise surface as
|
|
1967
|
+
* a 403 from Google hours later inside an alarm, with nobody watching.
|
|
1968
|
+
*/
|
|
1969
|
+
const gcpProjectSchema = z.string().trim().refine((value) => /^[a-z][a-z0-9-]{4,28}[a-z0-9]$/.test(value) || /^\d{1,20}$/.test(value), "must be a Google Cloud project ID (e.g. acme-prod) or project number");
|
|
1970
|
+
/**
|
|
1971
|
+
* Google Cloud project scope — which project's Secret Manager to write.
|
|
1972
|
+
*
|
|
1973
|
+
* The service account is *not* here, unlike AWS's access key id: a GCP
|
|
1974
|
+
* credential is a key JSON that names its own `client_email`, so the identity
|
|
1975
|
+
* arrives with the credential the way a Vercel token's does. What the
|
|
1976
|
+
* credential cannot say is which project to write, because a service account
|
|
1977
|
+
* can be granted access to secrets in projects other than its own — so that is
|
|
1978
|
+
* this field, exactly as Cloudflare's account id is.
|
|
1979
|
+
*
|
|
1980
|
+
* One project per connection. Syncing an environment into two projects means
|
|
1981
|
+
* two connections, which also keeps their key grants separate.
|
|
1982
|
+
*
|
|
1983
|
+
* Global secrets only: v1 addresses `secretmanager.googleapis.com`, not the
|
|
1984
|
+
* per-location `secretmanager.<location>.rep.googleapis.com` endpoints that
|
|
1985
|
+
* regional secrets live behind. Data residency is expressed instead through the
|
|
1986
|
+
* destination's user-managed replication.
|
|
1987
|
+
*/
|
|
1988
|
+
const gcpSecretManagerConnectionConfigSchema = z.object({
|
|
1989
|
+
provider: z.literal("gcp-secret-manager"),
|
|
1990
|
+
/** Project ID (`acme-prod`) or project number. */
|
|
1991
|
+
projectId: gcpProjectSchema
|
|
1992
|
+
});
|
|
1872
1993
|
const syncConnectionConfigSchema = z.discriminatedUnion("provider", [
|
|
1873
1994
|
vercelConnectionConfigSchema,
|
|
1874
1995
|
cloudflareWorkersConnectionConfigSchema,
|
|
@@ -1882,7 +2003,10 @@ const syncConnectionConfigSchema = z.discriminatedUnion("provider", [
|
|
|
1882
2003
|
northflankConnectionConfigSchema,
|
|
1883
2004
|
digitalOceanConnectionConfigSchema,
|
|
1884
2005
|
herokuConnectionConfigSchema,
|
|
1885
|
-
netlifyConnectionConfigSchema
|
|
2006
|
+
netlifyConnectionConfigSchema,
|
|
2007
|
+
bunnyshellConnectionConfigSchema,
|
|
2008
|
+
githubActionsConnectionConfigSchema,
|
|
2009
|
+
gcpSecretManagerConnectionConfigSchema
|
|
1886
2010
|
]);
|
|
1887
2011
|
/** Vercel's three deployment targets. A binding writes to one or more. */
|
|
1888
2012
|
const VERCEL_TARGETS = [
|
|
@@ -2307,6 +2431,292 @@ const netlifyDestinationSchema = z.object({
|
|
|
2307
2431
|
message: "a branch context needs the branch name it applies to",
|
|
2308
2432
|
path: ["branch"]
|
|
2309
2433
|
});
|
|
2434
|
+
/**
|
|
2435
|
+
* A Bunnyshell resource id, as the platform hands it out.
|
|
2436
|
+
*
|
|
2437
|
+
* Deliberately loose. Bunnyshell documents no format for these — they are
|
|
2438
|
+
* opaque strings from `bns environments list` or the dashboard URL — so
|
|
2439
|
+
* asserting a shape here would be inventing a rule the platform never stated,
|
|
2440
|
+
* and the failure mode would be seekrit refusing an id that works.
|
|
2441
|
+
*
|
|
2442
|
+
* Being loose is affordable here in a way it is not on Netlify, where an
|
|
2443
|
+
* unresolved `site_id` silently widens a write to the whole team. Both
|
|
2444
|
+
* Bunnyshell variable collections name their parent in the **request body** of
|
|
2445
|
+
* a create, as a required relation: an id the platform cannot resolve is a 422
|
|
2446
|
+
* naming the field, not a write that lands somewhere broader. The listing side
|
|
2447
|
+
* is fenced separately — the connector re-checks every variable's own parent
|
|
2448
|
+
* before it touches it, so a filter that failed to bite cannot turn into an
|
|
2449
|
+
* edit of a neighbouring environment's variables.
|
|
2450
|
+
*/
|
|
2451
|
+
const bunnyshellIdSchema = z.string().trim().min(1).max(64).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "must be a Bunnyshell ID — no slashes or spaces");
|
|
2452
|
+
/**
|
|
2453
|
+
* Variables on one Bunnyshell **environment** — the set every component in it
|
|
2454
|
+
* inherits, and the closest match to a seekrit environment.
|
|
2455
|
+
*
|
|
2456
|
+
* This is the destination for an environment that already exists and stays
|
|
2457
|
+
* around: a primary environment, or a long-lived ephemeral one.
|
|
2458
|
+
*/
|
|
2459
|
+
const bunnyshellEnvironmentDestinationSchema = z.object({
|
|
2460
|
+
provider: z.literal("bunnyshell"),
|
|
2461
|
+
kind: z.literal("environment"),
|
|
2462
|
+
/** Environment ID, as `bns environments list` prints it. */
|
|
2463
|
+
environmentId: bunnyshellIdSchema,
|
|
2464
|
+
/** Mark what seekrit creates as a Bunnyshell secret (default true). */
|
|
2465
|
+
secret: z.boolean().optional()
|
|
2466
|
+
});
|
|
2467
|
+
/**
|
|
2468
|
+
* Variables on a Bunnyshell **project** — inherited by every environment
|
|
2469
|
+
* created in it from then on.
|
|
2470
|
+
*
|
|
2471
|
+
* Worth thinking about twice, for the reason a Render environment group is:
|
|
2472
|
+
* the blast radius is the project, not one environment. It earns its place
|
|
2473
|
+
* anyway, because it is the only destination that reaches an environment which
|
|
2474
|
+
* *does not exist yet*. Bunnyshell's whole shape is ephemeral environments spun
|
|
2475
|
+
* up per branch or per pull request; pushing to the environment cannot seed one
|
|
2476
|
+
* that a webhook will create tomorrow, and pushing to the project can.
|
|
2477
|
+
*
|
|
2478
|
+
* An environment inherits the project's value at creation and may then be
|
|
2479
|
+
* overridden at its own scope — so a project binding does not fight an
|
|
2480
|
+
* environment binding pointed at the same name, it loses to it.
|
|
2481
|
+
*/
|
|
2482
|
+
const bunnyshellProjectDestinationSchema = z.object({
|
|
2483
|
+
provider: z.literal("bunnyshell"),
|
|
2484
|
+
kind: z.literal("project"),
|
|
2485
|
+
/** Project ID, as `bns projects list` prints it. */
|
|
2486
|
+
projectId: bunnyshellIdSchema,
|
|
2487
|
+
/** Mark what seekrit creates as a Bunnyshell secret (default true). */
|
|
2488
|
+
secret: z.boolean().optional()
|
|
2489
|
+
});
|
|
2490
|
+
/**
|
|
2491
|
+
* Where in Bunnyshell a binding writes.
|
|
2492
|
+
*
|
|
2493
|
+
* Split on `kind` rather than into two providers — the way Render's service and
|
|
2494
|
+
* environment group are, and unlike Cloudflare's three — because the two are the
|
|
2495
|
+
* same API twice over: `/v1/environment_variables` and `/v1/project_variables`
|
|
2496
|
+
* take the same fields, fail the same ways, and differ only in which parent they
|
|
2497
|
+
* name. One connector serves both, so one provider does too.
|
|
2498
|
+
*
|
|
2499
|
+
* `secret` is Bunnyshell's `isSecret`, and means less than Netlify's flag of the
|
|
2500
|
+
* same name: Bunnyshell encrypts every variable with an organization key whether
|
|
2501
|
+
* or not the flag is set, so this only decides whether the value is obscured in
|
|
2502
|
+
* the dashboard and stored encrypted in an exported definition. It is on by
|
|
2503
|
+
* default all the same — a value pushed from a secrets manager should not be
|
|
2504
|
+
* sitting in plain view of everyone with project access. It applies only to
|
|
2505
|
+
* variables seekrit **creates**: an update never sends the flag, so a variable
|
|
2506
|
+
* an operator deliberately un-secreted stays that way.
|
|
2507
|
+
*/
|
|
2508
|
+
const bunnyshellDestinationSchema = z.discriminatedUnion("kind", [bunnyshellEnvironmentDestinationSchema, bunnyshellProjectDestinationSchema]);
|
|
2509
|
+
/**
|
|
2510
|
+
* Which repositories in an organization can read an org-level secret.
|
|
2511
|
+
*
|
|
2512
|
+
* GitHub's own enum, unchanged. There is deliberately **no default**: `all` hands
|
|
2513
|
+
* the value to every repository in the organization — including ones added
|
|
2514
|
+
* tomorrow, and including forks' workflows to the extent the org allows them —
|
|
2515
|
+
* and that is not a blast radius a secrets manager should pick on an operator's
|
|
2516
|
+
* behalf. Naming it is the point.
|
|
2517
|
+
*/
|
|
2518
|
+
const GITHUB_ACTIONS_VISIBILITIES = [
|
|
2519
|
+
"all",
|
|
2520
|
+
"private",
|
|
2521
|
+
"selected"
|
|
2522
|
+
];
|
|
2523
|
+
/**
|
|
2524
|
+
* A GitHub account or organization login, matching GitHub's own rule:
|
|
2525
|
+
* alphanumeric with single internal hyphens, 39 characters at most.
|
|
2526
|
+
*
|
|
2527
|
+
* Checked here so the habitual slip — pasting a URL, or `owner/repo` into the
|
|
2528
|
+
* owner field — fails at the form rather than as a 404 from an alarm with nobody
|
|
2529
|
+
* watching.
|
|
2530
|
+
*/
|
|
2531
|
+
const githubOwnerSchema = z.string().trim().min(1).max(39).regex(/^[A-Za-z0-9](?:-?[A-Za-z0-9])*$/, "must be a GitHub user or organization login — not a URL or an owner/repo pair");
|
|
2532
|
+
/**
|
|
2533
|
+
* A repository name. GitHub's rules are looser than an owner's: letters,
|
|
2534
|
+
* numbers, hyphens, underscores, and periods, up to 100 characters. `.` and `..`
|
|
2535
|
+
* are refused outright — they would traverse the API path rather than name a
|
|
2536
|
+
* repository.
|
|
2537
|
+
*/
|
|
2538
|
+
const githubRepoSchema = z.string().trim().min(1).max(100).regex(/^[A-Za-z0-9._-]+$/, "must be a repository name alone (`api`), not `owner/repo` or a URL").refine((value) => value !== "." && value !== "..", "is not a repository name");
|
|
2539
|
+
/**
|
|
2540
|
+
* A deployment environment name.
|
|
2541
|
+
*
|
|
2542
|
+
* Deliberately permissive: GitHub allows spaces and most punctuation here, and
|
|
2543
|
+
* the dashboard shows names like `prod (eu-west)`. Only the two things that would
|
|
2544
|
+
* break the request are refused — an empty name, and the path separators that
|
|
2545
|
+
* would let a name escape its URL segment. Everything else is GitHub's to reject.
|
|
2546
|
+
*/
|
|
2547
|
+
const githubEnvironmentSchema = z.string().trim().min(1).max(255).refine((value) => !value.includes("/") && !value.includes("\\"), "cannot contain a slash — that is a path separator, not part of an environment name");
|
|
2548
|
+
/**
|
|
2549
|
+
* One repository's Actions secrets.
|
|
2550
|
+
*
|
|
2551
|
+
* Every workflow in the repository can read these, including one added by a pull
|
|
2552
|
+
* request from a collaborator with write access. That is GitHub's model, not a
|
|
2553
|
+
* choice this connector makes — but it is the reason `environment` exists below,
|
|
2554
|
+
* and the reason to prefer it for anything that touches production.
|
|
2555
|
+
*/
|
|
2556
|
+
const githubActionsRepoDestinationSchema = z.object({
|
|
2557
|
+
provider: z.literal("github-actions"),
|
|
2558
|
+
kind: z.literal("repo"),
|
|
2559
|
+
/** Repository owner — a user or organization login. */
|
|
2560
|
+
owner: githubOwnerSchema,
|
|
2561
|
+
/** Repository name, without the owner. */
|
|
2562
|
+
repo: githubRepoSchema
|
|
2563
|
+
});
|
|
2564
|
+
/**
|
|
2565
|
+
* One deployment environment's Actions secrets — the narrowest scope GitHub has.
|
|
2566
|
+
*
|
|
2567
|
+
* A job reads these only by declaring `environment: <name>`, which also subjects
|
|
2568
|
+
* it to that environment's protection rules: required reviewers, wait timers, and
|
|
2569
|
+
* the branch policy. That combination is the closest GitHub gets to "this secret
|
|
2570
|
+
* is for production, and reaching it requires approval", and it is the scope to
|
|
2571
|
+
* reach for by default.
|
|
2572
|
+
*
|
|
2573
|
+
* The environment must already exist. This connector will not create one: an
|
|
2574
|
+
* environment is a deployment gate, and silently creating an unprotected one
|
|
2575
|
+
* because a name was misspelled would quietly remove the protection the operator
|
|
2576
|
+
* was relying on.
|
|
2577
|
+
*/
|
|
2578
|
+
const githubActionsEnvironmentDestinationSchema = z.object({
|
|
2579
|
+
provider: z.literal("github-actions"),
|
|
2580
|
+
kind: z.literal("environment"),
|
|
2581
|
+
owner: githubOwnerSchema,
|
|
2582
|
+
repo: githubRepoSchema,
|
|
2583
|
+
/** Environment name, exactly as the repository's Settings → Environments shows it. */
|
|
2584
|
+
environment: githubEnvironmentSchema
|
|
2585
|
+
});
|
|
2586
|
+
/**
|
|
2587
|
+
* An organization's Actions secrets.
|
|
2588
|
+
*
|
|
2589
|
+
* The widest scope in the product, and the only destination on any provider that
|
|
2590
|
+
* can hand a value to repositories nobody named. Read {@link
|
|
2591
|
+
* GITHUB_ACTIONS_VISIBILITIES} before using it.
|
|
2592
|
+
*
|
|
2593
|
+
* `selectedRepositoryIds` takes numeric repository **ids**, not names, because
|
|
2594
|
+
* that is what GitHub's API takes. An id is visible at
|
|
2595
|
+
* `GET /repos/{owner}/{repo}` as `id`, and in the dashboard nowhere at all —
|
|
2596
|
+
* which is friction worth accepting rather than resolving names to ids here: name
|
|
2597
|
+
* resolution would mean this connector picking which repository an ambiguous
|
|
2598
|
+
* name meant, and getting that wrong widens a secret's reach silently.
|
|
2599
|
+
*/
|
|
2600
|
+
const githubActionsOrgDestinationSchema = z.object({
|
|
2601
|
+
provider: z.literal("github-actions"),
|
|
2602
|
+
kind: z.literal("org"),
|
|
2603
|
+
/** Organization login. */
|
|
2604
|
+
org: githubOwnerSchema,
|
|
2605
|
+
/** Which repositories may read these secrets. Stated, never defaulted. */
|
|
2606
|
+
visibility: z.enum(GITHUB_ACTIONS_VISIBILITIES),
|
|
2607
|
+
/** Numeric repository ids, required when `visibility` is `selected`. */
|
|
2608
|
+
selectedRepositoryIds: z.array(z.number().int().positive()).max(500).optional()
|
|
2609
|
+
}).refine((dest) => dest.visibility !== "selected" || dest.selectedRepositoryIds !== void 0 && dest.selectedRepositoryIds.length > 0, {
|
|
2610
|
+
message: "selected visibility needs at least one repository id",
|
|
2611
|
+
path: ["selectedRepositoryIds"]
|
|
2612
|
+
}).refine((dest) => dest.visibility === "selected" || dest.selectedRepositoryIds === void 0, {
|
|
2613
|
+
message: "repository ids only apply to selected visibility — remove them, or select it",
|
|
2614
|
+
path: ["selectedRepositoryIds"]
|
|
2615
|
+
});
|
|
2616
|
+
const githubActionsDestinationSchema = z.discriminatedUnion("kind", [
|
|
2617
|
+
githubActionsRepoDestinationSchema,
|
|
2618
|
+
githubActionsEnvironmentDestinationSchema,
|
|
2619
|
+
githubActionsOrgDestinationSchema
|
|
2620
|
+
]);
|
|
2621
|
+
/**
|
|
2622
|
+
* How a binding lays its secrets out in Secret Manager. The same two shapes the
|
|
2623
|
+
* AWS Secrets Manager destination offers, and for the same reasons:
|
|
2624
|
+
*
|
|
2625
|
+
* - `secret-per-name` — one GCP secret per seekrit secret. The direct
|
|
2626
|
+
* translation, and what Cloud Run's `--set-secrets` and GKE's Secret Manager
|
|
2627
|
+
* CSI driver mount one at a time.
|
|
2628
|
+
* - `json-bundle` — every value as one JSON object in a single secret. Costs one
|
|
2629
|
+
* active version instead of fifty, which is the whole billing unit here.
|
|
2630
|
+
*/
|
|
2631
|
+
const GCP_SECRET_MANAGER_LAYOUTS = ["secret-per-name", "json-bundle"];
|
|
2632
|
+
/**
|
|
2633
|
+
* Where Google keeps the copies of a secret. Chosen at creation and
|
|
2634
|
+
* **immutable** afterwards — changing it means deleting the secret and letting
|
|
2635
|
+
* the next run recreate it.
|
|
2636
|
+
*
|
|
2637
|
+
* - `automatic` — Google picks the locations. One billable replica, and what
|
|
2638
|
+
* you want unless a policy says otherwise.
|
|
2639
|
+
* - `user-managed` — the binding names the regions. This is how data residency
|
|
2640
|
+
* is expressed for global secrets, and each region is billed as its own
|
|
2641
|
+
* active version.
|
|
2642
|
+
*/
|
|
2643
|
+
const GCP_REPLICATION_POLICIES = ["automatic", "user-managed"];
|
|
2644
|
+
/**
|
|
2645
|
+
* A Secret Manager secret ID. Google's own rule, quoted from the API reference:
|
|
2646
|
+
* "a string with a maximum length of 255 characters and can contain uppercase
|
|
2647
|
+
* and lowercase letters, numerals, and the hyphen (`-`) and underscore (`_`)
|
|
2648
|
+
* characters."
|
|
2649
|
+
*
|
|
2650
|
+
* Notably **no slashes and no dots**, which is what makes this a different
|
|
2651
|
+
* field from AWS's `pathPrefix` rather than the same idea renamed: a Secret
|
|
2652
|
+
* Manager namespace is spelled `prod-storefront-DB_URL`, not
|
|
2653
|
+
* `prod/storefront/DB_URL`.
|
|
2654
|
+
*/
|
|
2655
|
+
const gcpSecretIdSchema = z.string().trim().min(1).max(255).regex(/^[A-Za-z0-9_-]+$/, "may contain letters, digits, hyphens, and underscores");
|
|
2656
|
+
/**
|
|
2657
|
+
* A GCP region for a user-managed replica (`us-east1`, `europe-west4`,
|
|
2658
|
+
* `northamerica-northeast1`). Validated by shape rather than against a list,
|
|
2659
|
+
* because Google adds regions faster than we ship — a name Secret Manager does
|
|
2660
|
+
* not know is refused by Google with a clear message at creation.
|
|
2661
|
+
*/
|
|
2662
|
+
const gcpLocationSchema = z.string().trim().regex(/^[a-z]+-[a-z]+\d+$/, "must be a GCP region ID, e.g. us-east1");
|
|
2663
|
+
/**
|
|
2664
|
+
* A Cloud KMS key, as its full resource name — the only form the API accepts:
|
|
2665
|
+
* `projects/p/locations/l/keyRings/r/cryptoKeys/k`.
|
|
2666
|
+
*
|
|
2667
|
+
* Stricter than AWS's `kmsKeyId` (which tolerates five spellings) because
|
|
2668
|
+
* Google tolerates exactly one, and because a key in the wrong *location* is
|
|
2669
|
+
* rejected at creation: an automatic-replication secret needs a `global` key,
|
|
2670
|
+
* and a user-managed replica needs one in its own region.
|
|
2671
|
+
*/
|
|
2672
|
+
const gcpKmsKeyNameSchema = z.string().trim().max(1024).regex(/^projects\/[^/]+\/locations\/[^/]+\/keyRings\/[^/]+\/cryptoKeys\/[^/]+$/, "must be a full Cloud KMS key name (projects/…/locations/…/keyRings/…/cryptoKeys/…)");
|
|
2673
|
+
/**
|
|
2674
|
+
* Where inside a project's Secret Manager a binding writes.
|
|
2675
|
+
*
|
|
2676
|
+
* ## Every push would otherwise cost a version
|
|
2677
|
+
*
|
|
2678
|
+
* Secret Manager has no "set the value" call — only `addVersion`, which appends.
|
|
2679
|
+
* A run pushes the whole environment (never a diff), so changing one secret in
|
|
2680
|
+
* an environment of fifty would leave fifty new versions behind, forty-nine of
|
|
2681
|
+
* them identical to their predecessors, each one billed for as long as it stays
|
|
2682
|
+
* active.
|
|
2683
|
+
*
|
|
2684
|
+
* So this connector writes a version only when the value actually changed,
|
|
2685
|
+
* decided from a keyed digest it keeps in the secret's own **annotations** — see
|
|
2686
|
+
* `apps/api/src/lib/sync/connectors/gcp-secret-manager.ts` for why it is keyed
|
|
2687
|
+
* and what that costs. `pruneVersions` is the other half of the bill: with it
|
|
2688
|
+
* on, the version a push supersedes is destroyed as soon as the new one lands,
|
|
2689
|
+
* so a secret keeps exactly one active version.
|
|
2690
|
+
*/
|
|
2691
|
+
const gcpSecretManagerDestinationSchema = z.object({
|
|
2692
|
+
provider: z.literal("gcp-secret-manager"),
|
|
2693
|
+
layout: z.enum(GCP_SECRET_MANAGER_LAYOUTS).default("secret-per-name"),
|
|
2694
|
+
/**
|
|
2695
|
+
* `secret-per-name` only: prepended to every secret ID, e.g.
|
|
2696
|
+
* `prod-storefront-`. Optional, but strongly advised in a project that holds
|
|
2697
|
+
* anything else — without it a binding writes at the root of a namespace it
|
|
2698
|
+
* does not own, and Secret Manager has no folders to hide behind.
|
|
2699
|
+
*/
|
|
2700
|
+
idPrefix: z.string().trim().max(200).regex(/^[A-Za-z0-9_-]*$/, "may contain letters, digits, hyphens, and underscores").optional(),
|
|
2701
|
+
/** `json-bundle` only: the one secret that holds every value, e.g. `prod-storefront-env`. */
|
|
2702
|
+
secretId: gcpSecretIdSchema.optional(),
|
|
2703
|
+
replication: z.enum(GCP_REPLICATION_POLICIES).default("automatic"),
|
|
2704
|
+
/** `user-managed` only: the regions to replicate to. At least one. */
|
|
2705
|
+
locations: z.array(gcpLocationSchema).min(1).max(16).optional(),
|
|
2706
|
+
/** Customer-managed encryption key. Omitted means Google-managed keys. */
|
|
2707
|
+
kmsKeyName: gcpKmsKeyNameSchema.optional(),
|
|
2708
|
+
/** Destroy the version each push supersedes, keeping one active version. */
|
|
2709
|
+
pruneVersions: z.boolean().optional()
|
|
2710
|
+
}).refine((d) => d.layout !== "json-bundle" || d.secretId !== void 0, {
|
|
2711
|
+
message: "a json-bundle destination needs the ID of the secret to write",
|
|
2712
|
+
path: ["secretId"]
|
|
2713
|
+
}).refine((d) => d.replication !== "user-managed" || (d.locations?.length ?? 0) > 0, {
|
|
2714
|
+
message: "user-managed replication needs at least one location",
|
|
2715
|
+
path: ["locations"]
|
|
2716
|
+
}).refine((d) => d.kmsKeyName === void 0 || d.replication === "automatic" || (d.locations?.length ?? 0) === 1, {
|
|
2717
|
+
message: "a customer-managed key covers one location — use automatic replication, or a single location",
|
|
2718
|
+
path: ["kmsKeyName"]
|
|
2719
|
+
});
|
|
2310
2720
|
const syncDestinationSchema = z.discriminatedUnion("provider", [
|
|
2311
2721
|
vercelDestinationSchema,
|
|
2312
2722
|
cloudflareWorkersDestinationSchema,
|
|
@@ -2320,7 +2730,10 @@ const syncDestinationSchema = z.discriminatedUnion("provider", [
|
|
|
2320
2730
|
northflankDestinationSchema,
|
|
2321
2731
|
digitalOceanDestinationSchema,
|
|
2322
2732
|
herokuDestinationSchema,
|
|
2323
|
-
netlifyDestinationSchema
|
|
2733
|
+
netlifyDestinationSchema,
|
|
2734
|
+
bunnyshellDestinationSchema,
|
|
2735
|
+
githubActionsDestinationSchema,
|
|
2736
|
+
gcpSecretManagerDestinationSchema
|
|
2324
2737
|
]);
|
|
2325
2738
|
/**
|
|
2326
2739
|
* How seekrit secret names become destination key names. Applied in order:
|
|
@@ -3526,7 +3939,7 @@ function isCliSessionToken(value) {
|
|
|
3526
3939
|
}
|
|
3527
3940
|
//#endregion
|
|
3528
3941
|
//#region package.json
|
|
3529
|
-
var version = "0.
|
|
3942
|
+
var version = "0.42.0";
|
|
3530
3943
|
//#endregion
|
|
3531
3944
|
//#region ../../packages/api-client/src/index.ts
|
|
3532
3945
|
var SeekritApiError = class extends Error {
|
|
@@ -3848,6 +4261,44 @@ var SeekritClient = class {
|
|
|
3848
4261
|
deleteHoneyToken(orgId, honeyTokenId) {
|
|
3849
4262
|
return this.request("DELETE", `/v1/orgs/${orgId}/honey-tokens/${honeyTokenId}`);
|
|
3850
4263
|
}
|
|
4264
|
+
listAgents(orgId) {
|
|
4265
|
+
return this.request("GET", `/v1/orgs/${orgId}/agents`);
|
|
4266
|
+
}
|
|
4267
|
+
createAgent(orgId, input) {
|
|
4268
|
+
return this.request("POST", `/v1/orgs/${orgId}/agents`, input);
|
|
4269
|
+
}
|
|
4270
|
+
getAgent(orgId, agentId) {
|
|
4271
|
+
return this.request("GET", `/v1/orgs/${orgId}/agents/${agentId}`);
|
|
4272
|
+
}
|
|
4273
|
+
updateAgent(orgId, agentId, input) {
|
|
4274
|
+
return this.request("PATCH", `/v1/orgs/${orgId}/agents/${agentId}`, input);
|
|
4275
|
+
}
|
|
4276
|
+
deleteAgent(orgId, agentId) {
|
|
4277
|
+
return this.request("DELETE", `/v1/orgs/${orgId}/agents/${agentId}`);
|
|
4278
|
+
}
|
|
4279
|
+
/** Published versions, newest first. Append-only; nothing here is rewritten. */
|
|
4280
|
+
listAgentPolicies(orgId, agentId) {
|
|
4281
|
+
return this.request("GET", `/v1/orgs/${orgId}/agents/${agentId}/policies`);
|
|
4282
|
+
}
|
|
4283
|
+
/**
|
|
4284
|
+
* Publish a bundle signed in the browser.
|
|
4285
|
+
*
|
|
4286
|
+
* The signing happens client-side (`signAgentPolicy` in `@seekrit/core`) with
|
|
4287
|
+
* the publishing admin's own key, so the API receives an opaque envelope it
|
|
4288
|
+
* cannot forge. A version mismatch answers `409`: the version is inside the
|
|
4289
|
+
* signature, so a concurrent publish has to be re-signed, not patched.
|
|
4290
|
+
*/
|
|
4291
|
+
publishAgentPolicy(orgId, agentId, bundle) {
|
|
4292
|
+
return this.request("POST", `/v1/orgs/${orgId}/agents/${agentId}/policies`, { bundle });
|
|
4293
|
+
}
|
|
4294
|
+
/** Republish an earlier version's bundle as the newest version. */
|
|
4295
|
+
rollbackAgentPolicy(orgId, agentId, version) {
|
|
4296
|
+
return this.request("POST", `/v1/orgs/${orgId}/agents/${agentId}/policies/${version}/rollback`);
|
|
4297
|
+
}
|
|
4298
|
+
/** The caller's own signing thumbprint, for the trust-anchor snippet. */
|
|
4299
|
+
getMyPolicySigner(orgId) {
|
|
4300
|
+
return this.request("GET", `/v1/orgs/${orgId}/agents/signers/me`);
|
|
4301
|
+
}
|
|
3851
4302
|
/** Keys the caller can see: all org keys for admins, granted keys otherwise. */
|
|
3852
4303
|
listKmsKeys(orgId) {
|
|
3853
4304
|
return this.request("GET", `/v1/orgs/${orgId}/kms/keys`);
|
|
@@ -7643,6 +8094,61 @@ function assertNetlifySite(value) {
|
|
|
7643
8094
|
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(site)) fail(`--netlify-site "${site}" is not a site API ID — Netlify shows it under Project configuration → General → Project information, and it is a UUID, not the site name or its .netlify.app address`);
|
|
7644
8095
|
return site;
|
|
7645
8096
|
}
|
|
8097
|
+
/**
|
|
8098
|
+
* A Bunnyshell environment or project ID, as the platform hands it out.
|
|
8099
|
+
*
|
|
8100
|
+
* Checked only for shape, not format: Bunnyshell documents none for these, so
|
|
8101
|
+
* asserting one would be inventing a rule and refusing IDs that work. Both
|
|
8102
|
+
* variable collections name their parent in the **body** of a create, as a
|
|
8103
|
+
* required relation, so an ID Bunnyshell cannot resolve is a 422 naming the
|
|
8104
|
+
* field — not a write that lands somewhere wider. That is why this is loose
|
|
8105
|
+
* where {@link assertNetlifySite} is strict.
|
|
8106
|
+
*/
|
|
8107
|
+
function assertBunnyshellId(value, flag) {
|
|
8108
|
+
if (!value) fail(`${flag} is required for bunnyshell (the ID from \`bns\` or the dashboard URL)`);
|
|
8109
|
+
const id = value.trim();
|
|
8110
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(id)) fail(`${flag} "${id}" is not a Bunnyshell ID — no slashes or spaces`);
|
|
8111
|
+
return id;
|
|
8112
|
+
}
|
|
8113
|
+
/**
|
|
8114
|
+
* Split `--gh-repo owner/name` into the two halves GitHub's paths need.
|
|
8115
|
+
*
|
|
8116
|
+
* Taken as one flag rather than two because `owner/name` is how GitHub writes a
|
|
8117
|
+
* repository everywhere — in its URLs, in `gh repo view`, in every workflow file —
|
|
8118
|
+
* and asking for it in two pieces invites the mistake of pasting the pair into
|
|
8119
|
+
* one of them.
|
|
8120
|
+
*/
|
|
8121
|
+
function assertGithubRepo(value) {
|
|
8122
|
+
if (!value) fail("--gh-repo is required for github-actions (owner/name)");
|
|
8123
|
+
const parts = value.trim().split("/");
|
|
8124
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) fail(`--gh-repo "${value}" should be owner/name, e.g. acme/storefront`);
|
|
8125
|
+
const [owner, repo] = parts;
|
|
8126
|
+
if (!/^[A-Za-z0-9](?:-?[A-Za-z0-9])*$/.test(owner) || owner.length > 39) fail(`--gh-repo owner "${owner}" is not a GitHub login`);
|
|
8127
|
+
if (!/^[A-Za-z0-9._-]+$/.test(repo) || repo === "." || repo === "..") fail(`--gh-repo name "${repo}" is not a repository name`);
|
|
8128
|
+
return {
|
|
8129
|
+
owner,
|
|
8130
|
+
repo
|
|
8131
|
+
};
|
|
8132
|
+
}
|
|
8133
|
+
/** Parse `--gh-repo-ids 1,2,3` — GitHub's org endpoint takes numeric ids, not names. */
|
|
8134
|
+
function assertRepoIds(raw) {
|
|
8135
|
+
const parts = list(raw, "");
|
|
8136
|
+
if (parts.length === 0) fail("--gh-repo-ids is required with `--gh-visibility selected` — numeric repository IDs, which `gh api repos/<owner>/<name> --jq .id` prints");
|
|
8137
|
+
return parts.map((part) => {
|
|
8138
|
+
const id = Number(part);
|
|
8139
|
+
if (!Number.isInteger(id) || id <= 0) fail(`--gh-repo-ids "${part}" is not a repository ID — GitHub takes numeric IDs, not names`);
|
|
8140
|
+
return id;
|
|
8141
|
+
});
|
|
8142
|
+
}
|
|
8143
|
+
/**
|
|
8144
|
+
* What `sync connect` calls the secret it reads from stdin. Only the wording
|
|
8145
|
+
* differs — every provider's credential is wrapped the same way.
|
|
8146
|
+
*/
|
|
8147
|
+
function credentialNoun(provider) {
|
|
8148
|
+
if (provider.startsWith("aws-")) return "secret access key";
|
|
8149
|
+
if (provider === "gcp-secret-manager") return "service-account key JSON";
|
|
8150
|
+
return "API token";
|
|
8151
|
+
}
|
|
7646
8152
|
/** Account-scope config for a connection (never the credential itself). */
|
|
7647
8153
|
function buildConfig(provider, options) {
|
|
7648
8154
|
switch (provider) {
|
|
@@ -7686,6 +8192,17 @@ function buildConfig(provider, options) {
|
|
|
7686
8192
|
provider: "netlify",
|
|
7687
8193
|
accountId: options.accountId
|
|
7688
8194
|
};
|
|
8195
|
+
case "bunnyshell": return { provider: "bunnyshell" };
|
|
8196
|
+
case "github-actions": return {
|
|
8197
|
+
provider: "github-actions",
|
|
8198
|
+
...options.baseUrl ? { baseUrl: options.baseUrl } : {}
|
|
8199
|
+
};
|
|
8200
|
+
case "gcp-secret-manager":
|
|
8201
|
+
if (!options.projectId) fail("--project-id is required for gcp-secret-manager — the project ID (or number) whose Secret Manager to write");
|
|
8202
|
+
return {
|
|
8203
|
+
provider: "gcp-secret-manager",
|
|
8204
|
+
projectId: options.projectId
|
|
8205
|
+
};
|
|
7689
8206
|
}
|
|
7690
8207
|
}
|
|
7691
8208
|
/** Where inside the platform a binding writes. */
|
|
@@ -7819,6 +8336,71 @@ function buildDestination(provider, options) {
|
|
|
7819
8336
|
secret: options.netlifySecret !== false
|
|
7820
8337
|
};
|
|
7821
8338
|
}
|
|
8339
|
+
case "bunnyshell": {
|
|
8340
|
+
const environmentId = options.bunnyshellEnvironment?.trim();
|
|
8341
|
+
const projectId = options.project?.trim();
|
|
8342
|
+
if (environmentId && projectId) fail("pass --bunnyshell-environment or --project for bunnyshell, not both — a binding writes to one");
|
|
8343
|
+
const secret = options.bunnyshellSecret !== false;
|
|
8344
|
+
if (projectId) return {
|
|
8345
|
+
provider: "bunnyshell",
|
|
8346
|
+
kind: "project",
|
|
8347
|
+
projectId: assertBunnyshellId(projectId, "--project"),
|
|
8348
|
+
secret
|
|
8349
|
+
};
|
|
8350
|
+
return {
|
|
8351
|
+
provider: "bunnyshell",
|
|
8352
|
+
kind: "environment",
|
|
8353
|
+
environmentId: assertBunnyshellId(environmentId, "--bunnyshell-environment"),
|
|
8354
|
+
secret
|
|
8355
|
+
};
|
|
8356
|
+
}
|
|
8357
|
+
case "github-actions": {
|
|
8358
|
+
if (options.ghOrg) {
|
|
8359
|
+
if (options.ghRepo || options.ghEnvironment) fail("--gh-org writes organization secrets — drop --gh-repo and --gh-environment");
|
|
8360
|
+
const visibility = assertMember(options.ghVisibility, GITHUB_ACTIONS_VISIBILITIES, "--gh-visibility", "private");
|
|
8361
|
+
if (visibility !== "selected" && options.ghRepoIds) fail(`--gh-repo-ids only applies to \`--gh-visibility selected\`, not ${visibility}`);
|
|
8362
|
+
return {
|
|
8363
|
+
provider: "github-actions",
|
|
8364
|
+
kind: "org",
|
|
8365
|
+
org: options.ghOrg.trim(),
|
|
8366
|
+
visibility,
|
|
8367
|
+
...visibility === "selected" ? { selectedRepositoryIds: assertRepoIds(options.ghRepoIds) } : {}
|
|
8368
|
+
};
|
|
8369
|
+
}
|
|
8370
|
+
const { owner, repo } = assertGithubRepo(options.ghRepo);
|
|
8371
|
+
const environment = options.ghEnvironment?.trim();
|
|
8372
|
+
if (!environment) return {
|
|
8373
|
+
provider: "github-actions",
|
|
8374
|
+
kind: "repo",
|
|
8375
|
+
owner,
|
|
8376
|
+
repo
|
|
8377
|
+
};
|
|
8378
|
+
return {
|
|
8379
|
+
provider: "github-actions",
|
|
8380
|
+
kind: "environment",
|
|
8381
|
+
owner,
|
|
8382
|
+
repo,
|
|
8383
|
+
environment
|
|
8384
|
+
};
|
|
8385
|
+
}
|
|
8386
|
+
case "gcp-secret-manager": {
|
|
8387
|
+
const layout = assertMember(options.layout, GCP_SECRET_MANAGER_LAYOUTS, "--layout", "secret-per-name");
|
|
8388
|
+
if (layout === "json-bundle" && !options.secretName) fail("--secret-name is required for --layout json-bundle (the one secret to write)");
|
|
8389
|
+
const replication = assertMember(options.gcpReplication, GCP_REPLICATION_POLICIES, "--gcp-replication", "automatic");
|
|
8390
|
+
const locations = options.gcpLocations ? list(options.gcpLocations, "") : [];
|
|
8391
|
+
if (replication === "user-managed" && locations.length === 0) fail("--gcp-locations is required with --gcp-replication user-managed, e.g. us-east1");
|
|
8392
|
+
if (options.gcpKmsKey && replication === "user-managed" && locations.length > 1) fail("--gcp-kms-key covers one location — use automatic replication, or a single --gcp-locations entry");
|
|
8393
|
+
return {
|
|
8394
|
+
provider: "gcp-secret-manager",
|
|
8395
|
+
layout,
|
|
8396
|
+
replication,
|
|
8397
|
+
...options.gcpPrefix ? { idPrefix: options.gcpPrefix } : {},
|
|
8398
|
+
...layout === "json-bundle" && options.secretName ? { secretId: options.secretName } : {},
|
|
8399
|
+
...replication === "user-managed" ? { locations } : {},
|
|
8400
|
+
...options.gcpKmsKey ? { kmsKeyName: options.gcpKmsKey } : {},
|
|
8401
|
+
...options.gcpPruneVersions ? { pruneVersions: true } : {}
|
|
8402
|
+
};
|
|
8403
|
+
}
|
|
7822
8404
|
}
|
|
7823
8405
|
}
|
|
7824
8406
|
/** One-line description of a destination, for list output. */
|
|
@@ -7837,6 +8419,13 @@ function describeDestination(destination) {
|
|
|
7837
8419
|
case "digitalocean": return `${destination.appId}${destination.kind === "component" ? ` / ${destination.componentName}` : ""} (${destination.scope})`;
|
|
7838
8420
|
case "heroku": return destination.app;
|
|
7839
8421
|
case "netlify": return `${destination.siteId} (${destination.contexts.map((context) => context === "branch" ? `branch @${destination.branch}` : context).join(", ")})`;
|
|
8422
|
+
case "bunnyshell": return destination.kind === "environment" ? `environment ${destination.environmentId}` : `project ${destination.projectId} (inherited by new environments)`;
|
|
8423
|
+
case "gcp-secret-manager": return destination.layout === "json-bundle" ? `${destination.secretId} · json` : `${destination.idPrefix ?? ""}* · one per name`;
|
|
8424
|
+
case "github-actions": switch (destination.kind) {
|
|
8425
|
+
case "repo": return `${destination.owner}/${destination.repo}`;
|
|
8426
|
+
case "environment": return `${destination.owner}/${destination.repo} @${destination.environment}`;
|
|
8427
|
+
case "org": return `org ${destination.org} (${destination.visibility}${destination.selectedRepositoryIds ? `: ${destination.selectedRepositoryIds.length} repos` : ""})`;
|
|
8428
|
+
}
|
|
7840
8429
|
}
|
|
7841
8430
|
}
|
|
7842
8431
|
/**
|
|
@@ -7847,7 +8436,7 @@ function describeDestination(destination) {
|
|
|
7847
8436
|
* application whose environment the binding reads from.
|
|
7848
8437
|
*/
|
|
7849
8438
|
function destinationOptions(command) {
|
|
7850
|
-
return command.option("--project <id>", "vercel: project id or name · cloudflare-pages / northflank: project name or slug").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").option("--railway-project <id>", "railway: project ID (a UUID)").option("--railway-environment <id>", "railway: environment ID (a UUID)").option("--service <id>", "railway: service ID (omit for the environment's shared variables) · render: service ID (srv-…, or crn-… for a cron job)").option("--skip-deploys", "railway: stage values without triggering a redeploy").option("--path <path>", "aws-parameter-store: hierarchy, e.g. /prod/storefront/ · aws-secrets-manager: name prefix").option("--layout <layout>", `aws-secrets-manager: ${AWS_SECRETS_MANAGER_LAYOUTS.join(" | ")}`).option("--secret-name <name>", "aws-secrets-manager: the secret a json-bundle writes to").option("--param-type <type>", `aws-parameter-store: ${AWS_PARAMETER_TYPES.join(" | ")}`).option("--tier <tier>", `aws-parameter-store: ${AWS_PARAMETER_TIERS.join(" | ")}`).option("--kms-key-id <id>", "aws: customer-managed KMS key id, ARN, or alias").option("--env-group <id>", "render: environment group ID (evg-…)").option("--fly-app <name>", "fly: app name, as `fly apps list` shows it").option("--secret-group <id>", "northflank: secret group ID (the slug in its URL)").option("--do-app <id>", "digitalocean: App Platform app ID (the UUID in its URL)").option("--component <name>", "digitalocean: write to one component's variables (omit for app-level)").option("--env-scope <scope>", `digitalocean: ${DIGITALOCEAN_ENV_SCOPES.join(" | ")}`).option("--heroku-app <name>", "heroku: app name, as `heroku apps` shows it (or its UUID)").option("--netlify-site <id>", "netlify: site API ID (the UUID under Project configuration)").option("--no-netlify-secret", "netlify: create readable variables instead of Netlify secrets (write-only)");
|
|
8439
|
+
return command.option("--project <id>", "vercel: project id or name · cloudflare-pages / northflank: project name or slug · bunnyshell: project ID (writes variables new environments inherit)").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").option("--railway-project <id>", "railway: project ID (a UUID)").option("--railway-environment <id>", "railway: environment ID (a UUID)").option("--service <id>", "railway: service ID (omit for the environment's shared variables) · render: service ID (srv-…, or crn-… for a cron job)").option("--skip-deploys", "railway: stage values without triggering a redeploy").option("--path <path>", "aws-parameter-store: hierarchy, e.g. /prod/storefront/ · aws-secrets-manager: name prefix").option("--layout <layout>", `aws-secrets-manager: ${AWS_SECRETS_MANAGER_LAYOUTS.join(" | ")}`).option("--secret-name <name>", "aws-secrets-manager: the secret a json-bundle writes to").option("--param-type <type>", `aws-parameter-store: ${AWS_PARAMETER_TYPES.join(" | ")}`).option("--tier <tier>", `aws-parameter-store: ${AWS_PARAMETER_TIERS.join(" | ")}`).option("--kms-key-id <id>", "aws: customer-managed KMS key id, ARN, or alias").option("--env-group <id>", "render: environment group ID (evg-…)").option("--fly-app <name>", "fly: app name, as `fly apps list` shows it").option("--secret-group <id>", "northflank: secret group ID (the slug in its URL)").option("--do-app <id>", "digitalocean: App Platform app ID (the UUID in its URL)").option("--component <name>", "digitalocean: write to one component's variables (omit for app-level)").option("--env-scope <scope>", `digitalocean: ${DIGITALOCEAN_ENV_SCOPES.join(" | ")}`).option("--heroku-app <name>", "heroku: app name, as `heroku apps` shows it (or its UUID)").option("--netlify-site <id>", "netlify: site API ID (the UUID under Project configuration)").option("--no-netlify-secret", "netlify: create readable variables instead of Netlify secrets (write-only)").option("--bunnyshell-environment <id>", "bunnyshell: environment ID (omit to write a project)").option("--no-bunnyshell-secret", "bunnyshell: create variables visible in the dashboard instead of secret ones").option("--gh-repo <owner/name>", "github-actions: repository, e.g. acme/storefront").option("--gh-environment <name>", "github-actions: write to one deployment environment's secrets (needs --gh-repo)").option("--gh-org <login>", "github-actions: write organization secrets instead of a repo's").option("--gh-visibility <v>", `github-actions org secrets: ${GITHUB_ACTIONS_VISIBILITIES.join(" | ")}`).option("--gh-repo-ids <ids>", "github-actions: comma-separated numeric repository IDs for --gh-visibility selected").option("--gcp-prefix <prefix>", "gcp: prepended to every secret ID, e.g. prod-storefront-").option("--gcp-replication <policy>", `gcp: ${GCP_REPLICATION_POLICIES.join(" | ")}`, "automatic").option("--gcp-locations <list>", "gcp: regions for user-managed replication, e.g. us-east1").option("--gcp-kms-key <name>", "gcp: Cloud KMS key (projects/…/cryptoKeys/…)").option("--gcp-prune-versions", "gcp: destroy the version each push supersedes, keeping one active version");
|
|
7851
8440
|
}
|
|
7852
8441
|
/** Find a connection by id or name — nobody keeps `syc_…` ids in their head. */
|
|
7853
8442
|
async function resolveConnection(ctx, orgId, ref) {
|
|
@@ -7871,11 +8460,11 @@ function registerSyncCommands(program) {
|
|
|
7871
8460
|
col("id", (c) => c.id)
|
|
7872
8461
|
], "no connections — add one with `seekrit sync connect`"));
|
|
7873
8462
|
});
|
|
7874
|
-
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("--token-kind <kind>", `railway: ${RAILWAY_TOKEN_KINDS.join(" | ")}`, "account").option("--account-id <id>", "cloudflare: account ID (32 hex, from the dashboard sidebar) · netlify: team slug or account ID").option("--region <region>", "aws: region ID, e.g. us-east-1").option("--access-key-id <id>", "aws: IAM access key ID (the secret key is read from stdin)").action(async (options) => {
|
|
8463
|
+
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("--token-kind <kind>", `railway: ${RAILWAY_TOKEN_KINDS.join(" | ")}`, "account").option("--account-id <id>", "cloudflare: account ID (32 hex, from the dashboard sidebar) · netlify: team slug or account ID").option("--region <region>", "aws: region ID, e.g. us-east-1").option("--access-key-id <id>", "aws: IAM access key ID (the secret key is read from stdin)").option("--base-url <url>", "github-actions: GitHub Enterprise Server API root (omit for github.com)").option("--project-id <id>", "gcp: project ID or number whose Secret Manager to write").action(async (options) => {
|
|
7875
8464
|
const provider = assertProvider(options.provider);
|
|
7876
8465
|
const ctx = buildContext();
|
|
7877
8466
|
const ref = await resolveOrg(ctx, options.org);
|
|
7878
|
-
const noun = provider
|
|
8467
|
+
const noun = credentialNoun(provider);
|
|
7879
8468
|
const credential = (process.stdin.isTTY ? await promptHidden(`${provider} ${noun}: `) : await readStdin()).trim();
|
|
7880
8469
|
if (!credential) fail(`no ${noun} given`);
|
|
7881
8470
|
const id = randomId("syc");
|
|
@@ -8224,7 +8813,11 @@ program.command("login").description("sign in through the browser (or pass a cre
|
|
|
8224
8813
|
} : {},
|
|
8225
8814
|
...options.clientId ? { clientId: options.clientId } : {},
|
|
8226
8815
|
...options.clientSecret ? { clientSecret: options.clientSecret } : {},
|
|
8227
|
-
...options.devUser ? {
|
|
8816
|
+
...options.devUser ? {
|
|
8817
|
+
devUser: options.devUser,
|
|
8818
|
+
sessionToken: void 0,
|
|
8819
|
+
token: void 0
|
|
8820
|
+
} : {},
|
|
8228
8821
|
...options.apiUrl ? { apiUrl: options.apiUrl } : {}
|
|
8229
8822
|
});
|
|
8230
8823
|
console.error("credentials saved");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seekrit/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.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/
|
|
30
|
+
"@seekrit/api-client": "0.0.1",
|
|
31
31
|
"@seekrit/crypto": "0.0.1",
|
|
32
|
-
"@seekrit/
|
|
32
|
+
"@seekrit/core": "0.0.1"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
35
|
"build": "tsdown",
|