@envseal/sdk 0.1.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/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined in Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity exercising
24
+ permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or Object
36
+ form, made available under the License, as indicated by a copyright
37
+ notice that is included in or attached to the work (an example is
38
+ provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original Work and any Derivative Works thereof, that is
50
+ intentionally submitted to, or received by, Licensor for inclusion
51
+ in the Work by the copyright owner or by an individual or Legal
52
+ Entity authorized to submit on behalf of the copyright owner. For
53
+ the purposes of this definition, "submitted" means any form of
54
+ electronic, verbal, or written communication sent to the Licensor
55
+ or its representatives, including but not limited to communication
56
+ on electronic mailing lists, source code control systems, and issue
57
+ tracking systems that are managed by, or on behalf of, the Licensor
58
+ for the purpose of discussing and improving the Work, but excluding
59
+ communication that is conspicuously marked or otherwise designated
60
+ in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any Legal Entity on behalf of
63
+ which a Contribution has been received by Licensor and subsequently
64
+ incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file, then any
107
+ Derivative Works that You distribute must include a readable
108
+ copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE from the Work, provided that
120
+ such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions of this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contribution.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in appropriate
184
+ comment syntax for the file type. We also recommend that you
185
+ include a file or class name and date of last modification in
186
+ the header comment to make it easier to identify the original
187
+ author and when the file was changed.
188
+
189
+ Copyright 2026 The envseal authors
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
@@ -0,0 +1,75 @@
1
+ import type { BrokerOptions } from '@envseal/core';
2
+ import type { ManifestEntry, VerifyResult } from '@envseal/protocol';
3
+ import type { Prompter } from '@envseal/prompters';
4
+ /**
5
+ * The interactive consent surface for the two operations that move a live
6
+ * value: `env_use` (inject into a child process) and `env_verify` against a
7
+ * host the registry does not allowlist.
8
+ *
9
+ * Before this existed, three of the four bindings constructed the Broker with
10
+ * no `onConfirm`, and exec.ts turned that absence into
11
+ * SEP_CONFIRMATION_DENIED — "The user denied the confirmation" — when no user
12
+ * had been asked and no user had denied. `env_use` was advertised in
13
+ * tools/list and in the OpenAPI document and could never succeed.
14
+ *
15
+ * There is deliberately no environment-variable bypass here. `envseal run`
16
+ * honours ENVSEAL_ASSUME_YES because a human typed that command; in these
17
+ * bindings the argv comes from a *model*, and this prompt is the only thing
18
+ * between a prompt-injected model and arbitrary code holding live
19
+ * credentials. In CI these operations are simply unavailable, and say so.
20
+ *
21
+ * DUPLICATION: this file is a hand-maintained twin of
22
+ * packages/mcp-server/src/confirm.ts. Its natural home is a `confirm()` method
23
+ * on the `Prompter` interface — every surface already knows how to draw a
24
+ * dialog — but @envseal/mcp-server does not depend on @envseal/sdk, and
25
+ * @envseal/prompters was outside the scope of the change that added this. Both
26
+ * packages test the behaviour independently, so drift shows up as a red test
27
+ * rather than as a binding that quietly stops asking.
28
+ */
29
+ /** Value-entry key name carrying the `env_use` confirmation. */
30
+ export declare const CONFIRM_KEY_USE = "APPROVE";
31
+ /** Value-entry key name carrying the `env_verify` probe-consent question. */
32
+ export declare const CONFIRM_KEY_PROBE = "APPROVE_PROBE";
33
+ export interface ConfirmSurface {
34
+ /** Shown so the user can tell which project is asking. */
35
+ projectRoot: string;
36
+ /**
37
+ * Resolves the surface to ask on. A thunk rather than a Prompter because
38
+ * `selectPrompter()` is async and `createBroker` is not.
39
+ */
40
+ prompter: () => Promise<Prompter>;
41
+ timeoutMs?: number;
42
+ }
43
+ export declare function useConfirmationBody(info: {
44
+ command: string[];
45
+ keys: string[];
46
+ networkEgress: boolean;
47
+ }, projectRoot: string): string;
48
+ export declare function probeConfirmationBody(entry: ManifestEntry): string | null;
49
+ /**
50
+ * `onConfirm` for the Broker: gates `env_use`.
51
+ *
52
+ * Throws rather than returning false when no human could be asked or when the
53
+ * ask expired with nobody answering it, because exec.ts maps a `false` to
54
+ * SEP_CONFIRMATION_DENIED and that would blame the user for a missing surface
55
+ * or for a silence — the defect this project already fixed once in the CLI.
56
+ */
57
+ export declare function createUseConfirm(surface: ConfirmSurface): NonNullable<BrokerOptions['onConfirm']>;
58
+ /**
59
+ * `onApprovalNeeded` for the Broker: PLAN.md §6.4 probe consent for
60
+ * `env_verify` against a host that is not registry-allowlisted.
61
+ *
62
+ * Never throws. verifyKey() calls this per key inside a loop that builds
63
+ * per-key results; throwing would abort the whole `env_verify` call, so a
64
+ * missing surface would take down the verification of keys whose probes are
65
+ * allowlisted and fine. Every non-approval returns false, which keeps the
66
+ * existing fail-closed `probe_not_approved` outcome for that one key.
67
+ */
68
+ export declare function createProbeApproval(surface: ConfirmSurface): NonNullable<BrokerOptions['onApprovalNeeded']>;
69
+ /**
70
+ * `probe_not_approved` on its own tells the caller nothing it can act on.
71
+ * verify.ts (core) names the host; this adds what to do about it, in the one
72
+ * place a binding can add it without reaching into core.
73
+ */
74
+ export declare function annotateVerifyResults(results: VerifyResult[]): VerifyResult[];
75
+ //# sourceMappingURL=confirm.d.ts.map
@@ -0,0 +1,247 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { SepError, zero } from '@envseal/protocol';
3
+ import { makeDisplayNonce } from '@envseal/prompters';
4
+ /**
5
+ * The interactive consent surface for the two operations that move a live
6
+ * value: `env_use` (inject into a child process) and `env_verify` against a
7
+ * host the registry does not allowlist.
8
+ *
9
+ * Before this existed, three of the four bindings constructed the Broker with
10
+ * no `onConfirm`, and exec.ts turned that absence into
11
+ * SEP_CONFIRMATION_DENIED — "The user denied the confirmation" — when no user
12
+ * had been asked and no user had denied. `env_use` was advertised in
13
+ * tools/list and in the OpenAPI document and could never succeed.
14
+ *
15
+ * There is deliberately no environment-variable bypass here. `envseal run`
16
+ * honours ENVSEAL_ASSUME_YES because a human typed that command; in these
17
+ * bindings the argv comes from a *model*, and this prompt is the only thing
18
+ * between a prompt-injected model and arbitrary code holding live
19
+ * credentials. In CI these operations are simply unavailable, and say so.
20
+ *
21
+ * DUPLICATION: this file is a hand-maintained twin of
22
+ * packages/mcp-server/src/confirm.ts. Its natural home is a `confirm()` method
23
+ * on the `Prompter` interface — every surface already knows how to draw a
24
+ * dialog — but @envseal/mcp-server does not depend on @envseal/sdk, and
25
+ * @envseal/prompters was outside the scope of the change that added this. Both
26
+ * packages test the behaviour independently, so drift shows up as a red test
27
+ * rather than as a binding that quietly stops asking.
28
+ */
29
+ /** Value-entry key name carrying the `env_use` confirmation. */
30
+ export const CONFIRM_KEY_USE = 'APPROVE';
31
+ /** Value-entry key name carrying the `env_verify` probe-consent question. */
32
+ export const CONFIRM_KEY_PROBE = 'APPROVE_PROBE';
33
+ const DEFAULT_TIMEOUT_MS = 120_000;
34
+ /** Per-argument display cap; longer arguments are shown truncated, and said to be. */
35
+ const MAX_ARG_CHARS = 300;
36
+ /** Whole-dialog cap. Past this we refuse rather than ask about something unreadable. */
37
+ const MAX_BODY_CHARS = 16 * 1024;
38
+ const INSTRUCTION = 'Type yes to approve, or submit an empty box to deny.';
39
+ /**
40
+ * Only one confirmation may be open per process. Without this a model can call
41
+ * `env_use` in a loop and stack up dialogs until one gets clicked through.
42
+ */
43
+ let confirmationOpen = false;
44
+ /**
45
+ * Model-supplied argv, key names and probe metadata land in a dialog the user
46
+ * is about to trust. A raw newline lets a crafted argument forge extra lines —
47
+ * "keys: none", "this command is safe" — inside the very block that exists to
48
+ * tell the truth about the command. Render control characters visibly instead.
49
+ */
50
+ function escapeForDisplay(value) {
51
+ let out = '';
52
+ for (const ch of value) {
53
+ const code = ch.codePointAt(0) ?? 0;
54
+ if (code < 0x20 || (code >= 0x7f && code <= 0x9f)) {
55
+ out += `<0x${code.toString(16).padStart(2, '0')}>`;
56
+ }
57
+ else {
58
+ out += ch;
59
+ }
60
+ }
61
+ return out;
62
+ }
63
+ function displayArg(arg) {
64
+ const escaped = escapeForDisplay(arg);
65
+ if (escaped.length <= MAX_ARG_CHARS) {
66
+ return escaped;
67
+ }
68
+ const hidden = escaped.length - MAX_ARG_CHARS;
69
+ return `${escaped.slice(0, MAX_ARG_CHARS)}[... ${hidden} more characters, not shown]`;
70
+ }
71
+ export function useConfirmationBody(info, projectRoot) {
72
+ const lines = [
73
+ 'EnvSeal is about to run a program with these secrets in its environment.',
74
+ '',
75
+ ` project: ${escapeForDisplay(projectRoot)}`,
76
+ ` keys: ${info.keys.length > 0 ? info.keys.map(escapeForDisplay).join(', ') : '(none)'}`,
77
+ '',
78
+ ' command, one argument per line, exactly as it will be run (no shell):',
79
+ ];
80
+ info.command.forEach((arg, index) => {
81
+ lines.push(` [${index}] ${displayArg(arg)}`);
82
+ });
83
+ lines.push('');
84
+ if (info.networkEgress) {
85
+ lines.push(' WARNING: this command can reach the network, so it could send these', ' values somewhere. Only continue if you trust it.');
86
+ }
87
+ else {
88
+ // Honest about what the check is worth: NETWORK_TOOLS plus a URL scan is a
89
+ // heuristic, and claiming more would be the kind of overstatement this
90
+ // project has already had to walk back once.
91
+ lines.push(' No network tool or URL was recognised in this command. That is a', ' heuristic, not a guarantee: any program can open a socket.');
92
+ }
93
+ lines.push('', `${INSTRUCTION} Nothing runs unless you approve.`);
94
+ return lines.join('\n');
95
+ }
96
+ export function probeConfirmationBody(entry) {
97
+ const probe = entry.verify;
98
+ if (!probe) {
99
+ return null;
100
+ }
101
+ const lines = [
102
+ `EnvSeal is about to send the stored value of ${escapeForDisplay(entry.key)} to a host`,
103
+ 'that is not on its bundled allowlist. Nothing has been sent yet.',
104
+ '',
105
+ ` key: ${escapeForDisplay(entry.key)}`,
106
+ ` method: ${escapeForDisplay(probe.method)}`,
107
+ ` url: ${displayArg(probe.url)}`,
108
+ ' headers:',
109
+ ];
110
+ for (const [header, template] of Object.entries(probe.headerTemplate)) {
111
+ lines.push(` ${escapeForDisplay(header)}: ${displayArg(template)}`);
112
+ }
113
+ lines.push('', ' {{value}} is replaced with the real secret when the request is sent.', '', 'Type yes to approve exactly this probe. The answer is recorded in', '.envseal/approvals.json and replayed without asking again until the', 'method, URL or headers change. Submit an empty box to deny.');
114
+ return lines.join('\n');
115
+ }
116
+ async function ask(surface, keyName, headline, body) {
117
+ if (body === null || body.length > MAX_BODY_CHARS) {
118
+ return 'too-large';
119
+ }
120
+ const prompter = await surface.prompter();
121
+ if (prompter.id === 'none') {
122
+ return 'no-surface';
123
+ }
124
+ if (confirmationOpen) {
125
+ return 'busy';
126
+ }
127
+ confirmationOpen = true;
128
+ try {
129
+ const response = await prompter.prompt({
130
+ ticket: `confirm-${randomBytes(8).toString('hex')}`,
131
+ nonce: makeDisplayNonce(),
132
+ projectRoot: surface.projectRoot,
133
+ reason: headline,
134
+ keys: [{ key: keyName, description: body, formatHint: INSTRUCTION }],
135
+ timeoutMs: surface.timeoutMs ?? DEFAULT_TIMEOUT_MS,
136
+ });
137
+ const result = response.results.find((r) => r.key === keyName);
138
+ // A timeout is kept apart from a denial: nobody answered at all, and
139
+ // reporting that as "the user denied" blames a user who never spoke — the
140
+ // defect class the CLI fixed for the missing-surface case.
141
+ if (result !== undefined && result.outcome === 'timeout') {
142
+ return 'timed-out';
143
+ }
144
+ // skipped / cancelled / a surface that answered about some other key: we
145
+ // did not get a yes, and none of them names anyone, so they land on the
146
+ // honest-but-blunt denial.
147
+ if (result === undefined || result.outcome !== 'entered') {
148
+ return 'denied';
149
+ }
150
+ const typed = result.value.toString('utf8');
151
+ zero(result.value);
152
+ return /^y(es)?$/i.test(typed.trim()) ? 'approved' : 'denied';
153
+ }
154
+ finally {
155
+ confirmationOpen = false;
156
+ }
157
+ }
158
+ /**
159
+ * `onConfirm` for the Broker: gates `env_use`.
160
+ *
161
+ * Throws rather than returning false when no human could be asked or when the
162
+ * ask expired with nobody answering it, because exec.ts maps a `false` to
163
+ * SEP_CONFIRMATION_DENIED and that would blame the user for a missing surface
164
+ * or for a silence — the defect this project already fixed once in the CLI.
165
+ */
166
+ export function createUseConfirm(surface) {
167
+ return async (info) => {
168
+ const outcome = await ask(surface, CONFIRM_KEY_USE, 'Approve running a command with secrets in its environment? Nothing has run yet.', useConfirmationBody(info, surface.projectRoot));
169
+ switch (outcome) {
170
+ case 'approved':
171
+ return true;
172
+ case 'denied':
173
+ return false;
174
+ case 'timed-out':
175
+ // SEP_TICKET_EXPIRED, not SEP_CONFIRMATION_DENIED: the repo already
176
+ // treats an unanswered prompt as an expired ticket (exit-codes.ts maps
177
+ // outcome `timeout` and this code to the same exit), and a model that
178
+ // can tell "nobody answered" from "the user said no" retries instead
179
+ // of reporting a refusal that never happened.
180
+ throw new SepError({
181
+ code: 'SEP_TICKET_EXPIRED',
182
+ userMessage: 'The env_use confirmation closed after its timeout with nobody answering it. Nothing was ' +
183
+ 'run and no value was read. This is not a denial: ask the user to approve it, then call ' +
184
+ 'env_use again.',
185
+ });
186
+ case 'no-surface':
187
+ throw new SepError({
188
+ code: 'SEP_NO_INTERACTIVE_SURFACE',
189
+ userMessage: 'env_use needs the user to confirm before secrets are injected into a child process, ' +
190
+ 'but there is no interactive surface here to ask on (this is what CI looks like to envseal). ' +
191
+ 'Nothing was run and no value was read. ' +
192
+ 'There is no flag or environment variable that skips this prompt in this binding: the command ' +
193
+ 'came from a model, and the confirmation is the only control on it. ' +
194
+ 'Run the command yourself with `envseal run -- <command>` in a session that has a browser or a terminal.',
195
+ });
196
+ case 'busy':
197
+ throw new SepError({
198
+ code: 'SEP_RATE_LIMITED',
199
+ userMessage: 'Another envseal confirmation is already open. Answer that one first, then call env_use again.',
200
+ });
201
+ case 'too-large':
202
+ throw new SepError({
203
+ code: 'SEP_FORMAT_INVALID',
204
+ userMessage: 'This command is too large to display in a confirmation dialog, and envseal will not ask ' +
205
+ 'anyone to approve something it cannot show them. Run it with fewer or shorter arguments.',
206
+ });
207
+ }
208
+ };
209
+ }
210
+ /**
211
+ * `onApprovalNeeded` for the Broker: PLAN.md §6.4 probe consent for
212
+ * `env_verify` against a host that is not registry-allowlisted.
213
+ *
214
+ * Never throws. verifyKey() calls this per key inside a loop that builds
215
+ * per-key results; throwing would abort the whole `env_verify` call, so a
216
+ * missing surface would take down the verification of keys whose probes are
217
+ * allowlisted and fine. Every non-approval returns false, which keeps the
218
+ * existing fail-closed `probe_not_approved` outcome for that one key.
219
+ */
220
+ export function createProbeApproval(surface) {
221
+ return async (entry) => {
222
+ const outcome = await ask(surface, CONFIRM_KEY_PROBE, `Approve sending ${entry.key} to a host that is not on envseal's allowlist? Nothing has been sent yet.`, probeConfirmationBody(entry));
223
+ return outcome === 'approved';
224
+ };
225
+ }
226
+ /**
227
+ * `probe_not_approved` on its own tells the caller nothing it can act on.
228
+ * verify.ts (core) names the host; this adds what to do about it, in the one
229
+ * place a binding can add it without reaching into core.
230
+ */
231
+ export function annotateVerifyResults(results) {
232
+ return results.map((result) => {
233
+ if (result.result !== 'probe_not_approved') {
234
+ return result;
235
+ }
236
+ return {
237
+ ...result,
238
+ message: `${result.message}. This host is not on envseal's bundled allowlist and no approval for this ` +
239
+ `exact probe is recorded, so the credential was NOT sent. Run \`envseal verify ${result.key}\` ` +
240
+ 'in an interactive terminal on a machine with access to this ' +
241
+ 'project to review the method, URL and header template and decide. The decision is recorded in ' +
242
+ '.envseal/approvals.json and replayed without asking again, until the key, method, URL or header ' +
243
+ 'template changes.',
244
+ };
245
+ });
246
+ }
247
+ //# sourceMappingURL=confirm.js.map
@@ -0,0 +1,17 @@
1
+ import { Broker } from '@envseal/core';
2
+ import type { Prompter } from '@envseal/prompters';
3
+ import { INPUT_SCHEMAS } from '@envseal/protocol';
4
+ export interface CreateBrokerOptions {
5
+ root?: string;
6
+ prompter?: Prompter;
7
+ }
8
+ export declare function createBroker(opts?: CreateBrokerOptions): Broker;
9
+ /** True for the seven SEP/1 tool names. Exported so a transport can 404 an unknown route. */
10
+ export declare function isSepToolName(name: string): name is keyof typeof INPUT_SCHEMAS;
11
+ export type Dialect = 'openai' | 'anthropic' | 'gemini';
12
+ export declare function toolsFor(dialect: Dialect): unknown[];
13
+ export declare function dispatch(broker: Broker, name: string, args: unknown): Promise<unknown>;
14
+ export { SEP_TOOL_NAMES } from '@envseal/protocol';
15
+ export { createUseConfirm, createProbeApproval, annotateVerifyResults, CONFIRM_KEY_USE, CONFIRM_KEY_PROBE, } from './confirm.js';
16
+ export type { ConfirmSurface } from './confirm.js';
17
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,223 @@
1
+ import { findProjectRoot } from '@envseal/core';
2
+ import { Broker } from '@envseal/core';
3
+ import { selectPrompter } from '@envseal/prompters';
4
+ import { SEP_TOOL_NAMES, INPUT_SCHEMAS, SepError, isSepError, } from '@envseal/protocol';
5
+ import { zodToJsonSchema } from 'zod-to-json-schema';
6
+ import { annotateVerifyResults, createProbeApproval, createUseConfirm } from './confirm.js';
7
+ export function createBroker(opts) {
8
+ const root = opts?.root ?? findProjectRoot(process.cwd());
9
+ // Resolved lazily and memoised: `selectPrompter()` is async and this factory
10
+ // is not, and the surface is only needed if someone actually calls env_use or
11
+ // trips a non-allowlisted probe. `selectPrompter` hands back process-wide
12
+ // singletons, so this is the same instance the Broker resolves for
13
+ // env_request value entry.
14
+ let surfacePromise = null;
15
+ const prompter = () => {
16
+ const injected = opts?.prompter;
17
+ surfacePromise ??= injected === undefined ? selectPrompter() : Promise.resolve(injected);
18
+ return surfacePromise;
19
+ };
20
+ const surface = { projectRoot: root, prompter };
21
+ const brokerOpts = {
22
+ root,
23
+ prompter: opts?.prompter,
24
+ // Without these two the broker has no way to ask a human, and exec.ts
25
+ // reports the absent callback as SEP_CONFIRMATION_DENIED — blaming a user
26
+ // who was never asked. See confirm.ts.
27
+ onConfirm: createUseConfirm(surface),
28
+ onApprovalNeeded: createProbeApproval(surface),
29
+ };
30
+ return new Broker(brokerOpts);
31
+ }
32
+ /** True for the seven SEP/1 tool names. Exported so a transport can 404 an unknown route. */
33
+ export function isSepToolName(name) {
34
+ return Object.prototype.hasOwnProperty.call(INPUT_SCHEMAS, name);
35
+ }
36
+ const TOOL_DESCRIPTIONS = {
37
+ env_describe: 'Returns only redacted status: whether each key is present, a length bucket, and a salted fingerprint. ' +
38
+ 'This never returns secret values and there is no flag, option, or debug mode that makes it do so. ' +
39
+ 'Do not attempt to read .env directly — that is blocked and unnecessary. ' +
40
+ 'Use this to see which declared keys exist, whether a value is present, and whether a stored value ' +
41
+ 'changed since your last call (compare fingerprints). ' +
42
+ 'It will NOT return, echo, or reconstruct any value. ' +
43
+ 'To collect a missing value, first call env_declare (if the key is not yet declared), then env_request.',
44
+ env_declare: 'Declares that this project needs the given environment variables by writing entries to the project ' +
45
+ 'manifest (env.schema.jsonc). Idempotent; does not prompt the user. ' +
46
+ 'Declare metadata only: key name, description, and optional format/provider constraints. ' +
47
+ 'It will NOT collect values and rejects any entry that tries to carry a value. ' +
48
+ 'After declaring, call env_describe to check presence or env_request to prompt the user for the value.',
49
+ env_request: 'Opens a secure input surface where the user types the value directly. You will never see the value. ' +
50
+ 'Returns a ticket immediately; poll env_await for the outcome. ' +
51
+ 'You must call env_declare for a key before you can request it. ' +
52
+ "The 'reason' field is shown verbatim to the user, so write it as a clear, honest ask explaining why " +
53
+ 'the project needs the key. ' +
54
+ 'It will NOT return the value, and the typed value never crosses this channel — only a ticket that ' +
55
+ 'you poll with env_await.',
56
+ env_await: 'Blocks up to timeoutMs (default 90000, max 120000) for a pending env_request ticket to resolve, then ' +
57
+ 'returns per-key outcomes: stored, skipped, cancelled, invalid_format, verify_failed, or timeout. ' +
58
+ 'If the outcome is timeout, the prompt is still open — call env_await again with the same ticket. ' +
59
+ 'It will NOT return the value the user typed, only outcomes. ' +
60
+ 'To retry a failed request, call env_request again.',
61
+ env_verify: 'Tests a stored credential against its provider and returns a classified result. ' +
62
+ "Never returns the provider's response body. " +
63
+ 'Results are classified as ok, auth_failed, forbidden, rate_limited, network_error, no_probe, or ' +
64
+ 'probe_not_approved, plus a short sanitized message. ' +
65
+ 'It will NOT return raw provider responses, headers, or the credential itself. ' +
66
+ 'To check mere presence rather than validity, call env_describe instead.',
67
+ env_use: 'Runs a command with the named secrets injected into the child environment only. ' +
68
+ 'Output is filtered so the values cannot appear in what you read back. ' +
69
+ 'Requires user confirmation. ' +
70
+ 'Pass the command as an argv array with no shell. ' +
71
+ 'It will NOT print the secrets to you, will NOT export them into your own environment, and refuses to ' +
72
+ 'run without explicit user confirmation. ' +
73
+ 'To check whether a key exists instead of running a command, call env_describe.',
74
+ env_revoke: 'Removes stored credentials. Records in the audit log and emits the provider ' +
75
+ 'rotation URL so you can help the user invalidate the old key.',
76
+ };
77
+ export function toolsFor(dialect) {
78
+ const tools = [];
79
+ for (const toolName of SEP_TOOL_NAMES) {
80
+ const schema = INPUT_SCHEMAS[toolName];
81
+ const jsonSchema = zodToJsonSchema(schema);
82
+ const description = TOOL_DESCRIPTIONS[toolName];
83
+ if (dialect === 'openai') {
84
+ const parameters = {
85
+ type: 'object',
86
+ ...jsonSchema,
87
+ };
88
+ tools.push({
89
+ type: 'function',
90
+ function: {
91
+ name: toolName,
92
+ description,
93
+ parameters,
94
+ },
95
+ });
96
+ }
97
+ else if (dialect === 'anthropic') {
98
+ tools.push({
99
+ name: toolName,
100
+ description,
101
+ input_schema: jsonSchema,
102
+ });
103
+ }
104
+ else if (dialect === 'gemini') {
105
+ if (tools.length === 0) {
106
+ tools.push({
107
+ functionDeclarations: [],
108
+ });
109
+ }
110
+ const toolsObj = tools[0];
111
+ if (toolsObj !== undefined) {
112
+ toolsObj.functionDeclarations.push({
113
+ name: toolName,
114
+ description,
115
+ parameters: jsonSchema,
116
+ });
117
+ }
118
+ }
119
+ }
120
+ return tools;
121
+ }
122
+ export async function dispatch(broker, name, args) {
123
+ // Narrow `name` to the tool-name union up front so the switch below can be
124
+ // checked for exhaustiveness rather than falling through at runtime.
125
+ if (!isSepToolName(name)) {
126
+ return {
127
+ error: {
128
+ code: 'SEP_UNKNOWN_KEY',
129
+ userMessage: `Unknown tool: ${name}`,
130
+ retriable: false,
131
+ },
132
+ };
133
+ }
134
+ const schema = INPUT_SCHEMAS[name];
135
+ if (!schema) {
136
+ return {
137
+ error: {
138
+ code: 'SEP_UNKNOWN_KEY',
139
+ userMessage: 'Unknown tool',
140
+ retriable: false,
141
+ },
142
+ };
143
+ }
144
+ try {
145
+ const validated = schema.parse(args);
146
+ // Explicit table rather than `broker[name]`. The tool names are protocol
147
+ // surface (`env_request`); the Broker's methods are not (`request`). Indexing
148
+ // the broker by tool name therefore misses on six of the seven tools, and an
149
+ // `as any` on that lookup turns the mismatch into a runtime "Tool not
150
+ // available" instead of a compile error.
151
+ switch (name) {
152
+ case 'env_describe':
153
+ // describe takes no arguments; the input schema exists only so callers
154
+ // can pass an empty object without tripping validation.
155
+ return await broker.describe();
156
+ case 'env_declare':
157
+ return await broker.declare(validated);
158
+ case 'env_request':
159
+ return await broker.request(validated);
160
+ case 'env_await':
161
+ return await broker.await(validated);
162
+ case 'env_verify':
163
+ // `probe_not_approved` alone names a host and nothing the caller can
164
+ // act on. See annotateVerifyResults.
165
+ return annotateVerifyResults(await broker.verify(validated));
166
+ case 'env_use':
167
+ return await broker.use(validated);
168
+ case 'env_revoke':
169
+ return await broker.revoke(validated);
170
+ default: {
171
+ // Exhaustiveness: adding a tool without wiring it here is a build error.
172
+ const unreachable = name;
173
+ return {
174
+ error: {
175
+ code: 'SEP_UNKNOWN_KEY',
176
+ userMessage: `Unknown tool: ${String(unreachable)}`,
177
+ retriable: false,
178
+ },
179
+ };
180
+ }
181
+ }
182
+ }
183
+ catch (error) {
184
+ if (isSepError(error)) {
185
+ return {
186
+ error: {
187
+ code: error.code,
188
+ userMessage: error.userMessage,
189
+ retriable: error.retriable,
190
+ },
191
+ };
192
+ }
193
+ // Everything below is NOT a SepError, so its message is uncurated and may
194
+ // embed a value or a filesystem path. W2 swept 22 HTTP exchanges and found
195
+ // zero body leaks; surfacing `error.message` here is exactly how that
196
+ // property would be lost. Zod is the concrete case: `invalid_enum_value`
197
+ // quotes the value it received, which for a mis-typed argument is the
198
+ // secret itself. So: report the real *kind* of failure, never its text.
199
+ //
200
+ // Detected structurally rather than with `instanceof ZodError` because zod
201
+ // reaches this package only as a transitive dependency of @envseal/protocol.
202
+ if (error instanceof Error && error.name === 'ZodError') {
203
+ return {
204
+ error: {
205
+ code: 'SEP_FORMAT_INVALID',
206
+ userMessage: `Arguments did not match the input schema for ${name}. Re-read that tool's inputSchema ` +
207
+ 'and call it again. The offending values are not echoed back because they may contain a secret.',
208
+ retriable: true,
209
+ },
210
+ };
211
+ }
212
+ return {
213
+ error: {
214
+ code: 'SEP_INTERNAL',
215
+ userMessage: 'An internal error occurred. Details were suppressed because they may contain sensitive information.',
216
+ retriable: false,
217
+ },
218
+ };
219
+ }
220
+ }
221
+ export { SEP_TOOL_NAMES } from '@envseal/protocol';
222
+ export { createUseConfirm, createProbeApproval, annotateVerifyResults, CONFIRM_KEY_USE, CONFIRM_KEY_PROBE, } from './confirm.js';
223
+ //# sourceMappingURL=index.js.map
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@envseal/sdk",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "license": "Apache-2.0",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "!dist/**/*.map"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public",
20
+ "provenance": true
21
+ },
22
+ "dependencies": {
23
+ "zod-to-json-schema": "^3.24.1",
24
+ "@envseal/protocol": "0.1.0",
25
+ "@envseal/prompters": "0.1.0",
26
+ "@envseal/core": "0.1.0",
27
+ "@envseal/registry": "0.1.0"
28
+ },
29
+ "devDependencies": {
30
+ "vitest": "^2.1.8"
31
+ },
32
+ "scripts": {
33
+ "build": "tsc -p tsconfig.json",
34
+ "typecheck": "tsc -p tsconfig.json --noEmit",
35
+ "test": "vitest run"
36
+ }
37
+ }