@seekrit/cli 0.38.0 → 0.40.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 +734 -55
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -28,6 +28,12 @@ const ENTITLEMENT_KEYS = Object.keys({
|
|
|
28
28
|
description: "Vault-style short-lived database and cloud credentials.",
|
|
29
29
|
default: true
|
|
30
30
|
},
|
|
31
|
+
"feature.rotation": {
|
|
32
|
+
kind: "feature",
|
|
33
|
+
label: "Secret rotation",
|
|
34
|
+
description: "Managed, scheduled rotation of stored credentials.",
|
|
35
|
+
default: true
|
|
36
|
+
},
|
|
31
37
|
"feature.log_sink": {
|
|
32
38
|
kind: "feature",
|
|
33
39
|
label: "Audit log export (SIEM)",
|
|
@@ -106,6 +112,12 @@ const ENTITLEMENT_KEYS = Object.keys({
|
|
|
106
112
|
description: "Maximum registered third-party sync destinations.",
|
|
107
113
|
default: null
|
|
108
114
|
},
|
|
115
|
+
"rotation.policies.max": {
|
|
116
|
+
kind: "limit",
|
|
117
|
+
label: "Rotation policies",
|
|
118
|
+
description: "Maximum secrets with managed rotation configured.",
|
|
119
|
+
default: null
|
|
120
|
+
},
|
|
109
121
|
members: {
|
|
110
122
|
kind: "metered",
|
|
111
123
|
label: "Members",
|
|
@@ -754,7 +766,7 @@ const connectionSchema = z.object({
|
|
|
754
766
|
database: z.string().min(1)
|
|
755
767
|
});
|
|
756
768
|
/** A `{{name}}`/`{{verifier}}`/`{{valid_until}}` templated SQL statement. */
|
|
757
|
-
const statementSchema = z.string().min(1).max(4e3);
|
|
769
|
+
const statementSchema$1 = z.string().min(1).max(4e3);
|
|
758
770
|
/** A bare SQL identifier (schema name) — no quotes/whitespace/semicolons. */
|
|
759
771
|
const identifierSchema = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,62}$/, "must be an identifier");
|
|
760
772
|
const postgresTargetConfigSchema = z.object({
|
|
@@ -764,20 +776,20 @@ const postgresTargetConfigSchema = z.object({
|
|
|
764
776
|
schema: identifierSchema.optional(),
|
|
765
777
|
connection: connectionSchema,
|
|
766
778
|
provisionerUrl: z.url().optional(),
|
|
767
|
-
createStatements: z.array(statementSchema).max(16).optional(),
|
|
768
|
-
revokeStatements: z.array(statementSchema).max(16).optional()
|
|
779
|
+
createStatements: z.array(statementSchema$1).max(16).optional(),
|
|
780
|
+
revokeStatements: z.array(statementSchema$1).max(16).optional()
|
|
769
781
|
});
|
|
770
782
|
/** A MySQL account host part (`'name'@'<host>'`) — no quotes/whitespace. */
|
|
771
|
-
const mysqlHostSchema = z.string().regex(/^[A-Za-z0-9_.%:-]{1,255}$/, "must be a host pattern");
|
|
783
|
+
const mysqlHostSchema$1 = z.string().regex(/^[A-Za-z0-9_.%:-]{1,255}$/, "must be a host pattern");
|
|
772
784
|
const mysqlTargetConfigSchema = z.object({
|
|
773
785
|
provider: z.literal("mysql"),
|
|
774
786
|
executor: executorModeSchema,
|
|
775
787
|
accessLevel: mysqlAccessLevelSchema.optional(),
|
|
776
788
|
connection: connectionSchema,
|
|
777
|
-
userHost: mysqlHostSchema.optional(),
|
|
789
|
+
userHost: mysqlHostSchema$1.optional(),
|
|
778
790
|
provisionerUrl: z.url().optional(),
|
|
779
|
-
createStatements: z.array(statementSchema).max(16).optional(),
|
|
780
|
-
revokeStatements: z.array(statementSchema).max(16).optional()
|
|
791
|
+
createStatements: z.array(statementSchema$1).max(16).optional(),
|
|
792
|
+
revokeStatements: z.array(statementSchema$1).max(16).optional()
|
|
781
793
|
});
|
|
782
794
|
const redisConnectionSchema = z.object({
|
|
783
795
|
host: z.string().min(1),
|
|
@@ -791,8 +803,8 @@ const redisTargetConfigSchema = z.object({
|
|
|
791
803
|
accessLevel: redisAccessLevelSchema.optional(),
|
|
792
804
|
connection: redisConnectionSchema,
|
|
793
805
|
provisionerUrl: z.url().optional(),
|
|
794
|
-
createStatements: z.array(statementSchema).max(16).optional(),
|
|
795
|
-
revokeStatements: z.array(statementSchema).max(16).optional()
|
|
806
|
+
createStatements: z.array(statementSchema$1).max(16).optional(),
|
|
807
|
+
revokeStatements: z.array(statementSchema$1).max(16).optional()
|
|
796
808
|
});
|
|
797
809
|
const sshTargetConfigSchema = z.object({
|
|
798
810
|
provider: z.literal("ssh"),
|
|
@@ -1122,6 +1134,11 @@ const AUDIT_ACTIONS = [
|
|
|
1122
1134
|
"secret.deleted",
|
|
1123
1135
|
"secret.read",
|
|
1124
1136
|
"secret.restored",
|
|
1137
|
+
"secret.rotation_configured",
|
|
1138
|
+
"secret.rotation_updated",
|
|
1139
|
+
"secret.rotation_disabled",
|
|
1140
|
+
"secret.rotated",
|
|
1141
|
+
"secret.rotation_failed",
|
|
1125
1142
|
"token.created",
|
|
1126
1143
|
"token.revoked",
|
|
1127
1144
|
"token.deleted",
|
|
@@ -1147,6 +1164,9 @@ const AUDIT_ACTIONS = [
|
|
|
1147
1164
|
"entitlement.override_cleared",
|
|
1148
1165
|
"billing.checkout_started",
|
|
1149
1166
|
"billing.portal_opened",
|
|
1167
|
+
"promo.created",
|
|
1168
|
+
"promo.updated",
|
|
1169
|
+
"promo.redeemed",
|
|
1150
1170
|
"kms.key_created",
|
|
1151
1171
|
"kms.key_granted",
|
|
1152
1172
|
"kms.key_revoked",
|
|
@@ -1187,7 +1207,8 @@ const NOTIFICATION_TYPES = [
|
|
|
1187
1207
|
"token_expiring",
|
|
1188
1208
|
"lease_expired",
|
|
1189
1209
|
"sync_failed",
|
|
1190
|
-
"honey_token_tripped"
|
|
1210
|
+
"honey_token_tripped",
|
|
1211
|
+
"secret_rotation_failed"
|
|
1191
1212
|
];
|
|
1192
1213
|
/** UI-facing copy + default for each notification type. */
|
|
1193
1214
|
const NOTIFICATION_TYPE_META = {
|
|
@@ -1240,6 +1261,11 @@ const NOTIFICATION_TYPE_META = {
|
|
|
1240
1261
|
label: "Honey token tripped",
|
|
1241
1262
|
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
1263
|
defaultEnabled: true
|
|
1264
|
+
},
|
|
1265
|
+
secret_rotation_failed: {
|
|
1266
|
+
label: "Secret rotation failed",
|
|
1267
|
+
description: "A scheduled secret rotation could not complete — the stored value may be out of step with the system it authenticates to.",
|
|
1268
|
+
defaultEnabled: true
|
|
1243
1269
|
}
|
|
1244
1270
|
};
|
|
1245
1271
|
//#endregion
|
|
@@ -1467,6 +1493,25 @@ z.object({
|
|
|
1467
1493
|
note: z.string().max(500).nullish(),
|
|
1468
1494
|
expiresAt: z.iso.datetime().nullish()
|
|
1469
1495
|
});
|
|
1496
|
+
z.object({
|
|
1497
|
+
code: z.string().min(4).max(40),
|
|
1498
|
+
family: planFamilySchema,
|
|
1499
|
+
version: z.number().int().positive().optional(),
|
|
1500
|
+
durationDays: z.number().int().positive().max(3650).nullish(),
|
|
1501
|
+
maxRedemptions: z.number().int().positive().nullish(),
|
|
1502
|
+
startsAt: z.iso.datetime().nullish(),
|
|
1503
|
+
endsAt: z.iso.datetime().nullish(),
|
|
1504
|
+
note: z.string().max(500).nullish()
|
|
1505
|
+
});
|
|
1506
|
+
z.object({
|
|
1507
|
+
maxRedemptions: z.number().int().positive().nullish(),
|
|
1508
|
+
startsAt: z.iso.datetime().nullish(),
|
|
1509
|
+
endsAt: z.iso.datetime().nullish(),
|
|
1510
|
+
note: z.string().max(500).nullish(),
|
|
1511
|
+
/** Kill switch. Disabling stops new redemptions; live grants are untouched. */
|
|
1512
|
+
disabled: z.boolean().optional()
|
|
1513
|
+
});
|
|
1514
|
+
z.object({ code: z.string().min(1).max(40) });
|
|
1470
1515
|
z.object({ family: planFamilySchema });
|
|
1471
1516
|
z.object({
|
|
1472
1517
|
sessionId: z.string().regex(/^skc_[0-9A-Za-z]+$/),
|
|
@@ -1484,6 +1529,99 @@ z.object({
|
|
|
1484
1529
|
action: z.string().optional(),
|
|
1485
1530
|
resourceType: z.string().optional()
|
|
1486
1531
|
});
|
|
1532
|
+
z.enum([
|
|
1533
|
+
"generated",
|
|
1534
|
+
"postgres",
|
|
1535
|
+
"mysql",
|
|
1536
|
+
"redis"
|
|
1537
|
+
]);
|
|
1538
|
+
z.enum([
|
|
1539
|
+
"active",
|
|
1540
|
+
"paused",
|
|
1541
|
+
"failed"
|
|
1542
|
+
]);
|
|
1543
|
+
/** Statuses an admin may set directly (`failed` is only reached by the sweep). */
|
|
1544
|
+
const settableRotationStatusSchema = z.enum(["active", "paused"]);
|
|
1545
|
+
const rotationAlphabetSchema = z.enum([
|
|
1546
|
+
"alphanumeric",
|
|
1547
|
+
"hex",
|
|
1548
|
+
"base64url",
|
|
1549
|
+
"printable"
|
|
1550
|
+
]);
|
|
1551
|
+
const rotationIntervalSchema = z.number().int().min(300).max(3600 * 24 * 365);
|
|
1552
|
+
/**
|
|
1553
|
+
* A database user name we are willing to *re-key*. Deliberately more permissive
|
|
1554
|
+
* than the lease providers' name schemas — those name accounts seekrit creates,
|
|
1555
|
+
* whereas this names an account the customer's DBA created years ago, which may
|
|
1556
|
+
* be mixed-case or contain dots or dashes.
|
|
1557
|
+
*
|
|
1558
|
+
* It stays injection-safe for every place it is interpolated: a double-quoted
|
|
1559
|
+
* Postgres identifier, a single-quoted MySQL literal, and a bare Redis command
|
|
1560
|
+
* token. The charset excludes both quote characters, backslash, whitespace, and
|
|
1561
|
+
* `;`, so there is no way out of the surrounding quoting, and no whitespace to
|
|
1562
|
+
* split one Redis argument into two.
|
|
1563
|
+
*/
|
|
1564
|
+
const rotationUsernameSchema = z.string().regex(/^[A-Za-z0-9_$.-]{1,63}$/, "must be 1–63 chars of letters, digits, underscore, dollar, dot or dash");
|
|
1565
|
+
/** A `{{name}}`/`{{host}}`/`{{verifier}}` templated statement or command line. */
|
|
1566
|
+
const statementSchema = z.string().min(1).max(4e3);
|
|
1567
|
+
const passwordLengthSchema = z.number().int().min(16).max(256);
|
|
1568
|
+
/** A MySQL account host part (`'name'@'<host>'`) — no quotes/whitespace. */
|
|
1569
|
+
const mysqlHostSchema = z.string().regex(/^[A-Za-z0-9_.%:-]{1,255}$/, "must be a host pattern");
|
|
1570
|
+
const generatedRotationConfigSchema = z.object({
|
|
1571
|
+
kind: z.literal("generated"),
|
|
1572
|
+
length: passwordLengthSchema.optional(),
|
|
1573
|
+
alphabet: rotationAlphabetSchema.optional()
|
|
1574
|
+
});
|
|
1575
|
+
const postgresRotationConfigSchema = z.object({
|
|
1576
|
+
kind: z.literal("postgres"),
|
|
1577
|
+
username: rotationUsernameSchema,
|
|
1578
|
+
passwordLength: passwordLengthSchema.optional(),
|
|
1579
|
+
statements: z.array(statementSchema).max(16).optional()
|
|
1580
|
+
});
|
|
1581
|
+
const mysqlRotationConfigSchema = z.object({
|
|
1582
|
+
kind: z.literal("mysql"),
|
|
1583
|
+
username: rotationUsernameSchema,
|
|
1584
|
+
userHost: mysqlHostSchema.optional(),
|
|
1585
|
+
passwordLength: passwordLengthSchema.optional(),
|
|
1586
|
+
statements: z.array(statementSchema).max(16).optional()
|
|
1587
|
+
});
|
|
1588
|
+
const redisRotationConfigSchema = z.object({
|
|
1589
|
+
kind: z.literal("redis"),
|
|
1590
|
+
username: rotationUsernameSchema,
|
|
1591
|
+
passwordLength: passwordLengthSchema.optional(),
|
|
1592
|
+
statements: z.array(statementSchema).max(16).optional()
|
|
1593
|
+
});
|
|
1594
|
+
const rotationConfigSchema = z.discriminatedUnion("kind", [
|
|
1595
|
+
generatedRotationConfigSchema,
|
|
1596
|
+
postgresRotationConfigSchema,
|
|
1597
|
+
mysqlRotationConfigSchema,
|
|
1598
|
+
redisRotationConfigSchema
|
|
1599
|
+
]);
|
|
1600
|
+
z.object({
|
|
1601
|
+
environmentId: z.string().min(1),
|
|
1602
|
+
/** The secret whose value rotates. It must already exist. */
|
|
1603
|
+
secretName: secretNameSchema,
|
|
1604
|
+
config: rotationConfigSchema,
|
|
1605
|
+
intervalSeconds: rotationIntervalSchema,
|
|
1606
|
+
/**
|
|
1607
|
+
* The registered lease target supplying the connection, executor mode, and
|
|
1608
|
+
* wrapped admin credential. Required for every kind but `generated`.
|
|
1609
|
+
*/
|
|
1610
|
+
targetId: z.string().min(1).optional(),
|
|
1611
|
+
/** Environment DEK wrapped to the rotator public key (`wd1.` blob). */
|
|
1612
|
+
wrappedDek: z.string().min(1).optional(),
|
|
1613
|
+
/** Rotate once immediately instead of waiting for the first interval. */
|
|
1614
|
+
rotateNow: z.boolean().optional()
|
|
1615
|
+
});
|
|
1616
|
+
z.object({
|
|
1617
|
+
intervalSeconds: rotationIntervalSchema.optional(),
|
|
1618
|
+
config: rotationConfigSchema.optional(),
|
|
1619
|
+
/**
|
|
1620
|
+
* `paused` stops the sweep; `active` resumes it and clears the failure
|
|
1621
|
+
* streak, which is also how a `failed` policy is recovered.
|
|
1622
|
+
*/
|
|
1623
|
+
status: settableRotationStatusSchema.optional()
|
|
1624
|
+
}).refine((v) => v.intervalSeconds !== void 0 || v.config !== void 0 || v.status !== void 0, "provide at least one of intervalSeconds, config, or status");
|
|
1487
1625
|
//#endregion
|
|
1488
1626
|
//#region ../../packages/core/src/sync.ts
|
|
1489
1627
|
/**
|
|
@@ -1523,7 +1661,10 @@ const SYNC_PROVIDER_KINDS = [
|
|
|
1523
1661
|
"aws-parameter-store",
|
|
1524
1662
|
"render",
|
|
1525
1663
|
"fly",
|
|
1526
|
-
"northflank"
|
|
1664
|
+
"northflank",
|
|
1665
|
+
"digitalocean",
|
|
1666
|
+
"heroku",
|
|
1667
|
+
"netlify"
|
|
1527
1668
|
];
|
|
1528
1669
|
z.enum(SYNC_PROVIDER_KINDS);
|
|
1529
1670
|
/**
|
|
@@ -1688,6 +1829,46 @@ const northflankIdSchema = z.string().trim().min(3).max(100).regex(/^[a-zA-Z0-9]
|
|
|
1688
1829
|
* address. The kind exists so the discriminated union stays uniform.
|
|
1689
1830
|
*/
|
|
1690
1831
|
const northflankConnectionConfigSchema = z.object({ provider: z.literal("northflank") });
|
|
1832
|
+
/**
|
|
1833
|
+
* DigitalOcean account scope — deliberately empty, as Render's and Fly's are.
|
|
1834
|
+
*
|
|
1835
|
+
* A DigitalOcean personal access token belongs to one account (or one team, if
|
|
1836
|
+
* it was issued inside one) and carries that scope itself, and every endpoint
|
|
1837
|
+
* this connector calls addresses its app by id. There is no team id to
|
|
1838
|
+
* disambiguate the way Vercel needs one: the token plus the destination's app
|
|
1839
|
+
* id is the whole address.
|
|
1840
|
+
*/
|
|
1841
|
+
const digitalOceanConnectionConfigSchema = z.object({ provider: z.literal("digitalocean") });
|
|
1842
|
+
/**
|
|
1843
|
+
* Heroku account scope — empty, as Fly's and Render's are.
|
|
1844
|
+
*
|
|
1845
|
+
* A Heroku API token carries its user's access to every app and team they can
|
|
1846
|
+
* reach, and app names are globally unique, so the destination's app is the
|
|
1847
|
+
* whole address. There is no team id to state: unlike Vercel, where a personal
|
|
1848
|
+
* token 403s a team-owned project without `teamId`, Heroku resolves
|
|
1849
|
+
* `/apps/{app_id_or_name}` against everything the token can see, team-owned or
|
|
1850
|
+
* not.
|
|
1851
|
+
*/
|
|
1852
|
+
const herokuConnectionConfigSchema = z.object({ provider: z.literal("heroku") });
|
|
1853
|
+
/**
|
|
1854
|
+
* Netlify team scope — the one thing a Netlify token cannot tell us itself.
|
|
1855
|
+
*
|
|
1856
|
+
* Every environment variable endpoint is account-scoped
|
|
1857
|
+
* (`/accounts/{account_id}/env`), and a personal access token belongs to a
|
|
1858
|
+
* *user*, who may sit in several teams. So unlike Fly's or Heroku's, this
|
|
1859
|
+
* config is not empty: the token says who you are, and this says which team's
|
|
1860
|
+
* variables to write.
|
|
1861
|
+
*
|
|
1862
|
+
* Netlify treats the team's id and its slug as interchangeable wherever
|
|
1863
|
+
* `{account_id}` appears, so both are accepted. The slug is the one an operator
|
|
1864
|
+
* can find without an API call — it is in the dashboard URL
|
|
1865
|
+
* (`app.netlify.com/teams/<slug>`) and under Team settings → General.
|
|
1866
|
+
*/
|
|
1867
|
+
const netlifyConnectionConfigSchema = z.object({
|
|
1868
|
+
provider: z.literal("netlify"),
|
|
1869
|
+
/** Netlify team slug (`acme`) or account id — `{account_id}` accepts either. */
|
|
1870
|
+
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
|
+
});
|
|
1691
1872
|
const syncConnectionConfigSchema = z.discriminatedUnion("provider", [
|
|
1692
1873
|
vercelConnectionConfigSchema,
|
|
1693
1874
|
cloudflareWorkersConnectionConfigSchema,
|
|
@@ -1698,7 +1879,10 @@ const syncConnectionConfigSchema = z.discriminatedUnion("provider", [
|
|
|
1698
1879
|
awsParameterStoreConnectionConfigSchema,
|
|
1699
1880
|
renderConnectionConfigSchema,
|
|
1700
1881
|
flyConnectionConfigSchema,
|
|
1701
|
-
northflankConnectionConfigSchema
|
|
1882
|
+
northflankConnectionConfigSchema,
|
|
1883
|
+
digitalOceanConnectionConfigSchema,
|
|
1884
|
+
herokuConnectionConfigSchema,
|
|
1885
|
+
netlifyConnectionConfigSchema
|
|
1702
1886
|
]);
|
|
1703
1887
|
/** Vercel's three deployment targets. A binding writes to one or more. */
|
|
1704
1888
|
const VERCEL_TARGETS = [
|
|
@@ -1942,6 +2126,187 @@ const northflankDestinationSchema = z.object({
|
|
|
1942
2126
|
/** Secret group id — the slug in the group's URL (`example-secret-group`). */
|
|
1943
2127
|
secretGroupId: northflankIdSchema
|
|
1944
2128
|
});
|
|
2129
|
+
/**
|
|
2130
|
+
* When App Platform makes a variable visible. DigitalOcean's enum also has
|
|
2131
|
+
* `UNSET`, which is not offered: it means "no scope stated", and a secrets
|
|
2132
|
+
* manager that writes a value should say when that value applies.
|
|
2133
|
+
*
|
|
2134
|
+
* The default here is `RUN_TIME` rather than DigitalOcean's own
|
|
2135
|
+
* `RUN_AND_BUILD_TIME`, and the difference is deliberate. A build-time variable
|
|
2136
|
+
* is visible to every build command, every buildpack, and anything they print;
|
|
2137
|
+
* a secret only the running process needs has no business being there. Binding
|
|
2138
|
+
* a value a build genuinely needs — a private registry token, a sourcemap
|
|
2139
|
+
* upload key — is a decision worth making explicitly.
|
|
2140
|
+
*/
|
|
2141
|
+
const DIGITALOCEAN_ENV_SCOPES = [
|
|
2142
|
+
"RUN_TIME",
|
|
2143
|
+
"BUILD_TIME",
|
|
2144
|
+
"RUN_AND_BUILD_TIME"
|
|
2145
|
+
];
|
|
2146
|
+
/**
|
|
2147
|
+
* A DigitalOcean app id — the UUID in the app's dashboard URL
|
|
2148
|
+
* (`cloud.digitalocean.com/apps/<id>`), and what `doctl apps list` prints.
|
|
2149
|
+
*
|
|
2150
|
+
* DigitalOcean's own spec types this as a bare string, but every app id it
|
|
2151
|
+
* issues is a UUID, and the slip worth catching is the one the API cannot tell
|
|
2152
|
+
* from a typo: pasting the app's *name* (`storefront`), which `GET /v2/apps/{id}`
|
|
2153
|
+
* answers with a flat 404 hours later inside an alarm, with nobody watching.
|
|
2154
|
+
*/
|
|
2155
|
+
const digitalOceanAppIdSchema = 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 DigitalOcean app ID (the UUID in the app's URL), not its name");
|
|
2156
|
+
/**
|
|
2157
|
+
* A component name, matching App Platform's own pattern for one. Names are
|
|
2158
|
+
* unique within an app, which is what makes a name — rather than an index into
|
|
2159
|
+
* `services` — the stable way to address a component's variables.
|
|
2160
|
+
*/
|
|
2161
|
+
const digitalOceanComponentNameSchema = z.string().trim().regex(/^[a-z][a-z0-9-]{0,30}[a-z0-9]$/, "must be an App Platform component name (lowercase letters, numbers, and dashes)");
|
|
2162
|
+
/**
|
|
2163
|
+
* Where inside a DigitalOcean app a binding writes.
|
|
2164
|
+
*
|
|
2165
|
+
* ## A push is a deployment
|
|
2166
|
+
*
|
|
2167
|
+
* App Platform has no per-variable endpoint. Environment variables live in the
|
|
2168
|
+
* app spec, and the only way to change one is to submit a new spec — which
|
|
2169
|
+
* starts a **new deployment** of the app. That is not a side effect this
|
|
2170
|
+
* connector chose and there is no flag to suppress it; it is what "set an
|
|
2171
|
+
* environment variable" means on this platform, in the control panel and in
|
|
2172
|
+
* `doctl` alike.
|
|
2173
|
+
*
|
|
2174
|
+
* The deployment reuses each component's current source (seekrit never sends
|
|
2175
|
+
* `update_all_source_versions`), so it redeploys the code already running
|
|
2176
|
+
* rather than pulling a newer commit or image. It is still a real deployment:
|
|
2177
|
+
* a build, a health check, and a rollout. Bind an environment here knowing that
|
|
2178
|
+
* changing a secret in it will roll the app.
|
|
2179
|
+
*
|
|
2180
|
+
* ## Values are written encrypted
|
|
2181
|
+
*
|
|
2182
|
+
* Everything seekrit writes goes in as `type: SECRET`, so App Platform encrypts
|
|
2183
|
+
* it at rest and hands it back as an opaque `EV[1:…]` blob rather than as
|
|
2184
|
+
* plaintext. That is also why this connector cannot tell whether a value it is
|
|
2185
|
+
* about to write is already there — see
|
|
2186
|
+
* `apps/api/src/lib/sync/connectors/digitalocean.ts`.
|
|
2187
|
+
*/
|
|
2188
|
+
const digitalOceanAppDestinationSchema = z.object({
|
|
2189
|
+
provider: z.literal("digitalocean"),
|
|
2190
|
+
kind: z.literal("app"),
|
|
2191
|
+
/** App id — the UUID in `cloud.digitalocean.com/apps/<id>`. */
|
|
2192
|
+
appId: digitalOceanAppIdSchema,
|
|
2193
|
+
scope: z.enum(DIGITALOCEAN_ENV_SCOPES).default("RUN_TIME")
|
|
2194
|
+
});
|
|
2195
|
+
/**
|
|
2196
|
+
* One component's own environment variables. Narrower than the app-level list:
|
|
2197
|
+
* only this service, worker, job, static site, or function sees them, and a key
|
|
2198
|
+
* here wins over the same key at app level.
|
|
2199
|
+
*/
|
|
2200
|
+
const digitalOceanComponentDestinationSchema = z.object({
|
|
2201
|
+
provider: z.literal("digitalocean"),
|
|
2202
|
+
kind: z.literal("component"),
|
|
2203
|
+
appId: digitalOceanAppIdSchema,
|
|
2204
|
+
/** Component name, as it appears in the app spec — not its type. */
|
|
2205
|
+
componentName: digitalOceanComponentNameSchema,
|
|
2206
|
+
scope: z.enum(DIGITALOCEAN_ENV_SCOPES).default("RUN_TIME")
|
|
2207
|
+
});
|
|
2208
|
+
const digitalOceanDestinationSchema = z.discriminatedUnion("kind", [digitalOceanAppDestinationSchema, digitalOceanComponentDestinationSchema]);
|
|
2209
|
+
/**
|
|
2210
|
+
* A Heroku app, named the way `/apps/{app_id_or_name}` names one: either the
|
|
2211
|
+
* app name or its UUID id. Both are accepted because both work, and the id is
|
|
2212
|
+
* the durable one — renaming an app in the dashboard breaks a binding that
|
|
2213
|
+
* holds its name, and does not break one that holds its id.
|
|
2214
|
+
*
|
|
2215
|
+
* The name pattern is Heroku's own (`^[a-z][a-z0-9-]{1,28}[a-z0-9]$`): 3–30
|
|
2216
|
+
* characters, starting with a letter and ending alphanumeric. Checking it here
|
|
2217
|
+
* turns the habitual slip — pasting `example.herokuapp.com`, or a name with
|
|
2218
|
+
* capitals — into a message at the form rather than a bare 404 from an alarm
|
|
2219
|
+
* with nobody watching.
|
|
2220
|
+
*/
|
|
2221
|
+
const herokuAppSchema = z.string().trim().refine((value) => /^[a-z][a-z0-9-]{1,28}[a-z0-9]$/.test(value) || /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value), "must be a Heroku app name (lowercase letters, numbers, and dashes) or its UUID");
|
|
2222
|
+
/**
|
|
2223
|
+
* The Heroku app whose config vars a binding owns.
|
|
2224
|
+
*
|
|
2225
|
+
* A Heroku app has **one** set of config vars, shared by every dyno and every
|
|
2226
|
+
* process type — there is no per-target split to state the way Vercel and Pages
|
|
2227
|
+
* have one. Heroku's convention is that staging and production are separate
|
|
2228
|
+
* *apps* (`storefront`, `storefront-staging`), so pointing at an environment
|
|
2229
|
+
* means naming that app, exactly as it does on Fly.
|
|
2230
|
+
*
|
|
2231
|
+
* Unlike Fly, values take effect **immediately**: setting config vars cuts a new
|
|
2232
|
+
* release and restarts the app's dynos, which is why a run sends exactly one
|
|
2233
|
+
* request — see the note in `apps/api/src/lib/sync/connectors/heroku.ts`.
|
|
2234
|
+
*/
|
|
2235
|
+
const herokuDestinationSchema = z.object({
|
|
2236
|
+
provider: z.literal("heroku"),
|
|
2237
|
+
/** App name as `heroku apps` prints it, or the app's UUID. */
|
|
2238
|
+
app: herokuAppSchema
|
|
2239
|
+
});
|
|
2240
|
+
/**
|
|
2241
|
+
* The deploy contexts a Netlify value can be set for.
|
|
2242
|
+
*
|
|
2243
|
+
* These are Netlify's own, minus two. `all` is missing deliberately: Netlify
|
|
2244
|
+
* requires a **secret** value to be set against explicit contexts, and its
|
|
2245
|
+
* `setEnvVarValue` endpoint is reported to fail outright on `context: "all"` —
|
|
2246
|
+
* so the union offers only contexts that work under both. Naming the contexts
|
|
2247
|
+
* you mean is what you want here anyway; a binding already exists to map one
|
|
2248
|
+
* seekrit environment onto one deploy context. `dev-server` (Preview Server) is
|
|
2249
|
+
* left out for want of anyone asking.
|
|
2250
|
+
*
|
|
2251
|
+
* `branch` is the odd one: it needs a branch name alongside it, which the
|
|
2252
|
+
* destination carries as {@link netlifyDestinationSchema}'s `branch`.
|
|
2253
|
+
*/
|
|
2254
|
+
const NETLIFY_CONTEXTS = [
|
|
2255
|
+
"production",
|
|
2256
|
+
"deploy-preview",
|
|
2257
|
+
"branch-deploy",
|
|
2258
|
+
"branch",
|
|
2259
|
+
"dev"
|
|
2260
|
+
];
|
|
2261
|
+
/**
|
|
2262
|
+
* A Netlify site, by its **API ID** — the UUID under Project configuration →
|
|
2263
|
+
* General → Project information.
|
|
2264
|
+
*
|
|
2265
|
+
* Netlify accepts a site's domain in place of its id where a site appears in a
|
|
2266
|
+
* *path* (`/sites/{site_id}`), but the environment variable endpoints take the
|
|
2267
|
+
* site as a `?site_id=` **query parameter** instead, and Netlify documents no
|
|
2268
|
+
* name resolution there. That asymmetry is why this is strict where the Heroku
|
|
2269
|
+
* and Fly destinations are permissive: a `site_id` Netlify does not resolve
|
|
2270
|
+
* does not 404 — the write lands on the *team*, as a shared variable inherited
|
|
2271
|
+
* by every site in it. Refusing anything but the UUID keeps a slip from turning
|
|
2272
|
+
* into a much wider blast radius than the operator asked for.
|
|
2273
|
+
*/
|
|
2274
|
+
const netlifySiteIdSchema = 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 the site's API ID (a UUID), not its name or URL");
|
|
2275
|
+
/**
|
|
2276
|
+
* The Netlify site, and which of its deploy contexts a binding owns.
|
|
2277
|
+
*
|
|
2278
|
+
* Netlify keys a value by (site, context), so the destination is that pair.
|
|
2279
|
+
* Contexts are a list rather than a single value for the same reason Vercel's
|
|
2280
|
+
* `targets` is one: a binding is unique per (connection, environment), so an
|
|
2281
|
+
* environment that feeds both production and deploy previews has to say so in
|
|
2282
|
+
* one destination or not at all.
|
|
2283
|
+
*
|
|
2284
|
+
* A push writes **only** the contexts listed here. Other contexts of the same
|
|
2285
|
+
* variable — and every variable this binding does not manage — are left as they
|
|
2286
|
+
* are, which is what makes it safe to point at a site that already has
|
|
2287
|
+
* variables set by hand.
|
|
2288
|
+
*
|
|
2289
|
+
* `secret` marks what seekrit creates as a Netlify **secret**: write-only, and
|
|
2290
|
+
* unreadable afterwards through the UI, CLI, and API. On by default, since that
|
|
2291
|
+
* is the whole point of pushing from a secrets manager. It applies only to
|
|
2292
|
+
* variables seekrit *creates* — Netlify will not let a flag be added to an
|
|
2293
|
+
* existing variable, or removed from one ever — and it needs a plan that
|
|
2294
|
+
* includes Secrets Controller.
|
|
2295
|
+
*/
|
|
2296
|
+
const netlifyDestinationSchema = z.object({
|
|
2297
|
+
provider: z.literal("netlify"),
|
|
2298
|
+
/** Site API ID (a UUID), from Project configuration → General. */
|
|
2299
|
+
siteId: netlifySiteIdSchema,
|
|
2300
|
+
/** Which deploy contexts receive these values. At least one. */
|
|
2301
|
+
contexts: z.array(z.enum(NETLIFY_CONTEXTS)).min(1),
|
|
2302
|
+
/** Branch name, required when `contexts` includes `branch`; ignored otherwise. */
|
|
2303
|
+
branch: z.string().trim().min(1).max(255).optional(),
|
|
2304
|
+
/** Create variables as Netlify secrets (default true). */
|
|
2305
|
+
secret: z.boolean().optional()
|
|
2306
|
+
}).refine((d) => !d.contexts.includes("branch") || d.branch !== void 0, {
|
|
2307
|
+
message: "a branch context needs the branch name it applies to",
|
|
2308
|
+
path: ["branch"]
|
|
2309
|
+
});
|
|
1945
2310
|
const syncDestinationSchema = z.discriminatedUnion("provider", [
|
|
1946
2311
|
vercelDestinationSchema,
|
|
1947
2312
|
cloudflareWorkersDestinationSchema,
|
|
@@ -1952,7 +2317,10 @@ const syncDestinationSchema = z.discriminatedUnion("provider", [
|
|
|
1952
2317
|
awsParameterStoreDestinationSchema,
|
|
1953
2318
|
renderDestinationSchema,
|
|
1954
2319
|
flyDestinationSchema,
|
|
1955
|
-
northflankDestinationSchema
|
|
2320
|
+
northflankDestinationSchema,
|
|
2321
|
+
digitalOceanDestinationSchema,
|
|
2322
|
+
herokuDestinationSchema,
|
|
2323
|
+
netlifyDestinationSchema
|
|
1956
2324
|
]);
|
|
1957
2325
|
/**
|
|
1958
2326
|
* How seekrit secret names become destination key names. Applied in order:
|
|
@@ -2401,6 +2769,43 @@ async function decryptDataKey(material, wrapped) {
|
|
|
2401
2769
|
}
|
|
2402
2770
|
}
|
|
2403
2771
|
//#endregion
|
|
2772
|
+
//#region ../../packages/crypto/src/random.ts
|
|
2773
|
+
const ALPHABETS = {
|
|
2774
|
+
alphanumeric: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
|
|
2775
|
+
hex: "0123456789abcdef",
|
|
2776
|
+
base64url: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_",
|
|
2777
|
+
/**
|
|
2778
|
+
* Alphanumerics plus punctuation chosen to survive being pasted anywhere a
|
|
2779
|
+
* secret goes: no quote of either kind, no backslash, backtick, `$`, or
|
|
2780
|
+
* whitespace, so the value can't break out of a shell word, a SQL literal, a
|
|
2781
|
+
* URL component, or a `.env` line.
|
|
2782
|
+
*/
|
|
2783
|
+
printable: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-._~!*+="
|
|
2784
|
+
};
|
|
2785
|
+
/**
|
|
2786
|
+
* A cryptographically random string of `length` characters drawn uniformly from
|
|
2787
|
+
* `alphabet` (default `alphanumeric`, ≈5.95 bits/char — 32 chars ≈ 190 bits).
|
|
2788
|
+
*
|
|
2789
|
+
* Uses rejection sampling: bytes at or above the largest multiple of the
|
|
2790
|
+
* alphabet size are discarded rather than folded, so `% n` introduces no modulo
|
|
2791
|
+
* bias toward the low end of the alphabet.
|
|
2792
|
+
*/
|
|
2793
|
+
function generateSecretValue(length, alphabet = "alphanumeric") {
|
|
2794
|
+
if (!Number.isInteger(length) || length < 1) throw new RangeError("length must be a positive integer");
|
|
2795
|
+
const chars = ALPHABETS[alphabet];
|
|
2796
|
+
const n = chars.length;
|
|
2797
|
+
const limit = 256 - 256 % n;
|
|
2798
|
+
let out = "";
|
|
2799
|
+
while (out.length < length) {
|
|
2800
|
+
const bytes = crypto.getRandomValues(new Uint8Array(length - out.length));
|
|
2801
|
+
for (const byte of bytes) {
|
|
2802
|
+
if (byte < limit) out += chars[byte % n];
|
|
2803
|
+
if (out.length === length) break;
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
return out;
|
|
2807
|
+
}
|
|
2808
|
+
//#endregion
|
|
2404
2809
|
//#region ../../packages/crypto/src/mongodb.ts
|
|
2405
2810
|
/** Generate the ephemeral P-256 keypair a client uses to receive one MongoDB lease. */
|
|
2406
2811
|
async function generateMongoRecipientKeyPair() {
|
|
@@ -2465,7 +2870,6 @@ function mongoConnectionUri(cred) {
|
|
|
2465
2870
|
* SCRAM helper does, with no hand-rolled hash primitive.
|
|
2466
2871
|
*/
|
|
2467
2872
|
const DEFAULT_PASSWORD_LENGTH$2 = 32;
|
|
2468
|
-
const PASSWORD_ALPHABET$2 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
2469
2873
|
async function sha1(data) {
|
|
2470
2874
|
return new Uint8Array(await crypto.subtle.digest("SHA-1", data));
|
|
2471
2875
|
}
|
|
@@ -2474,17 +2878,6 @@ function toUpperHex(bytes) {
|
|
|
2474
2878
|
for (const b of bytes) hex += b.toString(16).padStart(2, "0");
|
|
2475
2879
|
return hex.toUpperCase();
|
|
2476
2880
|
}
|
|
2477
|
-
function randomPassword$2(length) {
|
|
2478
|
-
let out = "";
|
|
2479
|
-
while (out.length < length) {
|
|
2480
|
-
const bytes = crypto.getRandomValues(new Uint8Array(length - out.length));
|
|
2481
|
-
for (const byte of bytes) {
|
|
2482
|
-
if (byte < 248) out += PASSWORD_ALPHABET$2[byte % 62];
|
|
2483
|
-
if (out.length === length) break;
|
|
2484
|
-
}
|
|
2485
|
-
}
|
|
2486
|
-
return out;
|
|
2487
|
-
}
|
|
2488
2881
|
/**
|
|
2489
2882
|
* Compute the `mysql_native_password` auth string `*<UPPER(HEX(SHA1(SHA1(pw))))>`
|
|
2490
2883
|
* for a known password. Pass the result straight to
|
|
@@ -2498,7 +2891,7 @@ async function mysqlNativePasswordVerifier(password) {
|
|
|
2498
2891
|
* — the client-side half of a Vault-style dynamic MySQL credential.
|
|
2499
2892
|
*/
|
|
2500
2893
|
async function generateMysqlCredential(options = {}) {
|
|
2501
|
-
const password =
|
|
2894
|
+
const password = generateSecretValue(options.length ?? DEFAULT_PASSWORD_LENGTH$2);
|
|
2502
2895
|
return {
|
|
2503
2896
|
password,
|
|
2504
2897
|
verifier: await mysqlNativePasswordVerifier(password)
|
|
@@ -2778,7 +3171,6 @@ async function combineRecoveryShares(shareBytes) {
|
|
|
2778
3171
|
* helper does, with no hand-rolled hash primitive.
|
|
2779
3172
|
*/
|
|
2780
3173
|
const DEFAULT_PASSWORD_LENGTH$1 = 32;
|
|
2781
|
-
const PASSWORD_ALPHABET$1 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
2782
3174
|
async function sha256$1(data) {
|
|
2783
3175
|
return new Uint8Array(await crypto.subtle.digest("SHA-256", data));
|
|
2784
3176
|
}
|
|
@@ -2787,17 +3179,6 @@ function toLowerHex(bytes) {
|
|
|
2787
3179
|
for (const b of bytes) hex += b.toString(16).padStart(2, "0");
|
|
2788
3180
|
return hex;
|
|
2789
3181
|
}
|
|
2790
|
-
function randomPassword$1(length) {
|
|
2791
|
-
let out = "";
|
|
2792
|
-
while (out.length < length) {
|
|
2793
|
-
const bytes = crypto.getRandomValues(new Uint8Array(length - out.length));
|
|
2794
|
-
for (const byte of bytes) {
|
|
2795
|
-
if (byte < 248) out += PASSWORD_ALPHABET$1[byte % 62];
|
|
2796
|
-
if (out.length === length) break;
|
|
2797
|
-
}
|
|
2798
|
-
}
|
|
2799
|
-
return out;
|
|
2800
|
-
}
|
|
2801
3182
|
/**
|
|
2802
3183
|
* Compute the Redis ACL password verifier `LOWER(HEX(SHA256(password)))` for a
|
|
2803
3184
|
* known password. Pass the result straight to `ACL SETUSER … on #<verifier>`.
|
|
@@ -2810,7 +3191,7 @@ async function redisSha256Verifier(password) {
|
|
|
2810
3191
|
* client-side half of a Vault-style dynamic Redis credential.
|
|
2811
3192
|
*/
|
|
2812
3193
|
async function generateRedisCredential(options = {}) {
|
|
2813
|
-
const password =
|
|
3194
|
+
const password = generateSecretValue(options.length ?? DEFAULT_PASSWORD_LENGTH$1);
|
|
2814
3195
|
return {
|
|
2815
3196
|
password,
|
|
2816
3197
|
verifier: await redisSha256Verifier(password)
|
|
@@ -2818,7 +3199,6 @@ async function generateRedisCredential(options = {}) {
|
|
|
2818
3199
|
}
|
|
2819
3200
|
const SALT_LENGTH = 16;
|
|
2820
3201
|
const DEFAULT_PASSWORD_LENGTH = 32;
|
|
2821
|
-
const PASSWORD_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
2822
3202
|
async function hmacSha256(key, message) {
|
|
2823
3203
|
const k = await crypto.subtle.importKey("raw", key, {
|
|
2824
3204
|
name: "HMAC",
|
|
@@ -2839,17 +3219,6 @@ async function saltPassword(password, salt, iterations) {
|
|
|
2839
3219
|
}, material, 256);
|
|
2840
3220
|
return new Uint8Array(bits);
|
|
2841
3221
|
}
|
|
2842
|
-
function randomPassword(length) {
|
|
2843
|
-
let out = "";
|
|
2844
|
-
while (out.length < length) {
|
|
2845
|
-
const bytes = crypto.getRandomValues(new Uint8Array(length - out.length));
|
|
2846
|
-
for (const byte of bytes) {
|
|
2847
|
-
if (byte < 248) out += PASSWORD_ALPHABET[byte % 62];
|
|
2848
|
-
if (out.length === length) break;
|
|
2849
|
-
}
|
|
2850
|
-
}
|
|
2851
|
-
return out;
|
|
2852
|
-
}
|
|
2853
3222
|
/**
|
|
2854
3223
|
* Compute the `SCRAM-SHA-256$<i>:<salt>$<StoredKey>:<ServerKey>` verifier for a
|
|
2855
3224
|
* known password. Pass the result straight to `CREATE ROLE … PASSWORD`.
|
|
@@ -2867,7 +3236,7 @@ async function scramSha256Verifier(password, options = {}) {
|
|
|
2867
3236
|
* client-side half of a Vault-style dynamic Postgres credential.
|
|
2868
3237
|
*/
|
|
2869
3238
|
async function generatePostgresCredential(options = {}) {
|
|
2870
|
-
const password =
|
|
3239
|
+
const password = generateSecretValue(options.length ?? DEFAULT_PASSWORD_LENGTH);
|
|
2871
3240
|
const iterations = options.iterations ?? 4096;
|
|
2872
3241
|
return {
|
|
2873
3242
|
password,
|
|
@@ -3157,7 +3526,7 @@ function isCliSessionToken(value) {
|
|
|
3157
3526
|
}
|
|
3158
3527
|
//#endregion
|
|
3159
3528
|
//#region package.json
|
|
3160
|
-
var version = "0.
|
|
3529
|
+
var version = "0.40.0";
|
|
3161
3530
|
//#endregion
|
|
3162
3531
|
//#region ../../packages/api-client/src/index.ts
|
|
3163
3532
|
var SeekritApiError = class extends Error {
|
|
@@ -3589,6 +3958,39 @@ var SeekritClient = class {
|
|
|
3589
3958
|
revokeLease(orgId, leaseId) {
|
|
3590
3959
|
return this.request("DELETE", `/v1/orgs/${orgId}/leases/${leaseId}`);
|
|
3591
3960
|
}
|
|
3961
|
+
/**
|
|
3962
|
+
* The rotator public key (the broker DO's), plus the environments that have
|
|
3963
|
+
* already granted it. Wrap an environment's DEK to this key client-side before
|
|
3964
|
+
* configuring rotation — that wrap IS the grant, and the server can't make it.
|
|
3965
|
+
*/
|
|
3966
|
+
getRotatorKey(orgId) {
|
|
3967
|
+
return this.request("GET", `/v1/orgs/${orgId}/rotation/rotator-key`);
|
|
3968
|
+
}
|
|
3969
|
+
listRotations(orgId) {
|
|
3970
|
+
return this.request("GET", `/v1/orgs/${orgId}/rotation`);
|
|
3971
|
+
}
|
|
3972
|
+
getRotation(orgId, rotationId) {
|
|
3973
|
+
return this.request("GET", `/v1/orgs/${orgId}/rotation/${rotationId}`);
|
|
3974
|
+
}
|
|
3975
|
+
/**
|
|
3976
|
+
* Configure (or replace) a secret's rotation policy. `version` comes back only
|
|
3977
|
+
* when `rotateNow` was set — a rotated secret's new version number, never its
|
|
3978
|
+
* value.
|
|
3979
|
+
*/
|
|
3980
|
+
configureRotation(orgId, input) {
|
|
3981
|
+
return this.request("POST", `/v1/orgs/${orgId}/rotation`, input);
|
|
3982
|
+
}
|
|
3983
|
+
updateRotation(orgId, rotationId, input) {
|
|
3984
|
+
return this.request("PATCH", `/v1/orgs/${orgId}/rotation/${rotationId}`, input);
|
|
3985
|
+
}
|
|
3986
|
+
/** Rotate now. Returns the new version — the value stays where it belongs. */
|
|
3987
|
+
rotateSecretNow(orgId, rotationId) {
|
|
3988
|
+
return this.request("POST", `/v1/orgs/${orgId}/rotation/${rotationId}/rotate`);
|
|
3989
|
+
}
|
|
3990
|
+
/** Disable rotation. `rotatorRevoked` reports whether the broker's key grant went too. */
|
|
3991
|
+
disableRotation(orgId, rotationId) {
|
|
3992
|
+
return this.request("DELETE", `/v1/orgs/${orgId}/rotation/${rotationId}`);
|
|
3993
|
+
}
|
|
3592
3994
|
listAudit(orgId, query = {}) {
|
|
3593
3995
|
const params = new URLSearchParams();
|
|
3594
3996
|
if (query.cursor) params.set("cursor", query.cursor);
|
|
@@ -3645,6 +4047,19 @@ var SeekritClient = class {
|
|
|
3645
4047
|
cancelSubscription(orgId) {
|
|
3646
4048
|
return this.request("POST", `/v1/orgs/${orgId}/billing/cancel`);
|
|
3647
4049
|
}
|
|
4050
|
+
/**
|
|
4051
|
+
* Redeem a promo code, comping the org onto the plan the code grants.
|
|
4052
|
+
* Admin-only. Casing, spaces, and dashes are normalized server-side, so pass
|
|
4053
|
+
* the code as the user typed it. Returns the refreshed billing view.
|
|
4054
|
+
*
|
|
4055
|
+
* Every invalid code fails the same way regardless of why (unknown, expired,
|
|
4056
|
+
* fully redeemed, already used by this org) — the API deliberately won't
|
|
4057
|
+
* confirm that a code exists. Show the returned message as-is rather than
|
|
4058
|
+
* guessing at a more specific one.
|
|
4059
|
+
*/
|
|
4060
|
+
redeemPromoCode(orgId, input) {
|
|
4061
|
+
return this.request("POST", `/v1/orgs/${orgId}/billing/promo`, input);
|
|
4062
|
+
}
|
|
3648
4063
|
};
|
|
3649
4064
|
async function unauthenticatedPost(baseUrl, path, body, fetchImpl, client) {
|
|
3650
4065
|
const headers = {
|
|
@@ -6632,6 +7047,184 @@ function collect$1(value, acc) {
|
|
|
6632
7047
|
acc.push(value);
|
|
6633
7048
|
return acc;
|
|
6634
7049
|
}
|
|
7050
|
+
//#endregion
|
|
7051
|
+
//#region src/rotation.ts
|
|
7052
|
+
/**
|
|
7053
|
+
* `seekrit rotation` — managed, scheduled rotation of a stored secret's value.
|
|
7054
|
+
*
|
|
7055
|
+
* Zero-knowledge: enabling rotation unwraps this environment's DEK **on this
|
|
7056
|
+
* machine** and re-wraps it to the rotator public key (the per-org broker
|
|
7057
|
+
* Durable Object's), so the control plane only ever relays ciphertext. That wrap
|
|
7058
|
+
* is what lets the broker write a new value in place — and it is exactly why the
|
|
7059
|
+
* server can't enable rotation on its own.
|
|
7060
|
+
*
|
|
7061
|
+
* Rotated values are never printed here. Read them like any other secret
|
|
7062
|
+
* (`seekrit secrets get NAME`), which decrypts locally.
|
|
7063
|
+
*/
|
|
7064
|
+
/** Parse a duration like `30m`, `24h`, `90d`, or a bare seconds count. */
|
|
7065
|
+
function parseDurationSeconds(input, flag) {
|
|
7066
|
+
const m = /^(\d+)\s*([smhd]?)$/.exec(input.trim());
|
|
7067
|
+
if (!m) fail(`invalid ${flag} "${input}" (try 12h, 7d, 90d)`);
|
|
7068
|
+
return Number(m[1]) * ({
|
|
7069
|
+
s: 1,
|
|
7070
|
+
m: 60,
|
|
7071
|
+
h: 3600,
|
|
7072
|
+
d: 86400
|
|
7073
|
+
}[m[2] || "s"] ?? 1);
|
|
7074
|
+
}
|
|
7075
|
+
function formatInterval(seconds) {
|
|
7076
|
+
if (seconds % 86400 === 0) return `${seconds / 86400}d`;
|
|
7077
|
+
if (seconds % 3600 === 0) return `${seconds / 3600}h`;
|
|
7078
|
+
if (seconds % 60 === 0) return `${seconds / 60}m`;
|
|
7079
|
+
return `${seconds}s`;
|
|
7080
|
+
}
|
|
7081
|
+
/** One policy as a tab-separated line: name, kind, cadence, status, next run. */
|
|
7082
|
+
function rotationLine(r) {
|
|
7083
|
+
const status = r.failureCount > 0 ? `${r.status} (${r.failureCount} failed)` : r.status;
|
|
7084
|
+
return [
|
|
7085
|
+
r.id,
|
|
7086
|
+
r.secretName,
|
|
7087
|
+
r.kind,
|
|
7088
|
+
`every ${formatInterval(r.intervalSeconds)}`,
|
|
7089
|
+
status,
|
|
7090
|
+
`next ${r.nextRotateAt}`
|
|
7091
|
+
].join(" ");
|
|
7092
|
+
}
|
|
7093
|
+
/** Find a policy by id, or by secret name when it is unambiguous in the org. */
|
|
7094
|
+
async function resolveRotation(ctx, orgId, ref) {
|
|
7095
|
+
const { rotations } = await ctx.client.listRotations(orgId);
|
|
7096
|
+
const byId = rotations.find((r) => r.id === ref);
|
|
7097
|
+
if (byId) return byId;
|
|
7098
|
+
const byName = rotations.filter((r) => r.secretName === ref);
|
|
7099
|
+
if (byName.length === 1) return byName[0];
|
|
7100
|
+
if (byName.length > 1) fail(`"${ref}" rotates in ${byName.length} environments — pass the rotation id instead:\n${byName.map((r) => ` ${r.id}\t${r.environmentId}`).join("\n")}`);
|
|
7101
|
+
fail(`no rotation policy "${ref}"`);
|
|
7102
|
+
}
|
|
7103
|
+
/** Build the rotation config from the CLI flags for a given kind. */
|
|
7104
|
+
function buildConfig$1(kind, options) {
|
|
7105
|
+
const length = Number.parseInt(options.length, 10);
|
|
7106
|
+
if (!Number.isFinite(length)) fail(`invalid --length "${options.length}"`);
|
|
7107
|
+
if (kind === "generated") {
|
|
7108
|
+
const alphabets = [
|
|
7109
|
+
"alphanumeric",
|
|
7110
|
+
"hex",
|
|
7111
|
+
"base64url",
|
|
7112
|
+
"printable"
|
|
7113
|
+
];
|
|
7114
|
+
if (!alphabets.includes(options.alphabet)) fail(`--alphabet must be one of ${alphabets.join(", ")}`);
|
|
7115
|
+
return {
|
|
7116
|
+
kind: "generated",
|
|
7117
|
+
length,
|
|
7118
|
+
alphabet: options.alphabet
|
|
7119
|
+
};
|
|
7120
|
+
}
|
|
7121
|
+
if (!options.username) fail(`--username is required for a ${kind} rotation`);
|
|
7122
|
+
const username = options.username;
|
|
7123
|
+
if (kind === "postgres") return {
|
|
7124
|
+
kind: "postgres",
|
|
7125
|
+
username,
|
|
7126
|
+
passwordLength: length
|
|
7127
|
+
};
|
|
7128
|
+
if (kind === "mysql") return {
|
|
7129
|
+
kind: "mysql",
|
|
7130
|
+
username,
|
|
7131
|
+
passwordLength: length,
|
|
7132
|
+
...options.userHost ? { userHost: options.userHost } : {}
|
|
7133
|
+
};
|
|
7134
|
+
if (kind === "redis") return {
|
|
7135
|
+
kind: "redis",
|
|
7136
|
+
username,
|
|
7137
|
+
passwordLength: length
|
|
7138
|
+
};
|
|
7139
|
+
return fail(`--kind must be generated, postgres, mysql, or redis (got "${kind}")`);
|
|
7140
|
+
}
|
|
7141
|
+
function registerRotationCommands(program) {
|
|
7142
|
+
const rotation = program.command("rotation").description("managed rotation of stored secret values (scheduled, zero-knowledge)");
|
|
7143
|
+
rotation.command("enable <secretName>").description("configure rotation for an existing secret").option("--org <slug>").option("--app <slug>").option("--group <slug>", "rotate a secret in a group environment").requiredOption("--env <slug>").requiredOption("--kind <kind>", "generated | postgres | mysql | redis").requiredOption("--every <duration>", "rotation cadence, e.g. 24h, 30d").option("--username <name>", "the EXISTING database account to re-key (db kinds)").option("--target <idOrName>", "registered lease target to rotate against (db kinds)").option("--user-host <host>", "MySQL account host part (default %)").option("--length <n>", "generated value length", "32").option("--alphabet <set>", "generated kind only: alphanumeric | hex | base64url | printable", "alphanumeric").option("--now", "rotate immediately as well as on the schedule").action(async (secretName, options) => {
|
|
7144
|
+
const ctx = buildContext();
|
|
7145
|
+
const target = await resolveEnvTarget(ctx, options);
|
|
7146
|
+
const config = buildConfig$1(options.kind, options);
|
|
7147
|
+
const intervalSeconds = parseDurationSeconds(options.every, "--every");
|
|
7148
|
+
let targetId;
|
|
7149
|
+
if (config.kind !== "generated") {
|
|
7150
|
+
if (!options.target) fail(`--target is required for a ${config.kind} rotation`);
|
|
7151
|
+
const { targets } = await ctx.client.listLeaseTargets(target.orgId);
|
|
7152
|
+
const t = targets.find((x) => x.id === options.target || x.name === options.target);
|
|
7153
|
+
if (!t) fail(`no lease target "${options.target}" — register one with \`seekrit ${config.kind === "postgres" ? "pg" : config.kind} target add\``);
|
|
7154
|
+
targetId = t.id;
|
|
7155
|
+
}
|
|
7156
|
+
const { publicKeyJwk, grantedEnvironmentIds } = await ctx.client.getRotatorKey(target.orgId);
|
|
7157
|
+
let wrappedDek;
|
|
7158
|
+
if (!grantedEnvironmentIds.includes(target.envId)) wrappedDek = await wrapDek(await getDek(ctx, target.orgId, target.envId), publicKeyJwk);
|
|
7159
|
+
const { rotation: created, version } = await ctx.client.configureRotation(target.orgId, {
|
|
7160
|
+
environmentId: target.envId,
|
|
7161
|
+
secretName,
|
|
7162
|
+
config,
|
|
7163
|
+
intervalSeconds,
|
|
7164
|
+
...targetId ? { targetId } : {},
|
|
7165
|
+
...wrappedDek ? { wrappedDek } : {},
|
|
7166
|
+
...options.now ? { rotateNow: true } : {}
|
|
7167
|
+
});
|
|
7168
|
+
console.error(`rotating ${created.secretName} in ${target.label} every ${formatInterval(created.intervalSeconds)} (${created.kind})${wrappedDek ? " — rotator key granted" : ""}`);
|
|
7169
|
+
if (version !== void 0) console.error(`rotated now — ${created.secretName} is at version ${version}`);
|
|
7170
|
+
else console.error(`first rotation: ${created.nextRotateAt}`);
|
|
7171
|
+
console.log(created.id);
|
|
7172
|
+
});
|
|
7173
|
+
rotation.command("list").description("list rotation policies (schedules only — never values)").option("--org <slug>").option("--json", "print the full policies as JSON").action(async (options) => {
|
|
7174
|
+
const ctx = buildContext();
|
|
7175
|
+
const org = await resolveOrg(ctx, options.org);
|
|
7176
|
+
const { rotations } = await ctx.client.listRotations(org.id);
|
|
7177
|
+
if (options.json) {
|
|
7178
|
+
console.log(JSON.stringify(rotations, null, 2));
|
|
7179
|
+
return;
|
|
7180
|
+
}
|
|
7181
|
+
if (rotations.length === 0) {
|
|
7182
|
+
console.error(`no rotation policies in ${org.slug}`);
|
|
7183
|
+
return;
|
|
7184
|
+
}
|
|
7185
|
+
for (const r of rotations) console.log(rotationLine(r));
|
|
7186
|
+
});
|
|
7187
|
+
rotation.command("show <rotationOrSecret>").description("show one policy, including the last failure if any").option("--org <slug>").action(async (ref, options) => {
|
|
7188
|
+
const ctx = buildContext();
|
|
7189
|
+
const r = await resolveRotation(ctx, (await resolveOrg(ctx, options.org)).id, ref);
|
|
7190
|
+
console.log(JSON.stringify(r, null, 2));
|
|
7191
|
+
});
|
|
7192
|
+
rotation.command("rotate <rotationOrSecret>").description("rotate now (the same path the scheduler uses)").option("--org <slug>").action(async (ref, options) => {
|
|
7193
|
+
const ctx = buildContext();
|
|
7194
|
+
const org = await resolveOrg(ctx, options.org);
|
|
7195
|
+
const r = await resolveRotation(ctx, org.id, ref);
|
|
7196
|
+
const { version, rotatedAt } = await ctx.client.rotateSecretNow(org.id, r.id);
|
|
7197
|
+
console.error(`rotated ${r.secretName} at ${rotatedAt} — now at version ${version}. Read it with \`seekrit secrets get ${r.secretName}\`.`);
|
|
7198
|
+
});
|
|
7199
|
+
rotation.command("pause <rotationOrSecret>").description("stop rotating, keeping the policy").option("--org <slug>").action(async (ref, options) => {
|
|
7200
|
+
const ctx = buildContext();
|
|
7201
|
+
const org = await resolveOrg(ctx, options.org);
|
|
7202
|
+
const r = await resolveRotation(ctx, org.id, ref);
|
|
7203
|
+
await ctx.client.updateRotation(org.id, r.id, { status: "paused" });
|
|
7204
|
+
console.error(`paused rotation of ${r.secretName}`);
|
|
7205
|
+
});
|
|
7206
|
+
rotation.command("resume <rotationOrSecret>").description("resume rotating (also clears a failed streak)").option("--org <slug>").action(async (ref, options) => {
|
|
7207
|
+
const ctx = buildContext();
|
|
7208
|
+
const org = await resolveOrg(ctx, options.org);
|
|
7209
|
+
const r = await resolveRotation(ctx, org.id, ref);
|
|
7210
|
+
const { rotation: updated } = await ctx.client.updateRotation(org.id, r.id, { status: "active" });
|
|
7211
|
+
console.error(`resumed rotation of ${r.secretName} — next ${updated.nextRotateAt}`);
|
|
7212
|
+
});
|
|
7213
|
+
rotation.command("set-interval <rotationOrSecret>").description("change the cadence").requiredOption("--every <duration>", "new cadence, e.g. 24h, 30d").option("--org <slug>").action(async (ref, options) => {
|
|
7214
|
+
const ctx = buildContext();
|
|
7215
|
+
const org = await resolveOrg(ctx, options.org);
|
|
7216
|
+
const r = await resolveRotation(ctx, org.id, ref);
|
|
7217
|
+
const { rotation: updated } = await ctx.client.updateRotation(org.id, r.id, { intervalSeconds: parseDurationSeconds(options.every, "--every") });
|
|
7218
|
+
console.error(`${updated.secretName} now rotates every ${formatInterval(updated.intervalSeconds)} — next ${updated.nextRotateAt}`);
|
|
7219
|
+
});
|
|
7220
|
+
rotation.command("disable <rotationOrSecret>").description("stop rotating and remove the policy (the secret is untouched)").option("--org <slug>").action(async (ref, options) => {
|
|
7221
|
+
const ctx = buildContext();
|
|
7222
|
+
const org = await resolveOrg(ctx, options.org);
|
|
7223
|
+
const r = await resolveRotation(ctx, org.id, ref);
|
|
7224
|
+
const { rotatorRevoked } = await ctx.client.disableRotation(org.id, r.id);
|
|
7225
|
+
console.error(`disabled rotation of ${r.secretName}${rotatorRevoked ? " — rotator key access revoked for this environment" : ""}`);
|
|
7226
|
+
});
|
|
7227
|
+
}
|
|
6635
7228
|
/**
|
|
6636
7229
|
* Fetch + decrypt every secret in a single environment.
|
|
6637
7230
|
*
|
|
@@ -6998,6 +7591,19 @@ function assertFlyApp(value) {
|
|
|
6998
7591
|
return appName;
|
|
6999
7592
|
}
|
|
7000
7593
|
/**
|
|
7594
|
+
* Heroku takes an app name or the app's UUID, so both are accepted here. The
|
|
7595
|
+
* habitual slips are the hostname (`storefront.herokuapp.com`) and capitals;
|
|
7596
|
+
* both are a 404 from Heroku much later, so they are caught here with a message
|
|
7597
|
+
* that says which.
|
|
7598
|
+
*/
|
|
7599
|
+
function assertHerokuApp(value) {
|
|
7600
|
+
if (!value) fail("--heroku-app is required for heroku (the app name, e.g. storefront-production)");
|
|
7601
|
+
const app = value.trim();
|
|
7602
|
+
if (/\.(herokuapp\.com|herokudns\.com)$/.test(app)) fail(`--heroku-app takes the app name, not its hostname — try "${app.split(".")[0]}"`);
|
|
7603
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(app) && !/^[a-z][a-z0-9-]{1,28}[a-z0-9]$/.test(app)) fail(`--heroku-app "${app}" is not a Heroku app name — 3 to 30 characters, starting with a lowercase letter, made of lowercase letters, numbers, and dashes (or the app's UUID)`);
|
|
7604
|
+
return app;
|
|
7605
|
+
}
|
|
7606
|
+
/**
|
|
7001
7607
|
* Northflank ids are slugs from the resource's URL, so the slip to catch is the
|
|
7002
7608
|
* *display name* — "App Secrets" where "app-secrets" belongs.
|
|
7003
7609
|
*/
|
|
@@ -7007,10 +7613,36 @@ function assertNorthflankId(value, flag) {
|
|
|
7007
7613
|
if (!/^[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*$/.test(id)) fail(`${flag} should be a Northflank ID like "app-secrets", not "${id}"`);
|
|
7008
7614
|
return id;
|
|
7009
7615
|
}
|
|
7616
|
+
/**
|
|
7617
|
+
* DigitalOcean app ids are UUIDs, and the slip to catch is the app *name* —
|
|
7618
|
+
* what the dashboard headline shows, and what the API answers with a bare 404.
|
|
7619
|
+
*/
|
|
7620
|
+
function assertDigitalOceanApp(value) {
|
|
7621
|
+
if (!value) fail("--do-app is required for digitalocean (the UUID in the app's URL)");
|
|
7622
|
+
const appId = value.trim();
|
|
7623
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(appId)) fail(`--do-app should be the app's UUID, from its URL — not "${appId}"`);
|
|
7624
|
+
return appId;
|
|
7625
|
+
}
|
|
7010
7626
|
function assertProvider(value) {
|
|
7011
7627
|
if (!SYNC_PROVIDER_KINDS.includes(value)) fail(`unknown provider "${value}" — one of: ${SYNC_PROVIDER_KINDS.join(", ")}`);
|
|
7012
7628
|
return value;
|
|
7013
7629
|
}
|
|
7630
|
+
/**
|
|
7631
|
+
* Netlify names a site by its **API ID**, and only that.
|
|
7632
|
+
*
|
|
7633
|
+
* Netlify does accept a site's domain where a site appears in a URL path, which
|
|
7634
|
+
* makes the strictness here look gratuitous — but the environment variable
|
|
7635
|
+
* endpoints take the site as a `?site_id=` query parameter, where Netlify
|
|
7636
|
+
* documents no name resolution. A site id it cannot resolve does not fail: the
|
|
7637
|
+
* variables land on the **team**, shared by every site in it. Refusing anything
|
|
7638
|
+
* but the UUID keeps a slip from writing much wider than was asked.
|
|
7639
|
+
*/
|
|
7640
|
+
function assertNetlifySite(value) {
|
|
7641
|
+
if (!value) fail("--netlify-site is required for netlify (the site's API ID, a UUID)");
|
|
7642
|
+
const site = value.trim();
|
|
7643
|
+
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
|
+
return site;
|
|
7645
|
+
}
|
|
7014
7646
|
/** Account-scope config for a connection (never the credential itself). */
|
|
7015
7647
|
function buildConfig(provider, options) {
|
|
7016
7648
|
switch (provider) {
|
|
@@ -7046,6 +7678,14 @@ function buildConfig(provider, options) {
|
|
|
7046
7678
|
case "render": return { provider: "render" };
|
|
7047
7679
|
case "fly": return { provider: "fly" };
|
|
7048
7680
|
case "northflank": return { provider: "northflank" };
|
|
7681
|
+
case "digitalocean": return { provider: "digitalocean" };
|
|
7682
|
+
case "heroku": return { provider: "heroku" };
|
|
7683
|
+
case "netlify":
|
|
7684
|
+
if (!options.accountId) fail("--account-id is required for netlify — the team slug from app.netlify.com/teams/<slug>, or the account ID");
|
|
7685
|
+
return {
|
|
7686
|
+
provider: "netlify",
|
|
7687
|
+
accountId: options.accountId
|
|
7688
|
+
};
|
|
7049
7689
|
}
|
|
7050
7690
|
}
|
|
7051
7691
|
/** Where inside the platform a binding writes. */
|
|
@@ -7144,6 +7784,41 @@ function buildDestination(provider, options) {
|
|
|
7144
7784
|
projectId: assertNorthflankId(options.project, "--project"),
|
|
7145
7785
|
secretGroupId: assertNorthflankId(options.secretGroup, "--secret-group")
|
|
7146
7786
|
};
|
|
7787
|
+
case "digitalocean": {
|
|
7788
|
+
const appId = assertDigitalOceanApp(options.doApp);
|
|
7789
|
+
const scope = assertMember(options.envScope, DIGITALOCEAN_ENV_SCOPES, "--env-scope", "RUN_TIME");
|
|
7790
|
+
const componentName = options.component?.trim();
|
|
7791
|
+
if (!componentName) return {
|
|
7792
|
+
provider: "digitalocean",
|
|
7793
|
+
kind: "app",
|
|
7794
|
+
appId,
|
|
7795
|
+
scope
|
|
7796
|
+
};
|
|
7797
|
+
if (!/^[a-z][a-z0-9-]{0,30}[a-z0-9]$/.test(componentName)) fail(`--component "${componentName}" is not an App Platform component name (lowercase letters, numbers, and dashes)`);
|
|
7798
|
+
return {
|
|
7799
|
+
provider: "digitalocean",
|
|
7800
|
+
kind: "component",
|
|
7801
|
+
appId,
|
|
7802
|
+
componentName,
|
|
7803
|
+
scope
|
|
7804
|
+
};
|
|
7805
|
+
}
|
|
7806
|
+
case "heroku": return {
|
|
7807
|
+
provider: "heroku",
|
|
7808
|
+
app: assertHerokuApp(options.herokuApp)
|
|
7809
|
+
};
|
|
7810
|
+
case "netlify": {
|
|
7811
|
+
const contexts = assertMembers(list(options.target, "production"), NETLIFY_CONTEXTS, "--target");
|
|
7812
|
+
const branch = options.gitBranch?.trim();
|
|
7813
|
+
if (contexts.includes("branch") && !branch) fail("--git-branch is required with `--target branch` — the branch the values apply to");
|
|
7814
|
+
return {
|
|
7815
|
+
provider: "netlify",
|
|
7816
|
+
siteId: assertNetlifySite(options.netlifySite),
|
|
7817
|
+
contexts,
|
|
7818
|
+
...contexts.includes("branch") && branch ? { branch } : {},
|
|
7819
|
+
secret: options.netlifySecret !== false
|
|
7820
|
+
};
|
|
7821
|
+
}
|
|
7147
7822
|
}
|
|
7148
7823
|
}
|
|
7149
7824
|
/** One-line description of a destination, for list output. */
|
|
@@ -7159,6 +7834,9 @@ function describeDestination(destination) {
|
|
|
7159
7834
|
case "render": return destination.kind === "service" ? destination.serviceId : `env group ${destination.envGroupId}`;
|
|
7160
7835
|
case "fly": return destination.appName;
|
|
7161
7836
|
case "northflank": return `${destination.projectId} / ${destination.secretGroupId}`;
|
|
7837
|
+
case "digitalocean": return `${destination.appId}${destination.kind === "component" ? ` / ${destination.componentName}` : ""} (${destination.scope})`;
|
|
7838
|
+
case "heroku": return destination.app;
|
|
7839
|
+
case "netlify": return `${destination.siteId} (${destination.contexts.map((context) => context === "branch" ? `branch @${destination.branch}` : context).join(", ")})`;
|
|
7162
7840
|
}
|
|
7163
7841
|
}
|
|
7164
7842
|
/**
|
|
@@ -7169,7 +7847,7 @@ function describeDestination(destination) {
|
|
|
7169
7847
|
* application whose environment the binding reads from.
|
|
7170
7848
|
*/
|
|
7171
7849
|
function destinationOptions(command) {
|
|
7172
|
-
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)");
|
|
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)");
|
|
7173
7851
|
}
|
|
7174
7852
|
/** Find a connection by id or name — nobody keeps `syc_…` ids in their head. */
|
|
7175
7853
|
async function resolveConnection(ctx, orgId, ref) {
|
|
@@ -7193,7 +7871,7 @@ function registerSyncCommands(program) {
|
|
|
7193
7871
|
col("id", (c) => c.id)
|
|
7194
7872
|
], "no connections — add one with `seekrit sync connect`"));
|
|
7195
7873
|
});
|
|
7196
|
-
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)").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) => {
|
|
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) => {
|
|
7197
7875
|
const provider = assertProvider(options.provider);
|
|
7198
7876
|
const ctx = buildContext();
|
|
7199
7877
|
const ref = await resolveOrg(ctx, options.org);
|
|
@@ -8061,6 +8739,7 @@ registerAwsCommands(program);
|
|
|
8061
8739
|
registerGcpCommands(program);
|
|
8062
8740
|
registerMongoCommands(program);
|
|
8063
8741
|
registerKmsCommands(program);
|
|
8742
|
+
registerRotationCommands(program);
|
|
8064
8743
|
registerRecoveryCommands(program);
|
|
8065
8744
|
program.command("mcp").description("run an MCP server over stdio so AI agents can drive seekrit").action(async () => {
|
|
8066
8745
|
const { runMcpServer } = await import("./mcp-DR-zla_u.js");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seekrit/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.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",
|