@seekrit/cli 0.17.2 → 0.19.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 +538 -20
- package/dist/{mcp-CVhEQDfd.js → mcp-DR_Ghb4w.js} +2 -2
- package/package.json +6 -3
package/dist/index.js
CHANGED
|
@@ -96,26 +96,40 @@ const ENTITLEMENT_KEYS = Object.keys({
|
|
|
96
96
|
metric: "monthly_resolves"
|
|
97
97
|
}
|
|
98
98
|
});
|
|
99
|
-
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region ../../packages/core/src/plans.ts
|
|
101
|
+
const PLAN_FAMILIES = {
|
|
100
102
|
free: {
|
|
101
103
|
id: "free",
|
|
102
104
|
name: "Free",
|
|
103
105
|
description: "Get started with the essentials.",
|
|
104
|
-
current: 1
|
|
106
|
+
current: 1,
|
|
107
|
+
hidden: false
|
|
108
|
+
},
|
|
109
|
+
team: {
|
|
110
|
+
id: "team",
|
|
111
|
+
name: "Team",
|
|
112
|
+
description: "For small teams collaborating on secrets.",
|
|
113
|
+
current: 1,
|
|
114
|
+
hidden: false
|
|
105
115
|
},
|
|
106
116
|
pro: {
|
|
107
117
|
id: "pro",
|
|
108
118
|
name: "Pro",
|
|
109
119
|
description: "For teams running secrets in production.",
|
|
110
|
-
current: 1
|
|
120
|
+
current: 1,
|
|
121
|
+
hidden: true
|
|
111
122
|
},
|
|
112
123
|
enterprise: {
|
|
113
124
|
id: "enterprise",
|
|
114
125
|
name: "Enterprise",
|
|
115
126
|
description: "Unlimited scale with advanced governance.",
|
|
116
|
-
current: 1
|
|
127
|
+
current: 1,
|
|
128
|
+
hidden: false
|
|
117
129
|
}
|
|
118
|
-
}
|
|
130
|
+
};
|
|
131
|
+
const PLAN_FAMILY_IDS = Object.keys(PLAN_FAMILIES);
|
|
132
|
+
PLAN_FAMILY_IDS.filter((family) => !PLAN_FAMILIES[family].hidden);
|
|
119
133
|
//#endregion
|
|
120
134
|
//#region ../../packages/core/src/billing.ts
|
|
121
135
|
/**
|
|
@@ -665,6 +679,7 @@ z.object({
|
|
|
665
679
|
z.object({ name: nameSchema });
|
|
666
680
|
z.object({ name: nameSchema });
|
|
667
681
|
z.object({ name: nameSchema });
|
|
682
|
+
z.object({ required: z.boolean() });
|
|
668
683
|
z.object({
|
|
669
684
|
email: emailSchema,
|
|
670
685
|
role: inviteRoleSchema.default("member")
|
|
@@ -682,7 +697,13 @@ z.object({
|
|
|
682
697
|
name: nameSchema,
|
|
683
698
|
slug: slugSchema,
|
|
684
699
|
/** Environment DEK wrapped to the creator's public key — created client-side. */
|
|
685
|
-
wrappedDek: z.string().min(1)
|
|
700
|
+
wrappedDek: z.string().min(1),
|
|
701
|
+
/**
|
|
702
|
+
* When the org has recovery enabled, the same DEK additionally wrapped to the
|
|
703
|
+
* org recovery public key, so the environment is recovery-protected from
|
|
704
|
+
* creation. Omitted when recovery is off (backfilled later by `recovery sync`).
|
|
705
|
+
*/
|
|
706
|
+
recoveryWrappedDek: z.string().min(1).nullish()
|
|
686
707
|
});
|
|
687
708
|
z.object({
|
|
688
709
|
/** Opaque versioned ciphertext blob from @seekrit/crypto. */
|
|
@@ -758,6 +779,55 @@ z.object({
|
|
|
758
779
|
publicKeyJwk: z.string().min(1).nullish(),
|
|
759
780
|
grants: z.array(kmsGrantInputSchema).min(1)
|
|
760
781
|
});
|
|
782
|
+
/**
|
|
783
|
+
* One custodian's wrapped Shamir share of the org recovery private key. The
|
|
784
|
+
* client generates the recovery keypair, splits the private half M-of-N, and
|
|
785
|
+
* wraps each share to a custodian's public key — the server stores only the
|
|
786
|
+
* opaque `wrappedShare` and can reconstruct nothing.
|
|
787
|
+
*/
|
|
788
|
+
const recoveryShareInputSchema = z.object({
|
|
789
|
+
principalType: principalTypeSchema,
|
|
790
|
+
principalId: z.string().min(1),
|
|
791
|
+
/** Shamir x-coordinate carried by the share (1..255). */
|
|
792
|
+
shareIndex: z.number().int().min(1).max(255),
|
|
793
|
+
/** The recovery-key share wrapped to the custodian's public key (`wd1.`). */
|
|
794
|
+
wrappedShare: z.string().min(1)
|
|
795
|
+
});
|
|
796
|
+
/** An environment DEK additionally wrapped to the org recovery public key. */
|
|
797
|
+
const recoveryEnvGrantSchema = z.object({
|
|
798
|
+
environmentId: z.string().min(1),
|
|
799
|
+
/** The environment's DEK wrapped to the recovery public key (`wd1.`). */
|
|
800
|
+
wrappedDek: z.string().min(1)
|
|
801
|
+
});
|
|
802
|
+
z.object({
|
|
803
|
+
recoveryPublicKeyJwk: z.string().min(1),
|
|
804
|
+
threshold: z.number().int().min(1).max(255),
|
|
805
|
+
shares: z.array(recoveryShareInputSchema).min(1).max(255),
|
|
806
|
+
grants: z.array(recoveryEnvGrantSchema).default([])
|
|
807
|
+
}).refine((v) => v.threshold <= v.shares.length, {
|
|
808
|
+
message: "threshold cannot exceed the number of custodians",
|
|
809
|
+
path: ["threshold"]
|
|
810
|
+
}).refine((v) => new Set(v.shares.map((s) => `${s.principalType}:${s.principalId}`)).size === v.shares.length, {
|
|
811
|
+
message: "custodians must be distinct",
|
|
812
|
+
path: ["shares"]
|
|
813
|
+
});
|
|
814
|
+
z.object({ grants: z.array(recoveryEnvGrantSchema).min(1).max(500) });
|
|
815
|
+
z.object({
|
|
816
|
+
targetPublicKeyJwk: z.string().min(1),
|
|
817
|
+
targetType: principalTypeSchema.nullish(),
|
|
818
|
+
targetId: z.string().min(1).nullish(),
|
|
819
|
+
reason: z.string().max(500).nullish()
|
|
820
|
+
});
|
|
821
|
+
z.object({
|
|
822
|
+
shareIndex: z.number().int().min(1).max(255),
|
|
823
|
+
/** The custodian's share re-wrapped to the target public key (`wd1.`). */
|
|
824
|
+
contributedShare: z.string().min(1)
|
|
825
|
+
});
|
|
826
|
+
z.object({
|
|
827
|
+
principalType: principalTypeSchema,
|
|
828
|
+
principalId: z.string().min(1),
|
|
829
|
+
grants: z.array(recoveryEnvGrantSchema).min(1).max(500)
|
|
830
|
+
});
|
|
761
831
|
z.object({ prefs: z.partialRecord(z.enum(NOTIFICATION_TYPES), z.boolean()) });
|
|
762
832
|
z.object({
|
|
763
833
|
endpoint: z.url().max(2048),
|
|
@@ -1343,6 +1413,191 @@ async function decryptPrivateKey(passphrase, blob) {
|
|
|
1343
1413
|
}
|
|
1344
1414
|
}
|
|
1345
1415
|
//#endregion
|
|
1416
|
+
//#region ../../packages/crypto/src/shamir.ts
|
|
1417
|
+
/**
|
|
1418
|
+
* Shamir's Secret Sharing over GF(2^8) — the same field AES uses, with the
|
|
1419
|
+
* reduction polynomial x^8 + x^4 + x^3 + x + 1 (0x11b). Splits a byte string
|
|
1420
|
+
* into `shares` shares such that any `threshold` of them reconstruct the secret
|
|
1421
|
+
* exactly and any fewer reveal nothing about it.
|
|
1422
|
+
*
|
|
1423
|
+
* This is the one hand-rolled primitive behind org recovery (P0-1): the org
|
|
1424
|
+
* recovery private key is split into shares, each wrapped to a designated
|
|
1425
|
+
* custodian's public key, so a quorum — never seekrit, never any single
|
|
1426
|
+
* custodian below the threshold — can reconstruct it. See recovery.ts for the
|
|
1427
|
+
* composition with key wrapping.
|
|
1428
|
+
*
|
|
1429
|
+
* Each secret byte gets its own degree-(threshold-1) polynomial whose constant
|
|
1430
|
+
* term is that byte; a share is that polynomial family evaluated at one distinct
|
|
1431
|
+
* nonzero x-coordinate. Reconstruction is Lagrange interpolation back to x = 0.
|
|
1432
|
+
*
|
|
1433
|
+
* Share wire format: a Uint8Array whose first byte is the share's distinct
|
|
1434
|
+
* nonzero x-coordinate and whose remaining bytes are the evaluations p_j(x) for
|
|
1435
|
+
* each secret byte j. Self-describing, so combineSecret() needs no external
|
|
1436
|
+
* index — the x-coordinate survives being wrapped/unwrapped/re-wrapped intact.
|
|
1437
|
+
*/
|
|
1438
|
+
/** Russian-peasant multiply in GF(2^8) (mod 0x11b) — used only to seed tables. */
|
|
1439
|
+
function peasantMul(a, b) {
|
|
1440
|
+
let product = 0;
|
|
1441
|
+
let x = a;
|
|
1442
|
+
let y = b;
|
|
1443
|
+
for (let i = 0; i < 8; i++) {
|
|
1444
|
+
if (y & 1) product ^= x;
|
|
1445
|
+
const carry = x & 128;
|
|
1446
|
+
x = x << 1 & 255;
|
|
1447
|
+
if (carry) x ^= 27;
|
|
1448
|
+
y >>= 1;
|
|
1449
|
+
}
|
|
1450
|
+
return product;
|
|
1451
|
+
}
|
|
1452
|
+
const EXP = /* @__PURE__ */ new Uint8Array(512);
|
|
1453
|
+
const LOG = /* @__PURE__ */ new Uint8Array(256);
|
|
1454
|
+
{
|
|
1455
|
+
let a = 1;
|
|
1456
|
+
for (let i = 0; i < 255; i++) {
|
|
1457
|
+
EXP[i] = a;
|
|
1458
|
+
LOG[a] = i;
|
|
1459
|
+
a = peasantMul(a, 3);
|
|
1460
|
+
}
|
|
1461
|
+
for (let i = 255; i < 512; i++) EXP[i] = EXP[i - 255];
|
|
1462
|
+
}
|
|
1463
|
+
/** Multiply in GF(2^8) via the log/exp tables. */
|
|
1464
|
+
function mul(a, b) {
|
|
1465
|
+
if (a === 0 || b === 0) return 0;
|
|
1466
|
+
return EXP[LOG[a] + LOG[b]];
|
|
1467
|
+
}
|
|
1468
|
+
/** Divide in GF(2^8) (a / b). Caller guarantees b !== 0. */
|
|
1469
|
+
function div(a, b) {
|
|
1470
|
+
if (a === 0) return 0;
|
|
1471
|
+
return EXP[LOG[a] + 255 - LOG[b]];
|
|
1472
|
+
}
|
|
1473
|
+
/** Evaluate a polynomial (coeffs low-degree-first) at x, via Horner's rule. */
|
|
1474
|
+
function evalPoly(coeffs, x) {
|
|
1475
|
+
let result = 0;
|
|
1476
|
+
for (let i = coeffs.length - 1; i >= 0; i--) result = mul(result, x) ^ coeffs[i];
|
|
1477
|
+
return result;
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1480
|
+
* Split `secret` into `shares` shares, any `threshold` of which reconstruct it.
|
|
1481
|
+
* x-coordinates are 1..shares, so shares must be in 1..255 and threshold in
|
|
1482
|
+
* 1..shares. threshold = shares means every share is needed; threshold = 1 is
|
|
1483
|
+
* the degenerate case where each share equals the secret (the "single recovery
|
|
1484
|
+
* admin" configuration).
|
|
1485
|
+
*/
|
|
1486
|
+
function splitSecret(secret, threshold, shares) {
|
|
1487
|
+
if (secret.length < 1) throw new SeekritCryptoError("MALFORMED_BLOB", "cannot split an empty secret");
|
|
1488
|
+
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`);
|
|
1489
|
+
const out = [];
|
|
1490
|
+
for (let s = 0; s < shares; s++) {
|
|
1491
|
+
const share = new Uint8Array(1 + secret.length);
|
|
1492
|
+
share[0] = s + 1;
|
|
1493
|
+
out.push(share);
|
|
1494
|
+
}
|
|
1495
|
+
const randomCoeffs = threshold > 1 ? crypto.getRandomValues(new Uint8Array(secret.length * (threshold - 1))) : /* @__PURE__ */ new Uint8Array(0);
|
|
1496
|
+
const coeffs = new Uint8Array(threshold);
|
|
1497
|
+
for (let j = 0; j < secret.length; j++) {
|
|
1498
|
+
coeffs[0] = secret[j];
|
|
1499
|
+
for (let k = 1; k < threshold; k++) coeffs[k] = randomCoeffs[j * (threshold - 1) + (k - 1)];
|
|
1500
|
+
for (const share of out) share[1 + j] = evalPoly(coeffs, share[0]);
|
|
1501
|
+
}
|
|
1502
|
+
return out;
|
|
1503
|
+
}
|
|
1504
|
+
/**
|
|
1505
|
+
* Reconstruct a secret from shares in the {@link splitSecret} wire format.
|
|
1506
|
+
* Supplying at least `threshold` valid shares returns the original secret;
|
|
1507
|
+
* fewer returns a plausible-but-wrong value (that is the security property —
|
|
1508
|
+
* downstream key use is what actually verifies the result). Shares must be the
|
|
1509
|
+
* same length and carry distinct nonzero x-coordinates.
|
|
1510
|
+
*/
|
|
1511
|
+
function combineSecret(shares) {
|
|
1512
|
+
if (shares.length < 1) throw new SeekritCryptoError("MALFORMED_BLOB", "need at least one share to combine");
|
|
1513
|
+
const width = shares[0].length;
|
|
1514
|
+
if (width < 2) throw new SeekritCryptoError("MALFORMED_BLOB", "share is too short to carry a secret");
|
|
1515
|
+
const xs = new Uint8Array(shares.length);
|
|
1516
|
+
for (let i = 0; i < shares.length; i++) {
|
|
1517
|
+
const share = shares[i];
|
|
1518
|
+
if (share.length !== width) throw new SeekritCryptoError("MALFORMED_BLOB", "shares have mismatched lengths");
|
|
1519
|
+
const x = share[0];
|
|
1520
|
+
if (x === 0) throw new SeekritCryptoError("MALFORMED_BLOB", "share has an invalid zero x-coordinate");
|
|
1521
|
+
for (let p = 0; p < i; p++) if (xs[p] === x) throw new SeekritCryptoError("MALFORMED_BLOB", "shares have duplicate x-coordinates");
|
|
1522
|
+
xs[i] = x;
|
|
1523
|
+
}
|
|
1524
|
+
const basis = new Uint8Array(shares.length);
|
|
1525
|
+
for (let i = 0; i < shares.length; i++) {
|
|
1526
|
+
const xi = xs[i];
|
|
1527
|
+
let num = 1;
|
|
1528
|
+
let den = 1;
|
|
1529
|
+
for (let m = 0; m < shares.length; m++) {
|
|
1530
|
+
if (m === i) continue;
|
|
1531
|
+
const xm = xs[m];
|
|
1532
|
+
num = mul(num, xm);
|
|
1533
|
+
den = mul(den, xi ^ xm);
|
|
1534
|
+
}
|
|
1535
|
+
basis[i] = div(num, den);
|
|
1536
|
+
}
|
|
1537
|
+
const secret = new Uint8Array(width - 1);
|
|
1538
|
+
for (let j = 0; j < secret.length; j++) {
|
|
1539
|
+
let acc = 0;
|
|
1540
|
+
for (let i = 0; i < shares.length; i++) acc ^= mul(shares[i][1 + j], basis[i]);
|
|
1541
|
+
secret[j] = acc;
|
|
1542
|
+
}
|
|
1543
|
+
return secret;
|
|
1544
|
+
}
|
|
1545
|
+
//#endregion
|
|
1546
|
+
//#region ../../packages/crypto/src/recovery.ts
|
|
1547
|
+
/** Generate a fresh org recovery keypair (P-256, same as any principal). */
|
|
1548
|
+
function generateRecoveryKey() {
|
|
1549
|
+
return generateKeyPair();
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* Split a recovery private key into one wrapped share per custodian, such that
|
|
1553
|
+
* any `threshold` custodians can reconstruct it. The share count is the number
|
|
1554
|
+
* of custodians.
|
|
1555
|
+
*/
|
|
1556
|
+
async function splitRecoveryKey(privateKeyJwk, threshold, custodians) {
|
|
1557
|
+
if (custodians.length < 1) throw new SeekritCryptoError("MALFORMED_BLOB", "recovery needs at least one custodian");
|
|
1558
|
+
const shares = splitSecret(utf8Encode(privateKeyJwk), threshold, custodians.length);
|
|
1559
|
+
return Promise.all(custodians.map(async (custodian, i) => {
|
|
1560
|
+
const share = shares[i];
|
|
1561
|
+
return {
|
|
1562
|
+
principalType: custodian.principalType,
|
|
1563
|
+
principalId: custodian.principalId,
|
|
1564
|
+
shareIndex: share[0],
|
|
1565
|
+
wrappedShare: await wrapDek(share, custodian.publicKeyJwk)
|
|
1566
|
+
};
|
|
1567
|
+
}));
|
|
1568
|
+
}
|
|
1569
|
+
/**
|
|
1570
|
+
* Unwrap a single recovery share with the holder's private key, returning the
|
|
1571
|
+
* raw share bytes. Used by a custodian to unlock their own share, and by a
|
|
1572
|
+
* recovery target to unlock a share re-wrapped to them — both are plain
|
|
1573
|
+
* {@link unwrapDek}, since a share is just wrapped bytes.
|
|
1574
|
+
*/
|
|
1575
|
+
function unwrapRecoveryShare(wrappedShare, privateKey) {
|
|
1576
|
+
return unwrapDek(wrappedShare, privateKey);
|
|
1577
|
+
}
|
|
1578
|
+
/**
|
|
1579
|
+
* Re-wrap an already-unwrapped share to a recovery target's public key. This is
|
|
1580
|
+
* the custodian's contribution during a recovery ceremony: the server collects
|
|
1581
|
+
* a quorum of shares all wrapped to the target, none of which it can read.
|
|
1582
|
+
*/
|
|
1583
|
+
function rewrapRecoveryShare(shareBytes, targetPublicKeyJwk) {
|
|
1584
|
+
return wrapDek(shareBytes, targetPublicKeyJwk);
|
|
1585
|
+
}
|
|
1586
|
+
/**
|
|
1587
|
+
* Combine a quorum of unwrapped shares back into the recovery private key,
|
|
1588
|
+
* imported and ready to unwrap DEK recovery grants. Fewer than the threshold, or
|
|
1589
|
+
* shares from different recovery keys, yield garbage that fails to import or
|
|
1590
|
+
* decrypt — surfaced as DECRYPT_FAILED, never a silent wrong key.
|
|
1591
|
+
*/
|
|
1592
|
+
async function combineRecoveryShares(shareBytes) {
|
|
1593
|
+
const privateKeyJwk = utf8Decode(combineSecret(shareBytes));
|
|
1594
|
+
try {
|
|
1595
|
+
return await importPrivateKey(privateKeyJwk);
|
|
1596
|
+
} catch {
|
|
1597
|
+
throw new SeekritCryptoError("DECRYPT_FAILED", "recovery reconstruction failed: wrong, insufficient, or mismatched shares");
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
//#endregion
|
|
1346
1601
|
//#region ../../packages/crypto/src/redis.ts
|
|
1347
1602
|
/**
|
|
1348
1603
|
* Client-side construction of a Redis (6+) ACL password verifier, for minting
|
|
@@ -1721,7 +1976,7 @@ function isServiceToken(value) {
|
|
|
1721
1976
|
}
|
|
1722
1977
|
//#endregion
|
|
1723
1978
|
//#region package.json
|
|
1724
|
-
var version = "0.
|
|
1979
|
+
var version = "0.19.0";
|
|
1725
1980
|
//#endregion
|
|
1726
1981
|
//#region ../../packages/api-client/src/index.ts
|
|
1727
1982
|
var SeekritApiError = class extends Error {
|
|
@@ -1798,6 +2053,17 @@ var SeekritClient = class {
|
|
|
1798
2053
|
listMembers(orgId) {
|
|
1799
2054
|
return this.request("GET", `/v1/orgs/${orgId}/members`);
|
|
1800
2055
|
}
|
|
2056
|
+
/**
|
|
2057
|
+
* The org-wide "require a second factor for all members" policy. `configured`
|
|
2058
|
+
* is false when the identity provider isn't wired up (local dev), in which
|
|
2059
|
+
* case the toggle is inert.
|
|
2060
|
+
*/
|
|
2061
|
+
getMfaPolicy(orgId) {
|
|
2062
|
+
return this.request("GET", `/v1/orgs/${orgId}/mfa-policy`);
|
|
2063
|
+
}
|
|
2064
|
+
setMfaPolicy(orgId, input) {
|
|
2065
|
+
return this.request("PATCH", `/v1/orgs/${orgId}/mfa-policy`, input);
|
|
2066
|
+
}
|
|
1801
2067
|
listInvites(orgId) {
|
|
1802
2068
|
return this.request("GET", `/v1/orgs/${orgId}/invites`);
|
|
1803
2069
|
}
|
|
@@ -1896,6 +2162,53 @@ var SeekritClient = class {
|
|
|
1896
2162
|
revokeEnvKey(orgId, envId, grantId) {
|
|
1897
2163
|
return this.request("DELETE", `/v1/orgs/${orgId}/envs/${envId}/keys/${grantId}`);
|
|
1898
2164
|
}
|
|
2165
|
+
/** Org recovery status: threshold, custodians, and environment coverage. */
|
|
2166
|
+
getRecovery(orgId) {
|
|
2167
|
+
return this.request("GET", `/v1/orgs/${orgId}/recovery`);
|
|
2168
|
+
}
|
|
2169
|
+
/** Enable recovery with the recovery public key, custodian shares, and grants. */
|
|
2170
|
+
configureRecovery(orgId, input) {
|
|
2171
|
+
return this.request("POST", `/v1/orgs/${orgId}/recovery`, input);
|
|
2172
|
+
}
|
|
2173
|
+
/** Rotate the recovery key: new keypair, custodian set, and env re-wraps. */
|
|
2174
|
+
rotateRecovery(orgId, input) {
|
|
2175
|
+
return this.request("POST", `/v1/orgs/${orgId}/recovery/rotate`, input);
|
|
2176
|
+
}
|
|
2177
|
+
disableRecovery(orgId) {
|
|
2178
|
+
return this.request("DELETE", `/v1/orgs/${orgId}/recovery`);
|
|
2179
|
+
}
|
|
2180
|
+
/** The calling principal's own wrapped recovery share (custodian only). */
|
|
2181
|
+
getMyRecoveryShare(orgId) {
|
|
2182
|
+
return this.request("GET", `/v1/orgs/${orgId}/recovery/share`);
|
|
2183
|
+
}
|
|
2184
|
+
/** Every environment DEK wrapped to the org recovery key (admin only). */
|
|
2185
|
+
getRecoveryEnvKeys(orgId) {
|
|
2186
|
+
return this.request("GET", `/v1/orgs/${orgId}/recovery/env-keys`);
|
|
2187
|
+
}
|
|
2188
|
+
/** Backfill recovery grants for environments the caller can decrypt. */
|
|
2189
|
+
uploadRecoveryGrants(orgId, input) {
|
|
2190
|
+
return this.request("POST", `/v1/orgs/${orgId}/recovery/grants`, input);
|
|
2191
|
+
}
|
|
2192
|
+
listRecoveryRequests(orgId) {
|
|
2193
|
+
return this.request("GET", `/v1/orgs/${orgId}/recovery/requests`);
|
|
2194
|
+
}
|
|
2195
|
+
createRecoveryRequest(orgId, input) {
|
|
2196
|
+
return this.request("POST", `/v1/orgs/${orgId}/recovery/requests`, input);
|
|
2197
|
+
}
|
|
2198
|
+
getRecoveryRequest(orgId, requestId) {
|
|
2199
|
+
return this.request("GET", `/v1/orgs/${orgId}/recovery/requests/${requestId}`);
|
|
2200
|
+
}
|
|
2201
|
+
/** A custodian contributes their share, re-wrapped to the request target. */
|
|
2202
|
+
contributeRecoveryShare(orgId, requestId, input) {
|
|
2203
|
+
return this.request("POST", `/v1/orgs/${orgId}/recovery/requests/${requestId}/shares`, input);
|
|
2204
|
+
}
|
|
2205
|
+
/** The target finalizes recovery, re-granting itself the recovered DEKs. */
|
|
2206
|
+
completeRecoveryRequest(orgId, requestId, input) {
|
|
2207
|
+
return this.request("POST", `/v1/orgs/${orgId}/recovery/requests/${requestId}/complete`, input);
|
|
2208
|
+
}
|
|
2209
|
+
cancelRecoveryRequest(orgId, requestId) {
|
|
2210
|
+
return this.request("POST", `/v1/orgs/${orgId}/recovery/requests/${requestId}/cancel`);
|
|
2211
|
+
}
|
|
1899
2212
|
listTokens(orgId) {
|
|
1900
2213
|
return this.request("GET", `/v1/orgs/${orgId}/tokens`);
|
|
1901
2214
|
}
|
|
@@ -2586,7 +2899,7 @@ function registerGcpCommands(program) {
|
|
|
2586
2899
|
//#endregion
|
|
2587
2900
|
//#region src/kms.ts
|
|
2588
2901
|
/** Collect a repeatable option into a list. */
|
|
2589
|
-
function collect$
|
|
2902
|
+
function collect$6(value, acc = []) {
|
|
2590
2903
|
acc.push(value);
|
|
2591
2904
|
return acc;
|
|
2592
2905
|
}
|
|
@@ -2654,7 +2967,7 @@ async function kmsRecoverMaterial(ctx, orgId, keyId, version) {
|
|
|
2654
2967
|
}
|
|
2655
2968
|
function registerKmsCommands(program) {
|
|
2656
2969
|
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$
|
|
2970
|
+
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
2971
|
if (options.purpose !== "encrypt" && options.purpose !== "sign") fail("--purpose must be encrypt or sign");
|
|
2659
2972
|
if (options.app && options.group) fail("pass at most one of --app or --group");
|
|
2660
2973
|
const ctx = buildContext();
|
|
@@ -2888,7 +3201,7 @@ function parseTtlSeconds$4(input) {
|
|
|
2888
3201
|
}[m[2] || "s"] ?? 1);
|
|
2889
3202
|
}
|
|
2890
3203
|
/** Collect a repeatable option into an array. */
|
|
2891
|
-
function collect$
|
|
3204
|
+
function collect$5(value, previous) {
|
|
2892
3205
|
return [...previous, value];
|
|
2893
3206
|
}
|
|
2894
3207
|
/** Parse `readWrite@app` → { role, db } for a custom target. */
|
|
@@ -2913,7 +3226,7 @@ function resolveAdminUri(uri) {
|
|
|
2913
3226
|
function registerMongoCommands(program) {
|
|
2914
3227
|
const mongo = program.command("mongodb").description("temporary MongoDB credentials (createUser, zero-knowledge delivery)");
|
|
2915
3228
|
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$
|
|
3229
|
+
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
3230
|
const ctx = buildContext();
|
|
2918
3231
|
const org = await resolveOrg(ctx, options.org);
|
|
2919
3232
|
const adminUri = resolveAdminUri(options.uri);
|
|
@@ -3073,7 +3386,7 @@ function generateUserName$1(prefix = "tmp") {
|
|
|
3073
3386
|
function registerMysqlCommands(program) {
|
|
3074
3387
|
const mysql = program.command("mysql").description("temporary MySQL/MariaDB credentials (short-lived, zero-knowledge)");
|
|
3075
3388
|
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$
|
|
3389
|
+
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
3390
|
const ctx = buildContext();
|
|
3078
3391
|
const org = await resolveOrg(ctx, options.org);
|
|
3079
3392
|
const executor = options.executor === "remote" ? "remote" : "in_do";
|
|
@@ -3174,7 +3487,7 @@ function registerMysqlCommands(program) {
|
|
|
3174
3487
|
});
|
|
3175
3488
|
}
|
|
3176
3489
|
/** Collect a repeatable option into an array. */
|
|
3177
|
-
function collect$
|
|
3490
|
+
function collect$4(value, acc) {
|
|
3178
3491
|
acc.push(value);
|
|
3179
3492
|
return acc;
|
|
3180
3493
|
}
|
|
@@ -3211,7 +3524,7 @@ function generateRoleName(prefix = "tmp") {
|
|
|
3211
3524
|
function registerPgCommands(program) {
|
|
3212
3525
|
const pg = program.command("pg").description("temporary Postgres credentials (short-lived, zero-knowledge)");
|
|
3213
3526
|
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$
|
|
3527
|
+
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
3528
|
const ctx = buildContext();
|
|
3216
3529
|
const org = await resolveOrg(ctx, options.org);
|
|
3217
3530
|
const executor = options.executor === "remote" ? "remote" : "in_do";
|
|
@@ -3325,11 +3638,209 @@ function registerPgCommands(program) {
|
|
|
3325
3638
|
});
|
|
3326
3639
|
}
|
|
3327
3640
|
/** Collect a repeatable option into an array. */
|
|
3328
|
-
function collect$
|
|
3641
|
+
function collect$3(value, acc) {
|
|
3329
3642
|
acc.push(value);
|
|
3330
3643
|
return acc;
|
|
3331
3644
|
}
|
|
3332
3645
|
//#endregion
|
|
3646
|
+
//#region src/recovery.ts
|
|
3647
|
+
/** Collect a repeatable option into a list. */
|
|
3648
|
+
function collect$2(value, acc = []) {
|
|
3649
|
+
acc.push(value);
|
|
3650
|
+
return acc;
|
|
3651
|
+
}
|
|
3652
|
+
/** Resolve a custodian reference: a `skt_…` token id, otherwise a member email. */
|
|
3653
|
+
function resolveCustodian(ctx, orgId, ref) {
|
|
3654
|
+
return ref.startsWith("skt_") ? kmsResolveRecipient(ctx, orgId, { token: ref }) : kmsResolveRecipient(ctx, orgId, { user: ref });
|
|
3655
|
+
}
|
|
3656
|
+
/**
|
|
3657
|
+
* The env DEK additionally wrapped to the org recovery key, when recovery is
|
|
3658
|
+
* enabled — so a newly created environment is recovery-protected from birth.
|
|
3659
|
+
* Returns undefined when recovery is off (the env is backfilled by `recovery
|
|
3660
|
+
* sync` later).
|
|
3661
|
+
*/
|
|
3662
|
+
async function recoveryWrapForNewEnv(ctx, orgId, dek) {
|
|
3663
|
+
let recoveryPublicKeyJwk;
|
|
3664
|
+
try {
|
|
3665
|
+
const { recovery } = await ctx.client.getRecovery(orgId);
|
|
3666
|
+
recoveryPublicKeyJwk = recovery.enabled ? recovery.recoveryPublicKeyJwk : null;
|
|
3667
|
+
} catch (e) {
|
|
3668
|
+
if (e instanceof SeekritApiError && (e.status === 403 || e.status === 404)) return void 0;
|
|
3669
|
+
throw e;
|
|
3670
|
+
}
|
|
3671
|
+
if (!recoveryPublicKeyJwk) return void 0;
|
|
3672
|
+
return wrapDek(dek, recoveryPublicKeyJwk);
|
|
3673
|
+
}
|
|
3674
|
+
/**
|
|
3675
|
+
* Wrap every environment the caller can decrypt but that lacks a recovery grant,
|
|
3676
|
+
* and upload the grants. Idempotent — safe to re-run and to run from several
|
|
3677
|
+
* admins to complete coverage.
|
|
3678
|
+
*/
|
|
3679
|
+
async function syncRecoveryGrants(ctx, orgId) {
|
|
3680
|
+
const { recovery } = await ctx.client.getRecovery(orgId);
|
|
3681
|
+
if (!recovery.enabled || !recovery.recoveryPublicKeyJwk) fail("recovery is not enabled");
|
|
3682
|
+
const recoveryPublicKeyJwk = recovery.recoveryPublicKeyJwk;
|
|
3683
|
+
const privateKey = await getPrivateKey(ctx);
|
|
3684
|
+
const grants = [];
|
|
3685
|
+
let skipped = 0;
|
|
3686
|
+
for (const environmentId of recovery.coverage.unprotectedEnvIds) {
|
|
3687
|
+
let wrappedDek;
|
|
3688
|
+
try {
|
|
3689
|
+
({wrappedDek} = await ctx.client.getMyEnvKey(orgId, environmentId));
|
|
3690
|
+
} catch (e) {
|
|
3691
|
+
if (e instanceof SeekritApiError && (e.status === 403 || e.status === 404)) {
|
|
3692
|
+
skipped++;
|
|
3693
|
+
continue;
|
|
3694
|
+
}
|
|
3695
|
+
throw e;
|
|
3696
|
+
}
|
|
3697
|
+
const dek = await unwrapDek(wrappedDek, privateKey);
|
|
3698
|
+
grants.push({
|
|
3699
|
+
environmentId,
|
|
3700
|
+
wrappedDek: await wrapDek(dek, recoveryPublicKeyJwk)
|
|
3701
|
+
});
|
|
3702
|
+
}
|
|
3703
|
+
if (grants.length > 0) await ctx.client.uploadRecoveryGrants(orgId, { grants });
|
|
3704
|
+
return {
|
|
3705
|
+
wrapped: grants.length,
|
|
3706
|
+
skipped
|
|
3707
|
+
};
|
|
3708
|
+
}
|
|
3709
|
+
/** Generate + split a fresh recovery key across the given custodians. */
|
|
3710
|
+
async function buildRecoveryConfig(ctx, orgId, thresholdRaw, custodianRefs) {
|
|
3711
|
+
const threshold = Number.parseInt(thresholdRaw, 10);
|
|
3712
|
+
if (!Number.isInteger(threshold) || threshold < 1) fail("--threshold must be a positive integer");
|
|
3713
|
+
if (custodianRefs.length === 0) fail("pass at least one --custodian <email|skt_id>");
|
|
3714
|
+
if (threshold > custodianRefs.length) fail("--threshold cannot exceed the number of custodians");
|
|
3715
|
+
const custodians = await Promise.all(custodianRefs.map((ref) => resolveCustodian(ctx, orgId, ref)));
|
|
3716
|
+
const recovery = await generateRecoveryKey();
|
|
3717
|
+
const shares = await splitRecoveryKey(recovery.privateKeyJwk, threshold, custodians);
|
|
3718
|
+
return {
|
|
3719
|
+
recoveryPublicKeyJwk: recovery.publicKeyJwk,
|
|
3720
|
+
threshold,
|
|
3721
|
+
shares: shares.map((s) => ({
|
|
3722
|
+
principalType: s.principalType,
|
|
3723
|
+
principalId: s.principalId,
|
|
3724
|
+
shareIndex: s.shareIndex,
|
|
3725
|
+
wrappedShare: s.wrappedShare
|
|
3726
|
+
}))
|
|
3727
|
+
};
|
|
3728
|
+
}
|
|
3729
|
+
function registerRecoveryCommands(program) {
|
|
3730
|
+
const recovery = program.command("recovery").description("customer-controlled M-of-N recovery (zero-knowledge)");
|
|
3731
|
+
recovery.command("status").description("show recovery configuration and environment coverage").option("--org <slug>").action(async (options) => {
|
|
3732
|
+
const ctx = buildContext();
|
|
3733
|
+
const org = await resolveOrg(ctx, options.org);
|
|
3734
|
+
const { recovery: status } = await ctx.client.getRecovery(org.id);
|
|
3735
|
+
if (!status.enabled) {
|
|
3736
|
+
console.log("recovery: disabled");
|
|
3737
|
+
return;
|
|
3738
|
+
}
|
|
3739
|
+
console.log(`recovery: enabled (${status.threshold}-of-${status.shareCount})`);
|
|
3740
|
+
console.log(`coverage: ${status.coverage.protected}/${status.coverage.total} environments protected`);
|
|
3741
|
+
console.log("custodians:");
|
|
3742
|
+
for (const cst of status.custodians) console.log(` - ${cst.label ?? cst.principalId} (${cst.principalType}, share #${cst.shareIndex})`);
|
|
3743
|
+
if (status.coverage.unprotectedEnvIds.length > 0) console.log(`${status.coverage.unprotectedEnvIds.length} environment(s) not yet protected — run \`seekrit recovery sync\``);
|
|
3744
|
+
});
|
|
3745
|
+
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) => {
|
|
3746
|
+
const ctx = buildContext();
|
|
3747
|
+
const org = await resolveOrg(ctx, options.org);
|
|
3748
|
+
const config = await buildRecoveryConfig(ctx, org.id, options.threshold, options.custodian);
|
|
3749
|
+
await ctx.client.configureRecovery(org.id, {
|
|
3750
|
+
...config,
|
|
3751
|
+
grants: []
|
|
3752
|
+
});
|
|
3753
|
+
console.error(`recovery enabled: ${config.threshold}-of-${config.shares.length}`);
|
|
3754
|
+
const { wrapped, skipped } = await syncRecoveryGrants(ctx, org.id);
|
|
3755
|
+
console.error(`recovery-protected ${wrapped} environment(s) you can decrypt`);
|
|
3756
|
+
if (skipped > 0) console.error(`${skipped} environment(s) need another admin to run \`seekrit recovery sync\``);
|
|
3757
|
+
});
|
|
3758
|
+
recovery.command("sync").description("recovery-protect environments you can decrypt but that aren't yet covered").option("--org <slug>").action(async (options) => {
|
|
3759
|
+
const ctx = buildContext();
|
|
3760
|
+
const { wrapped, skipped } = await syncRecoveryGrants(ctx, (await resolveOrg(ctx, options.org)).id);
|
|
3761
|
+
console.error(`recovery-protected ${wrapped} environment(s); skipped ${skipped} you cannot decrypt`);
|
|
3762
|
+
});
|
|
3763
|
+
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) => {
|
|
3764
|
+
const ctx = buildContext();
|
|
3765
|
+
const org = await resolveOrg(ctx, options.org);
|
|
3766
|
+
const config = await buildRecoveryConfig(ctx, org.id, options.threshold, options.custodian);
|
|
3767
|
+
await ctx.client.rotateRecovery(org.id, {
|
|
3768
|
+
...config,
|
|
3769
|
+
grants: []
|
|
3770
|
+
});
|
|
3771
|
+
console.error(`recovery rotated: ${config.threshold}-of-${config.shares.length}`);
|
|
3772
|
+
const { wrapped, skipped } = await syncRecoveryGrants(ctx, org.id);
|
|
3773
|
+
console.error(`re-wrapped ${wrapped} environment(s) you can decrypt to the new recovery key`);
|
|
3774
|
+
if (skipped > 0) console.error(`${skipped} environment(s) still need another admin to run \`seekrit recovery sync\``);
|
|
3775
|
+
});
|
|
3776
|
+
recovery.command("disable").description("disable recovery and remove all recovery grants").option("--org <slug>").action(async (options) => {
|
|
3777
|
+
const ctx = buildContext();
|
|
3778
|
+
const org = await resolveOrg(ctx, options.org);
|
|
3779
|
+
await ctx.client.disableRecovery(org.id);
|
|
3780
|
+
console.error("recovery disabled; recovery grants removed");
|
|
3781
|
+
});
|
|
3782
|
+
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) => {
|
|
3783
|
+
const ctx = buildContext();
|
|
3784
|
+
const org = await resolveOrg(ctx, options.org);
|
|
3785
|
+
const target = options.targetUser || options.targetToken ? await kmsResolveRecipient(ctx, org.id, {
|
|
3786
|
+
user: options.targetUser,
|
|
3787
|
+
token: options.targetToken
|
|
3788
|
+
}) : await kmsCallerIdentity(ctx);
|
|
3789
|
+
const { request } = await ctx.client.createRecoveryRequest(org.id, {
|
|
3790
|
+
targetPublicKeyJwk: target.publicKeyJwk,
|
|
3791
|
+
targetType: target.principalType,
|
|
3792
|
+
targetId: target.principalId,
|
|
3793
|
+
reason: options.reason
|
|
3794
|
+
});
|
|
3795
|
+
console.error(`recovery request ${request.id} created (needs ${request.threshold} custodians)`);
|
|
3796
|
+
console.error(` custodians run: seekrit recovery approve ${request.id}`);
|
|
3797
|
+
console.error(` then the target: seekrit recovery complete ${request.id}`);
|
|
3798
|
+
});
|
|
3799
|
+
recovery.command("approve").description("as a custodian, contribute your share to a recovery request").argument("<requestId>").option("--org <slug>").action(async (requestId, options) => {
|
|
3800
|
+
const ctx = buildContext();
|
|
3801
|
+
const org = await resolveOrg(ctx, options.org);
|
|
3802
|
+
const { request } = await ctx.client.getRecoveryRequest(org.id, requestId);
|
|
3803
|
+
const myShare = await ctx.client.getMyRecoveryShare(org.id);
|
|
3804
|
+
const privateKey = await getPrivateKey(ctx);
|
|
3805
|
+
const contributedShare = await rewrapRecoveryShare(await unwrapRecoveryShare(myShare.wrappedShare, privateKey), request.targetPublicKeyJwk);
|
|
3806
|
+
const res = await ctx.client.contributeRecoveryShare(org.id, requestId, {
|
|
3807
|
+
shareIndex: myShare.shareIndex,
|
|
3808
|
+
contributedShare
|
|
3809
|
+
});
|
|
3810
|
+
console.error(`contributed share #${myShare.shareIndex}: ${res.contributed}/${res.threshold} collected${res.quorumReached ? " — quorum reached" : ""}`);
|
|
3811
|
+
});
|
|
3812
|
+
recovery.command("complete").description("as the recovery target, reconstruct the key and restore your access").argument("<requestId>").option("--org <slug>").action(async (requestId, options) => {
|
|
3813
|
+
const ctx = buildContext();
|
|
3814
|
+
const org = await resolveOrg(ctx, options.org);
|
|
3815
|
+
const { request, contributions, quorumReached } = await ctx.client.getRecoveryRequest(org.id, requestId);
|
|
3816
|
+
if (!quorumReached) fail(`only ${contributions.length}/${request.threshold} custodians have contributed`);
|
|
3817
|
+
const me = await kmsCallerIdentity(ctx);
|
|
3818
|
+
const targetPrivateKey = await getPrivateKey(ctx);
|
|
3819
|
+
const recoveryPrivateKey = await combineRecoveryShares(await Promise.all(contributions.map((cont) => unwrapRecoveryShare(cont.contributedShare, targetPrivateKey))));
|
|
3820
|
+
const { grants: recoveryEnvKeys } = await ctx.client.getRecoveryEnvKeys(org.id);
|
|
3821
|
+
const restored = [];
|
|
3822
|
+
for (const g of recoveryEnvKeys) {
|
|
3823
|
+
const dek = await unwrapDek(g.wrappedDek, recoveryPrivateKey);
|
|
3824
|
+
restored.push({
|
|
3825
|
+
environmentId: g.environmentId,
|
|
3826
|
+
wrappedDek: await wrapDek(dek, me.publicKeyJwk)
|
|
3827
|
+
});
|
|
3828
|
+
}
|
|
3829
|
+
await ctx.client.completeRecoveryRequest(org.id, requestId, {
|
|
3830
|
+
principalType: me.principalType,
|
|
3831
|
+
principalId: me.principalId,
|
|
3832
|
+
grants: restored
|
|
3833
|
+
});
|
|
3834
|
+
console.error(`recovery complete: restored access to ${restored.length} environment(s)`);
|
|
3835
|
+
});
|
|
3836
|
+
recovery.command("cancel").description("cancel an open recovery request").argument("<requestId>").option("--org <slug>").action(async (requestId, options) => {
|
|
3837
|
+
const ctx = buildContext();
|
|
3838
|
+
const org = await resolveOrg(ctx, options.org);
|
|
3839
|
+
await ctx.client.cancelRecoveryRequest(org.id, requestId);
|
|
3840
|
+
console.error(`recovery request ${requestId} canceled`);
|
|
3841
|
+
});
|
|
3842
|
+
}
|
|
3843
|
+
//#endregion
|
|
3333
3844
|
//#region src/redis.ts
|
|
3334
3845
|
/**
|
|
3335
3846
|
* `seekrit redis` — temporary Redis (6+) credentials (Vault-style dynamic
|
|
@@ -3792,11 +4303,14 @@ env.command("create").description("create an application environment (generates
|
|
|
3792
4303
|
if (!appRow) fail(`no app "${options.app}" in ${orgRef.slug}`);
|
|
3793
4304
|
const { user } = await ctx.client.me();
|
|
3794
4305
|
if (!user.publicKeyJwk) fail("run `seekrit keys setup` first");
|
|
3795
|
-
const
|
|
4306
|
+
const dek = generateDek();
|
|
4307
|
+
const wrappedDek = await wrapDek(dek, user.publicKeyJwk);
|
|
4308
|
+
const recoveryWrappedDek = await recoveryWrapForNewEnv(ctx, orgRef.id, dek);
|
|
3796
4309
|
const created = await ctx.client.createEnv(orgRef.id, appRow.id, {
|
|
3797
4310
|
name: options.name,
|
|
3798
4311
|
slug: options.slug,
|
|
3799
|
-
wrappedDek
|
|
4312
|
+
wrappedDek,
|
|
4313
|
+
recoveryWrappedDek
|
|
3800
4314
|
});
|
|
3801
4315
|
console.error(`created environment ${created.environment.slug} (${created.environment.id})`);
|
|
3802
4316
|
});
|
|
@@ -3848,11 +4362,14 @@ group.command("env").description("manage a group’s environments (per-slug valu
|
|
|
3848
4362
|
});
|
|
3849
4363
|
const { user } = await ctx.client.me();
|
|
3850
4364
|
if (!user.publicKeyJwk) fail("run `seekrit keys setup` first");
|
|
3851
|
-
const
|
|
4365
|
+
const dek = generateDek();
|
|
4366
|
+
const wrappedDek = await wrapDek(dek, user.publicKeyJwk);
|
|
4367
|
+
const recoveryWrappedDek = await recoveryWrapForNewEnv(ctx, groupRef.orgId, dek);
|
|
3852
4368
|
const created = await ctx.client.createGroupEnv(groupRef.orgId, groupRef.id, {
|
|
3853
4369
|
name: options.name,
|
|
3854
4370
|
slug: options.slug,
|
|
3855
|
-
wrappedDek
|
|
4371
|
+
wrappedDek,
|
|
4372
|
+
recoveryWrappedDek
|
|
3856
4373
|
});
|
|
3857
4374
|
console.error(`created ${groupRef.slug}@${created.environment.slug} (${created.environment.id})`);
|
|
3858
4375
|
});
|
|
@@ -4096,8 +4613,9 @@ registerAwsCommands(program);
|
|
|
4096
4613
|
registerGcpCommands(program);
|
|
4097
4614
|
registerMongoCommands(program);
|
|
4098
4615
|
registerKmsCommands(program);
|
|
4616
|
+
registerRecoveryCommands(program);
|
|
4099
4617
|
program.command("mcp").description("run an MCP server over stdio so AI agents can drive seekrit").action(async () => {
|
|
4100
|
-
const { runMcpServer } = await import("./mcp-
|
|
4618
|
+
const { runMcpServer } = await import("./mcp-DR_Ghb4w.js");
|
|
4101
4619
|
await runMcpServer();
|
|
4102
4620
|
});
|
|
4103
4621
|
program.command("audit").description("show the org audit trail").option("--org <slug>").option("--limit <n>", "entries to fetch", "50").action(async (options) => {
|
|
@@ -115,11 +115,11 @@ async function resolveTargetEnv(ctx, o) {
|
|
|
115
115
|
}
|
|
116
116
|
return resolveEnvTarget(ctx, o);
|
|
117
117
|
}
|
|
118
|
-
async function runMcpServer() {
|
|
118
|
+
async function runMcpServer(options = {}) {
|
|
119
119
|
setFailThrows(true);
|
|
120
120
|
const server = new McpServer({
|
|
121
121
|
name: "seekrit",
|
|
122
|
-
version
|
|
122
|
+
version: options.version ?? version
|
|
123
123
|
});
|
|
124
124
|
/** Register a tool whose handler returns data (serialized) or throws (→ isError). */
|
|
125
125
|
const tool = (name, description, shape, handler) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seekrit/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "End-to-end encrypted secrets manager CLI — inject decrypted secrets into any command.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
"bin": {
|
|
10
10
|
"seekrit": "./dist/index.js"
|
|
11
11
|
},
|
|
12
|
+
"exports": {
|
|
13
|
+
"./mcp": "./src/mcp.ts"
|
|
14
|
+
},
|
|
12
15
|
"files": [
|
|
13
16
|
"dist"
|
|
14
17
|
],
|
|
@@ -24,8 +27,8 @@
|
|
|
24
27
|
"@types/node": "^26.1.0",
|
|
25
28
|
"tsdown": "^0.22.3",
|
|
26
29
|
"@seekrit/api-client": "0.0.1",
|
|
27
|
-
"@seekrit/
|
|
28
|
-
"@seekrit/
|
|
30
|
+
"@seekrit/crypto": "0.0.1",
|
|
31
|
+
"@seekrit/core": "0.0.1"
|
|
29
32
|
},
|
|
30
33
|
"scripts": {
|
|
31
34
|
"build": "tsdown",
|