@seekrit/cli 0.17.2 → 0.18.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 +506 -14
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -682,7 +682,13 @@ z.object({
|
|
|
682
682
|
name: nameSchema,
|
|
683
683
|
slug: slugSchema,
|
|
684
684
|
/** Environment DEK wrapped to the creator's public key — created client-side. */
|
|
685
|
-
wrappedDek: z.string().min(1)
|
|
685
|
+
wrappedDek: z.string().min(1),
|
|
686
|
+
/**
|
|
687
|
+
* When the org has recovery enabled, the same DEK additionally wrapped to the
|
|
688
|
+
* org recovery public key, so the environment is recovery-protected from
|
|
689
|
+
* creation. Omitted when recovery is off (backfilled later by `recovery sync`).
|
|
690
|
+
*/
|
|
691
|
+
recoveryWrappedDek: z.string().min(1).nullish()
|
|
686
692
|
});
|
|
687
693
|
z.object({
|
|
688
694
|
/** Opaque versioned ciphertext blob from @seekrit/crypto. */
|
|
@@ -758,6 +764,55 @@ z.object({
|
|
|
758
764
|
publicKeyJwk: z.string().min(1).nullish(),
|
|
759
765
|
grants: z.array(kmsGrantInputSchema).min(1)
|
|
760
766
|
});
|
|
767
|
+
/**
|
|
768
|
+
* One custodian's wrapped Shamir share of the org recovery private key. The
|
|
769
|
+
* client generates the recovery keypair, splits the private half M-of-N, and
|
|
770
|
+
* wraps each share to a custodian's public key — the server stores only the
|
|
771
|
+
* opaque `wrappedShare` and can reconstruct nothing.
|
|
772
|
+
*/
|
|
773
|
+
const recoveryShareInputSchema = z.object({
|
|
774
|
+
principalType: principalTypeSchema,
|
|
775
|
+
principalId: z.string().min(1),
|
|
776
|
+
/** Shamir x-coordinate carried by the share (1..255). */
|
|
777
|
+
shareIndex: z.number().int().min(1).max(255),
|
|
778
|
+
/** The recovery-key share wrapped to the custodian's public key (`wd1.`). */
|
|
779
|
+
wrappedShare: z.string().min(1)
|
|
780
|
+
});
|
|
781
|
+
/** An environment DEK additionally wrapped to the org recovery public key. */
|
|
782
|
+
const recoveryEnvGrantSchema = z.object({
|
|
783
|
+
environmentId: z.string().min(1),
|
|
784
|
+
/** The environment's DEK wrapped to the recovery public key (`wd1.`). */
|
|
785
|
+
wrappedDek: z.string().min(1)
|
|
786
|
+
});
|
|
787
|
+
z.object({
|
|
788
|
+
recoveryPublicKeyJwk: z.string().min(1),
|
|
789
|
+
threshold: z.number().int().min(1).max(255),
|
|
790
|
+
shares: z.array(recoveryShareInputSchema).min(1).max(255),
|
|
791
|
+
grants: z.array(recoveryEnvGrantSchema).default([])
|
|
792
|
+
}).refine((v) => v.threshold <= v.shares.length, {
|
|
793
|
+
message: "threshold cannot exceed the number of custodians",
|
|
794
|
+
path: ["threshold"]
|
|
795
|
+
}).refine((v) => new Set(v.shares.map((s) => `${s.principalType}:${s.principalId}`)).size === v.shares.length, {
|
|
796
|
+
message: "custodians must be distinct",
|
|
797
|
+
path: ["shares"]
|
|
798
|
+
});
|
|
799
|
+
z.object({ grants: z.array(recoveryEnvGrantSchema).min(1).max(500) });
|
|
800
|
+
z.object({
|
|
801
|
+
targetPublicKeyJwk: z.string().min(1),
|
|
802
|
+
targetType: principalTypeSchema.nullish(),
|
|
803
|
+
targetId: z.string().min(1).nullish(),
|
|
804
|
+
reason: z.string().max(500).nullish()
|
|
805
|
+
});
|
|
806
|
+
z.object({
|
|
807
|
+
shareIndex: z.number().int().min(1).max(255),
|
|
808
|
+
/** The custodian's share re-wrapped to the target public key (`wd1.`). */
|
|
809
|
+
contributedShare: z.string().min(1)
|
|
810
|
+
});
|
|
811
|
+
z.object({
|
|
812
|
+
principalType: principalTypeSchema,
|
|
813
|
+
principalId: z.string().min(1),
|
|
814
|
+
grants: z.array(recoveryEnvGrantSchema).min(1).max(500)
|
|
815
|
+
});
|
|
761
816
|
z.object({ prefs: z.partialRecord(z.enum(NOTIFICATION_TYPES), z.boolean()) });
|
|
762
817
|
z.object({
|
|
763
818
|
endpoint: z.url().max(2048),
|
|
@@ -1343,6 +1398,191 @@ async function decryptPrivateKey(passphrase, blob) {
|
|
|
1343
1398
|
}
|
|
1344
1399
|
}
|
|
1345
1400
|
//#endregion
|
|
1401
|
+
//#region ../../packages/crypto/src/shamir.ts
|
|
1402
|
+
/**
|
|
1403
|
+
* Shamir's Secret Sharing over GF(2^8) — the same field AES uses, with the
|
|
1404
|
+
* reduction polynomial x^8 + x^4 + x^3 + x + 1 (0x11b). Splits a byte string
|
|
1405
|
+
* into `shares` shares such that any `threshold` of them reconstruct the secret
|
|
1406
|
+
* exactly and any fewer reveal nothing about it.
|
|
1407
|
+
*
|
|
1408
|
+
* This is the one hand-rolled primitive behind org recovery (P0-1): the org
|
|
1409
|
+
* recovery private key is split into shares, each wrapped to a designated
|
|
1410
|
+
* custodian's public key, so a quorum — never seekrit, never any single
|
|
1411
|
+
* custodian below the threshold — can reconstruct it. See recovery.ts for the
|
|
1412
|
+
* composition with key wrapping.
|
|
1413
|
+
*
|
|
1414
|
+
* Each secret byte gets its own degree-(threshold-1) polynomial whose constant
|
|
1415
|
+
* term is that byte; a share is that polynomial family evaluated at one distinct
|
|
1416
|
+
* nonzero x-coordinate. Reconstruction is Lagrange interpolation back to x = 0.
|
|
1417
|
+
*
|
|
1418
|
+
* Share wire format: a Uint8Array whose first byte is the share's distinct
|
|
1419
|
+
* nonzero x-coordinate and whose remaining bytes are the evaluations p_j(x) for
|
|
1420
|
+
* each secret byte j. Self-describing, so combineSecret() needs no external
|
|
1421
|
+
* index — the x-coordinate survives being wrapped/unwrapped/re-wrapped intact.
|
|
1422
|
+
*/
|
|
1423
|
+
/** Russian-peasant multiply in GF(2^8) (mod 0x11b) — used only to seed tables. */
|
|
1424
|
+
function peasantMul(a, b) {
|
|
1425
|
+
let product = 0;
|
|
1426
|
+
let x = a;
|
|
1427
|
+
let y = b;
|
|
1428
|
+
for (let i = 0; i < 8; i++) {
|
|
1429
|
+
if (y & 1) product ^= x;
|
|
1430
|
+
const carry = x & 128;
|
|
1431
|
+
x = x << 1 & 255;
|
|
1432
|
+
if (carry) x ^= 27;
|
|
1433
|
+
y >>= 1;
|
|
1434
|
+
}
|
|
1435
|
+
return product;
|
|
1436
|
+
}
|
|
1437
|
+
const EXP = /* @__PURE__ */ new Uint8Array(512);
|
|
1438
|
+
const LOG = /* @__PURE__ */ new Uint8Array(256);
|
|
1439
|
+
{
|
|
1440
|
+
let a = 1;
|
|
1441
|
+
for (let i = 0; i < 255; i++) {
|
|
1442
|
+
EXP[i] = a;
|
|
1443
|
+
LOG[a] = i;
|
|
1444
|
+
a = peasantMul(a, 3);
|
|
1445
|
+
}
|
|
1446
|
+
for (let i = 255; i < 512; i++) EXP[i] = EXP[i - 255];
|
|
1447
|
+
}
|
|
1448
|
+
/** Multiply in GF(2^8) via the log/exp tables. */
|
|
1449
|
+
function mul(a, b) {
|
|
1450
|
+
if (a === 0 || b === 0) return 0;
|
|
1451
|
+
return EXP[LOG[a] + LOG[b]];
|
|
1452
|
+
}
|
|
1453
|
+
/** Divide in GF(2^8) (a / b). Caller guarantees b !== 0. */
|
|
1454
|
+
function div(a, b) {
|
|
1455
|
+
if (a === 0) return 0;
|
|
1456
|
+
return EXP[LOG[a] + 255 - LOG[b]];
|
|
1457
|
+
}
|
|
1458
|
+
/** Evaluate a polynomial (coeffs low-degree-first) at x, via Horner's rule. */
|
|
1459
|
+
function evalPoly(coeffs, x) {
|
|
1460
|
+
let result = 0;
|
|
1461
|
+
for (let i = coeffs.length - 1; i >= 0; i--) result = mul(result, x) ^ coeffs[i];
|
|
1462
|
+
return result;
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Split `secret` into `shares` shares, any `threshold` of which reconstruct it.
|
|
1466
|
+
* x-coordinates are 1..shares, so shares must be in 1..255 and threshold in
|
|
1467
|
+
* 1..shares. threshold = shares means every share is needed; threshold = 1 is
|
|
1468
|
+
* the degenerate case where each share equals the secret (the "single recovery
|
|
1469
|
+
* admin" configuration).
|
|
1470
|
+
*/
|
|
1471
|
+
function splitSecret(secret, threshold, shares) {
|
|
1472
|
+
if (secret.length < 1) throw new SeekritCryptoError("MALFORMED_BLOB", "cannot split an empty secret");
|
|
1473
|
+
if (!Number.isInteger(threshold) || !Number.isInteger(shares) || threshold < 1 || shares < threshold || shares > 255) throw new SeekritCryptoError("MALFORMED_BLOB", `invalid Shamir parameters: need 1 <= threshold (${threshold}) <= shares (${shares}) <= 255`);
|
|
1474
|
+
const out = [];
|
|
1475
|
+
for (let s = 0; s < shares; s++) {
|
|
1476
|
+
const share = new Uint8Array(1 + secret.length);
|
|
1477
|
+
share[0] = s + 1;
|
|
1478
|
+
out.push(share);
|
|
1479
|
+
}
|
|
1480
|
+
const randomCoeffs = threshold > 1 ? crypto.getRandomValues(new Uint8Array(secret.length * (threshold - 1))) : /* @__PURE__ */ new Uint8Array(0);
|
|
1481
|
+
const coeffs = new Uint8Array(threshold);
|
|
1482
|
+
for (let j = 0; j < secret.length; j++) {
|
|
1483
|
+
coeffs[0] = secret[j];
|
|
1484
|
+
for (let k = 1; k < threshold; k++) coeffs[k] = randomCoeffs[j * (threshold - 1) + (k - 1)];
|
|
1485
|
+
for (const share of out) share[1 + j] = evalPoly(coeffs, share[0]);
|
|
1486
|
+
}
|
|
1487
|
+
return out;
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* Reconstruct a secret from shares in the {@link splitSecret} wire format.
|
|
1491
|
+
* Supplying at least `threshold` valid shares returns the original secret;
|
|
1492
|
+
* fewer returns a plausible-but-wrong value (that is the security property —
|
|
1493
|
+
* downstream key use is what actually verifies the result). Shares must be the
|
|
1494
|
+
* same length and carry distinct nonzero x-coordinates.
|
|
1495
|
+
*/
|
|
1496
|
+
function combineSecret(shares) {
|
|
1497
|
+
if (shares.length < 1) throw new SeekritCryptoError("MALFORMED_BLOB", "need at least one share to combine");
|
|
1498
|
+
const width = shares[0].length;
|
|
1499
|
+
if (width < 2) throw new SeekritCryptoError("MALFORMED_BLOB", "share is too short to carry a secret");
|
|
1500
|
+
const xs = new Uint8Array(shares.length);
|
|
1501
|
+
for (let i = 0; i < shares.length; i++) {
|
|
1502
|
+
const share = shares[i];
|
|
1503
|
+
if (share.length !== width) throw new SeekritCryptoError("MALFORMED_BLOB", "shares have mismatched lengths");
|
|
1504
|
+
const x = share[0];
|
|
1505
|
+
if (x === 0) throw new SeekritCryptoError("MALFORMED_BLOB", "share has an invalid zero x-coordinate");
|
|
1506
|
+
for (let p = 0; p < i; p++) if (xs[p] === x) throw new SeekritCryptoError("MALFORMED_BLOB", "shares have duplicate x-coordinates");
|
|
1507
|
+
xs[i] = x;
|
|
1508
|
+
}
|
|
1509
|
+
const basis = new Uint8Array(shares.length);
|
|
1510
|
+
for (let i = 0; i < shares.length; i++) {
|
|
1511
|
+
const xi = xs[i];
|
|
1512
|
+
let num = 1;
|
|
1513
|
+
let den = 1;
|
|
1514
|
+
for (let m = 0; m < shares.length; m++) {
|
|
1515
|
+
if (m === i) continue;
|
|
1516
|
+
const xm = xs[m];
|
|
1517
|
+
num = mul(num, xm);
|
|
1518
|
+
den = mul(den, xi ^ xm);
|
|
1519
|
+
}
|
|
1520
|
+
basis[i] = div(num, den);
|
|
1521
|
+
}
|
|
1522
|
+
const secret = new Uint8Array(width - 1);
|
|
1523
|
+
for (let j = 0; j < secret.length; j++) {
|
|
1524
|
+
let acc = 0;
|
|
1525
|
+
for (let i = 0; i < shares.length; i++) acc ^= mul(shares[i][1 + j], basis[i]);
|
|
1526
|
+
secret[j] = acc;
|
|
1527
|
+
}
|
|
1528
|
+
return secret;
|
|
1529
|
+
}
|
|
1530
|
+
//#endregion
|
|
1531
|
+
//#region ../../packages/crypto/src/recovery.ts
|
|
1532
|
+
/** Generate a fresh org recovery keypair (P-256, same as any principal). */
|
|
1533
|
+
function generateRecoveryKey() {
|
|
1534
|
+
return generateKeyPair();
|
|
1535
|
+
}
|
|
1536
|
+
/**
|
|
1537
|
+
* Split a recovery private key into one wrapped share per custodian, such that
|
|
1538
|
+
* any `threshold` custodians can reconstruct it. The share count is the number
|
|
1539
|
+
* of custodians.
|
|
1540
|
+
*/
|
|
1541
|
+
async function splitRecoveryKey(privateKeyJwk, threshold, custodians) {
|
|
1542
|
+
if (custodians.length < 1) throw new SeekritCryptoError("MALFORMED_BLOB", "recovery needs at least one custodian");
|
|
1543
|
+
const shares = splitSecret(utf8Encode(privateKeyJwk), threshold, custodians.length);
|
|
1544
|
+
return Promise.all(custodians.map(async (custodian, i) => {
|
|
1545
|
+
const share = shares[i];
|
|
1546
|
+
return {
|
|
1547
|
+
principalType: custodian.principalType,
|
|
1548
|
+
principalId: custodian.principalId,
|
|
1549
|
+
shareIndex: share[0],
|
|
1550
|
+
wrappedShare: await wrapDek(share, custodian.publicKeyJwk)
|
|
1551
|
+
};
|
|
1552
|
+
}));
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1555
|
+
* Unwrap a single recovery share with the holder's private key, returning the
|
|
1556
|
+
* raw share bytes. Used by a custodian to unlock their own share, and by a
|
|
1557
|
+
* recovery target to unlock a share re-wrapped to them — both are plain
|
|
1558
|
+
* {@link unwrapDek}, since a share is just wrapped bytes.
|
|
1559
|
+
*/
|
|
1560
|
+
function unwrapRecoveryShare(wrappedShare, privateKey) {
|
|
1561
|
+
return unwrapDek(wrappedShare, privateKey);
|
|
1562
|
+
}
|
|
1563
|
+
/**
|
|
1564
|
+
* Re-wrap an already-unwrapped share to a recovery target's public key. This is
|
|
1565
|
+
* the custodian's contribution during a recovery ceremony: the server collects
|
|
1566
|
+
* a quorum of shares all wrapped to the target, none of which it can read.
|
|
1567
|
+
*/
|
|
1568
|
+
function rewrapRecoveryShare(shareBytes, targetPublicKeyJwk) {
|
|
1569
|
+
return wrapDek(shareBytes, targetPublicKeyJwk);
|
|
1570
|
+
}
|
|
1571
|
+
/**
|
|
1572
|
+
* Combine a quorum of unwrapped shares back into the recovery private key,
|
|
1573
|
+
* imported and ready to unwrap DEK recovery grants. Fewer than the threshold, or
|
|
1574
|
+
* shares from different recovery keys, yield garbage that fails to import or
|
|
1575
|
+
* decrypt — surfaced as DECRYPT_FAILED, never a silent wrong key.
|
|
1576
|
+
*/
|
|
1577
|
+
async function combineRecoveryShares(shareBytes) {
|
|
1578
|
+
const privateKeyJwk = utf8Decode(combineSecret(shareBytes));
|
|
1579
|
+
try {
|
|
1580
|
+
return await importPrivateKey(privateKeyJwk);
|
|
1581
|
+
} catch {
|
|
1582
|
+
throw new SeekritCryptoError("DECRYPT_FAILED", "recovery reconstruction failed: wrong, insufficient, or mismatched shares");
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
//#endregion
|
|
1346
1586
|
//#region ../../packages/crypto/src/redis.ts
|
|
1347
1587
|
/**
|
|
1348
1588
|
* Client-side construction of a Redis (6+) ACL password verifier, for minting
|
|
@@ -1721,7 +1961,7 @@ function isServiceToken(value) {
|
|
|
1721
1961
|
}
|
|
1722
1962
|
//#endregion
|
|
1723
1963
|
//#region package.json
|
|
1724
|
-
var version = "0.
|
|
1964
|
+
var version = "0.18.0";
|
|
1725
1965
|
//#endregion
|
|
1726
1966
|
//#region ../../packages/api-client/src/index.ts
|
|
1727
1967
|
var SeekritApiError = class extends Error {
|
|
@@ -1896,6 +2136,53 @@ var SeekritClient = class {
|
|
|
1896
2136
|
revokeEnvKey(orgId, envId, grantId) {
|
|
1897
2137
|
return this.request("DELETE", `/v1/orgs/${orgId}/envs/${envId}/keys/${grantId}`);
|
|
1898
2138
|
}
|
|
2139
|
+
/** Org recovery status: threshold, custodians, and environment coverage. */
|
|
2140
|
+
getRecovery(orgId) {
|
|
2141
|
+
return this.request("GET", `/v1/orgs/${orgId}/recovery`);
|
|
2142
|
+
}
|
|
2143
|
+
/** Enable recovery with the recovery public key, custodian shares, and grants. */
|
|
2144
|
+
configureRecovery(orgId, input) {
|
|
2145
|
+
return this.request("POST", `/v1/orgs/${orgId}/recovery`, input);
|
|
2146
|
+
}
|
|
2147
|
+
/** Rotate the recovery key: new keypair, custodian set, and env re-wraps. */
|
|
2148
|
+
rotateRecovery(orgId, input) {
|
|
2149
|
+
return this.request("POST", `/v1/orgs/${orgId}/recovery/rotate`, input);
|
|
2150
|
+
}
|
|
2151
|
+
disableRecovery(orgId) {
|
|
2152
|
+
return this.request("DELETE", `/v1/orgs/${orgId}/recovery`);
|
|
2153
|
+
}
|
|
2154
|
+
/** The calling principal's own wrapped recovery share (custodian only). */
|
|
2155
|
+
getMyRecoveryShare(orgId) {
|
|
2156
|
+
return this.request("GET", `/v1/orgs/${orgId}/recovery/share`);
|
|
2157
|
+
}
|
|
2158
|
+
/** Every environment DEK wrapped to the org recovery key (admin only). */
|
|
2159
|
+
getRecoveryEnvKeys(orgId) {
|
|
2160
|
+
return this.request("GET", `/v1/orgs/${orgId}/recovery/env-keys`);
|
|
2161
|
+
}
|
|
2162
|
+
/** Backfill recovery grants for environments the caller can decrypt. */
|
|
2163
|
+
uploadRecoveryGrants(orgId, input) {
|
|
2164
|
+
return this.request("POST", `/v1/orgs/${orgId}/recovery/grants`, input);
|
|
2165
|
+
}
|
|
2166
|
+
listRecoveryRequests(orgId) {
|
|
2167
|
+
return this.request("GET", `/v1/orgs/${orgId}/recovery/requests`);
|
|
2168
|
+
}
|
|
2169
|
+
createRecoveryRequest(orgId, input) {
|
|
2170
|
+
return this.request("POST", `/v1/orgs/${orgId}/recovery/requests`, input);
|
|
2171
|
+
}
|
|
2172
|
+
getRecoveryRequest(orgId, requestId) {
|
|
2173
|
+
return this.request("GET", `/v1/orgs/${orgId}/recovery/requests/${requestId}`);
|
|
2174
|
+
}
|
|
2175
|
+
/** A custodian contributes their share, re-wrapped to the request target. */
|
|
2176
|
+
contributeRecoveryShare(orgId, requestId, input) {
|
|
2177
|
+
return this.request("POST", `/v1/orgs/${orgId}/recovery/requests/${requestId}/shares`, input);
|
|
2178
|
+
}
|
|
2179
|
+
/** The target finalizes recovery, re-granting itself the recovered DEKs. */
|
|
2180
|
+
completeRecoveryRequest(orgId, requestId, input) {
|
|
2181
|
+
return this.request("POST", `/v1/orgs/${orgId}/recovery/requests/${requestId}/complete`, input);
|
|
2182
|
+
}
|
|
2183
|
+
cancelRecoveryRequest(orgId, requestId) {
|
|
2184
|
+
return this.request("POST", `/v1/orgs/${orgId}/recovery/requests/${requestId}/cancel`);
|
|
2185
|
+
}
|
|
1899
2186
|
listTokens(orgId) {
|
|
1900
2187
|
return this.request("GET", `/v1/orgs/${orgId}/tokens`);
|
|
1901
2188
|
}
|
|
@@ -2586,7 +2873,7 @@ function registerGcpCommands(program) {
|
|
|
2586
2873
|
//#endregion
|
|
2587
2874
|
//#region src/kms.ts
|
|
2588
2875
|
/** Collect a repeatable option into a list. */
|
|
2589
|
-
function collect$
|
|
2876
|
+
function collect$6(value, acc = []) {
|
|
2590
2877
|
acc.push(value);
|
|
2591
2878
|
return acc;
|
|
2592
2879
|
}
|
|
@@ -2654,7 +2941,7 @@ async function kmsRecoverMaterial(ctx, orgId, keyId, version) {
|
|
|
2654
2941
|
}
|
|
2655
2942
|
function registerKmsCommands(program) {
|
|
2656
2943
|
const kms = program.command("kms").description("managed keys for application-layer encryption & signing (client-side)");
|
|
2657
|
-
kms.command("create").description("create a managed key (material is generated locally and wrapped, never sent)").requiredOption("--name <name>", "org-unique key name").requiredOption("--purpose <purpose>", "encrypt | sign").option("--org <slug>").option("--app <slug>", "scope the key to an application").option("--group <slug>", "scope the key to a group").option("--grant-user <email>", "also grant an org member (repeatable)", collect$
|
|
2944
|
+
kms.command("create").description("create a managed key (material is generated locally and wrapped, never sent)").requiredOption("--name <name>", "org-unique key name").requiredOption("--purpose <purpose>", "encrypt | sign").option("--org <slug>").option("--app <slug>", "scope the key to an application").option("--group <slug>", "scope the key to a group").option("--grant-user <email>", "also grant an org member (repeatable)", collect$6, []).option("--grant-token <tokenId>", "also grant a service token (repeatable)", collect$6, []).action(async (options) => {
|
|
2658
2945
|
if (options.purpose !== "encrypt" && options.purpose !== "sign") fail("--purpose must be encrypt or sign");
|
|
2659
2946
|
if (options.app && options.group) fail("pass at most one of --app or --group");
|
|
2660
2947
|
const ctx = buildContext();
|
|
@@ -2888,7 +3175,7 @@ function parseTtlSeconds$4(input) {
|
|
|
2888
3175
|
}[m[2] || "s"] ?? 1);
|
|
2889
3176
|
}
|
|
2890
3177
|
/** Collect a repeatable option into an array. */
|
|
2891
|
-
function collect$
|
|
3178
|
+
function collect$5(value, previous) {
|
|
2892
3179
|
return [...previous, value];
|
|
2893
3180
|
}
|
|
2894
3181
|
/** Parse `readWrite@app` → { role, db } for a custom target. */
|
|
@@ -2913,7 +3200,7 @@ function resolveAdminUri(uri) {
|
|
|
2913
3200
|
function registerMongoCommands(program) {
|
|
2914
3201
|
const mongo = program.command("mongodb").description("temporary MongoDB credentials (createUser, zero-knowledge delivery)");
|
|
2915
3202
|
const target = mongo.command("target").description("manage MongoDB targets");
|
|
2916
|
-
target.command("add").description("register a MongoDB cluster to issue temporary credentials from").requiredOption("--name <name>", "display name, e.g. prod-app").requiredOption("--database <db>", "the database leased users get access to, e.g. app").option("--uri <uri>", "admin connection string (else SEEKRIT_MONGODB_ADMIN_URL)").option("--org <slug>").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--role <role@db>", "grant for a custom target (repeatable)", collect$
|
|
3203
|
+
target.command("add").description("register a MongoDB cluster to issue temporary credentials from").requiredOption("--name <name>", "display name, e.g. prod-app").requiredOption("--database <db>", "the database leased users get access to, e.g. app").option("--uri <uri>", "admin connection string (else SEEKRIT_MONGODB_ADMIN_URL)").option("--org <slug>").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--role <role@db>", "grant for a custom target (repeatable)", collect$5, []).option("--auth-source <db>", "authentication database (default admin)").option("--max-ttl <duration>", "clamp requested credential lifetime, e.g. 8h").option("--no-tls", "disable TLS to the cluster (TLS is on by default)").action(async (options) => {
|
|
2917
3204
|
const ctx = buildContext();
|
|
2918
3205
|
const org = await resolveOrg(ctx, options.org);
|
|
2919
3206
|
const adminUri = resolveAdminUri(options.uri);
|
|
@@ -3073,7 +3360,7 @@ function generateUserName$1(prefix = "tmp") {
|
|
|
3073
3360
|
function registerMysqlCommands(program) {
|
|
3074
3361
|
const mysql = program.command("mysql").description("temporary MySQL/MariaDB credentials (short-lived, zero-knowledge)");
|
|
3075
3362
|
const target = mysql.command("target").description("manage provisioning targets");
|
|
3076
|
-
target.command("add").description("register a MySQL/MariaDB server to lease credentials from").requiredOption("--name <name>", "display name, e.g. prod-db").option("--org <slug>").requiredOption("--host <host>", "database host").option("--port <port>", "database port", "3306").requiredOption("--database <name>", "database to grant access to").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--user-host <host>", "host part of created accounts ('name'@'<host>')", "%").option("--executor <mode>", "in_do | remote", "in_do").option("--provisioner-url <url>", "customer provisioner URL (remote executor)").option("--hmac-key <base64>", "shared HMAC key for the remote executor (or set SEEKRIT_PROVISIONER_HMAC_KEY); `seekrit provisioner keygen` mints one").option("--admin-url <url>", "in_do: admin mysql:// connection string (or set SEEKRIT_MYSQL_ADMIN_URL); wrapped locally").option("--create-statement <sql>", "custom CREATE template (repeatable)", collect$
|
|
3363
|
+
target.command("add").description("register a MySQL/MariaDB server to lease credentials from").requiredOption("--name <name>", "display name, e.g. prod-db").option("--org <slug>").requiredOption("--host <host>", "database host").option("--port <port>", "database port", "3306").requiredOption("--database <name>", "database to grant access to").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--user-host <host>", "host part of created accounts ('name'@'<host>')", "%").option("--executor <mode>", "in_do | remote", "in_do").option("--provisioner-url <url>", "customer provisioner URL (remote executor)").option("--hmac-key <base64>", "shared HMAC key for the remote executor (or set SEEKRIT_PROVISIONER_HMAC_KEY); `seekrit provisioner keygen` mints one").option("--admin-url <url>", "in_do: admin mysql:// connection string (or set SEEKRIT_MYSQL_ADMIN_URL); wrapped locally").option("--create-statement <sql>", "custom CREATE template (repeatable)", collect$4, []).option("--revoke-statement <sql>", "custom REVOKE template (repeatable)", collect$4, []).action(async (options) => {
|
|
3077
3364
|
const ctx = buildContext();
|
|
3078
3365
|
const org = await resolveOrg(ctx, options.org);
|
|
3079
3366
|
const executor = options.executor === "remote" ? "remote" : "in_do";
|
|
@@ -3174,7 +3461,7 @@ function registerMysqlCommands(program) {
|
|
|
3174
3461
|
});
|
|
3175
3462
|
}
|
|
3176
3463
|
/** Collect a repeatable option into an array. */
|
|
3177
|
-
function collect$
|
|
3464
|
+
function collect$4(value, acc) {
|
|
3178
3465
|
acc.push(value);
|
|
3179
3466
|
return acc;
|
|
3180
3467
|
}
|
|
@@ -3211,7 +3498,7 @@ function generateRoleName(prefix = "tmp") {
|
|
|
3211
3498
|
function registerPgCommands(program) {
|
|
3212
3499
|
const pg = program.command("pg").description("temporary Postgres credentials (short-lived, zero-knowledge)");
|
|
3213
3500
|
const target = pg.command("target").description("manage provisioning targets");
|
|
3214
|
-
target.command("add").description("register a Postgres cluster to lease credentials from").requiredOption("--name <name>", "display name, e.g. prod-db").option("--org <slug>").requiredOption("--host <host>", "database host").option("--port <port>", "database port", "5432").requiredOption("--database <name>", "database to connect to").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--schema <name>", "schema for preset grants", "public").option("--executor <mode>", "in_do | remote", "in_do").option("--provisioner-url <url>", "customer provisioner URL (remote executor)").option("--hmac-key <base64>", "shared HMAC key for the remote executor (or set SEEKRIT_PROVISIONER_HMAC_KEY); `seekrit provisioner keygen` mints one").option("--admin-url <url>", "in_do: admin postgres:// connection string (or set SEEKRIT_PG_ADMIN_URL); wrapped locally").option("--create-statement <sql>", "custom CREATE template (repeatable)", collect$
|
|
3501
|
+
target.command("add").description("register a Postgres cluster to lease credentials from").requiredOption("--name <name>", "display name, e.g. prod-db").option("--org <slug>").requiredOption("--host <host>", "database host").option("--port <port>", "database port", "5432").requiredOption("--database <name>", "database to connect to").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--schema <name>", "schema for preset grants", "public").option("--executor <mode>", "in_do | remote", "in_do").option("--provisioner-url <url>", "customer provisioner URL (remote executor)").option("--hmac-key <base64>", "shared HMAC key for the remote executor (or set SEEKRIT_PROVISIONER_HMAC_KEY); `seekrit provisioner keygen` mints one").option("--admin-url <url>", "in_do: admin postgres:// connection string (or set SEEKRIT_PG_ADMIN_URL); wrapped locally").option("--create-statement <sql>", "custom CREATE template (repeatable)", collect$3, []).option("--revoke-statement <sql>", "custom REVOKE template (repeatable)", collect$3, []).action(async (options) => {
|
|
3215
3502
|
const ctx = buildContext();
|
|
3216
3503
|
const org = await resolveOrg(ctx, options.org);
|
|
3217
3504
|
const executor = options.executor === "remote" ? "remote" : "in_do";
|
|
@@ -3325,11 +3612,209 @@ function registerPgCommands(program) {
|
|
|
3325
3612
|
});
|
|
3326
3613
|
}
|
|
3327
3614
|
/** Collect a repeatable option into an array. */
|
|
3328
|
-
function collect$
|
|
3615
|
+
function collect$3(value, acc) {
|
|
3329
3616
|
acc.push(value);
|
|
3330
3617
|
return acc;
|
|
3331
3618
|
}
|
|
3332
3619
|
//#endregion
|
|
3620
|
+
//#region src/recovery.ts
|
|
3621
|
+
/** Collect a repeatable option into a list. */
|
|
3622
|
+
function collect$2(value, acc = []) {
|
|
3623
|
+
acc.push(value);
|
|
3624
|
+
return acc;
|
|
3625
|
+
}
|
|
3626
|
+
/** Resolve a custodian reference: a `skt_…` token id, otherwise a member email. */
|
|
3627
|
+
function resolveCustodian(ctx, orgId, ref) {
|
|
3628
|
+
return ref.startsWith("skt_") ? kmsResolveRecipient(ctx, orgId, { token: ref }) : kmsResolveRecipient(ctx, orgId, { user: ref });
|
|
3629
|
+
}
|
|
3630
|
+
/**
|
|
3631
|
+
* The env DEK additionally wrapped to the org recovery key, when recovery is
|
|
3632
|
+
* enabled — so a newly created environment is recovery-protected from birth.
|
|
3633
|
+
* Returns undefined when recovery is off (the env is backfilled by `recovery
|
|
3634
|
+
* sync` later).
|
|
3635
|
+
*/
|
|
3636
|
+
async function recoveryWrapForNewEnv(ctx, orgId, dek) {
|
|
3637
|
+
let recoveryPublicKeyJwk;
|
|
3638
|
+
try {
|
|
3639
|
+
const { recovery } = await ctx.client.getRecovery(orgId);
|
|
3640
|
+
recoveryPublicKeyJwk = recovery.enabled ? recovery.recoveryPublicKeyJwk : null;
|
|
3641
|
+
} catch (e) {
|
|
3642
|
+
if (e instanceof SeekritApiError && (e.status === 403 || e.status === 404)) return void 0;
|
|
3643
|
+
throw e;
|
|
3644
|
+
}
|
|
3645
|
+
if (!recoveryPublicKeyJwk) return void 0;
|
|
3646
|
+
return wrapDek(dek, recoveryPublicKeyJwk);
|
|
3647
|
+
}
|
|
3648
|
+
/**
|
|
3649
|
+
* Wrap every environment the caller can decrypt but that lacks a recovery grant,
|
|
3650
|
+
* and upload the grants. Idempotent — safe to re-run and to run from several
|
|
3651
|
+
* admins to complete coverage.
|
|
3652
|
+
*/
|
|
3653
|
+
async function syncRecoveryGrants(ctx, orgId) {
|
|
3654
|
+
const { recovery } = await ctx.client.getRecovery(orgId);
|
|
3655
|
+
if (!recovery.enabled || !recovery.recoveryPublicKeyJwk) fail("recovery is not enabled");
|
|
3656
|
+
const recoveryPublicKeyJwk = recovery.recoveryPublicKeyJwk;
|
|
3657
|
+
const privateKey = await getPrivateKey(ctx);
|
|
3658
|
+
const grants = [];
|
|
3659
|
+
let skipped = 0;
|
|
3660
|
+
for (const environmentId of recovery.coverage.unprotectedEnvIds) {
|
|
3661
|
+
let wrappedDek;
|
|
3662
|
+
try {
|
|
3663
|
+
({wrappedDek} = await ctx.client.getMyEnvKey(orgId, environmentId));
|
|
3664
|
+
} catch (e) {
|
|
3665
|
+
if (e instanceof SeekritApiError && (e.status === 403 || e.status === 404)) {
|
|
3666
|
+
skipped++;
|
|
3667
|
+
continue;
|
|
3668
|
+
}
|
|
3669
|
+
throw e;
|
|
3670
|
+
}
|
|
3671
|
+
const dek = await unwrapDek(wrappedDek, privateKey);
|
|
3672
|
+
grants.push({
|
|
3673
|
+
environmentId,
|
|
3674
|
+
wrappedDek: await wrapDek(dek, recoveryPublicKeyJwk)
|
|
3675
|
+
});
|
|
3676
|
+
}
|
|
3677
|
+
if (grants.length > 0) await ctx.client.uploadRecoveryGrants(orgId, { grants });
|
|
3678
|
+
return {
|
|
3679
|
+
wrapped: grants.length,
|
|
3680
|
+
skipped
|
|
3681
|
+
};
|
|
3682
|
+
}
|
|
3683
|
+
/** Generate + split a fresh recovery key across the given custodians. */
|
|
3684
|
+
async function buildRecoveryConfig(ctx, orgId, thresholdRaw, custodianRefs) {
|
|
3685
|
+
const threshold = Number.parseInt(thresholdRaw, 10);
|
|
3686
|
+
if (!Number.isInteger(threshold) || threshold < 1) fail("--threshold must be a positive integer");
|
|
3687
|
+
if (custodianRefs.length === 0) fail("pass at least one --custodian <email|skt_id>");
|
|
3688
|
+
if (threshold > custodianRefs.length) fail("--threshold cannot exceed the number of custodians");
|
|
3689
|
+
const custodians = await Promise.all(custodianRefs.map((ref) => resolveCustodian(ctx, orgId, ref)));
|
|
3690
|
+
const recovery = await generateRecoveryKey();
|
|
3691
|
+
const shares = await splitRecoveryKey(recovery.privateKeyJwk, threshold, custodians);
|
|
3692
|
+
return {
|
|
3693
|
+
recoveryPublicKeyJwk: recovery.publicKeyJwk,
|
|
3694
|
+
threshold,
|
|
3695
|
+
shares: shares.map((s) => ({
|
|
3696
|
+
principalType: s.principalType,
|
|
3697
|
+
principalId: s.principalId,
|
|
3698
|
+
shareIndex: s.shareIndex,
|
|
3699
|
+
wrappedShare: s.wrappedShare
|
|
3700
|
+
}))
|
|
3701
|
+
};
|
|
3702
|
+
}
|
|
3703
|
+
function registerRecoveryCommands(program) {
|
|
3704
|
+
const recovery = program.command("recovery").description("customer-controlled M-of-N recovery (zero-knowledge)");
|
|
3705
|
+
recovery.command("status").description("show recovery configuration and environment coverage").option("--org <slug>").action(async (options) => {
|
|
3706
|
+
const ctx = buildContext();
|
|
3707
|
+
const org = await resolveOrg(ctx, options.org);
|
|
3708
|
+
const { recovery: status } = await ctx.client.getRecovery(org.id);
|
|
3709
|
+
if (!status.enabled) {
|
|
3710
|
+
console.log("recovery: disabled");
|
|
3711
|
+
return;
|
|
3712
|
+
}
|
|
3713
|
+
console.log(`recovery: enabled (${status.threshold}-of-${status.shareCount})`);
|
|
3714
|
+
console.log(`coverage: ${status.coverage.protected}/${status.coverage.total} environments protected`);
|
|
3715
|
+
console.log("custodians:");
|
|
3716
|
+
for (const cst of status.custodians) console.log(` - ${cst.label ?? cst.principalId} (${cst.principalType}, share #${cst.shareIndex})`);
|
|
3717
|
+
if (status.coverage.unprotectedEnvIds.length > 0) console.log(`${status.coverage.unprotectedEnvIds.length} environment(s) not yet protected — run \`seekrit recovery sync\``);
|
|
3718
|
+
});
|
|
3719
|
+
recovery.command("setup").description("enable recovery: split a fresh recovery key across custodians").requiredOption("--threshold <M>", "custodians required to recover (M in M-of-N)").option("--custodian <email|skt_id>", "a recovery custodian (repeatable)", collect$2, []).option("--org <slug>").action(async (options) => {
|
|
3720
|
+
const ctx = buildContext();
|
|
3721
|
+
const org = await resolveOrg(ctx, options.org);
|
|
3722
|
+
const config = await buildRecoveryConfig(ctx, org.id, options.threshold, options.custodian);
|
|
3723
|
+
await ctx.client.configureRecovery(org.id, {
|
|
3724
|
+
...config,
|
|
3725
|
+
grants: []
|
|
3726
|
+
});
|
|
3727
|
+
console.error(`recovery enabled: ${config.threshold}-of-${config.shares.length}`);
|
|
3728
|
+
const { wrapped, skipped } = await syncRecoveryGrants(ctx, org.id);
|
|
3729
|
+
console.error(`recovery-protected ${wrapped} environment(s) you can decrypt`);
|
|
3730
|
+
if (skipped > 0) console.error(`${skipped} environment(s) need another admin to run \`seekrit recovery sync\``);
|
|
3731
|
+
});
|
|
3732
|
+
recovery.command("sync").description("recovery-protect environments you can decrypt but that aren't yet covered").option("--org <slug>").action(async (options) => {
|
|
3733
|
+
const ctx = buildContext();
|
|
3734
|
+
const { wrapped, skipped } = await syncRecoveryGrants(ctx, (await resolveOrg(ctx, options.org)).id);
|
|
3735
|
+
console.error(`recovery-protected ${wrapped} environment(s); skipped ${skipped} you cannot decrypt`);
|
|
3736
|
+
});
|
|
3737
|
+
recovery.command("rotate").description("rotate the recovery key (new keypair, custodians, and env re-wraps)").requiredOption("--threshold <M>", "custodians required to recover (M in M-of-N)").option("--custodian <email|skt_id>", "a recovery custodian (repeatable)", collect$2, []).option("--org <slug>").action(async (options) => {
|
|
3738
|
+
const ctx = buildContext();
|
|
3739
|
+
const org = await resolveOrg(ctx, options.org);
|
|
3740
|
+
const config = await buildRecoveryConfig(ctx, org.id, options.threshold, options.custodian);
|
|
3741
|
+
await ctx.client.rotateRecovery(org.id, {
|
|
3742
|
+
...config,
|
|
3743
|
+
grants: []
|
|
3744
|
+
});
|
|
3745
|
+
console.error(`recovery rotated: ${config.threshold}-of-${config.shares.length}`);
|
|
3746
|
+
const { wrapped, skipped } = await syncRecoveryGrants(ctx, org.id);
|
|
3747
|
+
console.error(`re-wrapped ${wrapped} environment(s) you can decrypt to the new recovery key`);
|
|
3748
|
+
if (skipped > 0) console.error(`${skipped} environment(s) still need another admin to run \`seekrit recovery sync\``);
|
|
3749
|
+
});
|
|
3750
|
+
recovery.command("disable").description("disable recovery and remove all recovery grants").option("--org <slug>").action(async (options) => {
|
|
3751
|
+
const ctx = buildContext();
|
|
3752
|
+
const org = await resolveOrg(ctx, options.org);
|
|
3753
|
+
await ctx.client.disableRecovery(org.id);
|
|
3754
|
+
console.error("recovery disabled; recovery grants removed");
|
|
3755
|
+
});
|
|
3756
|
+
recovery.command("request").description("start a recovery ceremony (defaults to recovering access for yourself)").option("--target-user <email>", "recover access for another member").option("--target-token <id>", "recover access for a service token").option("--reason <text>", "note recorded in the audit trail").option("--org <slug>").action(async (options) => {
|
|
3757
|
+
const ctx = buildContext();
|
|
3758
|
+
const org = await resolveOrg(ctx, options.org);
|
|
3759
|
+
const target = options.targetUser || options.targetToken ? await kmsResolveRecipient(ctx, org.id, {
|
|
3760
|
+
user: options.targetUser,
|
|
3761
|
+
token: options.targetToken
|
|
3762
|
+
}) : await kmsCallerIdentity(ctx);
|
|
3763
|
+
const { request } = await ctx.client.createRecoveryRequest(org.id, {
|
|
3764
|
+
targetPublicKeyJwk: target.publicKeyJwk,
|
|
3765
|
+
targetType: target.principalType,
|
|
3766
|
+
targetId: target.principalId,
|
|
3767
|
+
reason: options.reason
|
|
3768
|
+
});
|
|
3769
|
+
console.error(`recovery request ${request.id} created (needs ${request.threshold} custodians)`);
|
|
3770
|
+
console.error(` custodians run: seekrit recovery approve ${request.id}`);
|
|
3771
|
+
console.error(` then the target: seekrit recovery complete ${request.id}`);
|
|
3772
|
+
});
|
|
3773
|
+
recovery.command("approve").description("as a custodian, contribute your share to a recovery request").argument("<requestId>").option("--org <slug>").action(async (requestId, options) => {
|
|
3774
|
+
const ctx = buildContext();
|
|
3775
|
+
const org = await resolveOrg(ctx, options.org);
|
|
3776
|
+
const { request } = await ctx.client.getRecoveryRequest(org.id, requestId);
|
|
3777
|
+
const myShare = await ctx.client.getMyRecoveryShare(org.id);
|
|
3778
|
+
const privateKey = await getPrivateKey(ctx);
|
|
3779
|
+
const contributedShare = await rewrapRecoveryShare(await unwrapRecoveryShare(myShare.wrappedShare, privateKey), request.targetPublicKeyJwk);
|
|
3780
|
+
const res = await ctx.client.contributeRecoveryShare(org.id, requestId, {
|
|
3781
|
+
shareIndex: myShare.shareIndex,
|
|
3782
|
+
contributedShare
|
|
3783
|
+
});
|
|
3784
|
+
console.error(`contributed share #${myShare.shareIndex}: ${res.contributed}/${res.threshold} collected${res.quorumReached ? " — quorum reached" : ""}`);
|
|
3785
|
+
});
|
|
3786
|
+
recovery.command("complete").description("as the recovery target, reconstruct the key and restore your access").argument("<requestId>").option("--org <slug>").action(async (requestId, options) => {
|
|
3787
|
+
const ctx = buildContext();
|
|
3788
|
+
const org = await resolveOrg(ctx, options.org);
|
|
3789
|
+
const { request, contributions, quorumReached } = await ctx.client.getRecoveryRequest(org.id, requestId);
|
|
3790
|
+
if (!quorumReached) fail(`only ${contributions.length}/${request.threshold} custodians have contributed`);
|
|
3791
|
+
const me = await kmsCallerIdentity(ctx);
|
|
3792
|
+
const targetPrivateKey = await getPrivateKey(ctx);
|
|
3793
|
+
const recoveryPrivateKey = await combineRecoveryShares(await Promise.all(contributions.map((cont) => unwrapRecoveryShare(cont.contributedShare, targetPrivateKey))));
|
|
3794
|
+
const { grants: recoveryEnvKeys } = await ctx.client.getRecoveryEnvKeys(org.id);
|
|
3795
|
+
const restored = [];
|
|
3796
|
+
for (const g of recoveryEnvKeys) {
|
|
3797
|
+
const dek = await unwrapDek(g.wrappedDek, recoveryPrivateKey);
|
|
3798
|
+
restored.push({
|
|
3799
|
+
environmentId: g.environmentId,
|
|
3800
|
+
wrappedDek: await wrapDek(dek, me.publicKeyJwk)
|
|
3801
|
+
});
|
|
3802
|
+
}
|
|
3803
|
+
await ctx.client.completeRecoveryRequest(org.id, requestId, {
|
|
3804
|
+
principalType: me.principalType,
|
|
3805
|
+
principalId: me.principalId,
|
|
3806
|
+
grants: restored
|
|
3807
|
+
});
|
|
3808
|
+
console.error(`recovery complete: restored access to ${restored.length} environment(s)`);
|
|
3809
|
+
});
|
|
3810
|
+
recovery.command("cancel").description("cancel an open recovery request").argument("<requestId>").option("--org <slug>").action(async (requestId, options) => {
|
|
3811
|
+
const ctx = buildContext();
|
|
3812
|
+
const org = await resolveOrg(ctx, options.org);
|
|
3813
|
+
await ctx.client.cancelRecoveryRequest(org.id, requestId);
|
|
3814
|
+
console.error(`recovery request ${requestId} canceled`);
|
|
3815
|
+
});
|
|
3816
|
+
}
|
|
3817
|
+
//#endregion
|
|
3333
3818
|
//#region src/redis.ts
|
|
3334
3819
|
/**
|
|
3335
3820
|
* `seekrit redis` — temporary Redis (6+) credentials (Vault-style dynamic
|
|
@@ -3792,11 +4277,14 @@ env.command("create").description("create an application environment (generates
|
|
|
3792
4277
|
if (!appRow) fail(`no app "${options.app}" in ${orgRef.slug}`);
|
|
3793
4278
|
const { user } = await ctx.client.me();
|
|
3794
4279
|
if (!user.publicKeyJwk) fail("run `seekrit keys setup` first");
|
|
3795
|
-
const
|
|
4280
|
+
const dek = generateDek();
|
|
4281
|
+
const wrappedDek = await wrapDek(dek, user.publicKeyJwk);
|
|
4282
|
+
const recoveryWrappedDek = await recoveryWrapForNewEnv(ctx, orgRef.id, dek);
|
|
3796
4283
|
const created = await ctx.client.createEnv(orgRef.id, appRow.id, {
|
|
3797
4284
|
name: options.name,
|
|
3798
4285
|
slug: options.slug,
|
|
3799
|
-
wrappedDek
|
|
4286
|
+
wrappedDek,
|
|
4287
|
+
recoveryWrappedDek
|
|
3800
4288
|
});
|
|
3801
4289
|
console.error(`created environment ${created.environment.slug} (${created.environment.id})`);
|
|
3802
4290
|
});
|
|
@@ -3848,11 +4336,14 @@ group.command("env").description("manage a group’s environments (per-slug valu
|
|
|
3848
4336
|
});
|
|
3849
4337
|
const { user } = await ctx.client.me();
|
|
3850
4338
|
if (!user.publicKeyJwk) fail("run `seekrit keys setup` first");
|
|
3851
|
-
const
|
|
4339
|
+
const dek = generateDek();
|
|
4340
|
+
const wrappedDek = await wrapDek(dek, user.publicKeyJwk);
|
|
4341
|
+
const recoveryWrappedDek = await recoveryWrapForNewEnv(ctx, groupRef.orgId, dek);
|
|
3852
4342
|
const created = await ctx.client.createGroupEnv(groupRef.orgId, groupRef.id, {
|
|
3853
4343
|
name: options.name,
|
|
3854
4344
|
slug: options.slug,
|
|
3855
|
-
wrappedDek
|
|
4345
|
+
wrappedDek,
|
|
4346
|
+
recoveryWrappedDek
|
|
3856
4347
|
});
|
|
3857
4348
|
console.error(`created ${groupRef.slug}@${created.environment.slug} (${created.environment.id})`);
|
|
3858
4349
|
});
|
|
@@ -4096,6 +4587,7 @@ registerAwsCommands(program);
|
|
|
4096
4587
|
registerGcpCommands(program);
|
|
4097
4588
|
registerMongoCommands(program);
|
|
4098
4589
|
registerKmsCommands(program);
|
|
4590
|
+
registerRecoveryCommands(program);
|
|
4099
4591
|
program.command("mcp").description("run an MCP server over stdio so AI agents can drive seekrit").action(async () => {
|
|
4100
4592
|
const { runMcpServer } = await import("./mcp-CVhEQDfd.js");
|
|
4101
4593
|
await runMcpServer();
|