@ai21/gateway 0.5.3

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.
Files changed (44) hide show
  1. package/README.md +91 -0
  2. package/dist/args.d.ts +74 -0
  3. package/dist/args.js +304 -0
  4. package/dist/args.js.map +1 -0
  5. package/dist/cli.d.ts +10 -0
  6. package/dist/cli.js +140 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/config.d.ts +76 -0
  9. package/dist/config.js +187 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/login/browser.d.ts +7 -0
  12. package/dist/login/browser.js +41 -0
  13. package/dist/login/browser.js.map +1 -0
  14. package/dist/login/contract.d.ts +42 -0
  15. package/dist/login/contract.js +60 -0
  16. package/dist/login/contract.js.map +1 -0
  17. package/dist/login/loopback.d.ts +42 -0
  18. package/dist/login/loopback.js +192 -0
  19. package/dist/login/loopback.js.map +1 -0
  20. package/dist/login/session.d.ts +36 -0
  21. package/dist/login/session.js +75 -0
  22. package/dist/login/session.js.map +1 -0
  23. package/dist/prompt.d.ts +32 -0
  24. package/dist/prompt.js +72 -0
  25. package/dist/prompt.js.map +1 -0
  26. package/dist/report.d.ts +50 -0
  27. package/dist/report.js +214 -0
  28. package/dist/report.js.map +1 -0
  29. package/dist/result.d.ts +26 -0
  30. package/dist/result.js +22 -0
  31. package/dist/result.js.map +1 -0
  32. package/dist/revert.d.ts +60 -0
  33. package/dist/revert.js +230 -0
  34. package/dist/revert.js.map +1 -0
  35. package/dist/router.d.ts +21 -0
  36. package/dist/router.js +271 -0
  37. package/dist/router.js.map +1 -0
  38. package/dist/snippets.d.ts +52 -0
  39. package/dist/snippets.js +71 -0
  40. package/dist/snippets.js.map +1 -0
  41. package/dist/verify.d.ts +96 -0
  42. package/dist/verify.js +309 -0
  43. package/dist/verify.js.map +1 -0
  44. package/package.json +61 -0
