@seekrit/mcp 0.7.0 → 0.8.1
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 +607 -31
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -45,6 +45,192 @@ z.object({
|
|
|
45
45
|
path: z.string().trim().min(1).max(2048),
|
|
46
46
|
secret: policySecretNameSchema.optional()
|
|
47
47
|
});
|
|
48
|
+
/** One aggregated cell: a dimension tuple and how many times it happened. */
|
|
49
|
+
const activityEntrySchema = z.object({
|
|
50
|
+
host: policyHostSchema,
|
|
51
|
+
method: policyMethodSchema,
|
|
52
|
+
decision: z.enum([
|
|
53
|
+
"allow",
|
|
54
|
+
"no_rule",
|
|
55
|
+
"method_not_allowed",
|
|
56
|
+
"path_not_allowed",
|
|
57
|
+
"secret_not_allowed",
|
|
58
|
+
"unknown_secret",
|
|
59
|
+
"ratchet_withdrawn",
|
|
60
|
+
"policy_unavailable"
|
|
61
|
+
]),
|
|
62
|
+
/**
|
|
63
|
+
* Which published rule decided, when one did. Null for refusals that never
|
|
64
|
+
* reached a rule (`no_rule`, `policy_unavailable`) — the distinction matters to
|
|
65
|
+
* a review, because "rule 3 refused this" and "nothing covered this" call for
|
|
66
|
+
* opposite changes.
|
|
67
|
+
*/
|
|
68
|
+
ruleIndex: z.number().int().min(0).max(255).nullable(),
|
|
69
|
+
count: z.number().int().min(1).max(1e6),
|
|
70
|
+
/**
|
|
71
|
+
* Secret names actually injected, name → count. Only meaningful on `allow`.
|
|
72
|
+
* This is what lets a review say "rule 2 permits three secrets and the agent
|
|
73
|
+
* has only ever used one" — the most useful narrowing there is, and impossible
|
|
74
|
+
* to see from policy alone.
|
|
75
|
+
*/
|
|
76
|
+
secrets: z.record(policySecretNameSchema, z.number().int().min(1)).optional()
|
|
77
|
+
});
|
|
78
|
+
z.object({
|
|
79
|
+
/** Start of the window these counts cover (ISO 8601). */
|
|
80
|
+
windowStart: z.string().trim().min(20).max(40),
|
|
81
|
+
/** Policy version in force while they were collected, for the ledger. */
|
|
82
|
+
policyVersion: z.number().int().min(0).optional(),
|
|
83
|
+
/**
|
|
84
|
+
* Capped so one report cannot be unbounded work. A proxy with more distinct
|
|
85
|
+
* cells than this in a window has a policy far broader than a review can help
|
|
86
|
+
* with, and truncating loudly beats accepting anything.
|
|
87
|
+
*/
|
|
88
|
+
entries: z.array(activityEntrySchema).min(1).max(500)
|
|
89
|
+
});
|
|
90
|
+
/**
|
|
91
|
+
* Twelve hours, matching `[control] max_ttl` in a proxy config. A task is meant
|
|
92
|
+
* to bound one run; something that needs longer wants a policy change, not a
|
|
93
|
+
* longer ticket.
|
|
94
|
+
*/
|
|
95
|
+
const TASK_MAX_TTL_SECONDS = 720 * 60;
|
|
96
|
+
/** An EC P-256 public JWK, for a sender-constraint proof key. */
|
|
97
|
+
const taskProofJwkSchema = z.object({
|
|
98
|
+
kty: z.literal("EC"),
|
|
99
|
+
crv: z.literal("P-256"),
|
|
100
|
+
x: z.string().min(1).max(128),
|
|
101
|
+
y: z.string().min(1).max(128)
|
|
102
|
+
});
|
|
103
|
+
z.object({
|
|
104
|
+
/**
|
|
105
|
+
* The public `skd_…` segment of the minted token. Sent because the API never
|
|
106
|
+
* sees the token at dispatch and still needs a readable handle for the audit
|
|
107
|
+
* row and for a revoke to name — the id half of a credential, without the
|
|
108
|
+
* secret half.
|
|
109
|
+
*/
|
|
110
|
+
taskRef: z.string().trim().regex(/^skd_[0-9A-Za-z]+$/, "taskRef must be the skd_… segment of the minted token"),
|
|
111
|
+
/**
|
|
112
|
+
* SHA-256 (base64url) of the token the dispatcher minted. The token itself
|
|
113
|
+
* never reaches this API on the dispatch path — only on introspection, where
|
|
114
|
+
* it is hashed and discarded.
|
|
115
|
+
*/
|
|
116
|
+
tokenHash: z.string().trim().min(16).max(128),
|
|
117
|
+
/**
|
|
118
|
+
* Secret names this run may use. Omit for "whatever the agent's policy
|
|
119
|
+
* allows" — mirroring `Session.scopes: Option<BTreeSet<String>>` in the proxy,
|
|
120
|
+
* so absent means unnarrowed in both places.
|
|
121
|
+
*/
|
|
122
|
+
scopes: z.array(policySecretNameSchema).max(64).optional(),
|
|
123
|
+
ttlSeconds: z.number().int().min(60).max(TASK_MAX_TTL_SECONDS).optional(),
|
|
124
|
+
/**
|
|
125
|
+
* What this run is for, for the audit row and the operator's task list. Free
|
|
126
|
+
* text, and **not** a security input: never put a secret value in it.
|
|
127
|
+
*/
|
|
128
|
+
label: z.string().trim().max(200).optional(),
|
|
129
|
+
/**
|
|
130
|
+
* Public half of a proof key the presenter holds, recorded as an RFC 7638
|
|
131
|
+
* thumbprint. See `AgentTaskSession.proofThumbprint` for what this does and —
|
|
132
|
+
* importantly — does not yet do.
|
|
133
|
+
*/
|
|
134
|
+
proofJwk: taskProofJwkSchema.optional()
|
|
135
|
+
});
|
|
136
|
+
z.object({
|
|
137
|
+
/** The presented token. In the body, never a URL — it is a credential. */
|
|
138
|
+
token: z.string().trim().min(8).max(512) });
|
|
139
|
+
//#endregion
|
|
140
|
+
//#region ../../packages/core/src/archive.ts
|
|
141
|
+
/**
|
|
142
|
+
* The **break-glass archive** format: one signed JSON file holding everything
|
|
143
|
+
* seekrit stores for an org, in the form seekrit stores it — ciphertext stays
|
|
144
|
+
* ciphertext. Its whole purpose is to be openable on a machine that has never
|
|
145
|
+
* heard of seekrit, so the format is plain JSON, self-describing, and versioned
|
|
146
|
+
* by name (`seekrit-archive/v1`); a breaking change ships a new format string
|
|
147
|
+
* rather than mutating this one, exactly like the `sc1.`/`wd1.` blob prefixes.
|
|
148
|
+
*
|
|
149
|
+
* Three parts:
|
|
150
|
+
* - `manifest` — what this archive is, and a SHA-256 digest per section.
|
|
151
|
+
* - `signature` — Ed25519 over the canonical manifest, or null when the
|
|
152
|
+
* producing deployment has no signing key configured.
|
|
153
|
+
* - `data` — the sections themselves.
|
|
154
|
+
*
|
|
155
|
+
* Integrity fields (`digest`, `signature.value`, `publicKey`, `keyId`) are
|
|
156
|
+
* lowercase hex. Every blob *inside* `data` keeps its native base64url form, so
|
|
157
|
+
* the one encoding rule to remember is "the archive's own bookkeeping is hex,
|
|
158
|
+
* seekrit's blobs are unchanged".
|
|
159
|
+
*
|
|
160
|
+
* See docs/break-glass-export.md for what is deliberately excluded and why.
|
|
161
|
+
*/
|
|
162
|
+
const ARCHIVE_FORMAT = "seekrit-archive/v1";
|
|
163
|
+
const sectionHeaderSchema = z.object({
|
|
164
|
+
name: z.enum([
|
|
165
|
+
"organization",
|
|
166
|
+
"users",
|
|
167
|
+
"memberships",
|
|
168
|
+
"invites",
|
|
169
|
+
"applications",
|
|
170
|
+
"groups",
|
|
171
|
+
"environments",
|
|
172
|
+
"environmentGroups",
|
|
173
|
+
"environmentKeys",
|
|
174
|
+
"secrets",
|
|
175
|
+
"secretVersions",
|
|
176
|
+
"serviceTokens",
|
|
177
|
+
"m2mClients",
|
|
178
|
+
"kmsKeys",
|
|
179
|
+
"kmsKeyVersions",
|
|
180
|
+
"kmsKeyGrants",
|
|
181
|
+
"recoveryConfig",
|
|
182
|
+
"recoveryShares",
|
|
183
|
+
"rotations",
|
|
184
|
+
"syncConnections",
|
|
185
|
+
"syncBindings",
|
|
186
|
+
"leaseTargets",
|
|
187
|
+
"agentIdentities",
|
|
188
|
+
"agentPolicies",
|
|
189
|
+
"auditLog",
|
|
190
|
+
"keyMaterial"
|
|
191
|
+
]),
|
|
192
|
+
count: z.number().int().min(0),
|
|
193
|
+
truncated: z.boolean(),
|
|
194
|
+
digest: z.string().regex(/^sha256:[0-9a-f]{64}$/)
|
|
195
|
+
});
|
|
196
|
+
const manifestSchema = z.object({
|
|
197
|
+
archiveId: z.string().min(1),
|
|
198
|
+
createdAt: z.string().min(1),
|
|
199
|
+
org: z.object({
|
|
200
|
+
id: z.string(),
|
|
201
|
+
slug: z.string(),
|
|
202
|
+
name: z.string()
|
|
203
|
+
}),
|
|
204
|
+
producer: z.object({
|
|
205
|
+
service: z.string(),
|
|
206
|
+
environment: z.string(),
|
|
207
|
+
formatVersion: z.string()
|
|
208
|
+
}),
|
|
209
|
+
requestedBy: z.object({
|
|
210
|
+
actorType: z.string(),
|
|
211
|
+
actorId: z.string(),
|
|
212
|
+
label: z.string().nullable()
|
|
213
|
+
}),
|
|
214
|
+
options: z.object({
|
|
215
|
+
includeVersions: z.boolean(),
|
|
216
|
+
includeAudit: z.boolean(),
|
|
217
|
+
auditLimit: z.number().int().min(0)
|
|
218
|
+
}),
|
|
219
|
+
sections: z.array(sectionHeaderSchema),
|
|
220
|
+
digest: z.string().regex(/^sha256:[0-9a-f]{64}$/)
|
|
221
|
+
});
|
|
222
|
+
const signatureSchema = z.object({
|
|
223
|
+
algorithm: z.literal("ed25519"),
|
|
224
|
+
publicKey: z.string().regex(/^[0-9a-f]{64}$/),
|
|
225
|
+
keyId: z.string().regex(/^[0-9a-f]{16}$/),
|
|
226
|
+
value: z.string().regex(/^[0-9a-f]{128}$/)
|
|
227
|
+
});
|
|
228
|
+
z.object({
|
|
229
|
+
format: z.literal(ARCHIVE_FORMAT),
|
|
230
|
+
manifest: manifestSchema,
|
|
231
|
+
signature: signatureSchema.nullable(),
|
|
232
|
+
data: z.record(z.string(), z.unknown())
|
|
233
|
+
});
|
|
48
234
|
/** All catalog keys as a runtime array (for iteration / zod enums). */
|
|
49
235
|
const ENTITLEMENT_KEYS = Object.keys({
|
|
50
236
|
"feature.kms": {
|
|
@@ -891,6 +1077,13 @@ const inviteRoleSchema = z.enum(["admin", "member"]);
|
|
|
891
1077
|
const principalTypeSchema = z.enum(["user", "service_token"]);
|
|
892
1078
|
/** Org-level capability a service token can hold (never `owner`). */
|
|
893
1079
|
const serviceTokenRoleSchema = z.enum(["admin", "member"]);
|
|
1080
|
+
z.object({
|
|
1081
|
+
/** Include the full append-only ciphertext history of every secret. */
|
|
1082
|
+
includeVersions: z.boolean().optional(),
|
|
1083
|
+
includeAudit: z.boolean().optional(),
|
|
1084
|
+
/** Newest audit rows to keep. Exceeding the server cap is a 400, not a silent trim. */
|
|
1085
|
+
auditLimit: z.number().int().min(0).optional()
|
|
1086
|
+
});
|
|
894
1087
|
z.object({
|
|
895
1088
|
name: nameSchema,
|
|
896
1089
|
slug: slugSchema
|
|
@@ -902,6 +1095,8 @@ z.object({
|
|
|
902
1095
|
z.object({ name: nameSchema });
|
|
903
1096
|
z.object({ name: nameSchema });
|
|
904
1097
|
z.object({ name: nameSchema });
|
|
1098
|
+
z.object({ name: nameSchema });
|
|
1099
|
+
z.object({ name: nameSchema });
|
|
905
1100
|
z.object({ required: z.boolean() });
|
|
906
1101
|
z.object({
|
|
907
1102
|
email: emailSchema,
|
|
@@ -941,6 +1136,16 @@ z.object({
|
|
|
941
1136
|
*/
|
|
942
1137
|
encryptedPrivateKey: z.string().min(1)
|
|
943
1138
|
});
|
|
1139
|
+
z.object({
|
|
1140
|
+
/** Base64url WebAuthn credential id — what `allowCredentials` is built from. */
|
|
1141
|
+
credentialId: z.string().min(1).max(512),
|
|
1142
|
+
/** Human label for the device, e.g. "MacBook Touch ID". Display only. */
|
|
1143
|
+
label: z.string().min(1).max(64),
|
|
1144
|
+
/** Base64url PRF evaluation input for this credential; not a secret. */
|
|
1145
|
+
prfInput: z.string().min(1).max(256),
|
|
1146
|
+
/** The private key wrapped to this passkey's PRF output. */
|
|
1147
|
+
encryptedPrivateKey: z.string().min(1).max(8192).refine((v) => v.startsWith("pk2."), { message: "expected a pk2. passkey wrap" })
|
|
1148
|
+
});
|
|
944
1149
|
const grantEnvironmentKeySchema = z.object({
|
|
945
1150
|
principalType: principalTypeSchema,
|
|
946
1151
|
principalId: z.string().min(1),
|
|
@@ -1242,7 +1447,10 @@ z.enum([
|
|
|
1242
1447
|
"netlify",
|
|
1243
1448
|
"bunnyshell",
|
|
1244
1449
|
"github-actions",
|
|
1245
|
-
"gcp-secret-manager"
|
|
1450
|
+
"gcp-secret-manager",
|
|
1451
|
+
"langgraph-platform",
|
|
1452
|
+
"azure-key-vault",
|
|
1453
|
+
"huggingface-spaces"
|
|
1246
1454
|
]);
|
|
1247
1455
|
/**
|
|
1248
1456
|
* Vercel account scope. The API token itself is never here — it is wrapped to
|
|
@@ -1507,6 +1715,119 @@ const gcpSecretManagerConnectionConfigSchema = z.object({
|
|
|
1507
1715
|
/** Project ID (`acme-prod`) or project number. */
|
|
1508
1716
|
projectId: gcpProjectSchema
|
|
1509
1717
|
});
|
|
1718
|
+
/**
|
|
1719
|
+
* LangSmith workspace/tenant scope for LangGraph Platform.
|
|
1720
|
+
*
|
|
1721
|
+
* The API key is never here — it is wrapped to the connection's public key and
|
|
1722
|
+
* stored as ciphertext, exactly as Vercel's token is.
|
|
1723
|
+
*
|
|
1724
|
+
* Two optional fields, for two different situations, and setting both is
|
|
1725
|
+
* rejected rather than silently resolved:
|
|
1726
|
+
*
|
|
1727
|
+
* - `region` picks one of {@link LANGGRAPH_PLATFORM_HOSTS}. Omitted means
|
|
1728
|
+
* `us`, which is where an account created at `smith.langchain.com` lives.
|
|
1729
|
+
* - `baseUrl` points the connection at a **self-hosted** LangSmith install,
|
|
1730
|
+
* whose control plane is served from the customer's own host under
|
|
1731
|
+
* `/api-host` rather than from `*.api.host.langchain.com`.
|
|
1732
|
+
*
|
|
1733
|
+
* `tenantId` is the workspace a key was minted in. A workspace-scoped key names
|
|
1734
|
+
* its own tenant and does not need it; an organization-scoped key reaches
|
|
1735
|
+
* several workspaces and gets a bare 403 without it, which is the same trap
|
|
1736
|
+
* Vercel's `teamId` sets — so it is passed through as `X-Tenant-Id` whenever
|
|
1737
|
+
* it is present.
|
|
1738
|
+
*/
|
|
1739
|
+
const langgraphPlatformConnectionConfigSchema = z.object({
|
|
1740
|
+
provider: z.literal("langgraph-platform"),
|
|
1741
|
+
/** Control-plane region. Omit for `us`. Mutually exclusive with `baseUrl`. */
|
|
1742
|
+
region: z.enum([
|
|
1743
|
+
"us",
|
|
1744
|
+
"eu",
|
|
1745
|
+
"apac",
|
|
1746
|
+
"aws-us"
|
|
1747
|
+
]).optional(),
|
|
1748
|
+
/**
|
|
1749
|
+
* Self-hosted LangSmith control-plane root, e.g.
|
|
1750
|
+
* `https://langsmith.acme.com/api-host`. Omit for LangChain's own hosts.
|
|
1751
|
+
* Must be `https:` — this URL carries the API key.
|
|
1752
|
+
*/
|
|
1753
|
+
baseUrl: z.string().trim().max(300).refine((value) => {
|
|
1754
|
+
let parsed;
|
|
1755
|
+
try {
|
|
1756
|
+
parsed = new URL(value);
|
|
1757
|
+
} catch {
|
|
1758
|
+
return false;
|
|
1759
|
+
}
|
|
1760
|
+
return parsed.protocol === "https:" && !parsed.username && !parsed.password;
|
|
1761
|
+
}, "must be an https:// URL — the self-hosted control-plane root, e.g. https://langsmith.acme.com/api-host").optional(),
|
|
1762
|
+
/** LangSmith workspace (tenant) UUID, sent as `X-Tenant-Id`. */
|
|
1763
|
+
tenantId: z.string().trim().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, "must be a LangSmith workspace UUID").optional()
|
|
1764
|
+
}).refine((c) => !(c.baseUrl !== void 0 && c.region !== void 0), {
|
|
1765
|
+
message: "set region for a LangChain-hosted account or baseUrl for a self-hosted one, not both",
|
|
1766
|
+
path: ["baseUrl"]
|
|
1767
|
+
});
|
|
1768
|
+
/**
|
|
1769
|
+
* The Azure clouds a vault can live in.
|
|
1770
|
+
*
|
|
1771
|
+
* Unlike AWS, where the China partition can be read off the region string
|
|
1772
|
+
* (`cn-…`), nothing about a tenant id or a vault name says which cloud it
|
|
1773
|
+
* belongs to — and two hosts have to agree with the answer: the Entra authority
|
|
1774
|
+
* that issues the token and the DNS suffix the vault answers on. Getting either
|
|
1775
|
+
* wrong is a failure inside an alarm with nobody watching, so it is stated.
|
|
1776
|
+
*/
|
|
1777
|
+
const AZURE_CLOUDS = [
|
|
1778
|
+
"public",
|
|
1779
|
+
"usgov",
|
|
1780
|
+
"china"
|
|
1781
|
+
];
|
|
1782
|
+
/**
|
|
1783
|
+
* A Microsoft Entra directory (tenant) or application (client) id.
|
|
1784
|
+
*
|
|
1785
|
+
* Both are GUIDs. Entra accepts a verified domain name in place of a tenant id
|
|
1786
|
+
* in the token URL, but not a client id, and taking only the GUID for both
|
|
1787
|
+
* keeps one rule — a tenant's GUID is on the same admin-center page as the
|
|
1788
|
+
* client id it pairs with, so nothing is harder to find.
|
|
1789
|
+
*/
|
|
1790
|
+
const azureGuidSchema = z.string().trim().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, "must be a GUID, as the Entra admin center shows it");
|
|
1791
|
+
/**
|
|
1792
|
+
* Azure account scope: which directory to authenticate against, as which app.
|
|
1793
|
+
*
|
|
1794
|
+
* The split follows AWS's, and for the same reason. A service principal's
|
|
1795
|
+
* **client secret** is the credential and is wrapped to the connection's key;
|
|
1796
|
+
* the tenant and client ids are *identifiers* — they appear in the token
|
|
1797
|
+
* request URL, in sign-in logs, and on the app registration blade. Keeping them
|
|
1798
|
+
* here lets the dashboard say which principal a connection authenticates as,
|
|
1799
|
+
* which is the first thing worth knowing when a connection starts failing after
|
|
1800
|
+
* a secret expires.
|
|
1801
|
+
*
|
|
1802
|
+
* Client secrets are the only credential kind here: a certificate or federated
|
|
1803
|
+
* credential would need a private key or a trust relationship the sync engine
|
|
1804
|
+
* has nowhere to keep, and Entra caps a client secret at 24 months, which is a
|
|
1805
|
+
* rotation the connection's `lastError` will make loud.
|
|
1806
|
+
*/
|
|
1807
|
+
const azureKeyVaultConnectionConfigSchema = z.object({
|
|
1808
|
+
provider: z.literal("azure-key-vault"),
|
|
1809
|
+
/** Entra directory (tenant) ID. */
|
|
1810
|
+
tenantId: azureGuidSchema,
|
|
1811
|
+
/** Application (client) ID of the service principal seekrit signs in as. */
|
|
1812
|
+
clientId: azureGuidSchema,
|
|
1813
|
+
/** Which Azure cloud the tenant and its vaults live in. */
|
|
1814
|
+
cloud: z.enum(AZURE_CLOUDS).default("public")
|
|
1815
|
+
});
|
|
1816
|
+
/**
|
|
1817
|
+
* Hugging Face account scope — empty, as Render's and Fly's are.
|
|
1818
|
+
*
|
|
1819
|
+
* Neither half of "which account, which thing" needs stating. A Hub user access
|
|
1820
|
+
* token belongs to one user and carries their write access to every Space they
|
|
1821
|
+
* or their organizations own; a Space is addressed by `owner/name`, which is
|
|
1822
|
+
* globally unique. So the token plus the destination is the whole address.
|
|
1823
|
+
*
|
|
1824
|
+
* There is deliberately no `baseUrl` twin of the GitHub Enterprise Server
|
|
1825
|
+
* field. `HF_ENDPOINT` exists in the Python client for Hub *mirrors*, which
|
|
1826
|
+
* serve repository content — not the settings API this connector writes, and
|
|
1827
|
+
* not something a mirror is expected to accept a write on. Adding the field
|
|
1828
|
+
* would invite pointing a connection at a host that silently swallows secrets.
|
|
1829
|
+
*/
|
|
1830
|
+
const huggingfaceSpacesConnectionConfigSchema = z.object({ provider: z.literal("huggingface-spaces") });
|
|
1510
1831
|
const syncConnectionConfigSchema = z.discriminatedUnion("provider", [
|
|
1511
1832
|
vercelConnectionConfigSchema,
|
|
1512
1833
|
cloudflareWorkersConnectionConfigSchema,
|
|
@@ -1523,7 +1844,10 @@ const syncConnectionConfigSchema = z.discriminatedUnion("provider", [
|
|
|
1523
1844
|
netlifyConnectionConfigSchema,
|
|
1524
1845
|
bunnyshellConnectionConfigSchema,
|
|
1525
1846
|
githubActionsConnectionConfigSchema,
|
|
1526
|
-
gcpSecretManagerConnectionConfigSchema
|
|
1847
|
+
gcpSecretManagerConnectionConfigSchema,
|
|
1848
|
+
langgraphPlatformConnectionConfigSchema,
|
|
1849
|
+
azureKeyVaultConnectionConfigSchema,
|
|
1850
|
+
huggingfaceSpacesConnectionConfigSchema
|
|
1527
1851
|
]);
|
|
1528
1852
|
const vercelDestinationSchema = z.object({
|
|
1529
1853
|
provider: z.literal("vercel"),
|
|
@@ -2210,6 +2534,98 @@ const gcpSecretManagerDestinationSchema = z.object({
|
|
|
2210
2534
|
message: "a customer-managed key covers one location — use automatic replication, or a single location",
|
|
2211
2535
|
path: ["kmsKeyName"]
|
|
2212
2536
|
});
|
|
2537
|
+
/**
|
|
2538
|
+
* One LangGraph Platform (Agent Server) **deployment**, addressed by its id.
|
|
2539
|
+
*
|
|
2540
|
+
* A deployment is the whole unit here: its secrets are a property of the
|
|
2541
|
+
* deployment, delivered to the agent container as environment variables, and
|
|
2542
|
+
* there is nothing finer to point at — no per-revision or per-graph scope, and
|
|
2543
|
+
* no equivalent of Vercel's `production`/`preview` split. A deployment that
|
|
2544
|
+
* needs different values is a different deployment, so it is a different
|
|
2545
|
+
* binding.
|
|
2546
|
+
*
|
|
2547
|
+
* Validated as a UUID because `PATCH /v2/deployments/{deployment_id}` declares
|
|
2548
|
+
* the path parameter as one: a name or a URL slug in the slot fails validation
|
|
2549
|
+
* at the control plane hours later inside an alarm, with nobody watching. It is
|
|
2550
|
+
* the `id` from `GET /v2/deployments`, and the UUID in the deployment's
|
|
2551
|
+
* dashboard URL.
|
|
2552
|
+
*/
|
|
2553
|
+
const langgraphPlatformDestinationSchema = z.object({
|
|
2554
|
+
provider: z.literal("langgraph-platform"),
|
|
2555
|
+
/** Deployment UUID, from the dashboard URL or `GET /v2/deployments`. */
|
|
2556
|
+
deploymentId: z.string().trim().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, "must be a LangGraph Platform deployment UUID")
|
|
2557
|
+
});
|
|
2558
|
+
/**
|
|
2559
|
+
* A Key Vault's name — the leftmost label of its DNS name, so the vault
|
|
2560
|
+
* `acme-prod` answers at `acme-prod.vault.azure.net`.
|
|
2561
|
+
*
|
|
2562
|
+
* Azure's own rule, stated here because the failure it prevents is confusing:
|
|
2563
|
+
* 3–24 characters, alphanumerics and hyphens, starting with a letter, ending
|
|
2564
|
+
* with a letter or digit, and no run of two hyphens. A name that breaks it
|
|
2565
|
+
* cannot exist, so a typo is a DNS failure rather than a 404 — an error that
|
|
2566
|
+
* says nothing about what was wrong.
|
|
2567
|
+
*
|
|
2568
|
+
* The *base URL* is not taken instead. It would let a connection be pointed at
|
|
2569
|
+
* any host, and the whole address seekrit needs is this label plus the cloud
|
|
2570
|
+
* already named on the connection.
|
|
2571
|
+
*/
|
|
2572
|
+
const azureVaultNameSchema = z.string().trim().regex(/^[A-Za-z](?!.*--)[A-Za-z0-9-]{1,22}[A-Za-z0-9]$/, "must be a Key Vault name: 3–24 letters, digits, and single hyphens, starting with a letter");
|
|
2573
|
+
/**
|
|
2574
|
+
* Which vault a binding writes to, and under what names.
|
|
2575
|
+
*
|
|
2576
|
+
* There is no layout choice as Secrets Manager has: the reason a `json-bundle`
|
|
2577
|
+
* exists there is billing — AWS charges per secret per month — and Key Vault
|
|
2578
|
+
* charges per *operation*, so fifty names cost the same stored fifty ways. One
|
|
2579
|
+
* secret per name is simply correct here.
|
|
2580
|
+
*/
|
|
2581
|
+
const azureKeyVaultDestinationSchema = z.object({
|
|
2582
|
+
provider: z.literal("azure-key-vault"),
|
|
2583
|
+
/** Vault name, e.g. `acme-prod` for `acme-prod.vault.azure.net`. */
|
|
2584
|
+
vault: azureVaultNameSchema,
|
|
2585
|
+
/**
|
|
2586
|
+
* Prepended to every secret name, e.g. `storefront-`. Key Vault has no
|
|
2587
|
+
* hierarchy — its names are flat, and `/` is not among the characters it
|
|
2588
|
+
* accepts — so unlike Parameter Store's `path` this is a naming convention
|
|
2589
|
+
* and nothing more. Worth setting in a vault that holds anything else.
|
|
2590
|
+
*/
|
|
2591
|
+
prefix: z.string().trim().max(64).regex(/^[A-Za-z0-9-]*$/, "may contain letters, digits, and hyphens").optional(),
|
|
2592
|
+
/** What to do with a name Key Vault cannot store — see {@link AZURE_KEY_VAULT_NAME_MODES}. */
|
|
2593
|
+
nameMode: z.enum(["dash", "reject"]).default("dash")
|
|
2594
|
+
});
|
|
2595
|
+
/**
|
|
2596
|
+
* The Space whose secrets a binding owns, addressed the way the Hub addresses
|
|
2597
|
+
* every repository: `owner/name`, where `owner` is a user or an organization.
|
|
2598
|
+
*
|
|
2599
|
+
* A Space has **one** secret set, shared by every replica — there is no
|
|
2600
|
+
* per-target split to state, the way Vercel and Pages have one. The Hub's
|
|
2601
|
+
* convention is that staging and production are separate Spaces
|
|
2602
|
+
* (`acme/demo`, `acme/demo-staging`), so pointing at an environment means
|
|
2603
|
+
* naming that Space, exactly as a Fly environment means naming its own app.
|
|
2604
|
+
*
|
|
2605
|
+
* Validated by shape because the two habitual slips both fail *late*: pasting
|
|
2606
|
+
* the browser URL (`https://huggingface.co/spaces/acme/demo`) or the bare name
|
|
2607
|
+
* without its owner. Either one is a 404 from the Hub inside an alarm with
|
|
2608
|
+
* nobody watching, and a 404 says nothing about which half was wrong. The
|
|
2609
|
+
* leading character is held to alphanumeric, which is also what rejects the
|
|
2610
|
+
* `.` and `..` that no repository may be called.
|
|
2611
|
+
*/
|
|
2612
|
+
const huggingfaceRepoIdSchema = z.string().trim().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,95}\/[A-Za-z0-9][A-Za-z0-9._-]{0,95}$/, "must be a Space ID as owner/name, e.g. acme/support-demo — not a URL");
|
|
2613
|
+
/**
|
|
2614
|
+
* One Hugging Face **Space**, whose secrets arrive in the app's container as
|
|
2615
|
+
* environment variables.
|
|
2616
|
+
*
|
|
2617
|
+
* Only secrets. A Space also has *variables*, and they are not a second lane
|
|
2618
|
+
* seekrit could use: the Hub calls them "non-sensitive configuration values",
|
|
2619
|
+
* they are "publicly accessible and viewable", and they are copied into every
|
|
2620
|
+
* Space duplicated from this one. Writing a seekrit secret there would publish
|
|
2621
|
+
* it, so this connector has no variables mode — a binding that wants one is
|
|
2622
|
+
* asking for the wrong thing.
|
|
2623
|
+
*/
|
|
2624
|
+
const huggingfaceSpacesDestinationSchema = z.object({
|
|
2625
|
+
provider: z.literal("huggingface-spaces"),
|
|
2626
|
+
/** Space ID as `owner/name`, e.g. `acme/support-demo`. */
|
|
2627
|
+
repoId: huggingfaceRepoIdSchema
|
|
2628
|
+
});
|
|
2213
2629
|
const syncDestinationSchema = z.discriminatedUnion("provider", [
|
|
2214
2630
|
vercelDestinationSchema,
|
|
2215
2631
|
cloudflareWorkersDestinationSchema,
|
|
@@ -2226,7 +2642,10 @@ const syncDestinationSchema = z.discriminatedUnion("provider", [
|
|
|
2226
2642
|
netlifyDestinationSchema,
|
|
2227
2643
|
bunnyshellDestinationSchema,
|
|
2228
2644
|
githubActionsDestinationSchema,
|
|
2229
|
-
gcpSecretManagerDestinationSchema
|
|
2645
|
+
gcpSecretManagerDestinationSchema,
|
|
2646
|
+
langgraphPlatformDestinationSchema,
|
|
2647
|
+
azureKeyVaultDestinationSchema,
|
|
2648
|
+
huggingfaceSpacesDestinationSchema
|
|
2230
2649
|
]);
|
|
2231
2650
|
/**
|
|
2232
2651
|
* How seekrit secret names become destination key names. Applied in order:
|
|
@@ -2956,7 +3375,7 @@ function isServiceToken(value) {
|
|
|
2956
3375
|
}
|
|
2957
3376
|
//#endregion
|
|
2958
3377
|
//#region ../cli/package.json
|
|
2959
|
-
var version$1 = "
|
|
3378
|
+
var version$1 = "1.2.2";
|
|
2960
3379
|
const PROJECT_FILE = "seekrit.json";
|
|
2961
3380
|
function globalConfigPath() {
|
|
2962
3381
|
return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "seekrit", "config.json");
|
|
@@ -3078,6 +3497,22 @@ var SeekritClient = class {
|
|
|
3078
3497
|
revokeCliSession(sessionId) {
|
|
3079
3498
|
return this.request("DELETE", `/v1/me/cli-sessions/${sessionId}`);
|
|
3080
3499
|
}
|
|
3500
|
+
/**
|
|
3501
|
+
* The passkeys enrolled to unlock this user's keyring, each with the `pk2.`
|
|
3502
|
+
* blob its PRF output decrypts. One call is everything the unlock ceremony
|
|
3503
|
+
* needs; the blobs are opaque without the authenticator.
|
|
3504
|
+
*/
|
|
3505
|
+
listMyPasskeys() {
|
|
3506
|
+
return this.request("GET", "/v1/me/passkeys");
|
|
3507
|
+
}
|
|
3508
|
+
/** File a private key already wrapped, client-side, to a passkey's PRF output. */
|
|
3509
|
+
enrollMyPasskey(input) {
|
|
3510
|
+
return this.request("POST", "/v1/me/passkeys", input);
|
|
3511
|
+
}
|
|
3512
|
+
/** Stop a passkey unlocking the keyring. The key itself is untouched. */
|
|
3513
|
+
deleteMyPasskey(passkeyId) {
|
|
3514
|
+
return this.request("DELETE", `/v1/me/passkeys/${passkeyId}`);
|
|
3515
|
+
}
|
|
3081
3516
|
/** What a pending login request is asking for — for the approval screen. */
|
|
3082
3517
|
getCliLoginRequest(code) {
|
|
3083
3518
|
return this.request("GET", `/v1/cli-login/${encodeURIComponent(code)}`);
|
|
@@ -3157,6 +3592,10 @@ var SeekritClient = class {
|
|
|
3157
3592
|
getEnv(orgId, envId) {
|
|
3158
3593
|
return this.request("GET", `/v1/orgs/${orgId}/envs/${envId}`);
|
|
3159
3594
|
}
|
|
3595
|
+
/** Rename an environment (display name only — the slug is immutable). */
|
|
3596
|
+
updateEnv(orgId, envId, input) {
|
|
3597
|
+
return this.request("PATCH", `/v1/orgs/${orgId}/envs/${envId}`, input);
|
|
3598
|
+
}
|
|
3160
3599
|
deleteEnv(orgId, envId) {
|
|
3161
3600
|
return this.request("DELETE", `/v1/orgs/${orgId}/envs/${envId}`);
|
|
3162
3601
|
}
|
|
@@ -3310,6 +3749,10 @@ var SeekritClient = class {
|
|
|
3310
3749
|
createToken(orgId, input) {
|
|
3311
3750
|
return this.request("POST", `/v1/orgs/${orgId}/tokens`, input);
|
|
3312
3751
|
}
|
|
3752
|
+
/** Rename a token. Role, environment binding, and expiry are immutable. */
|
|
3753
|
+
updateToken(orgId, tokenId, input) {
|
|
3754
|
+
return this.request("PATCH", `/v1/orgs/${orgId}/tokens/${tokenId}`, input);
|
|
3755
|
+
}
|
|
3313
3756
|
revokeToken(orgId, tokenId) {
|
|
3314
3757
|
return this.request("DELETE", `/v1/orgs/${orgId}/tokens/${tokenId}`);
|
|
3315
3758
|
}
|
|
@@ -3365,6 +3808,77 @@ var SeekritClient = class {
|
|
|
3365
3808
|
getMyPolicySigner(orgId) {
|
|
3366
3809
|
return this.request("GET", `/v1/orgs/${orgId}/agents/signers/me`);
|
|
3367
3810
|
}
|
|
3811
|
+
/**
|
|
3812
|
+
* The bundle a proxy would see — `GET /v1/agents/:ref/policy`, the same route
|
|
3813
|
+
* `seekrit-proxy` polls, resolved by agent id or slug.
|
|
3814
|
+
*
|
|
3815
|
+
* Not org-scoped, because the caller is not: a proxy holds a service token that
|
|
3816
|
+
* knows an agent slug and nothing about org ids. Reachable with any service
|
|
3817
|
+
* token bound to the agent's org (or a user session), which is what lets
|
|
3818
|
+
* `seekrit proxy init` generate a config on the machine that holds the proxy's
|
|
3819
|
+
* own token rather than requiring an admin credential there.
|
|
3820
|
+
*
|
|
3821
|
+
* The `bundle` is signed and opaque to the API. Anything that *acts* on it must
|
|
3822
|
+
* verify the signature against locally pinned signers; decoding it for display
|
|
3823
|
+
* or to name a route is not acting on it.
|
|
3824
|
+
*/
|
|
3825
|
+
getAgentPolicyBundle(agentRef) {
|
|
3826
|
+
return this.request("GET", `/v1/agents/${encodeURIComponent(agentRef)}/policy`);
|
|
3827
|
+
}
|
|
3828
|
+
/**
|
|
3829
|
+
* Dispatch a task for one agent run.
|
|
3830
|
+
*
|
|
3831
|
+
* The caller mints the token (`createAgentTaskToken` in `@seekrit/crypto`) and
|
|
3832
|
+
* sends only its hash plus the public `skd_…` segment, so no presentable
|
|
3833
|
+
* credential ever reaches this API — the same shape as service-token and CLI
|
|
3834
|
+
* session creation. `scopes` may only narrow what the agent's published policy
|
|
3835
|
+
* already permits; a name outside it is refused rather than dropped.
|
|
3836
|
+
*
|
|
3837
|
+
* Not org-scoped, because an orchestrator is not: it knows an agent slug.
|
|
3838
|
+
*/
|
|
3839
|
+
dispatchAgentTask(agentRef, input) {
|
|
3840
|
+
return this.request("POST", `/v1/agents/${encodeURIComponent(agentRef)}/dispatch`, input);
|
|
3841
|
+
}
|
|
3842
|
+
/**
|
|
3843
|
+
* Exchange a presented token for the session it authorizes — what an
|
|
3844
|
+
* enforcement point calls once per task and caches until expiry.
|
|
3845
|
+
*
|
|
3846
|
+
* A POST because the token is a credential and must not land in a URL or an
|
|
3847
|
+
* access log. Fails closed and says which way: revoked, expired, or a disabled
|
|
3848
|
+
* identity are three different answers.
|
|
3849
|
+
*/
|
|
3850
|
+
introspectAgentTask(token) {
|
|
3851
|
+
return this.request("POST", "/v1/tasks/introspect", { token });
|
|
3852
|
+
}
|
|
3853
|
+
/** End a run's authority now. Idempotent. */
|
|
3854
|
+
revokeAgentTask(taskId) {
|
|
3855
|
+
return this.request("POST", `/v1/tasks/${taskId}/revoke`);
|
|
3856
|
+
}
|
|
3857
|
+
getAgentTask(taskId) {
|
|
3858
|
+
return this.request("GET", `/v1/tasks/${taskId}`);
|
|
3859
|
+
}
|
|
3860
|
+
/** Runs dispatched for one identity, newest first (admin). */
|
|
3861
|
+
listAgentTasks(orgId, agentId) {
|
|
3862
|
+
return this.request("GET", `/v1/orgs/${orgId}/agents/${agentId}/tasks`);
|
|
3863
|
+
}
|
|
3864
|
+
/**
|
|
3865
|
+
* Report aggregate decisions. Called by an enforcement point, not a person.
|
|
3866
|
+
*
|
|
3867
|
+
* Counts only — hosts, methods, secret *names*, decisions, and rule indices.
|
|
3868
|
+
* Never a request path: see the module comment in `agent-activity.ts` for why
|
|
3869
|
+
* that line is drawn where it is.
|
|
3870
|
+
*/
|
|
3871
|
+
reportAgentActivity(agentRef, input) {
|
|
3872
|
+
return this.request("POST", `/v1/agents/${encodeURIComponent(agentRef)}/activity`, input);
|
|
3873
|
+
}
|
|
3874
|
+
/**
|
|
3875
|
+
* What an agent actually did, collapsed onto its dimensions — the evidence a
|
|
3876
|
+
* grant review reasons over. The proposals themselves are computed client-side
|
|
3877
|
+
* (`reviewPolicy` in `@seekrit/core`), so the API never opines on policy.
|
|
3878
|
+
*/
|
|
3879
|
+
getAgentActivity(orgId, agentId, days = 14) {
|
|
3880
|
+
return this.request("GET", `/v1/orgs/${orgId}/agents/${agentId}/activity?days=${encodeURIComponent(String(days))}`);
|
|
3881
|
+
}
|
|
3368
3882
|
/** Keys the caller can see: all org keys for admins, granted keys otherwise. */
|
|
3369
3883
|
listKmsKeys(orgId) {
|
|
3370
3884
|
return this.request("GET", `/v1/orgs/${orgId}/kms/keys`);
|
|
@@ -3517,6 +4031,17 @@ var SeekritClient = class {
|
|
|
3517
4031
|
const qs = params.size > 0 ? `?${params}` : "";
|
|
3518
4032
|
return this.request("GET", `/v1/orgs/${orgId}/audit${qs}`);
|
|
3519
4033
|
}
|
|
4034
|
+
/**
|
|
4035
|
+
* Export the org as one signed archive: every row seekrit holds for it, with
|
|
4036
|
+
* ciphertext still ciphertext (docs/break-glass-export.md).
|
|
4037
|
+
*
|
|
4038
|
+
* The archive comes back inline rather than as a job handle, and it can be
|
|
4039
|
+
* megabytes — buffer it to a file rather than holding several copies. Requires
|
|
4040
|
+
* admin; deliberately not entitlement-gated.
|
|
4041
|
+
*/
|
|
4042
|
+
exportArchive(orgId, input = {}) {
|
|
4043
|
+
return this.request("POST", `/v1/orgs/${orgId}/export`, input);
|
|
4044
|
+
}
|
|
3520
4045
|
getLogSink(orgId) {
|
|
3521
4046
|
return this.request("GET", `/v1/orgs/${orgId}/log-sink`);
|
|
3522
4047
|
}
|
|
@@ -3594,7 +4119,18 @@ function fail(message) {
|
|
|
3594
4119
|
console.error(`error: ${message}`);
|
|
3595
4120
|
process.exit(1);
|
|
3596
4121
|
}
|
|
3597
|
-
/**
|
|
4122
|
+
/**
|
|
4123
|
+
* Prompt without echoing input (for passphrases).
|
|
4124
|
+
*
|
|
4125
|
+
* Piping the answer in (`echo … | seekrit secrets get …`) is supported and
|
|
4126
|
+
* common in CI, so this reads stdin rather than insisting on a TTY. But stdin
|
|
4127
|
+
* can also close with nothing on it — `< /dev/null`, a closed pipe, an agent
|
|
4128
|
+
* spawning us with no stdin — and readline signals that by emitting `close`
|
|
4129
|
+
* without ever calling the `question` callback. Left unhandled the promise
|
|
4130
|
+
* never settles, the event loop drains, and Node exits **0** having printed
|
|
4131
|
+
* nothing: `V=$(seekrit secrets get X)` silently yields an empty value and a
|
|
4132
|
+
* success status. So treat EOF-without-an-answer as the error it is.
|
|
4133
|
+
*/
|
|
3598
4134
|
function promptHidden(question) {
|
|
3599
4135
|
const muted = new Writable({ write(_chunk, _encoding, callback) {
|
|
3600
4136
|
callback();
|
|
@@ -3605,8 +4141,15 @@ function promptHidden(question) {
|
|
|
3605
4141
|
output: muted,
|
|
3606
4142
|
terminal: true
|
|
3607
4143
|
});
|
|
3608
|
-
return new Promise((resolve) => {
|
|
4144
|
+
return new Promise((resolve, reject) => {
|
|
4145
|
+
let answered = false;
|
|
4146
|
+
rl.on("close", () => {
|
|
4147
|
+
if (answered) return;
|
|
4148
|
+
process.stderr.write("\n");
|
|
4149
|
+
reject(/* @__PURE__ */ new Error("no passphrase on stdin — set SEEKRIT_PASSPHRASE, pipe it in, or run this in a terminal"));
|
|
4150
|
+
});
|
|
3609
4151
|
rl.question("", (answer) => {
|
|
4152
|
+
answered = true;
|
|
3610
4153
|
rl.close();
|
|
3611
4154
|
process.stderr.write("\n");
|
|
3612
4155
|
resolve(answer);
|
|
@@ -3628,9 +4171,23 @@ const CLI_CLIENT = `cli/${version$1}`;
|
|
|
3628
4171
|
* `flag > env > .env` credential resolution. Empty for every command but
|
|
3629
4172
|
* `seekrit run`, which loads `.env` before authenticating.
|
|
3630
4173
|
*/
|
|
4174
|
+
/**
|
|
4175
|
+
* A `SEEKRIT_*` value, or undefined if it is absent *or blank*.
|
|
4176
|
+
*
|
|
4177
|
+
* Blank has to mean absent. An unset CI secret, a `${VAR}` that expanded to
|
|
4178
|
+
* nothing, a bare `export SEEKRIT_TOKEN=` — all arrive as `""`, and `??` only
|
|
4179
|
+
* falls back on null/undefined. Left alone, an empty `SEEKRIT_API_URL` makes
|
|
4180
|
+
* every request relative and an empty `SEEKRIT_TOKEN` authenticates as a
|
|
4181
|
+
* bearer of nothing: a 401 where the honest answer is "you have no
|
|
4182
|
+
* credentials", pointing at the API instead of at the missing variable.
|
|
4183
|
+
*/
|
|
4184
|
+
function present(value) {
|
|
4185
|
+
const trimmed = value?.trim();
|
|
4186
|
+
return trimmed ? trimmed : void 0;
|
|
4187
|
+
}
|
|
3631
4188
|
function tryBuildContext(dotenvVars = {}) {
|
|
3632
4189
|
const config = readGlobalConfig();
|
|
3633
|
-
const fromEnv = (key) => process.env[key] ?? dotenvVars[key];
|
|
4190
|
+
const fromEnv = (key) => present(process.env[key]) ?? present(dotenvVars[key]);
|
|
3634
4191
|
const apiUrl = fromEnv("SEEKRIT_API_URL") ?? config.apiUrl ?? "https://api.seekrit.dev";
|
|
3635
4192
|
const token = fromEnv("SEEKRIT_TOKEN") ?? config.token ?? config.sessionToken;
|
|
3636
4193
|
const devUser = fromEnv("SEEKRIT_DEV_USER") ?? config.devUser;
|
|
@@ -3697,11 +4254,46 @@ async function resolveOrg(ctx, orgSlug) {
|
|
|
3697
4254
|
fail("specify --org (or run `seekrit init`)");
|
|
3698
4255
|
}
|
|
3699
4256
|
/**
|
|
4257
|
+
* The environment a service token is bound to.
|
|
4258
|
+
*
|
|
4259
|
+
* The binding lives on the token row, not in the token string — `GET
|
|
4260
|
+
* /v1/resolve` is where the API publishes it, and `seekrit whoami` reads it the
|
|
4261
|
+
* same way. One round trip, and it also resolves `--branch` against the bound
|
|
4262
|
+
* environment, which is the only environment a token may name a branch of.
|
|
4263
|
+
*/
|
|
4264
|
+
async function boundEnvTarget(ctx, opts) {
|
|
4265
|
+
let scope;
|
|
4266
|
+
try {
|
|
4267
|
+
({scope} = await ctx.client.resolve(opts.branch ? { branch: opts.branch } : {}));
|
|
4268
|
+
} catch (err) {
|
|
4269
|
+
fail(`specify --env — this token has no environment of its own to fall back to (${err instanceof Error ? err.message : String(err)})`);
|
|
4270
|
+
}
|
|
4271
|
+
const env = scope.branchOf ?? {
|
|
4272
|
+
envId: scope.envId,
|
|
4273
|
+
envSlug: scope.envSlug
|
|
4274
|
+
};
|
|
4275
|
+
const label = scope.branchOf ? `${scope.appSlug}/${scope.branchOf.envSlug}#${scope.envSlug}` : `${scope.appSlug}/${scope.envSlug}`;
|
|
4276
|
+
if (opts.org && opts.org !== scope.orgSlug && opts.org !== scope.orgId) fail(`this token belongs to ${scope.orgSlug}, not "${opts.org}"`);
|
|
4277
|
+
if (opts.env && opts.env !== env.envSlug && opts.env !== env.envId) fail(`this token is bound to ${scope.appSlug}/${env.envSlug}, not "${opts.env}" — add --app (or --group) to target another environment you hold a key for`);
|
|
4278
|
+
return {
|
|
4279
|
+
orgId: scope.orgId,
|
|
4280
|
+
envId: scope.envId,
|
|
4281
|
+
label
|
|
4282
|
+
};
|
|
4283
|
+
}
|
|
4284
|
+
/**
|
|
3700
4285
|
* Resolve an environment to operate on — an application env (`--app --env`,
|
|
3701
4286
|
* or the config's app + `--env`), a branch of one (`--branch`), or a group env
|
|
3702
4287
|
* (`--group --env`).
|
|
4288
|
+
*
|
|
4289
|
+
* `--env` is optional for a service token that names no `--app`/`--group`: it is
|
|
4290
|
+
* already bound to exactly one environment, so requiring the flag made every
|
|
4291
|
+
* documented one-liner (`seekrit secrets list`) fail for the CI job and the
|
|
4292
|
+
* agent that are the point of a token. The local MCP server has always inferred
|
|
4293
|
+
* it this way; this is the same rule, in one place, for both.
|
|
3703
4294
|
*/
|
|
3704
4295
|
async function resolveEnvTarget(ctx, opts) {
|
|
4296
|
+
if (!opts.app && !opts.group && isTokenAuth(ctx)) return boundEnvTarget(ctx, opts);
|
|
3705
4297
|
const org = await resolveOrg(ctx, opts.org);
|
|
3706
4298
|
if (!opts.env) fail("specify --env");
|
|
3707
4299
|
if (opts.group) {
|
|
@@ -4277,22 +4869,6 @@ const targetShape = {
|
|
|
4277
4869
|
group: z.string().optional().describe("target a group environment instead of an app"),
|
|
4278
4870
|
env: z.string().optional().describe("environment slug or id (a service token infers its own)")
|
|
4279
4871
|
};
|
|
4280
|
-
/**
|
|
4281
|
-
* Resolve which environment a secret tool addresses. A service token with no
|
|
4282
|
-
* explicit app/group targets its own bound environment (no flags needed);
|
|
4283
|
-
* everyone else names app|group + env.
|
|
4284
|
-
*/
|
|
4285
|
-
async function resolveTargetEnv(ctx, o) {
|
|
4286
|
-
if (isTokenAuth(ctx) && !o.app && !o.group) {
|
|
4287
|
-
const { scope } = await ctx.client.resolve();
|
|
4288
|
-
return {
|
|
4289
|
-
orgId: scope.orgId,
|
|
4290
|
-
envId: scope.envId,
|
|
4291
|
-
label: `${scope.appSlug}/${scope.envSlug}`
|
|
4292
|
-
};
|
|
4293
|
-
}
|
|
4294
|
-
return resolveEnvTarget(ctx, o);
|
|
4295
|
-
}
|
|
4296
4872
|
async function runMcpServer(options = {}) {
|
|
4297
4873
|
setFailThrows(true);
|
|
4298
4874
|
await ensureM2mAdminToken();
|
|
@@ -4573,7 +5149,7 @@ async function runMcpServer(options = {}) {
|
|
|
4573
5149
|
});
|
|
4574
5150
|
tool("list_secrets", "List secret names + versions in an environment (never values).", ro, targetShape, async (o) => {
|
|
4575
5151
|
const ctx = getCtx();
|
|
4576
|
-
const { orgId, envId } = await
|
|
5152
|
+
const { orgId, envId } = await resolveEnvTarget(ctx, o);
|
|
4577
5153
|
const { secrets } = await ctx.client.listSecrets(orgId, envId);
|
|
4578
5154
|
return secrets.map((s) => ({
|
|
4579
5155
|
name: s.name,
|
|
@@ -4762,7 +5338,7 @@ async function runMcpServer(options = {}) {
|
|
|
4762
5338
|
}, async (o) => {
|
|
4763
5339
|
const ctx = getCtx();
|
|
4764
5340
|
ensureDecryptable(ctx);
|
|
4765
|
-
const { orgId, envId } = await
|
|
5341
|
+
const { orgId, envId } = await resolveEnvTarget(ctx, o);
|
|
4766
5342
|
await encryptAndSetSecret(ctx, orgId, envId, o.name, o.value);
|
|
4767
5343
|
return {
|
|
4768
5344
|
ok: true,
|
|
@@ -4777,7 +5353,7 @@ async function runMcpServer(options = {}) {
|
|
|
4777
5353
|
version: z.number().int().positive().optional().describe("an earlier version from list_secret_versions (default: current)")
|
|
4778
5354
|
}, async (o) => {
|
|
4779
5355
|
const ctx = getCtx();
|
|
4780
|
-
const { orgId, envId } = await
|
|
5356
|
+
const { orgId, envId } = await resolveEnvTarget(ctx, o);
|
|
4781
5357
|
if (!o.reveal) {
|
|
4782
5358
|
const { secrets } = await ctx.client.listSecrets(orgId, envId);
|
|
4783
5359
|
const row = secrets.find((s) => s.name === o.name);
|
|
@@ -4812,7 +5388,7 @@ async function runMcpServer(options = {}) {
|
|
|
4812
5388
|
limit: z.number().int().min(1).max(200).optional().describe("default 20")
|
|
4813
5389
|
}, async (o) => {
|
|
4814
5390
|
const ctx = getCtx();
|
|
4815
|
-
const { orgId, envId } = await
|
|
5391
|
+
const { orgId, envId } = await resolveEnvTarget(ctx, o);
|
|
4816
5392
|
const { versions, currentVersion } = await ctx.client.listSecretVersions(orgId, envId, o.name, { limit: o.limit ?? 20 });
|
|
4817
5393
|
return {
|
|
4818
5394
|
currentVersion,
|
|
@@ -4830,7 +5406,7 @@ async function runMcpServer(options = {}) {
|
|
|
4830
5406
|
version: z.number().int().positive()
|
|
4831
5407
|
}, async (o) => {
|
|
4832
5408
|
const ctx = getCtx();
|
|
4833
|
-
const { orgId, envId } = await
|
|
5409
|
+
const { orgId, envId } = await resolveEnvTarget(ctx, o);
|
|
4834
5410
|
const { secret, restoredFrom } = await ctx.client.restoreSecret(orgId, envId, o.name, o.version);
|
|
4835
5411
|
return {
|
|
4836
5412
|
ok: true,
|
|
@@ -4844,7 +5420,7 @@ async function runMcpServer(options = {}) {
|
|
|
4844
5420
|
name: z.string()
|
|
4845
5421
|
}, async (o) => {
|
|
4846
5422
|
const ctx = getCtx();
|
|
4847
|
-
const { orgId, envId } = await
|
|
5423
|
+
const { orgId, envId } = await resolveEnvTarget(ctx, o);
|
|
4848
5424
|
await ctx.client.deleteSecret(orgId, envId, o.name);
|
|
4849
5425
|
return {
|
|
4850
5426
|
ok: true,
|
|
@@ -4967,7 +5543,7 @@ async function runMcpServer(options = {}) {
|
|
|
4967
5543
|
if (Boolean(o.user) === Boolean(o.token)) throw new Error("pass exactly one of user or token");
|
|
4968
5544
|
const ctx = getCtx();
|
|
4969
5545
|
ensureDecryptable(ctx);
|
|
4970
|
-
const { orgId, envId, label } = await
|
|
5546
|
+
const { orgId, envId, label } = await resolveEnvTarget(ctx, o);
|
|
4971
5547
|
const dek = await getDek(ctx, orgId, envId);
|
|
4972
5548
|
let principalType;
|
|
4973
5549
|
let principalId;
|
|
@@ -5145,7 +5721,7 @@ async function runMcpServer(options = {}) {
|
|
|
5145
5721
|
* `seekrit mcp`. tsdown bundles the shared server source in at build time, so the
|
|
5146
5722
|
* published package is self-contained and needs no `@seekrit/cli` install.
|
|
5147
5723
|
*/
|
|
5148
|
-
runMcpServer({ version: "0.
|
|
5724
|
+
runMcpServer({ version: "0.8.1" }).catch((err) => {
|
|
5149
5725
|
const message = err instanceof Error ? err.message : String(err);
|
|
5150
5726
|
process.stderr.write(`seekrit-mcp: fatal: ${message}\n`);
|
|
5151
5727
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seekrit/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "npx-able MCP server for seekrit — let Claude Code and other MCP clients provision, manage, and inject end-to-end encrypted secrets.",
|
|
5
5
|
"mcpName": "dev.seekrit/mcp",
|
|
6
6
|
"type": "module",
|
|
@@ -17,14 +17,14 @@
|
|
|
17
17
|
"node": ">=20"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
21
|
-
"zod": "^4.4
|
|
20
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
21
|
+
"zod": "^4.5.4"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/node": "^26.1.0",
|
|
25
25
|
"tsdown": "^0.22.3",
|
|
26
|
-
"vitest": "^4.1.
|
|
27
|
-
"@seekrit/cli": "
|
|
26
|
+
"vitest": "^4.1.11",
|
|
27
|
+
"@seekrit/cli": "1.2.2"
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
30
30
|
"build": "tsdown",
|