@seekrit/cli 0.34.0 → 0.35.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 +103 -6
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1517,7 +1517,8 @@ const SYNC_PROVIDER_KINDS = [
|
|
|
1517
1517
|
"vercel",
|
|
1518
1518
|
"cloudflare-workers",
|
|
1519
1519
|
"cloudflare-pages",
|
|
1520
|
-
"cloudflare-secrets-store"
|
|
1520
|
+
"cloudflare-secrets-store",
|
|
1521
|
+
"railway"
|
|
1521
1522
|
];
|
|
1522
1523
|
z.enum(SYNC_PROVIDER_KINDS);
|
|
1523
1524
|
/**
|
|
@@ -1565,11 +1566,46 @@ const cloudflareSecretsStoreConnectionConfigSchema = z.object({
|
|
|
1565
1566
|
provider: z.literal("cloudflare-secrets-store"),
|
|
1566
1567
|
accountId: cloudflareAccountIdSchema
|
|
1567
1568
|
});
|
|
1569
|
+
/**
|
|
1570
|
+
* A Railway id — every project, environment, and service is a UUID. Validated
|
|
1571
|
+
* by shape for the same reason Cloudflare's account id is: the alternative is a
|
|
1572
|
+
* bare GraphQL "Problem processing request" hours later inside an alarm, with
|
|
1573
|
+
* nobody watching.
|
|
1574
|
+
*/
|
|
1575
|
+
const railwayIdSchema = z.string().trim().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, "must be a Railway UUID");
|
|
1576
|
+
/**
|
|
1577
|
+
* Which kind of Railway API token the connection holds. This is connection
|
|
1578
|
+
* scope rather than a destination detail because it decides the *header* the
|
|
1579
|
+
* request carries, and getting it wrong fails every call identically:
|
|
1580
|
+
*
|
|
1581
|
+
* - `account` — a personal or workspace token, sent as `Authorization: Bearer`.
|
|
1582
|
+
* Reaches every project the token's owner can see.
|
|
1583
|
+
* - `project` — a project token, sent as `Project-Access-Token`. Scoped to one
|
|
1584
|
+
* project and environment by Railway itself, which makes it the
|
|
1585
|
+
* least-privilege choice when a connection serves a single destination.
|
|
1586
|
+
*
|
|
1587
|
+
* Railway rejects a project token sent as a bearer, so this cannot be sniffed
|
|
1588
|
+
* at request time — the operator states it once, when they paste the token.
|
|
1589
|
+
*/
|
|
1590
|
+
const RAILWAY_TOKEN_KINDS = ["account", "project"];
|
|
1591
|
+
/**
|
|
1592
|
+
* Railway account scope. The token is never here — it is wrapped to the
|
|
1593
|
+
* connection's public key and stored as ciphertext, exactly as Vercel's is.
|
|
1594
|
+
*
|
|
1595
|
+
* There is no workspace/team id to carry: Railway ids are globally unique and
|
|
1596
|
+
* a destination names its project outright, so the token plus the destination
|
|
1597
|
+
* is the whole address.
|
|
1598
|
+
*/
|
|
1599
|
+
const railwayConnectionConfigSchema = z.object({
|
|
1600
|
+
provider: z.literal("railway"),
|
|
1601
|
+
tokenKind: z.enum(RAILWAY_TOKEN_KINDS).default("account")
|
|
1602
|
+
});
|
|
1568
1603
|
const syncConnectionConfigSchema = z.discriminatedUnion("provider", [
|
|
1569
1604
|
vercelConnectionConfigSchema,
|
|
1570
1605
|
cloudflareWorkersConnectionConfigSchema,
|
|
1571
1606
|
cloudflarePagesConnectionConfigSchema,
|
|
1572
|
-
cloudflareSecretsStoreConnectionConfigSchema
|
|
1607
|
+
cloudflareSecretsStoreConnectionConfigSchema,
|
|
1608
|
+
railwayConnectionConfigSchema
|
|
1573
1609
|
]);
|
|
1574
1610
|
/** Vercel's three deployment targets. A binding writes to one or more. */
|
|
1575
1611
|
const VERCEL_TARGETS = [
|
|
@@ -1628,11 +1664,44 @@ const cloudflareSecretsStoreDestinationSchema = z.object({
|
|
|
1628
1664
|
/** Scopes applied to secrets this binding creates. At least one. */
|
|
1629
1665
|
scopes: z.array(z.enum(CLOUDFLARE_SECRETS_STORE_SCOPES)).min(1)
|
|
1630
1666
|
});
|
|
1667
|
+
/**
|
|
1668
|
+
* Where inside Railway a binding writes.
|
|
1669
|
+
*
|
|
1670
|
+
* Railway variables are addressed by (project, environment, service) — the
|
|
1671
|
+
* environment here is *Railway's* (`production`, `pr-42`), not the seekrit
|
|
1672
|
+
* environment the binding reads from; a binding is precisely the mapping
|
|
1673
|
+
* between the two.
|
|
1674
|
+
*
|
|
1675
|
+
* Omitting `serviceId` targets the project's **shared** variables for that
|
|
1676
|
+
* environment, which services opt into with `${{shared.NAME}}`. That is a
|
|
1677
|
+
* genuinely different destination from any one service's variables, so it is an
|
|
1678
|
+
* absent field rather than a sentinel.
|
|
1679
|
+
*/
|
|
1680
|
+
const railwayDestinationSchema = z.object({
|
|
1681
|
+
provider: z.literal("railway"),
|
|
1682
|
+
/** Railway project id (a UUID, from the project's Settings page or URL). */
|
|
1683
|
+
projectId: railwayIdSchema,
|
|
1684
|
+
/** Railway environment id (a UUID) — the deployment environment to write. */
|
|
1685
|
+
environmentId: railwayIdSchema,
|
|
1686
|
+
/** Service to write. Omit to write the environment's shared variables. */
|
|
1687
|
+
serviceId: railwayIdSchema.optional(),
|
|
1688
|
+
/**
|
|
1689
|
+
* Suppress the redeploy Railway triggers when a variable changes.
|
|
1690
|
+
*
|
|
1691
|
+
* Left off (the default), a sync that changes a value redeploys the service,
|
|
1692
|
+
* which is what makes the new value actually reach the running process —
|
|
1693
|
+
* Railway applies variables at deploy time. Turn it on when deploys are
|
|
1694
|
+
* gated behind a release process and a secrets push must not start one; the
|
|
1695
|
+
* values then sit staged until the next deploy.
|
|
1696
|
+
*/
|
|
1697
|
+
skipDeploys: z.boolean().optional()
|
|
1698
|
+
});
|
|
1631
1699
|
const syncDestinationSchema = z.discriminatedUnion("provider", [
|
|
1632
1700
|
vercelDestinationSchema,
|
|
1633
1701
|
cloudflareWorkersDestinationSchema,
|
|
1634
1702
|
cloudflarePagesDestinationSchema,
|
|
1635
|
-
cloudflareSecretsStoreDestinationSchema
|
|
1703
|
+
cloudflareSecretsStoreDestinationSchema,
|
|
1704
|
+
railwayDestinationSchema
|
|
1636
1705
|
]);
|
|
1637
1706
|
/**
|
|
1638
1707
|
* How seekrit secret names become destination key names. Applied in order:
|
|
@@ -2837,7 +2906,7 @@ function isCliSessionToken(value) {
|
|
|
2837
2906
|
}
|
|
2838
2907
|
//#endregion
|
|
2839
2908
|
//#region package.json
|
|
2840
|
-
var version = "0.
|
|
2909
|
+
var version = "0.35.0";
|
|
2841
2910
|
//#endregion
|
|
2842
2911
|
//#region ../../packages/api-client/src/index.ts
|
|
2843
2912
|
var SeekritApiError = class extends Error {
|
|
@@ -6652,6 +6721,13 @@ function assertMembers(values, allowed, flag) {
|
|
|
6652
6721
|
if (unknown.length > 0) fail(`unknown ${flag} ${unknown.join(", ")} — one of: ${allowed.join(", ")}`);
|
|
6653
6722
|
return values;
|
|
6654
6723
|
}
|
|
6724
|
+
/** Every Railway id is a UUID; a name or a URL slug in the slot is the common slip. */
|
|
6725
|
+
function assertRailwayId(value, flag) {
|
|
6726
|
+
if (!value) fail(`${flag} is required for railway (a UUID)`);
|
|
6727
|
+
const id = value.trim();
|
|
6728
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)) fail(`${flag} should be a Railway UUID, not "${id}"`);
|
|
6729
|
+
return id;
|
|
6730
|
+
}
|
|
6655
6731
|
function assertProvider(value) {
|
|
6656
6732
|
if (!SYNC_PROVIDER_KINDS.includes(value)) fail(`unknown provider "${value}" — one of: ${SYNC_PROVIDER_KINDS.join(", ")}`);
|
|
6657
6733
|
return value;
|
|
@@ -6671,6 +6747,14 @@ function buildConfig(provider, options) {
|
|
|
6671
6747
|
provider,
|
|
6672
6748
|
accountId: options.accountId
|
|
6673
6749
|
};
|
|
6750
|
+
case "railway": {
|
|
6751
|
+
const tokenKind = options.tokenKind ?? "account";
|
|
6752
|
+
if (!RAILWAY_TOKEN_KINDS.includes(tokenKind)) fail(`unknown --token-kind "${tokenKind}" — one of: ${RAILWAY_TOKEN_KINDS.join(", ")}`);
|
|
6753
|
+
return {
|
|
6754
|
+
provider: "railway",
|
|
6755
|
+
tokenKind
|
|
6756
|
+
};
|
|
6757
|
+
}
|
|
6674
6758
|
}
|
|
6675
6759
|
}
|
|
6676
6760
|
/** Where inside the platform a binding writes. */
|
|
@@ -6712,6 +6796,18 @@ function buildDestination(provider, options) {
|
|
|
6712
6796
|
scopes
|
|
6713
6797
|
};
|
|
6714
6798
|
}
|
|
6799
|
+
case "railway": {
|
|
6800
|
+
const projectId = assertRailwayId(options.railwayProject, "--railway-project");
|
|
6801
|
+
const environmentId = assertRailwayId(options.railwayEnvironment, "--railway-environment");
|
|
6802
|
+
const serviceId = options.service ? assertRailwayId(options.service, "--service") : void 0;
|
|
6803
|
+
return {
|
|
6804
|
+
provider: "railway",
|
|
6805
|
+
projectId,
|
|
6806
|
+
environmentId,
|
|
6807
|
+
...serviceId ? { serviceId } : {},
|
|
6808
|
+
...options.skipDeploys ? { skipDeploys: true } : {}
|
|
6809
|
+
};
|
|
6810
|
+
}
|
|
6715
6811
|
}
|
|
6716
6812
|
}
|
|
6717
6813
|
/** One-line description of a destination, for list output. */
|
|
@@ -6721,6 +6817,7 @@ function describeDestination(destination) {
|
|
|
6721
6817
|
case "cloudflare-workers": return destination.scriptName;
|
|
6722
6818
|
case "cloudflare-pages": return `${destination.projectName} (${destination.environments.join(", ")})`;
|
|
6723
6819
|
case "cloudflare-secrets-store": return `store ${destination.storeId} (${destination.scopes.join(", ")})`;
|
|
6820
|
+
case "railway": return `${destination.projectId} / ${destination.environmentId} (${destination.serviceId ? `service ${destination.serviceId}` : "shared"})`;
|
|
6724
6821
|
}
|
|
6725
6822
|
}
|
|
6726
6823
|
/**
|
|
@@ -6728,7 +6825,7 @@ function describeDestination(destination) {
|
|
|
6728
6825
|
* into accepting different ways of naming the same destination.
|
|
6729
6826
|
*/
|
|
6730
6827
|
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");
|
|
6828
|
+
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").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)").option("--skip-deploys", "railway: stage values without triggering a redeploy");
|
|
6732
6829
|
}
|
|
6733
6830
|
/** Find a connection by id or name — nobody keeps `syc_…` ids in their head. */
|
|
6734
6831
|
async function resolveConnection(ctx, orgId, ref) {
|
|
@@ -6752,7 +6849,7 @@ function registerSyncCommands(program) {
|
|
|
6752
6849
|
col("id", (c) => c.id)
|
|
6753
6850
|
], "no connections — add one with `seekrit sync connect`"));
|
|
6754
6851
|
});
|
|
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) => {
|
|
6852
|
+
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)").action(async (options) => {
|
|
6756
6853
|
const provider = assertProvider(options.provider);
|
|
6757
6854
|
const ctx = buildContext();
|
|
6758
6855
|
const ref = await resolveOrg(ctx, options.org);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seekrit/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
4
4
|
"description": "End-to-end encrypted secrets manager CLI — inject decrypted secrets into any command.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"tsdown": "^0.22.3",
|
|
29
29
|
"vitest": "^4.1.9",
|
|
30
30
|
"@seekrit/core": "0.0.1",
|
|
31
|
-
"@seekrit/
|
|
32
|
-
"@seekrit/
|
|
31
|
+
"@seekrit/api-client": "0.0.1",
|
|
32
|
+
"@seekrit/crypto": "0.0.1"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
35
|
"build": "tsdown",
|