@@ -0,0 +1,192 @@
1
+ /**
2
+ * The loopback listener that receives the login callback.
3
+ *
4
+ * This is the security surface of the whole handshake, so the invariants are
5
+ * asserted by tests rather than trusted: bound to `127.0.0.1` only, one valid
6
+ * callback accepted and no more, `state` compared before the payload is handed
7
+ * over, a hard timeout so the CLI cannot sit open indefinitely, and a body size
8
+ * cap so a stray client cannot make us buffer without bound.
9
+ */
10
+ import { randomBytes, timingSafeEqual } from "node:crypto";
11
+ import { createServer } from "node:http";
12
+ import { CALLBACK_PATH, START_PATH, parseCallback } from "./contract.js";
13
+ /** Two minutes: long enough to sign in, short enough not to look wedged. */
14
+ export const LOGIN_TIMEOUT_MS = 120_000;
15
+ const MAX_BODY_BYTES = 8_192;
16
+ const LOOPBACK_HOST = "127.0.0.1";
17
+ /** 256 bits from the CSPRNG. Guessing it is not a threat model we need to model. */
18
+ export function newState() {
19
+ return randomBytes(32).toString("base64url");
20
+ }
21
+ /** Constant-time where it can be; length mismatch is not secret. */
22
+ function statesMatch(expected, received) {
23
+ const a = Buffer.from(expected, "utf8");
24
+ const b = Buffer.from(received, "utf8");
25
+ return a.length === b.length && timingSafeEqual(a, b);
26
+ }
27
+ function readBody(req) {
28
+ return new Promise((resolve) => {
29
+ let body = "";
30
+ let tooBig = false;
31
+ req.on("data", (chunk) => {
32
+ if (tooBig)
33
+ return;
34
+ body += chunk.toString("utf8");
35
+ if (body.length > MAX_BODY_BYTES) {
36
+ tooBig = true;
37
+ // Destroy rather than keep reading: the only client we expect sends a few
38
+ // hundred bytes, so anything larger is not the flow we are in.
39
+ req.destroy();
40
+ }
41
+ });
42
+ req.on("end", () => resolve(tooBig ? undefined : body));
43
+ req.on("error", () => resolve(undefined));
44
+ });
45
+ }
46
+ /**
47
+ * Starts the listener. Resolves once it is bound, so the caller can put the real
48
+ * port into the URL it opens.
49
+ */
50
+ export function startLoopback(options) {
51
+ const state = newState();
52
+ const timeoutMs = options.timeoutMs ?? LOGIN_TIMEOUT_MS;
53
+ let settle;
54
+ const result = new Promise((resolve) => {
55
+ settle = resolve;
56
+ });
57
+ let done = false;
58
+ /**
59
+ * The single-use latch. A second callback — a replayed POST, a double-submitting
60
+ * page — must not overwrite credentials we have already accepted.
61
+ */
62
+ const finish = (outcome) => {
63
+ if (done)
64
+ return;
65
+ done = true;
66
+ settle(outcome);
67
+ };
68
+ const server = createServer((req, res) => {
69
+ void handle(req, res);
70
+ });
71
+ const cors = (res) => {
72
+ // The page POSTing here is on the webapp's origin, so without CORS the browser
73
+ // refuses the request outright. Named explicitly rather than `*`: any origin
74
+ // being able to talk to this port is the thing we are trying to avoid.
75
+ res.setHeader("Access-Control-Allow-Origin", options.allowedOrigin);
76
+ res.setHeader("Access-Control-Allow-Headers", "content-type");
77
+ res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
78
+ // Chrome's Private Network Access: a public HTTPS page reaching a loopback address
79
+ // must be granted it explicitly, or the preflight fails and the POST never happens.
80
+ res.setHeader("Access-Control-Allow-Private-Network", "true");
81
+ res.setHeader("Vary", "Origin");
82
+ };
83
+ // Single-use: an attacker who guesses the port and beats the browser to /start
84
+ // gets the state, but the browser then fails visibly instead of the run being
85
+ // hijacked in silence.
86
+ let redirected = false;
87
+ let signInUrl = "";
88
+ async function handle(req, res) {
89
+ cors(res);
90
+ const path = (req.url ?? "").split("?")[0];
91
+ if (path === START_PATH) {
92
+ if (req.method !== "GET") {
93
+ res.writeHead(405).end();
94
+ }
95
+ else if (redirected) {
96
+ res.writeHead(410, { "content-type": "text/plain" }).end("This sign-in link has already been used.");
97
+ }
98
+ else {
99
+ redirected = true;
100
+ res.writeHead(302, { location: signInUrl }).end();
101
+ }
102
+ return;
103
+ }
104
+ if (path !== CALLBACK_PATH) {
105
+ res.writeHead(404).end();
106
+ return;
107
+ }
108
+ if (req.method === "OPTIONS") {
109
+ res.writeHead(204).end();
110
+ return;
111
+ }
112
+ if (req.method !== "POST") {
113
+ // Notably this rejects GET, which is what a credential-in-the-URL flow would
114
+ // use. The secret must arrive in a body.
115
+ res.writeHead(405).end();
116
+ return;
117
+ }
118
+ const raw = await readBody(req);
119
+ if (raw === undefined) {
120
+ res.writeHead(413).end();
121
+ return;
122
+ }
123
+ let parsed;
124
+ try {
125
+ parsed = JSON.parse(raw);
126
+ }
127
+ catch {
128
+ res.writeHead(400).end();
129
+ return;
130
+ }
131
+ const payload = parseCallback(parsed);
132
+ if (payload === undefined) {
133
+ res.writeHead(400).end();
134
+ return;
135
+ }
136
+ if (!statesMatch(state, payload.state)) {
137
+ // Rejected, and the run is *not* abandoned: a spoofed or stale POST must not
138
+ // be able to cancel a legitimate login that is still in progress.
139
+ res.writeHead(403).end();
140
+ return;
141
+ }
142
+ if (done) {
143
+ res.writeHead(409).end();
144
+ return;
145
+ }
146
+ res.writeHead(200, { "content-type": "application/json" }).end('{"ok":true}');
147
+ finish({ kind: "callback", payload });
148
+ }
149
+ const timer = setTimeout(() => finish({ kind: "timeout" }), timeoutMs);
150
+ // Never hold the process open on our own account.
151
+ timer.unref();
152
+ const close = () => {
153
+ clearTimeout(timer);
154
+ server.close();
155
+ };
156
+ // Closing on settle rather than making the caller remember to: a listener left
157
+ // open after the flow ends is an open port nobody is watching.
158
+ void result.then(close);
159
+ return new Promise((resolve, reject) => {
160
+ const onError = (err) => {
161
+ // Nothing bound, so nothing to close — and the timer would otherwise
162
+ // outlive a run that never started.
163
+ clearTimeout(timer);
164
+ reject(err);
165
+ };
166
+ server.once("error", onError);
167
+ // Port 0 asks the OS for an ephemeral one. The CLI tells the browser where it
168
+ // landed, so nothing has to guess — and there is no fixed port to collide over.
169
+ server.listen({ port: 0, host: LOOPBACK_HOST }, () => {
170
+ server.removeListener("error", onError);
171
+ /* Swapped only now that the socket is bound: until then an error is a bind
172
+ * failure, which rejects the promise rather than settling a run that never
173
+ * started. */
174
+ server.on("error", (err) => finish({ kind: "failed", reason: err.message }));
175
+ const address = server.address();
176
+ const origin = `http://${LOOPBACK_HOST}:${address.port}`;
177
+ const callbackUrl = `${origin}${CALLBACK_PATH}`;
178
+ signInUrl = options.signInUrl(callbackUrl, state);
179
+ resolve({
180
+ startUrl: `${origin}${START_PATH}`,
181
+ signInUrl,
182
+ callbackUrl,
183
+ state,
184
+ host: address.address,
185
+ port: address.port,
186
+ result,
187
+ close,
188
+ });
189
+ });
190
+ });
191
+ }
192
+ //# sourceMappingURL=loopback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loopback.js","sourceRoot":"","sources":["../../src/login/loopback.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * `login` orchestration: open the browser, wait on the loopback listener, hand
3
+ * whatever comes back to the ordinary config writer.
4
+ *
5
+ * The credentials live in a local variable for the length of one run and are
6
+ * written only into `settings.json`. Nothing else is persisted — no token cache, no
7
+ * `~/.ai21/credentials` — so there is nothing for an attacker to find later.
8
+ */
9
+ import { type ResolvedUrls } from "../args.js";
10
+ import { type Listener, type LoopbackOptions } from "./loopback.js";
11
+ export interface LoginDeps {
12
+ readonly startListener: (options: LoopbackOptions) => Promise<Listener>;
13
+ readonly openBrowser: (url: string) => boolean;
14
+ }
15
+ export type LoginOutcome = {
16
+ readonly kind: "credentials";
17
+ readonly agentId: string;
18
+ readonly ai21Key: string;
19
+ /** Present only when the callback supplied a well-formed one. */
20
+ readonly workspaceId?: string | undefined;
21
+ } | {
22
+ readonly kind: "timeout";
23
+ } | {
24
+ readonly kind: "rejected";
25
+ readonly reason: string;
26
+ } | {
27
+ readonly kind: "failed";
28
+ readonly reason: string;
29
+ };
30
+ /** The `/welcome` URL, carrying where to call back and the nonce to echo. */
31
+ export declare function loginUrl(urls: ResolvedUrls, callbackUrl: string, state: string): string;
32
+ export interface LoginProgress {
33
+ /** Told what to say while waiting; the caller decides which stream it lands on. */
34
+ readonly notify: (message: string) => void;
35
+ }
36
+ export declare function runLogin(urls: ResolvedUrls, deps: LoginDeps, io: LoginProgress): Promise<LoginOutcome>;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * `login` orchestration: open the browser, wait on the loopback listener, hand
3
+ * whatever comes back to the ordinary config writer.
4
+ *
5
+ * The credentials live in a local variable for the length of one run and are
6
+ * written only into `settings.json`. Nothing else is persisted — no token cache, no
7
+ * `~/.ai21/credentials` — so there is nothing for an attacker to find later.
8
+ */
9
+ import { containsControlCharacters } from "../args.js";
10
+ import { CALLBACK_PARAM, STATE_PARAM } from "./contract.js";
11
+ /** The `/welcome` URL, carrying where to call back and the nonce to echo. */
12
+ export function loginUrl(urls, callbackUrl, state) {
13
+ const url = new URL("/welcome", urls.app);
14
+ url.searchParams.set(CALLBACK_PARAM, callbackUrl);
15
+ url.searchParams.set(STATE_PARAM, state);
16
+ return url.toString();
17
+ }
18
+ /**
19
+ * Callback values are remote input, so they are validated by exactly the rule the
20
+ * flags use. They land in the same `\n`-joined header string, where a newline
21
+ * forges a header that Claude Code would then send on every request — and unlike a
22
+ * flag, nobody typed this.
23
+ */
24
+ function rejectBadValues(payload) {
25
+ for (const [name, value] of [
26
+ ["agentId", payload.agentId],
27
+ ["ai21Key", payload.ai21Key],
28
+ ]) {
29
+ if (containsControlCharacters(value)) {
30
+ return `the ${name} in the login response contains control characters`;
31
+ }
32
+ }
33
+ return undefined;
34
+ }
35
+ export async function runLogin(urls, deps, io) {
36
+ let listener;
37
+ try {
38
+ listener = await deps.startListener({
39
+ allowedOrigin: urls.app,
40
+ signInUrl: (callbackUrl, state) => loginUrl(urls, callbackUrl, state),
41
+ });
42
+ }
43
+ catch (err) {
44
+ return { kind: "failed", reason: err instanceof Error ? err.message : "the local listener could not be started" };
45
+ }
46
+ // The browser is pointed at the loopback redirect, so the nonce never reaches its
47
+ // command line. The printed URL is the real one, since a manual path needs it.
48
+ //
49
+ // Claims nothing about whether a browser appeared: `spawn` reports a missing binary
50
+ // asynchronously, so a failed launch still looks like a success from here.
51
+ deps.openBrowser(listener.startUrl);
52
+ io.notify(`Sign in to continue — your browser should open. If it does not, visit:\n\n ${listener.signInUrl}\n\nWaiting…`);
53
+ return settle(await listener.result);
54
+ }
55
+ /** Shared tail: turn a listener outcome into the credentials, or say why not. */
56
+ function settle(outcome) {
57
+ switch (outcome.kind) {
58
+ case "timeout":
59
+ return { kind: "timeout" };
60
+ case "failed":
61
+ return { kind: "failed", reason: outcome.reason };
62
+ case "callback": {
63
+ const problem = rejectBadValues(outcome.payload);
64
+ if (problem !== undefined)
65
+ return { kind: "rejected", reason: problem };
66
+ return {
67
+ kind: "credentials",
68
+ agentId: outcome.payload.agentId,
69
+ ai21Key: outcome.payload.ai21Key,
70
+ ...(outcome.payload.workspaceId === undefined ? {} : { workspaceId: outcome.payload.workspaceId }),
71
+ };
72
+ }
73
+ }
74
+ }
75
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/login/session.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Reading the AI21 key from the terminal when `--key` was not given.
3
+ *
4
+ * Two properties matter: the key is never echoed — the whole reason for prompting
5
+ * rather than taking a flag — and a non-interactive run fails immediately instead
6
+ * of blocking on a stdin that will never deliver a line.
7
+ */
8
+ import type { Readable, Writable } from "node:stream";
9
+ /** How the prompt ended. `cancelled` covers Ctrl-C and a closed stdin. */
10
+ export type PromptOutcome = {
11
+ readonly kind: "key";
12
+ readonly value: string;
13
+ } | {
14
+ readonly kind: "no-tty";
15
+ } | {
16
+ readonly kind: "cancelled";
17
+ };
18
+ export interface PromptIo {
19
+ readonly input: Readable & {
20
+ isTTY?: boolean;
21
+ setRawMode?: (raw: boolean) => void;
22
+ };
23
+ readonly output: Writable;
24
+ /** Overrides the stream's own flag; tests drive a pipe as though it were a tty. */
25
+ readonly isTTY?: boolean;
26
+ }
27
+ /**
28
+ * Reads one line without echoing it. Raw mode is what makes that possible, and is
29
+ * restored on every exit path — a terminal left in raw mode makes the user's next
30
+ * command invisible.
31
+ */
32
+ export declare function promptForKey(io: PromptIo, label?: string): Promise<PromptOutcome>;
package/dist/prompt.js ADDED
@@ -0,0 +1,72 @@
1
+ const ETX = "\u0003"; // Ctrl-C
2
+ const EOT = "\u0004"; // Ctrl-D
3
+ // eslint-disable-next-line no-control-regex
4
+ const BACKSPACE = /[\u0008\u007F]/;
5
+ // C0 controls minus the ones handled above, plus DEL.
6
+ // eslint-disable-next-line no-control-regex
7
+ const OTHER_CONTROL = /[\u0000-\u001F\u007F]/;
8
+ /**
9
+ * Reads one line without echoing it. Raw mode is what makes that possible, and is
10
+ * restored on every exit path — a terminal left in raw mode makes the user's next
11
+ * command invisible.
12
+ */
13
+ export function promptForKey(io, label = "AI21 key: ") {
14
+ const interactive = io.isTTY ?? io.input.isTTY ?? false;
15
+ if (!interactive) {
16
+ return Promise.resolve({ kind: "no-tty" });
17
+ }
18
+ return new Promise((resolve) => {
19
+ let buffer = "";
20
+ let settled = false;
21
+ const restore = () => {
22
+ io.input.setRawMode?.(false);
23
+ io.input.pause();
24
+ io.input.off("data", onData);
25
+ io.input.off("end", onEnd);
26
+ io.input.off("close", onEnd);
27
+ };
28
+ // Ctrl-D arrives as a byte, but a stream that simply ends delivers none. Without
29
+ // this the promise never settles and the CLI wedges.
30
+ function onEnd() {
31
+ finish({ kind: "cancelled" });
32
+ }
33
+ const finish = (outcome) => {
34
+ if (settled)
35
+ return;
36
+ settled = true;
37
+ restore();
38
+ // The newline the user's Enter did not produce, because it was swallowed.
39
+ io.output.write("\n");
40
+ resolve(outcome);
41
+ };
42
+ function onData(chunk) {
43
+ for (const char of chunk.toString("utf8")) {
44
+ if (char === "\r" || char === "\n") {
45
+ finish({ kind: "key", value: buffer });
46
+ return;
47
+ }
48
+ if (char === ETX || char === EOT) {
49
+ finish({ kind: "cancelled" });
50
+ return;
51
+ }
52
+ if (BACKSPACE.test(char)) {
53
+ buffer = buffer.slice(0, -1);
54
+ continue;
55
+ }
56
+ // Arrow keys arrive as escape sequences and would corrupt the key. The flag
57
+ // path rejects the same characters, so accepting them here is a second way in.
58
+ if (OTHER_CONTROL.test(char))
59
+ continue;
60
+ // Unechoed, and no mask characters either: a visible length is information.
61
+ buffer += char;
62
+ }
63
+ }
64
+ io.output.write(label);
65
+ io.input.setRawMode?.(true);
66
+ io.input.resume();
67
+ io.input.on("data", onData);
68
+ io.input.once("end", onEnd);
69
+ io.input.once("close", onEnd);
70
+ });
71
+ }
72
+ //# sourceMappingURL=prompt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.js","sourceRoot":"","sources":["../src/prompt.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Turning an outcome into output: human text on the right stream, or exactly one
3
+ * JSON object, plus the warnings that apply either way.
4
+ *
5
+ * Two rules hold everywhere here. The key never appears, in any line. And `--json`
6
+ * emits a single object, since a consumer parses the whole of stdout.
7
+ */
8
+ import type { ResolvedUrls } from "./args.js";
9
+ import type { ConfigOutcome } from "./config.js";
10
+ import type { RevertOutcome } from "./revert.js";
11
+ import type { VerifyOutcome } from "./verify.js";
12
+ import { type RunResult } from "./result.js";
13
+ /**
14
+ * Outcome name → exit code. This is the map `--json` consumers branch on, so the
15
+ * numbers are a contract: they may gain entries but never change meaning.
16
+ */
17
+ export declare const EXIT_FOR: Record<ConfigOutcome["kind"], number>;
18
+ /**
19
+ * `uninstall` outcome → exit code. Same contract as EXIT_FOR: entries may be added,
20
+ * meanings never change. Actionable refusals share CONFLICT; the two
21
+ * "not-there" cases get their own codes so a script can tell them apart.
22
+ */
23
+ export declare const EXIT_FOR_REVERT: Record<RevertOutcome["kind"], number>;
24
+ export declare function reportRevert(outcome: RevertOutcome, json: boolean): RunResult;
25
+ export interface Warning {
26
+ /** Stable identifier, so `--json` consumers match on this and not on prose. */
27
+ readonly code: "anthropic-api-key-set" | "non-production-gateway" | "insecure-gateway" | "verification-skipped" | "verification-failed";
28
+ readonly message: string;
29
+ }
30
+ export interface WarningContext {
31
+ readonly urls: ResolvedUrls;
32
+ /** The process environment, injected so tests need not mutate the real one. */
33
+ readonly processEnv: Record<string, string | undefined>;
34
+ }
35
+ /**
36
+ * Not failures, but things that make a correct config look broken later — which is
37
+ * when a warning earns its noise.
38
+ */
39
+ export declare function collectWarnings(ctx: WarningContext): Warning[];
40
+ export interface ReportInput {
41
+ readonly outcome: ConfigOutcome;
42
+ readonly warnings: readonly Warning[];
43
+ readonly json: boolean;
44
+ /** Extra human-only detail, such as the per-key conflict breakdown. */
45
+ readonly detail?: string | undefined;
46
+ readonly urls: ResolvedUrls;
47
+ /** Absent when the write never succeeded, so verification was not attempted. */
48
+ readonly verification?: VerifyOutcome | undefined;
49
+ }
50
+ export declare function report(input: ReportInput): RunResult;