@seekrit/cli 0.13.0 → 0.15.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 +706 -215
- package/dist/{mcp-BEcV_KpM.js → mcp-ARBuneH3.js} +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -11,7 +11,9 @@ z.enum([
|
|
|
11
11
|
"postgres",
|
|
12
12
|
"mysql",
|
|
13
13
|
"ssh",
|
|
14
|
-
"redis"
|
|
14
|
+
"redis",
|
|
15
|
+
"aws",
|
|
16
|
+
"gcp"
|
|
15
17
|
]);
|
|
16
18
|
const executorModeSchema = z.enum(["in_do", "remote"]);
|
|
17
19
|
/**
|
|
@@ -66,6 +68,51 @@ const redisUserNameSchema = z.string().regex(/^[A-Za-z0-9_]{3,32}$/, "must be 3
|
|
|
66
68
|
* The alphabet is bare hex, so it is a safe bare command token.
|
|
67
69
|
*/
|
|
68
70
|
const redisSha256VerifierSchema = z.string().regex(/^[0-9a-f]{64}$/, "must be a lowercase-hex SHA-256 digest (64 chars)");
|
|
71
|
+
/**
|
|
72
|
+
* An IAM role ARN the broker is allowed to assume. Bounded and structurally
|
|
73
|
+
* validated: `arn:<partition>:iam::<account>:role/<path-and-name>`. Partition
|
|
74
|
+
* covers commercial (`aws`), GovCloud (`aws-us-gov`), and China (`aws-cn`).
|
|
75
|
+
*/
|
|
76
|
+
const awsRoleArnSchema = z.string().regex(/^arn:aws(?:-us-gov|-cn)?:iam::\d{12}:role\/[\w+=,.@/-]{1,512}$/, "must be an IAM role ARN (arn:aws:iam::<account>:role/<name>)");
|
|
77
|
+
/** An AWS region id, e.g. `us-east-1`, `eu-west-2`, `us-gov-west-1`. */
|
|
78
|
+
const awsRegionSchema = z.string().regex(/^[a-z]{2}(?:-[a-z]+)+-\d$/, "must be an AWS region id (e.g. us-east-1)");
|
|
79
|
+
/**
|
|
80
|
+
* An STS external id — the shared string a role's trust policy can require so a
|
|
81
|
+
* confused-deputy can't assume it. AWS allows a broad charset; we keep to the
|
|
82
|
+
* documented safe set and bound the length.
|
|
83
|
+
*/
|
|
84
|
+
const awsExternalIdSchema = z.string().regex(/^[\w+=,.@:/-]{2,1224}$/, "must be a valid STS external id");
|
|
85
|
+
z.string().regex(/^[\w+=,.@-]{2,64}$/, "must be 2–64 chars of [A-Za-z0-9_+=,.@-]");
|
|
86
|
+
/**
|
|
87
|
+
* A GCP service-account email the broker is allowed to impersonate (or that
|
|
88
|
+
* appears in a delegation chain). Structurally validated and bounded: it is
|
|
89
|
+
* interpolated into the IAM Credentials API URL path, so the charset excludes
|
|
90
|
+
* anything that could break out of a path segment. Covers user-managed
|
|
91
|
+
* (`name@<project>.iam.gserviceaccount.com`) and Google-managed
|
|
92
|
+
* (`<project-number>-compute@developer.gserviceaccount.com`) forms.
|
|
93
|
+
*/
|
|
94
|
+
const gcpServiceAccountEmailSchema = z.string().max(256).regex(/^[a-z0-9-]+@[a-z0-9.-]+\.gserviceaccount\.com$/, "must be a service-account email (…@….gserviceaccount.com)");
|
|
95
|
+
/**
|
|
96
|
+
* An OAuth 2.0 scope granted to the minted access token, e.g.
|
|
97
|
+
* `https://www.googleapis.com/auth/cloud-platform`. Bounded and whitespace-free
|
|
98
|
+
* (scopes are space-delimited); passed to the IAM Credentials API in a JSON body
|
|
99
|
+
* array, not a URL, so this is sanity/DoS hardening rather than an injection gate.
|
|
100
|
+
*/
|
|
101
|
+
const gcpOauthScopeSchema = z.string().min(1).max(256).regex(/^\S+$/, "must be a single OAuth scope with no whitespace");
|
|
102
|
+
/**
|
|
103
|
+
* The consumer's ephemeral P-256 public key (JWK-serialized) that a tier-2
|
|
104
|
+
* credential is wrapped to before it is returned. Validated structurally here;
|
|
105
|
+
* the executor imports it defensively before wrapping. Bounded so a giant blob
|
|
106
|
+
* can't be pushed through the control plane.
|
|
107
|
+
*/
|
|
108
|
+
const p256PublicKeyJwkSchema = z.string().max(2048).refine((s) => {
|
|
109
|
+
try {
|
|
110
|
+
const jwk = JSON.parse(s);
|
|
111
|
+
return jwk.kty === "EC" && jwk.crv === "P-256" && !!jwk.x && !!jwk.y;
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}, "must be a JWK-serialized P-256 public key");
|
|
69
116
|
const postgresAccessLevelSchema = z.enum([
|
|
70
117
|
"readonly",
|
|
71
118
|
"readwrite",
|
|
@@ -144,11 +191,32 @@ const sshTargetConfigSchema = z.object({
|
|
|
144
191
|
user: sshPrincipalSchema.optional()
|
|
145
192
|
}).optional()
|
|
146
193
|
});
|
|
194
|
+
const AWS_MAX_TTL_SECONDS = 3600 * 12;
|
|
195
|
+
const awsTargetConfigSchema = z.object({
|
|
196
|
+
provider: z.literal("aws"),
|
|
197
|
+
executor: z.literal("in_do"),
|
|
198
|
+
roleArn: awsRoleArnSchema,
|
|
199
|
+
region: awsRegionSchema,
|
|
200
|
+
externalId: awsExternalIdSchema.optional(),
|
|
201
|
+
sessionPolicy: z.string().min(1).max(4e3).optional(),
|
|
202
|
+
maxTtlSeconds: z.number().int().min(900).max(AWS_MAX_TTL_SECONDS).optional()
|
|
203
|
+
});
|
|
204
|
+
const GCP_MAX_TTL_SECONDS = 3600 * 12;
|
|
205
|
+
const gcpTargetConfigSchema = z.object({
|
|
206
|
+
provider: z.literal("gcp"),
|
|
207
|
+
executor: z.literal("in_do"),
|
|
208
|
+
serviceAccount: gcpServiceAccountEmailSchema,
|
|
209
|
+
scopes: z.array(gcpOauthScopeSchema).min(1).max(32).optional(),
|
|
210
|
+
delegates: z.array(gcpServiceAccountEmailSchema).max(8).optional(),
|
|
211
|
+
maxTtlSeconds: z.number().int().min(60).max(GCP_MAX_TTL_SECONDS).optional()
|
|
212
|
+
});
|
|
147
213
|
const leaseTargetConfigSchema = z.discriminatedUnion("provider", [
|
|
148
214
|
postgresTargetConfigSchema,
|
|
149
215
|
mysqlTargetConfigSchema,
|
|
150
216
|
redisTargetConfigSchema,
|
|
151
|
-
sshTargetConfigSchema
|
|
217
|
+
sshTargetConfigSchema,
|
|
218
|
+
awsTargetConfigSchema,
|
|
219
|
+
gcpTargetConfigSchema
|
|
152
220
|
]);
|
|
153
221
|
z.object({
|
|
154
222
|
name: z.string().trim().min(1).max(128),
|
|
@@ -210,13 +278,97 @@ const mintSshLeaseSchema = z.object({
|
|
|
210
278
|
principals: z.array(sshPrincipalSchema).min(1).max(32),
|
|
211
279
|
ttlSeconds: ttlSecondsSchema
|
|
212
280
|
});
|
|
281
|
+
/**
|
|
282
|
+
* Client → API: mint an AWS lease. The client generates an ephemeral P-256
|
|
283
|
+
* keypair locally and sends only the public key; STS mints the credential and
|
|
284
|
+
* the broker returns it wrapped to that key. The private key never leaves the
|
|
285
|
+
* requesting machine, so the plaintext credential is only decryptable there.
|
|
286
|
+
*
|
|
287
|
+
* TTL bounds are STS's own `DurationSeconds` limits (15 min – 12 h), not the
|
|
288
|
+
* generic lease bounds — STS rejects anything below 900 seconds.
|
|
289
|
+
*/
|
|
290
|
+
const mintAwsLeaseSchema = z.object({
|
|
291
|
+
provider: z.literal("aws"),
|
|
292
|
+
targetId: z.string().min(1),
|
|
293
|
+
recipientPublicKey: p256PublicKeyJwkSchema,
|
|
294
|
+
ttlSeconds: z.number().int().min(900).max(AWS_MAX_TTL_SECONDS)
|
|
295
|
+
});
|
|
296
|
+
/**
|
|
297
|
+
* Client → API: mint a GCP lease. Like AWS (tier 2): the client generates an
|
|
298
|
+
* ephemeral P-256 keypair locally and sends only the public key; the IAM
|
|
299
|
+
* Credentials API mints the access token and the broker returns it wrapped to
|
|
300
|
+
* that key. The private key never leaves the requesting machine, so the plaintext
|
|
301
|
+
* token is only decryptable there.
|
|
302
|
+
*
|
|
303
|
+
* TTL bounds are GCP's `generateAccessToken` limits (1 min – 12 h); tokens over
|
|
304
|
+
* 1 h require the credential-lifetime-extension org policy.
|
|
305
|
+
*/
|
|
306
|
+
const mintGcpLeaseSchema = z.object({
|
|
307
|
+
provider: z.literal("gcp"),
|
|
308
|
+
targetId: z.string().min(1),
|
|
309
|
+
recipientPublicKey: p256PublicKeyJwkSchema,
|
|
310
|
+
ttlSeconds: z.number().int().min(60).max(GCP_MAX_TTL_SECONDS)
|
|
311
|
+
});
|
|
213
312
|
z.discriminatedUnion("provider", [
|
|
214
313
|
mintPostgresLeaseSchema,
|
|
215
314
|
mintMysqlLeaseSchema,
|
|
216
315
|
mintRedisLeaseSchema,
|
|
217
|
-
mintSshLeaseSchema
|
|
316
|
+
mintSshLeaseSchema,
|
|
317
|
+
mintAwsLeaseSchema,
|
|
318
|
+
mintGcpLeaseSchema
|
|
218
319
|
]);
|
|
219
320
|
//#endregion
|
|
321
|
+
//#region ../../packages/core/src/providers/aws.ts
|
|
322
|
+
/**
|
|
323
|
+
* The one-time IAM setup an admin performs so seekrit can assume the target
|
|
324
|
+
* role. Analogue of `sshHostSetupInstructions` / `postgresGroupBootstrapSql` —
|
|
325
|
+
* printed for the admin to apply, never executed by seekrit. Shows the trust
|
|
326
|
+
* policy the role needs so the broker's base IAM principal may assume it.
|
|
327
|
+
*/
|
|
328
|
+
function awsTrustPolicyInstructions(config) {
|
|
329
|
+
return [
|
|
330
|
+
"# Attach a trust policy to the target role so the IAM principal whose",
|
|
331
|
+
"# access key you registered as the admin secret may assume it. Replace",
|
|
332
|
+
"# <ADMIN_PRINCIPAL_ARN> with that principal (user or role) ARN.",
|
|
333
|
+
"{",
|
|
334
|
+
" \"Version\": \"2012-10-17\",",
|
|
335
|
+
" \"Statement\": [",
|
|
336
|
+
" {",
|
|
337
|
+
" \"Effect\": \"Allow\",",
|
|
338
|
+
" \"Principal\": { \"AWS\": \"<ADMIN_PRINCIPAL_ARN>\" },",
|
|
339
|
+
` "Action": "sts:AssumeRole"${config.externalId ? `,\n "Condition": { "StringEquals": { "sts:ExternalId": "${config.externalId}" } }` : ""}`,
|
|
340
|
+
" }",
|
|
341
|
+
" ]",
|
|
342
|
+
"}",
|
|
343
|
+
"",
|
|
344
|
+
"# The admin principal also needs an identity policy allowing sts:AssumeRole",
|
|
345
|
+
`# on ${config.roleArn}. seekrit only ever calls AssumeRole on this role.`
|
|
346
|
+
].join("\n");
|
|
347
|
+
}
|
|
348
|
+
//#endregion
|
|
349
|
+
//#region ../../packages/core/src/providers/gcp.ts
|
|
350
|
+
/**
|
|
351
|
+
* The one-time IAM setup an admin performs so seekrit can impersonate the target
|
|
352
|
+
* service account. Analogue of `awsTrustPolicyInstructions` — printed for the
|
|
353
|
+
* admin to apply, never executed by seekrit. Grants the *source* service account
|
|
354
|
+
* (the one whose key was registered as the admin secret) the token-creator role
|
|
355
|
+
* on the target service account.
|
|
356
|
+
*/
|
|
357
|
+
function gcpSetupInstructions(config) {
|
|
358
|
+
return [
|
|
359
|
+
"# Grant the service account whose key you registered as the admin secret",
|
|
360
|
+
"# permission to mint tokens for the target service account. Replace",
|
|
361
|
+
"# <SOURCE_SA_EMAIL> with the client_email from that key JSON.",
|
|
362
|
+
`gcloud iam service-accounts add-iam-policy-binding ${config.serviceAccount} \\`,
|
|
363
|
+
" --member=\"serviceAccount:<SOURCE_SA_EMAIL>\" \\",
|
|
364
|
+
" --role=\"roles/iam.serviceAccountTokenCreator\"",
|
|
365
|
+
"",
|
|
366
|
+
"# The source service account's project must have the IAM Service Account",
|
|
367
|
+
"# Credentials API enabled (seekrit only ever calls generateAccessToken):",
|
|
368
|
+
"gcloud services enable iamcredentials.googleapis.com"
|
|
369
|
+
].join("\n");
|
|
370
|
+
}
|
|
371
|
+
//#endregion
|
|
220
372
|
//#region ../../packages/core/src/providers/postgres.ts
|
|
221
373
|
/**
|
|
222
374
|
* The one-time setup SQL an admin runs to create the shared group role that a
|
|
@@ -508,6 +660,130 @@ async function importPrivateKeyPkcs8(pkcs8) {
|
|
|
508
660
|
}, true, ["deriveBits"]);
|
|
509
661
|
}
|
|
510
662
|
//#endregion
|
|
663
|
+
//#region ../../packages/crypto/src/wrap.ts
|
|
664
|
+
/**
|
|
665
|
+
* ECIES-style key wrapping: an ephemeral P-256 keypair performs ECDH against
|
|
666
|
+
* the recipient's public key; the shared secret is run through HKDF-SHA256 to
|
|
667
|
+
* derive a one-time AES-256-GCM wrapping key. Only the holder of the
|
|
668
|
+
* recipient private key can unwrap.
|
|
669
|
+
*
|
|
670
|
+
* Blob format: `wd1.<ephemeral pub (raw)>.<hkdf salt>.<iv>.<ciphertext>`
|
|
671
|
+
*/
|
|
672
|
+
const WRAP_PREFIX = "wd1";
|
|
673
|
+
const HKDF_INFO = "seekrit/wrap-dek/v1";
|
|
674
|
+
async function deriveWrappingKey(ownPrivateKey, peerPublicKey, salt, usage) {
|
|
675
|
+
const ecdh = {
|
|
676
|
+
name: "ECDH",
|
|
677
|
+
public: peerPublicKey
|
|
678
|
+
};
|
|
679
|
+
const sharedBits = await crypto.subtle.deriveBits(ecdh, ownPrivateKey, 256);
|
|
680
|
+
const hkdfKey = await crypto.subtle.importKey("raw", sharedBits, "HKDF", false, ["deriveKey"]);
|
|
681
|
+
return crypto.subtle.deriveKey({
|
|
682
|
+
name: "HKDF",
|
|
683
|
+
hash: "SHA-256",
|
|
684
|
+
salt,
|
|
685
|
+
info: utf8Encode(HKDF_INFO)
|
|
686
|
+
}, hkdfKey, {
|
|
687
|
+
name: "AES-GCM",
|
|
688
|
+
length: 256
|
|
689
|
+
}, false, [usage]);
|
|
690
|
+
}
|
|
691
|
+
/** Wrap an environment DEK to a principal's public key. */
|
|
692
|
+
async function wrapDek(dek, recipientPublicKeyJwk) {
|
|
693
|
+
const recipientKey = await importPublicKey(recipientPublicKeyJwk);
|
|
694
|
+
const ephemeral = await crypto.subtle.generateKey({
|
|
695
|
+
name: "ECDH",
|
|
696
|
+
namedCurve: "P-256"
|
|
697
|
+
}, true, ["deriveBits"]);
|
|
698
|
+
const salt = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
|
|
699
|
+
const wrappingKey = await deriveWrappingKey(ephemeral.privateKey, recipientKey, salt, "encrypt");
|
|
700
|
+
const iv = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(12));
|
|
701
|
+
const ciphertext = await crypto.subtle.encrypt({
|
|
702
|
+
name: "AES-GCM",
|
|
703
|
+
iv
|
|
704
|
+
}, wrappingKey, dek);
|
|
705
|
+
const ephemeralRaw = new Uint8Array(await crypto.subtle.exportKey("raw", ephemeral.publicKey));
|
|
706
|
+
return [
|
|
707
|
+
WRAP_PREFIX,
|
|
708
|
+
toBase64Url(ephemeralRaw),
|
|
709
|
+
toBase64Url(salt),
|
|
710
|
+
toBase64Url(iv),
|
|
711
|
+
toBase64Url(new Uint8Array(ciphertext))
|
|
712
|
+
].join(".");
|
|
713
|
+
}
|
|
714
|
+
/** Unwrap an environment DEK with the principal's private ECDH key. */
|
|
715
|
+
async function unwrapDek(wrapped, privateKey) {
|
|
716
|
+
const [ephB64, saltB64, ivB64, ctB64] = splitBlob(wrapped, WRAP_PREFIX, 4);
|
|
717
|
+
const wrappingKey = await deriveWrappingKey(privateKey, await crypto.subtle.importKey("raw", fromBase64Url(ephB64), {
|
|
718
|
+
name: "ECDH",
|
|
719
|
+
namedCurve: "P-256"
|
|
720
|
+
}, false, []), fromBase64Url(saltB64), "decrypt");
|
|
721
|
+
try {
|
|
722
|
+
const dek = await crypto.subtle.decrypt({
|
|
723
|
+
name: "AES-GCM",
|
|
724
|
+
iv: fromBase64Url(ivB64)
|
|
725
|
+
}, wrappingKey, fromBase64Url(ctB64));
|
|
726
|
+
return new Uint8Array(dek);
|
|
727
|
+
} catch {
|
|
728
|
+
throw new SeekritCryptoError("DECRYPT_FAILED", "DEK unwrap failed: wrong private key or tampered grant");
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
//#endregion
|
|
732
|
+
//#region ../../packages/crypto/src/aws.ts
|
|
733
|
+
/** Generate the ephemeral P-256 keypair a client uses to receive one AWS lease. */
|
|
734
|
+
async function generateAwsRecipientKeyPair() {
|
|
735
|
+
return generateKeyPair();
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Unwrap the `wd1.` blob returned by an AWS lease mint using the ephemeral
|
|
739
|
+
* private key generated for that lease, yielding the plaintext STS credential.
|
|
740
|
+
*/
|
|
741
|
+
async function unwrapAwsCredential(wrapped, privateKeyJwk) {
|
|
742
|
+
const bytes = await unwrapDek(wrapped, await importPrivateKey(privateKeyJwk));
|
|
743
|
+
let parsed;
|
|
744
|
+
try {
|
|
745
|
+
parsed = JSON.parse(utf8Decode(bytes));
|
|
746
|
+
} catch {
|
|
747
|
+
throw new SeekritCryptoError("DECRYPT_FAILED", "unwrapped AWS credential was not valid JSON");
|
|
748
|
+
}
|
|
749
|
+
const c = parsed;
|
|
750
|
+
if (typeof c.accessKeyId !== "string" || typeof c.secretAccessKey !== "string" || typeof c.sessionToken !== "string" || typeof c.expiration !== "string" || typeof c.region !== "string") throw new SeekritCryptoError("DECRYPT_FAILED", "unwrapped AWS credential is missing fields");
|
|
751
|
+
return {
|
|
752
|
+
accessKeyId: c.accessKeyId,
|
|
753
|
+
secretAccessKey: c.secretAccessKey,
|
|
754
|
+
sessionToken: c.sessionToken,
|
|
755
|
+
expiration: c.expiration,
|
|
756
|
+
region: c.region
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
//#endregion
|
|
760
|
+
//#region ../../packages/crypto/src/gcp.ts
|
|
761
|
+
/** Generate the ephemeral P-256 keypair a client uses to receive one GCP lease. */
|
|
762
|
+
async function generateGcpRecipientKeyPair() {
|
|
763
|
+
return generateKeyPair();
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Unwrap the `wd1.` blob returned by a GCP lease mint using the ephemeral
|
|
767
|
+
* private key generated for that lease, yielding the plaintext access token.
|
|
768
|
+
*/
|
|
769
|
+
async function unwrapGcpCredential(wrapped, privateKeyJwk) {
|
|
770
|
+
const bytes = await unwrapDek(wrapped, await importPrivateKey(privateKeyJwk));
|
|
771
|
+
let parsed;
|
|
772
|
+
try {
|
|
773
|
+
parsed = JSON.parse(utf8Decode(bytes));
|
|
774
|
+
} catch {
|
|
775
|
+
throw new SeekritCryptoError("DECRYPT_FAILED", "unwrapped GCP credential was not valid JSON");
|
|
776
|
+
}
|
|
777
|
+
const c = parsed;
|
|
778
|
+
if (typeof c.accessToken !== "string" || typeof c.expiration !== "string" || typeof c.serviceAccount !== "string" || !Array.isArray(c.scopes) || !c.scopes.every((s) => typeof s === "string")) throw new SeekritCryptoError("DECRYPT_FAILED", "unwrapped GCP credential is missing fields");
|
|
779
|
+
return {
|
|
780
|
+
accessToken: c.accessToken,
|
|
781
|
+
expiration: c.expiration,
|
|
782
|
+
serviceAccount: c.serviceAccount,
|
|
783
|
+
scopes: c.scopes
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
//#endregion
|
|
511
787
|
//#region ../../packages/crypto/src/mysql.ts
|
|
512
788
|
/**
|
|
513
789
|
* Client-side construction of a MySQL/MariaDB `mysql_native_password`
|
|
@@ -951,111 +1227,8 @@ function isServiceToken(value) {
|
|
|
951
1227
|
return value.startsWith(`${TOKEN_PREFIX}_`);
|
|
952
1228
|
}
|
|
953
1229
|
//#endregion
|
|
954
|
-
//#region ../../packages/crypto/src/wrap.ts
|
|
955
|
-
/**
|
|
956
|
-
* ECIES-style key wrapping: an ephemeral P-256 keypair performs ECDH against
|
|
957
|
-
* the recipient's public key; the shared secret is run through HKDF-SHA256 to
|
|
958
|
-
* derive a one-time AES-256-GCM wrapping key. Only the holder of the
|
|
959
|
-
* recipient private key can unwrap.
|
|
960
|
-
*
|
|
961
|
-
* Blob format: `wd1.<ephemeral pub (raw)>.<hkdf salt>.<iv>.<ciphertext>`
|
|
962
|
-
*/
|
|
963
|
-
const WRAP_PREFIX = "wd1";
|
|
964
|
-
const HKDF_INFO = "seekrit/wrap-dek/v1";
|
|
965
|
-
async function deriveWrappingKey(ownPrivateKey, peerPublicKey, salt, usage) {
|
|
966
|
-
const ecdh = {
|
|
967
|
-
name: "ECDH",
|
|
968
|
-
public: peerPublicKey
|
|
969
|
-
};
|
|
970
|
-
const sharedBits = await crypto.subtle.deriveBits(ecdh, ownPrivateKey, 256);
|
|
971
|
-
const hkdfKey = await crypto.subtle.importKey("raw", sharedBits, "HKDF", false, ["deriveKey"]);
|
|
972
|
-
return crypto.subtle.deriveKey({
|
|
973
|
-
name: "HKDF",
|
|
974
|
-
hash: "SHA-256",
|
|
975
|
-
salt,
|
|
976
|
-
info: utf8Encode(HKDF_INFO)
|
|
977
|
-
}, hkdfKey, {
|
|
978
|
-
name: "AES-GCM",
|
|
979
|
-
length: 256
|
|
980
|
-
}, false, [usage]);
|
|
981
|
-
}
|
|
982
|
-
/** Wrap an environment DEK to a principal's public key. */
|
|
983
|
-
async function wrapDek(dek, recipientPublicKeyJwk) {
|
|
984
|
-
const recipientKey = await importPublicKey(recipientPublicKeyJwk);
|
|
985
|
-
const ephemeral = await crypto.subtle.generateKey({
|
|
986
|
-
name: "ECDH",
|
|
987
|
-
namedCurve: "P-256"
|
|
988
|
-
}, true, ["deriveBits"]);
|
|
989
|
-
const salt = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
|
|
990
|
-
const wrappingKey = await deriveWrappingKey(ephemeral.privateKey, recipientKey, salt, "encrypt");
|
|
991
|
-
const iv = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(12));
|
|
992
|
-
const ciphertext = await crypto.subtle.encrypt({
|
|
993
|
-
name: "AES-GCM",
|
|
994
|
-
iv
|
|
995
|
-
}, wrappingKey, dek);
|
|
996
|
-
const ephemeralRaw = new Uint8Array(await crypto.subtle.exportKey("raw", ephemeral.publicKey));
|
|
997
|
-
return [
|
|
998
|
-
WRAP_PREFIX,
|
|
999
|
-
toBase64Url(ephemeralRaw),
|
|
1000
|
-
toBase64Url(salt),
|
|
1001
|
-
toBase64Url(iv),
|
|
1002
|
-
toBase64Url(new Uint8Array(ciphertext))
|
|
1003
|
-
].join(".");
|
|
1004
|
-
}
|
|
1005
|
-
/** Unwrap an environment DEK with the principal's private ECDH key. */
|
|
1006
|
-
async function unwrapDek(wrapped, privateKey) {
|
|
1007
|
-
const [ephB64, saltB64, ivB64, ctB64] = splitBlob(wrapped, WRAP_PREFIX, 4);
|
|
1008
|
-
const wrappingKey = await deriveWrappingKey(privateKey, await crypto.subtle.importKey("raw", fromBase64Url(ephB64), {
|
|
1009
|
-
name: "ECDH",
|
|
1010
|
-
namedCurve: "P-256"
|
|
1011
|
-
}, false, []), fromBase64Url(saltB64), "decrypt");
|
|
1012
|
-
try {
|
|
1013
|
-
const dek = await crypto.subtle.decrypt({
|
|
1014
|
-
name: "AES-GCM",
|
|
1015
|
-
iv: fromBase64Url(ivB64)
|
|
1016
|
-
}, wrappingKey, fromBase64Url(ctB64));
|
|
1017
|
-
return new Uint8Array(dek);
|
|
1018
|
-
} catch {
|
|
1019
|
-
throw new SeekritCryptoError("DECRYPT_FAILED", "DEK unwrap failed: wrong private key or tampered grant");
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
//#endregion
|
|
1023
1230
|
//#region package.json
|
|
1024
|
-
var version = "0.
|
|
1025
|
-
const PROJECT_FILE = "seekrit.json";
|
|
1026
|
-
function globalConfigPath() {
|
|
1027
|
-
return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "seekrit", "config.json");
|
|
1028
|
-
}
|
|
1029
|
-
function readGlobalConfig() {
|
|
1030
|
-
const path = globalConfigPath();
|
|
1031
|
-
if (!existsSync(path)) return {};
|
|
1032
|
-
return JSON.parse(readFileSync(path, "utf8"));
|
|
1033
|
-
}
|
|
1034
|
-
function writeGlobalConfig(update) {
|
|
1035
|
-
const path = globalConfigPath();
|
|
1036
|
-
const merged = {
|
|
1037
|
-
...readGlobalConfig(),
|
|
1038
|
-
...update
|
|
1039
|
-
};
|
|
1040
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
1041
|
-
writeFileSync(path, `${JSON.stringify(merged, null, 2)}\n`, { mode: 384 });
|
|
1042
|
-
}
|
|
1043
|
-
/** Walk up from cwd looking for seekrit.json. */
|
|
1044
|
-
function findProjectConfig(startDir = process.cwd()) {
|
|
1045
|
-
let dir = startDir;
|
|
1046
|
-
const { root } = parse(dir);
|
|
1047
|
-
while (true) {
|
|
1048
|
-
const candidate = join(dir, PROJECT_FILE);
|
|
1049
|
-
if (existsSync(candidate)) return JSON.parse(readFileSync(candidate, "utf8"));
|
|
1050
|
-
if (dir === root) return null;
|
|
1051
|
-
dir = dirname(dir);
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
function writeProjectConfig(config, dir = process.cwd()) {
|
|
1055
|
-
const path = join(dir, PROJECT_FILE);
|
|
1056
|
-
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`);
|
|
1057
|
-
return path;
|
|
1058
|
-
}
|
|
1231
|
+
var version = "0.15.0";
|
|
1059
1232
|
//#endregion
|
|
1060
1233
|
//#region ../../packages/api-client/src/index.ts
|
|
1061
1234
|
var SeekritApiError = class extends Error {
|
|
@@ -1263,26 +1436,60 @@ var SeekritClient = class {
|
|
|
1263
1436
|
return this.request("POST", `/v1/orgs/${orgId}/log-sink/test`);
|
|
1264
1437
|
}
|
|
1265
1438
|
};
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
/**
|
|
1270
|
-
* In `seekrit mcp` the process is a long-lived stdio server, so a `fail()`
|
|
1271
|
-
* must surface as a catchable error (→ a tool error result) rather than
|
|
1272
|
-
* exiting and killing every other tool. Toggled on once at MCP startup.
|
|
1273
|
-
*/
|
|
1274
|
-
function setFailThrows(value) {
|
|
1275
|
-
failThrows = value;
|
|
1439
|
+
const PROJECT_FILE = "seekrit.json";
|
|
1440
|
+
function globalConfigPath() {
|
|
1441
|
+
return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "seekrit", "config.json");
|
|
1276
1442
|
}
|
|
1277
|
-
function
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1443
|
+
function readGlobalConfig() {
|
|
1444
|
+
const path = globalConfigPath();
|
|
1445
|
+
if (!existsSync(path)) return {};
|
|
1446
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
1281
1447
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
const
|
|
1285
|
-
|
|
1448
|
+
function writeGlobalConfig(update) {
|
|
1449
|
+
const path = globalConfigPath();
|
|
1450
|
+
const merged = {
|
|
1451
|
+
...readGlobalConfig(),
|
|
1452
|
+
...update
|
|
1453
|
+
};
|
|
1454
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1455
|
+
writeFileSync(path, `${JSON.stringify(merged, null, 2)}\n`, { mode: 384 });
|
|
1456
|
+
}
|
|
1457
|
+
/** Walk up from cwd looking for seekrit.json. */
|
|
1458
|
+
function findProjectConfig(startDir = process.cwd()) {
|
|
1459
|
+
let dir = startDir;
|
|
1460
|
+
const { root } = parse(dir);
|
|
1461
|
+
while (true) {
|
|
1462
|
+
const candidate = join(dir, PROJECT_FILE);
|
|
1463
|
+
if (existsSync(candidate)) return JSON.parse(readFileSync(candidate, "utf8"));
|
|
1464
|
+
if (dir === root) return null;
|
|
1465
|
+
dir = dirname(dir);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
function writeProjectConfig(config, dir = process.cwd()) {
|
|
1469
|
+
const path = join(dir, PROJECT_FILE);
|
|
1470
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`);
|
|
1471
|
+
return path;
|
|
1472
|
+
}
|
|
1473
|
+
//#endregion
|
|
1474
|
+
//#region src/io.ts
|
|
1475
|
+
let failThrows = false;
|
|
1476
|
+
/**
|
|
1477
|
+
* In `seekrit mcp` the process is a long-lived stdio server, so a `fail()`
|
|
1478
|
+
* must surface as a catchable error (→ a tool error result) rather than
|
|
1479
|
+
* exiting and killing every other tool. Toggled on once at MCP startup.
|
|
1480
|
+
*/
|
|
1481
|
+
function setFailThrows(value) {
|
|
1482
|
+
failThrows = value;
|
|
1483
|
+
}
|
|
1484
|
+
function fail(message) {
|
|
1485
|
+
if (failThrows) throw new Error(message);
|
|
1486
|
+
console.error(`error: ${message}`);
|
|
1487
|
+
process.exit(1);
|
|
1488
|
+
}
|
|
1489
|
+
/** Prompt without echoing input (for passphrases). */
|
|
1490
|
+
function promptHidden(question) {
|
|
1491
|
+
const muted = new Writable({ write(_chunk, _encoding, callback) {
|
|
1492
|
+
callback();
|
|
1286
1493
|
} });
|
|
1287
1494
|
process.stderr.write(question);
|
|
1288
1495
|
const rl = createInterface({
|
|
@@ -1368,6 +1575,235 @@ async function getDek(ctx, orgId, envId) {
|
|
|
1368
1575
|
return unwrapDek(wrappedDek, privateKey);
|
|
1369
1576
|
}
|
|
1370
1577
|
//#endregion
|
|
1578
|
+
//#region src/target.ts
|
|
1579
|
+
/** Resolve the target org from a flag, the committed config, or a lone org. */
|
|
1580
|
+
async function resolveOrg(ctx, orgSlug) {
|
|
1581
|
+
const wanted = orgSlug ?? findProjectConfig()?.org;
|
|
1582
|
+
const { orgs } = await ctx.client.listOrgs();
|
|
1583
|
+
if (wanted) {
|
|
1584
|
+
const org = orgs.find((o) => o.slug === wanted || o.id === wanted);
|
|
1585
|
+
if (!org) fail(`no accessible org "${wanted}"`);
|
|
1586
|
+
return {
|
|
1587
|
+
id: org.id,
|
|
1588
|
+
slug: org.slug
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
const only = orgs[0];
|
|
1592
|
+
if (orgs.length === 1 && only) return {
|
|
1593
|
+
id: only.id,
|
|
1594
|
+
slug: only.slug
|
|
1595
|
+
};
|
|
1596
|
+
fail("specify --org (or run `seekrit init`)");
|
|
1597
|
+
}
|
|
1598
|
+
/**
|
|
1599
|
+
* Resolve an environment to operate on — an application env (`--app --env`,
|
|
1600
|
+
* or the config's app + `--env`) or a group env (`--group --env`).
|
|
1601
|
+
*/
|
|
1602
|
+
async function resolveEnvTarget(ctx, opts) {
|
|
1603
|
+
const org = await resolveOrg(ctx, opts.org);
|
|
1604
|
+
if (!opts.env) fail("specify --env");
|
|
1605
|
+
if (opts.group) {
|
|
1606
|
+
const { groups } = await ctx.client.listGroups(org.id);
|
|
1607
|
+
const group = groups.find((g) => g.slug === opts.group || g.id === opts.group);
|
|
1608
|
+
if (!group) fail(`no group "${opts.group}" in ${org.slug}`);
|
|
1609
|
+
const { environments } = await ctx.client.listGroupEnvs(org.id, group.id);
|
|
1610
|
+
const env = environments.find((e) => e.slug === opts.env || e.id === opts.env);
|
|
1611
|
+
if (!env) fail(`no environment "${opts.env}" in group ${group.slug}`);
|
|
1612
|
+
return {
|
|
1613
|
+
orgId: org.id,
|
|
1614
|
+
envId: env.id,
|
|
1615
|
+
label: `${group.slug}@${env.slug}`
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
const appSlug = opts.app ?? findProjectConfig()?.app;
|
|
1619
|
+
if (!appSlug) fail("specify --app or --group (or run `seekrit init`)");
|
|
1620
|
+
const { apps } = await ctx.client.listApps(org.id);
|
|
1621
|
+
const app = apps.find((a) => a.slug === appSlug || a.id === appSlug);
|
|
1622
|
+
if (!app) fail(`no app "${appSlug}" in ${org.slug}`);
|
|
1623
|
+
const { environments } = await ctx.client.listEnvs(org.id, app.id);
|
|
1624
|
+
const env = environments.find((e) => e.slug === opts.env || e.id === opts.env);
|
|
1625
|
+
if (!env) fail(`no environment "${opts.env}" in ${app.slug}`);
|
|
1626
|
+
return {
|
|
1627
|
+
orgId: org.id,
|
|
1628
|
+
envId: env.id,
|
|
1629
|
+
label: `${app.slug}/${env.slug}`
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
/** Resolve an application environment, keeping ids + slugs (for token binding). */
|
|
1633
|
+
async function resolveAppEnv(ctx, opts) {
|
|
1634
|
+
const org = await resolveOrg(ctx, opts.org);
|
|
1635
|
+
const appSlug = opts.app ?? findProjectConfig()?.app;
|
|
1636
|
+
if (!appSlug) fail("specify --app (or run `seekrit init`)");
|
|
1637
|
+
if (!opts.env) fail("specify --env");
|
|
1638
|
+
const { apps } = await ctx.client.listApps(org.id);
|
|
1639
|
+
const app = apps.find((a) => a.slug === appSlug || a.id === appSlug);
|
|
1640
|
+
if (!app) fail(`no app "${appSlug}" in ${org.slug}`);
|
|
1641
|
+
const { environments } = await ctx.client.listEnvs(org.id, app.id);
|
|
1642
|
+
const env = environments.find((e) => e.slug === opts.env || e.id === opts.env);
|
|
1643
|
+
if (!env) fail(`no environment "${opts.env}" in ${app.slug}`);
|
|
1644
|
+
return {
|
|
1645
|
+
orgId: org.id,
|
|
1646
|
+
appId: app.id,
|
|
1647
|
+
appSlug: app.slug,
|
|
1648
|
+
envId: env.id,
|
|
1649
|
+
envSlug: env.slug
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
/** Resolve a group by slug within the target org. */
|
|
1653
|
+
async function resolveGroup(ctx, opts) {
|
|
1654
|
+
const org = await resolveOrg(ctx, opts.org);
|
|
1655
|
+
if (!opts.group) fail("specify --group");
|
|
1656
|
+
const { groups } = await ctx.client.listGroups(org.id);
|
|
1657
|
+
const group = groups.find((g) => g.slug === opts.group || g.id === opts.group);
|
|
1658
|
+
if (!group) fail(`no group "${opts.group}" in ${org.slug}`);
|
|
1659
|
+
return {
|
|
1660
|
+
orgId: org.id,
|
|
1661
|
+
id: group.id,
|
|
1662
|
+
slug: group.slug
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
//#endregion
|
|
1666
|
+
//#region src/aws.ts
|
|
1667
|
+
/**
|
|
1668
|
+
* `seekrit aws` — temporary AWS credentials via STS AssumeRole (Vault-style
|
|
1669
|
+
* dynamic secrets, the tier-2 sibling of `seekrit pg`).
|
|
1670
|
+
*
|
|
1671
|
+
* Zero-knowledge for the leased credential: minting generates an ephemeral P-256
|
|
1672
|
+
* keypair on THIS machine and sends only the public key; STS mints the
|
|
1673
|
+
* credential and the broker returns it wrapped to that key, so the control plane
|
|
1674
|
+
* only ever relays ciphertext and only this machine can unwrap it. Registering a
|
|
1675
|
+
* target wraps the base IAM credential to the broker's public key locally, so
|
|
1676
|
+
* the control plane never sees it either — its only needed permission is
|
|
1677
|
+
* `sts:AssumeRole` on the target role.
|
|
1678
|
+
*/
|
|
1679
|
+
/** Parse a duration like `30m`, `1h`, or a bare seconds count. */
|
|
1680
|
+
function parseTtlSeconds$5(input) {
|
|
1681
|
+
const m = /^(\d+)\s*([smhd]?)$/.exec(input.trim());
|
|
1682
|
+
if (!m) fail(`invalid --ttl "${input}" (try 30m, 1h, 12h)`);
|
|
1683
|
+
return Number(m[1]) * ({
|
|
1684
|
+
s: 1,
|
|
1685
|
+
m: 60,
|
|
1686
|
+
h: 3600,
|
|
1687
|
+
d: 86400
|
|
1688
|
+
}[m[2] || "s"] ?? 1);
|
|
1689
|
+
}
|
|
1690
|
+
/**
|
|
1691
|
+
* The base IAM credential the broker assumes the role with. From flags or the
|
|
1692
|
+
* standard AWS env vars; JSON-serialized so the executor can parse it. Never the
|
|
1693
|
+
* plaintext leaves this machine unwrapped — it is wrapped to the broker key.
|
|
1694
|
+
*/
|
|
1695
|
+
function resolveBaseCredential(opts) {
|
|
1696
|
+
const accessKeyId = opts.accessKeyId ?? process.env.AWS_ACCESS_KEY_ID;
|
|
1697
|
+
const secretAccessKey = opts.secretAccessKey ?? process.env.AWS_SECRET_ACCESS_KEY;
|
|
1698
|
+
const sessionToken = process.env.AWS_SESSION_TOKEN;
|
|
1699
|
+
if (!accessKeyId || !secretAccessKey) fail("provide the base IAM credential via --access-key-id/--secret-access-key or AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY (it needs only sts:AssumeRole on the role)");
|
|
1700
|
+
return JSON.stringify({
|
|
1701
|
+
accessKeyId,
|
|
1702
|
+
secretAccessKey,
|
|
1703
|
+
...sessionToken ? { sessionToken } : {}
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
function registerAwsCommands(program) {
|
|
1707
|
+
const aws = program.command("aws").description("temporary AWS credentials (STS AssumeRole, zero-knowledge)");
|
|
1708
|
+
const target = aws.command("target").description("manage AWS role targets");
|
|
1709
|
+
target.command("add").description("register an assumable IAM role to issue temporary credentials from").requiredOption("--name <name>", "display name, e.g. prod-deploy").requiredOption("--role-arn <arn>", "the IAM role to assume, arn:aws:iam::<acct>:role/<name>").requiredOption("--region <region>", "region whose STS endpoint to call, e.g. us-east-1").option("--org <slug>").option("--external-id <id>", "STS ExternalId the role's trust policy requires").option("--session-policy <file>", "path to an inline session policy JSON (further restricts)").option("--max-ttl <duration>", "clamp requested credential lifetime, e.g. 8h").option("--access-key-id <id>", "base IAM access key id (else AWS_ACCESS_KEY_ID)").option("--secret-access-key <secret>", "base IAM secret (else AWS_SECRET_ACCESS_KEY)").action(async (options) => {
|
|
1710
|
+
const ctx = buildContext();
|
|
1711
|
+
const org = await resolveOrg(ctx, options.org);
|
|
1712
|
+
const sessionPolicy = options.sessionPolicy ? readFileSync(options.sessionPolicy, "utf8").trim() : void 0;
|
|
1713
|
+
const config = {
|
|
1714
|
+
provider: "aws",
|
|
1715
|
+
executor: "in_do",
|
|
1716
|
+
roleArn: options.roleArn,
|
|
1717
|
+
region: options.region,
|
|
1718
|
+
...options.externalId ? { externalId: options.externalId } : {},
|
|
1719
|
+
...sessionPolicy ? { sessionPolicy } : {},
|
|
1720
|
+
...options.maxTtl ? { maxTtlSeconds: parseTtlSeconds$5(options.maxTtl) } : {}
|
|
1721
|
+
};
|
|
1722
|
+
const baseCredential = resolveBaseCredential(options);
|
|
1723
|
+
const { publicKeyJwk } = await ctx.client.getLeaseBrokerKey(org.id);
|
|
1724
|
+
const wrappedAdminSecret = await wrapDek(new TextEncoder().encode(baseCredential), publicKeyJwk);
|
|
1725
|
+
const { target: created } = await ctx.client.registerLeaseTarget(org.id, {
|
|
1726
|
+
name: options.name,
|
|
1727
|
+
config,
|
|
1728
|
+
wrappedAdminSecret
|
|
1729
|
+
});
|
|
1730
|
+
console.error(`registered AWS target ${created.name} (${created.id})`);
|
|
1731
|
+
console.error("\nEnsure the role trusts the admin principal, then `seekrit aws lease`:\n");
|
|
1732
|
+
console.log(awsTrustPolicyInstructions(config));
|
|
1733
|
+
});
|
|
1734
|
+
target.command("list").description("list AWS role targets").option("--org <slug>").action(async (options) => {
|
|
1735
|
+
const ctx = buildContext();
|
|
1736
|
+
const org = await resolveOrg(ctx, options.org);
|
|
1737
|
+
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
1738
|
+
for (const t of targets) {
|
|
1739
|
+
const cfg = t.config;
|
|
1740
|
+
if (cfg.provider !== "aws") continue;
|
|
1741
|
+
console.log(`${t.id}\t${t.name}\t${cfg.region}\t${cfg.roleArn}`);
|
|
1742
|
+
}
|
|
1743
|
+
});
|
|
1744
|
+
target.command("trust <targetId>").description("reprint the IAM trust-policy setup for an AWS target").option("--org <slug>").action(async (targetId, options) => {
|
|
1745
|
+
const ctx = buildContext();
|
|
1746
|
+
const org = await resolveOrg(ctx, options.org);
|
|
1747
|
+
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
1748
|
+
const t = targets.find((x) => x.id === targetId || x.name === targetId);
|
|
1749
|
+
if (!t) fail(`no target "${targetId}" in ${org.slug}`);
|
|
1750
|
+
const cfg = t.config;
|
|
1751
|
+
if (cfg.provider !== "aws") fail("not an aws target (see `seekrit pg`/`seekrit ssh`)");
|
|
1752
|
+
console.log(awsTrustPolicyInstructions(cfg));
|
|
1753
|
+
});
|
|
1754
|
+
target.command("rm <targetId>").description("delete an AWS role target").option("--org <slug>").action(async (targetId, options) => {
|
|
1755
|
+
const ctx = buildContext();
|
|
1756
|
+
const org = await resolveOrg(ctx, options.org);
|
|
1757
|
+
await ctx.client.deleteLeaseTarget(org.id, targetId);
|
|
1758
|
+
console.error(`deleted ${targetId}`);
|
|
1759
|
+
});
|
|
1760
|
+
aws.command("lease <target>").description("mint short-lived AWS credentials; prints ready-to-source export lines").option("--org <slug>").option("--ttl <duration>", "credential lifetime, e.g. 15m, 1h, 12h", "1h").option("--json", "print the full credential as JSON").action(async (targetRef, options) => {
|
|
1761
|
+
const ctx = buildContext();
|
|
1762
|
+
const org = await resolveOrg(ctx, options.org);
|
|
1763
|
+
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
1764
|
+
const t = targets.find((x) => x.id === targetRef || x.name === targetRef);
|
|
1765
|
+
if (!t) fail(`no target "${targetRef}" in ${org.slug}`);
|
|
1766
|
+
const cfg = t.config;
|
|
1767
|
+
if (cfg.provider !== "aws") fail(`"${t.name}" is not an aws target (see \`seekrit pg\`)`);
|
|
1768
|
+
const ttlSeconds = parseTtlSeconds$5(options.ttl);
|
|
1769
|
+
if (ttlSeconds < 900) fail(`--ttl must be at least ${900 / 60}m (STS minimum)`);
|
|
1770
|
+
const recipient = await generateAwsRecipientKeyPair();
|
|
1771
|
+
const { aws: leased } = await ctx.client.mintLease(org.id, {
|
|
1772
|
+
provider: "aws",
|
|
1773
|
+
targetId: t.id,
|
|
1774
|
+
recipientPublicKey: recipient.publicKeyJwk,
|
|
1775
|
+
ttlSeconds
|
|
1776
|
+
});
|
|
1777
|
+
const cred = await unwrapAwsCredential(leased.wrappedCredential, recipient.privateKeyJwk);
|
|
1778
|
+
console.error(`leased ${cfg.roleArn} in ${cred.region} — expires ${cred.expiration}`);
|
|
1779
|
+
if (options.json) console.log(JSON.stringify({
|
|
1780
|
+
...cred,
|
|
1781
|
+
roleArn: cfg.roleArn
|
|
1782
|
+
}, null, 2));
|
|
1783
|
+
else {
|
|
1784
|
+
console.log(`export AWS_ACCESS_KEY_ID=${cred.accessKeyId}`);
|
|
1785
|
+
console.log(`export AWS_SECRET_ACCESS_KEY=${cred.secretAccessKey}`);
|
|
1786
|
+
console.log(`export AWS_SESSION_TOKEN=${cred.sessionToken}`);
|
|
1787
|
+
console.log(`export AWS_REGION=${cred.region}`);
|
|
1788
|
+
}
|
|
1789
|
+
});
|
|
1790
|
+
aws.command("leases").description("list AWS leases (the ledger — never secret material)").option("--org <slug>").action(async (options) => {
|
|
1791
|
+
const ctx = buildContext();
|
|
1792
|
+
const org = await resolveOrg(ctx, options.org);
|
|
1793
|
+
const { leases } = await ctx.client.listLeases(org.id);
|
|
1794
|
+
for (const l of leases) {
|
|
1795
|
+
if (l.provider !== "aws") continue;
|
|
1796
|
+
console.log(`${l.id}\t${l.status}\texpires ${l.expiresAt}`);
|
|
1797
|
+
}
|
|
1798
|
+
});
|
|
1799
|
+
aws.command("revoke <leaseId>").description("mark a lease revoked in the ledger (STS credentials stay valid until they expire)").option("--org <slug>").action(async (leaseId, options) => {
|
|
1800
|
+
const ctx = buildContext();
|
|
1801
|
+
const org = await resolveOrg(ctx, options.org);
|
|
1802
|
+
await ctx.client.revokeLease(org.id, leaseId);
|
|
1803
|
+
console.error(`revoked ${leaseId} (issued credentials remain valid until they expire)`);
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
//#endregion
|
|
1371
1807
|
//#region src/dotenv.ts
|
|
1372
1808
|
/**
|
|
1373
1809
|
* Minimal `.env` parser: `KEY=VALUE`, `#` comments, an optional `export`
|
|
@@ -1419,6 +1855,147 @@ function formatSecrets(values, format) {
|
|
|
1419
1855
|
}
|
|
1420
1856
|
}
|
|
1421
1857
|
//#endregion
|
|
1858
|
+
//#region src/gcp.ts
|
|
1859
|
+
/**
|
|
1860
|
+
* `seekrit gcp` — temporary GCP credentials via IAM Credentials
|
|
1861
|
+
* `generateAccessToken` (Vault-style dynamic secrets, the tier-2 sibling of
|
|
1862
|
+
* `seekrit aws`).
|
|
1863
|
+
*
|
|
1864
|
+
* Zero-knowledge for the leased credential: minting generates an ephemeral P-256
|
|
1865
|
+
* keypair on THIS machine and sends only the public key; GCP mints the token and
|
|
1866
|
+
* the broker returns it wrapped to that key, so the control plane only ever
|
|
1867
|
+
* relays ciphertext and only this machine can unwrap it. Registering a target
|
|
1868
|
+
* wraps the service-account key JSON to the broker's public key locally, so the
|
|
1869
|
+
* control plane never sees it either — the source service account needs only
|
|
1870
|
+
* `roles/iam.serviceAccountTokenCreator` on the target.
|
|
1871
|
+
*/
|
|
1872
|
+
/** Parse a duration like `30m`, `1h`, or a bare seconds count. */
|
|
1873
|
+
function parseTtlSeconds$4(input) {
|
|
1874
|
+
const m = /^(\d+)\s*([smhd]?)$/.exec(input.trim());
|
|
1875
|
+
if (!m) fail(`invalid --ttl "${input}" (try 30m, 1h, 12h)`);
|
|
1876
|
+
return Number(m[1]) * ({
|
|
1877
|
+
s: 1,
|
|
1878
|
+
m: 60,
|
|
1879
|
+
h: 3600,
|
|
1880
|
+
d: 86400
|
|
1881
|
+
}[m[2] || "s"] ?? 1);
|
|
1882
|
+
}
|
|
1883
|
+
/** Collect a repeatable flag (e.g. --scope) into a list. */
|
|
1884
|
+
function collectList$1(value, acc = []) {
|
|
1885
|
+
acc.push(value);
|
|
1886
|
+
return acc;
|
|
1887
|
+
}
|
|
1888
|
+
/**
|
|
1889
|
+
* The service-account key JSON the broker impersonates with. From --key-file or
|
|
1890
|
+
* GOOGLE_APPLICATION_CREDENTIALS. Never leaves this machine unwrapped — it is
|
|
1891
|
+
* wrapped to the broker key before upload.
|
|
1892
|
+
*/
|
|
1893
|
+
function resolveServiceAccountKey(opts) {
|
|
1894
|
+
const path = opts.keyFile ?? process.env.GOOGLE_APPLICATION_CREDENTIALS;
|
|
1895
|
+
if (!path) fail("provide the source service-account key JSON via --key-file or GOOGLE_APPLICATION_CREDENTIALS (it needs roles/iam.serviceAccountTokenCreator on the target)");
|
|
1896
|
+
const raw = readFileSync(path, "utf8").trim();
|
|
1897
|
+
try {
|
|
1898
|
+
const parsed = JSON.parse(raw);
|
|
1899
|
+
if (typeof parsed.client_email !== "string" || typeof parsed.private_key !== "string") fail(`${path} is not a service-account key JSON (missing client_email/private_key)`);
|
|
1900
|
+
} catch {
|
|
1901
|
+
fail(`${path} is not valid JSON`);
|
|
1902
|
+
}
|
|
1903
|
+
return raw;
|
|
1904
|
+
}
|
|
1905
|
+
function registerGcpCommands(program) {
|
|
1906
|
+
const gcp = program.command("gcp").description("temporary GCP credentials (IAM generateAccessToken, zero-knowledge)");
|
|
1907
|
+
const target = gcp.command("target").description("manage GCP service-account targets");
|
|
1908
|
+
target.command("add").description("register an impersonable service account to issue temporary tokens from").requiredOption("--name <name>", "display name, e.g. prod-deploy").requiredOption("--service-account <email>", "the service account to impersonate, name@project.iam.gserviceaccount.com").option("--org <slug>").option("--scope <scope>", "OAuth scope to grant (repeatable; default cloud-platform)", collectList$1).option("--delegate <email>", "delegation-chain service account (repeatable)", collectList$1).option("--max-ttl <duration>", "clamp requested token lifetime, e.g. 1h").option("--key-file <path>", "source SA key JSON (else GOOGLE_APPLICATION_CREDENTIALS)").action(async (options) => {
|
|
1909
|
+
const ctx = buildContext();
|
|
1910
|
+
const org = await resolveOrg(ctx, options.org);
|
|
1911
|
+
const config = {
|
|
1912
|
+
provider: "gcp",
|
|
1913
|
+
executor: "in_do",
|
|
1914
|
+
serviceAccount: options.serviceAccount,
|
|
1915
|
+
...options.scope?.length ? { scopes: options.scope } : {},
|
|
1916
|
+
...options.delegate?.length ? { delegates: options.delegate } : {},
|
|
1917
|
+
...options.maxTtl ? { maxTtlSeconds: parseTtlSeconds$4(options.maxTtl) } : {}
|
|
1918
|
+
};
|
|
1919
|
+
const keyJson = resolveServiceAccountKey(options);
|
|
1920
|
+
const { publicKeyJwk } = await ctx.client.getLeaseBrokerKey(org.id);
|
|
1921
|
+
const wrappedAdminSecret = await wrapDek(new TextEncoder().encode(keyJson), publicKeyJwk);
|
|
1922
|
+
const { target: created } = await ctx.client.registerLeaseTarget(org.id, {
|
|
1923
|
+
name: options.name,
|
|
1924
|
+
config,
|
|
1925
|
+
wrappedAdminSecret
|
|
1926
|
+
});
|
|
1927
|
+
console.error(`registered GCP target ${created.name} (${created.id})`);
|
|
1928
|
+
console.error("\nGrant the source SA the token-creator role, then `seekrit gcp lease`:\n");
|
|
1929
|
+
console.log(gcpSetupInstructions(config));
|
|
1930
|
+
});
|
|
1931
|
+
target.command("list").description("list GCP service-account targets").option("--org <slug>").action(async (options) => {
|
|
1932
|
+
const ctx = buildContext();
|
|
1933
|
+
const org = await resolveOrg(ctx, options.org);
|
|
1934
|
+
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
1935
|
+
for (const t of targets) {
|
|
1936
|
+
const cfg = t.config;
|
|
1937
|
+
if (cfg.provider !== "gcp") continue;
|
|
1938
|
+
console.log(`${t.id}\t${t.name}\t${cfg.serviceAccount}`);
|
|
1939
|
+
}
|
|
1940
|
+
});
|
|
1941
|
+
target.command("setup <targetId>").description("reprint the IAM setup for a GCP target").option("--org <slug>").action(async (targetId, options) => {
|
|
1942
|
+
const ctx = buildContext();
|
|
1943
|
+
const org = await resolveOrg(ctx, options.org);
|
|
1944
|
+
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
1945
|
+
const t = targets.find((x) => x.id === targetId || x.name === targetId);
|
|
1946
|
+
if (!t) fail(`no target "${targetId}" in ${org.slug}`);
|
|
1947
|
+
const cfg = t.config;
|
|
1948
|
+
if (cfg.provider !== "gcp") fail("not a gcp target (see `seekrit aws`/`seekrit ssh`)");
|
|
1949
|
+
console.log(gcpSetupInstructions(cfg));
|
|
1950
|
+
});
|
|
1951
|
+
target.command("rm <targetId>").description("delete a GCP service-account target").option("--org <slug>").action(async (targetId, options) => {
|
|
1952
|
+
const ctx = buildContext();
|
|
1953
|
+
const org = await resolveOrg(ctx, options.org);
|
|
1954
|
+
await ctx.client.deleteLeaseTarget(org.id, targetId);
|
|
1955
|
+
console.error(`deleted ${targetId}`);
|
|
1956
|
+
});
|
|
1957
|
+
gcp.command("lease <target>").description("mint a short-lived GCP access token; prints ready-to-source export lines").option("--org <slug>").option("--ttl <duration>", "token lifetime, e.g. 15m, 1h", "1h").option("--json", "print the full credential as JSON").action(async (targetRef, options) => {
|
|
1958
|
+
const ctx = buildContext();
|
|
1959
|
+
const org = await resolveOrg(ctx, options.org);
|
|
1960
|
+
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
1961
|
+
const t = targets.find((x) => x.id === targetRef || x.name === targetRef);
|
|
1962
|
+
if (!t) fail(`no target "${targetRef}" in ${org.slug}`);
|
|
1963
|
+
if (t.config.provider !== "gcp") fail(`"${t.name}" is not a gcp target (see \`seekrit aws\`)`);
|
|
1964
|
+
const ttlSeconds = parseTtlSeconds$4(options.ttl);
|
|
1965
|
+
if (ttlSeconds < 60) fail(`--ttl must be at least 60s`);
|
|
1966
|
+
if (ttlSeconds > 43200) fail(`--ttl must be at most ${GCP_MAX_TTL_SECONDS / 3600}h`);
|
|
1967
|
+
const recipient = await generateGcpRecipientKeyPair();
|
|
1968
|
+
const { gcp: leased } = await ctx.client.mintLease(org.id, {
|
|
1969
|
+
provider: "gcp",
|
|
1970
|
+
targetId: t.id,
|
|
1971
|
+
recipientPublicKey: recipient.publicKeyJwk,
|
|
1972
|
+
ttlSeconds
|
|
1973
|
+
});
|
|
1974
|
+
const cred = await unwrapGcpCredential(leased.wrappedCredential, recipient.privateKeyJwk);
|
|
1975
|
+
console.error(`leased ${cred.serviceAccount} — expires ${cred.expiration}`);
|
|
1976
|
+
if (options.json) console.log(JSON.stringify(cred, null, 2));
|
|
1977
|
+
else {
|
|
1978
|
+
console.log(`export CLOUDSDK_AUTH_ACCESS_TOKEN=${cred.accessToken}`);
|
|
1979
|
+
console.log(`export GOOGLE_OAUTH_ACCESS_TOKEN=${cred.accessToken}`);
|
|
1980
|
+
}
|
|
1981
|
+
});
|
|
1982
|
+
gcp.command("leases").description("list GCP leases (the ledger — never secret material)").option("--org <slug>").action(async (options) => {
|
|
1983
|
+
const ctx = buildContext();
|
|
1984
|
+
const org = await resolveOrg(ctx, options.org);
|
|
1985
|
+
const { leases } = await ctx.client.listLeases(org.id);
|
|
1986
|
+
for (const l of leases) {
|
|
1987
|
+
if (l.provider !== "gcp") continue;
|
|
1988
|
+
console.log(`${l.id}\t${l.status}\texpires ${l.expiresAt}`);
|
|
1989
|
+
}
|
|
1990
|
+
});
|
|
1991
|
+
gcp.command("revoke <leaseId>").description("mark a lease revoked in the ledger (tokens stay valid until they expire)").option("--org <slug>").action(async (leaseId, options) => {
|
|
1992
|
+
const ctx = buildContext();
|
|
1993
|
+
const org = await resolveOrg(ctx, options.org);
|
|
1994
|
+
await ctx.client.revokeLease(org.id, leaseId);
|
|
1995
|
+
console.error(`revoked ${leaseId} (issued tokens remain valid until they expire)`);
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
//#endregion
|
|
1422
1999
|
//#region src/provisioner.ts
|
|
1423
2000
|
/**
|
|
1424
2001
|
* `seekrit provisioner` — helpers for the self-hosted **remote executor**
|
|
@@ -1462,94 +2039,6 @@ function resolveLeaseAdminSecret(opts) {
|
|
|
1462
2039
|
return adminUrl;
|
|
1463
2040
|
}
|
|
1464
2041
|
//#endregion
|
|
1465
|
-
//#region src/target.ts
|
|
1466
|
-
/** Resolve the target org from a flag, the committed config, or a lone org. */
|
|
1467
|
-
async function resolveOrg(ctx, orgSlug) {
|
|
1468
|
-
const wanted = orgSlug ?? findProjectConfig()?.org;
|
|
1469
|
-
const { orgs } = await ctx.client.listOrgs();
|
|
1470
|
-
if (wanted) {
|
|
1471
|
-
const org = orgs.find((o) => o.slug === wanted || o.id === wanted);
|
|
1472
|
-
if (!org) fail(`no accessible org "${wanted}"`);
|
|
1473
|
-
return {
|
|
1474
|
-
id: org.id,
|
|
1475
|
-
slug: org.slug
|
|
1476
|
-
};
|
|
1477
|
-
}
|
|
1478
|
-
const only = orgs[0];
|
|
1479
|
-
if (orgs.length === 1 && only) return {
|
|
1480
|
-
id: only.id,
|
|
1481
|
-
slug: only.slug
|
|
1482
|
-
};
|
|
1483
|
-
fail("specify --org (or run `seekrit init`)");
|
|
1484
|
-
}
|
|
1485
|
-
/**
|
|
1486
|
-
* Resolve an environment to operate on — an application env (`--app --env`,
|
|
1487
|
-
* or the config's app + `--env`) or a group env (`--group --env`).
|
|
1488
|
-
*/
|
|
1489
|
-
async function resolveEnvTarget(ctx, opts) {
|
|
1490
|
-
const org = await resolveOrg(ctx, opts.org);
|
|
1491
|
-
if (!opts.env) fail("specify --env");
|
|
1492
|
-
if (opts.group) {
|
|
1493
|
-
const { groups } = await ctx.client.listGroups(org.id);
|
|
1494
|
-
const group = groups.find((g) => g.slug === opts.group || g.id === opts.group);
|
|
1495
|
-
if (!group) fail(`no group "${opts.group}" in ${org.slug}`);
|
|
1496
|
-
const { environments } = await ctx.client.listGroupEnvs(org.id, group.id);
|
|
1497
|
-
const env = environments.find((e) => e.slug === opts.env || e.id === opts.env);
|
|
1498
|
-
if (!env) fail(`no environment "${opts.env}" in group ${group.slug}`);
|
|
1499
|
-
return {
|
|
1500
|
-
orgId: org.id,
|
|
1501
|
-
envId: env.id,
|
|
1502
|
-
label: `${group.slug}@${env.slug}`
|
|
1503
|
-
};
|
|
1504
|
-
}
|
|
1505
|
-
const appSlug = opts.app ?? findProjectConfig()?.app;
|
|
1506
|
-
if (!appSlug) fail("specify --app or --group (or run `seekrit init`)");
|
|
1507
|
-
const { apps } = await ctx.client.listApps(org.id);
|
|
1508
|
-
const app = apps.find((a) => a.slug === appSlug || a.id === appSlug);
|
|
1509
|
-
if (!app) fail(`no app "${appSlug}" in ${org.slug}`);
|
|
1510
|
-
const { environments } = await ctx.client.listEnvs(org.id, app.id);
|
|
1511
|
-
const env = environments.find((e) => e.slug === opts.env || e.id === opts.env);
|
|
1512
|
-
if (!env) fail(`no environment "${opts.env}" in ${app.slug}`);
|
|
1513
|
-
return {
|
|
1514
|
-
orgId: org.id,
|
|
1515
|
-
envId: env.id,
|
|
1516
|
-
label: `${app.slug}/${env.slug}`
|
|
1517
|
-
};
|
|
1518
|
-
}
|
|
1519
|
-
/** Resolve an application environment, keeping ids + slugs (for token binding). */
|
|
1520
|
-
async function resolveAppEnv(ctx, opts) {
|
|
1521
|
-
const org = await resolveOrg(ctx, opts.org);
|
|
1522
|
-
const appSlug = opts.app ?? findProjectConfig()?.app;
|
|
1523
|
-
if (!appSlug) fail("specify --app (or run `seekrit init`)");
|
|
1524
|
-
if (!opts.env) fail("specify --env");
|
|
1525
|
-
const { apps } = await ctx.client.listApps(org.id);
|
|
1526
|
-
const app = apps.find((a) => a.slug === appSlug || a.id === appSlug);
|
|
1527
|
-
if (!app) fail(`no app "${appSlug}" in ${org.slug}`);
|
|
1528
|
-
const { environments } = await ctx.client.listEnvs(org.id, app.id);
|
|
1529
|
-
const env = environments.find((e) => e.slug === opts.env || e.id === opts.env);
|
|
1530
|
-
if (!env) fail(`no environment "${opts.env}" in ${app.slug}`);
|
|
1531
|
-
return {
|
|
1532
|
-
orgId: org.id,
|
|
1533
|
-
appId: app.id,
|
|
1534
|
-
appSlug: app.slug,
|
|
1535
|
-
envId: env.id,
|
|
1536
|
-
envSlug: env.slug
|
|
1537
|
-
};
|
|
1538
|
-
}
|
|
1539
|
-
/** Resolve a group by slug within the target org. */
|
|
1540
|
-
async function resolveGroup(ctx, opts) {
|
|
1541
|
-
const org = await resolveOrg(ctx, opts.org);
|
|
1542
|
-
if (!opts.group) fail("specify --group");
|
|
1543
|
-
const { groups } = await ctx.client.listGroups(org.id);
|
|
1544
|
-
const group = groups.find((g) => g.slug === opts.group || g.id === opts.group);
|
|
1545
|
-
if (!group) fail(`no group "${opts.group}" in ${org.slug}`);
|
|
1546
|
-
return {
|
|
1547
|
-
orgId: org.id,
|
|
1548
|
-
id: group.id,
|
|
1549
|
-
slug: group.slug
|
|
1550
|
-
};
|
|
1551
|
-
}
|
|
1552
|
-
//#endregion
|
|
1553
2042
|
//#region src/mysql.ts
|
|
1554
2043
|
/**
|
|
1555
2044
|
* `seekrit mysql` — temporary MySQL/MariaDB credentials (Vault-style dynamic
|
|
@@ -2596,8 +3085,10 @@ registerMysqlCommands(program);
|
|
|
2596
3085
|
registerRedisCommands(program);
|
|
2597
3086
|
registerProvisionerCommands(program);
|
|
2598
3087
|
registerSshCommands(program);
|
|
3088
|
+
registerAwsCommands(program);
|
|
3089
|
+
registerGcpCommands(program);
|
|
2599
3090
|
program.command("mcp").description("run an MCP server over stdio so AI agents can drive seekrit").action(async () => {
|
|
2600
|
-
const { runMcpServer } = await import("./mcp-
|
|
3091
|
+
const { runMcpServer } = await import("./mcp-ARBuneH3.js");
|
|
2601
3092
|
await runMcpServer();
|
|
2602
3093
|
});
|
|
2603
3094
|
program.command("audit").description("show the org audit trail").option("--org <slug>").option("--limit <n>", "entries to fetch", "50").action(async (options) => {
|
|
@@ -2611,4 +3102,4 @@ program.parseAsync(argv).catch((err) => {
|
|
|
2611
3102
|
fail(err instanceof Error ? err.message : String(err));
|
|
2612
3103
|
});
|
|
2613
3104
|
//#endregion
|
|
2614
|
-
export {
|
|
3105
|
+
export { generatePostgresCredential as _, resolveEnvTarget as a, generateDek as b, getDek as c, setFailThrows as d, writeProjectConfig as f, parseServiceToken as g, isServiceToken as h, resolveAppEnv as i, isTokenAuth as l, createServiceToken as m, fetchDecryptedSecrets as n, resolveGroup as o, version as p, materializeEnv as r, resolveOrg as s, encryptAndSetSecret as t, tryBuildContext as u, generateMysqlCredential as v, wrapDek as y };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as
|
|
1
|
+
import { _ as generatePostgresCredential, a as resolveEnvTarget, b as generateDek, c as getDek, d as setFailThrows, f as writeProjectConfig, g as parseServiceToken, h as isServiceToken, i as resolveAppEnv, l as isTokenAuth, m as createServiceToken, n as fetchDecryptedSecrets, o as resolveGroup, p as version, r as materializeEnv, s as resolveOrg, t as encryptAndSetSecret, u as tryBuildContext, v as generateMysqlCredential, y as wrapDek } from "./index.js";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seekrit/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "End-to-end encrypted secrets manager CLI — inject decrypted secrets into any command.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/node": "^26.1.0",
|
|
25
25
|
"tsdown": "^0.22.3",
|
|
26
|
-
"@seekrit/api-client": "0.0.1",
|
|
27
26
|
"@seekrit/core": "0.0.1",
|
|
28
|
-
"@seekrit/crypto": "0.0.1"
|
|
27
|
+
"@seekrit/crypto": "0.0.1",
|
|
28
|
+
"@seekrit/api-client": "0.0.1"
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
31
|
"build": "tsdown",
|