@namzu/cli 8.2.0 → 8.3.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.
Files changed (52) hide show
  1. package/dist/doctor/checks/credentials.d.ts.map +1 -1
  2. package/dist/doctor/checks/credentials.js +2 -0
  3. package/dist/doctor/checks/credentials.js.map +1 -1
  4. package/dist/integrations/providers/credential-store.d.ts +118 -0
  5. package/dist/integrations/providers/credential-store.d.ts.map +1 -0
  6. package/dist/integrations/providers/credential-store.js +322 -0
  7. package/dist/integrations/providers/credential-store.js.map +1 -0
  8. package/dist/integrations/providers/discover.d.ts +37 -10
  9. package/dist/integrations/providers/discover.d.ts.map +1 -1
  10. package/dist/integrations/providers/discover.js +43 -13
  11. package/dist/integrations/providers/discover.js.map +1 -1
  12. package/dist/integrations/providers/identity.d.ts +75 -0
  13. package/dist/integrations/providers/identity.d.ts.map +1 -0
  14. package/dist/integrations/providers/identity.js +75 -0
  15. package/dist/integrations/providers/identity.js.map +1 -0
  16. package/dist/integrations/providers/index.d.ts +4 -1
  17. package/dist/integrations/providers/index.d.ts.map +1 -1
  18. package/dist/integrations/providers/index.js +4 -1
  19. package/dist/integrations/providers/index.js.map +1 -1
  20. package/dist/integrations/providers/oauth.d.ts +37 -7
  21. package/dist/integrations/providers/oauth.d.ts.map +1 -1
  22. package/dist/integrations/providers/oauth.js +62 -17
  23. package/dist/integrations/providers/oauth.js.map +1 -1
  24. package/dist/integrations/providers/subscription-login.d.ts +123 -0
  25. package/dist/integrations/providers/subscription-login.d.ts.map +1 -0
  26. package/dist/integrations/providers/subscription-login.js +348 -0
  27. package/dist/integrations/providers/subscription-login.js.map +1 -0
  28. package/dist/tui/App.d.ts.map +1 -1
  29. package/dist/tui/App.js +103 -1
  30. package/dist/tui/App.js.map +1 -1
  31. package/dist/tui/Picker.js +4 -0
  32. package/dist/tui/Picker.js.map +1 -1
  33. package/dist/tui/agent.d.ts.map +1 -1
  34. package/dist/tui/agent.js +15 -7
  35. package/dist/tui/agent.js.map +1 -1
  36. package/dist/tui/credential-entry.d.ts +16 -12
  37. package/dist/tui/credential-entry.d.ts.map +1 -1
  38. package/dist/tui/credential-entry.js +16 -12
  39. package/dist/tui/credential-entry.js.map +1 -1
  40. package/dist/tui/login-prompt.d.ts +41 -0
  41. package/dist/tui/login-prompt.d.ts.map +1 -0
  42. package/dist/tui/login-prompt.js +69 -0
  43. package/dist/tui/login-prompt.js.map +1 -0
  44. package/dist/tui/open-browser.d.ts +24 -0
  45. package/dist/tui/open-browser.d.ts.map +1 -0
  46. package/dist/tui/open-browser.js +57 -0
  47. package/dist/tui/open-browser.js.map +1 -0
  48. package/dist/tui/slashCommands.d.ts +16 -0
  49. package/dist/tui/slashCommands.d.ts.map +1 -1
  50. package/dist/tui/slashCommands.js +13 -0
  51. package/dist/tui/slashCommands.js.map +1 -1
  52. package/package.json +1 -1
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Obtaining a subscription credential from inside namzu.
3
+ *
4
+ * namzu could already USE and REFRESH a plan-backed credential; it could not
5
+ * get one. The only way in was to install another product, sign in there, and
6
+ * let discovery borrow what that product had stored — which works on exactly
7
+ * one operating system and requires the other product. This is the missing
8
+ * half: an authorization-code exchange, run by namzu, that ends with a
9
+ * credential in namzu's own store (`credential-store.ts`).
10
+ *
11
+ * ## The flow
12
+ *
13
+ * Authorization code with PKCE (RFC 7636, S256), redirected to a loopback
14
+ * address (RFC 8252 §7.3). Two ways to finish, and BOTH are always offered:
15
+ *
16
+ * 1. **Loopback.** A one-request server on `127.0.0.1` at the port the
17
+ * redirect is registered under. The browser lands on it and the code
18
+ * never touches a screen.
19
+ * 2. **Paste.** The operator copies the code — or the whole redirect URL —
20
+ * out of a browser and hands it back.
21
+ *
22
+ * The second is not a consolation prize. A container, a remote shell over
23
+ * SSH, a machine with no graphical session: none of them can be redirected
24
+ * to, and on all of them the loopback server is either unreachable from
25
+ * wherever the browser actually is or cannot bind at all. `start()` says
26
+ * which of the two it managed to arrange (`loopback`), and the paste path
27
+ * works whether or not it did.
28
+ *
29
+ * ## What never happens here
30
+ *
31
+ * No token is logged, printed, or put in an error. Failures name the status,
32
+ * the endpoint and the stage; they never carry the response body, because a
33
+ * token endpoint's body is where the token is. Nothing is written to the
34
+ * store until an exchange has fully succeeded, so an abandoned or rejected
35
+ * login leaves no partial credential behind.
36
+ */
37
+ import type { DetectedProvider } from './discover.js';
38
+ import type { AgentOAuthCredential } from './keychain.js';
39
+ /** What a completed attempt produced. Never carries a secret in `reason`. */
40
+ export type LoginOutcome = {
41
+ readonly ok: true;
42
+ /** The stored credential, so the caller can open a session without re-reading. */
43
+ readonly credential: AgentOAuthCredential;
44
+ /** Where it landed, so a surface can say so without guessing the path. */
45
+ readonly storedAt: string;
46
+ } | {
47
+ readonly ok: false;
48
+ readonly reason: string;
49
+ };
50
+ export interface SubscriptionLogin {
51
+ /** The page to open. Safe to print — it carries a challenge, never a secret. */
52
+ readonly url: string;
53
+ readonly redirectUri: string;
54
+ /**
55
+ * Whether the loopback listener came up.
56
+ *
57
+ * `false` is not a failure: the port was busy, or the environment forbids
58
+ * listening. The paste path is unaffected, and a surface should say so
59
+ * rather than imply the login is broken.
60
+ */
61
+ readonly loopback: boolean;
62
+ /**
63
+ * Resolve when the browser reaches the loopback listener, or never.
64
+ *
65
+ * `null` when `loopback` is false, so a caller cannot await a promise that
66
+ * has no way to settle. Racing this against `completeWithPastedCode` is the
67
+ * intended use; whichever finishes first wins and `cancel()` releases the
68
+ * other.
69
+ */
70
+ waitForCallback(): Promise<LoginOutcome> | null;
71
+ /** Finish from a pasted redirect URL, `code#state`, or a bare code. */
72
+ completeWithPastedCode(input: string): Promise<LoginOutcome>;
73
+ /** Release the listener. Safe to call twice, and safe to call after success. */
74
+ cancel(): void;
75
+ }
76
+ export interface SubscriptionLoginOptions {
77
+ /** Override `homedir()` — tests, and an operator with a relocated home. */
78
+ readonly home?: string;
79
+ /** Override the token-exchange transport (tests inject a stub). */
80
+ readonly fetch?: typeof fetch;
81
+ /**
82
+ * Try to listen on the loopback redirect. Default true.
83
+ *
84
+ * A caller that knows there is no browser here — `--no-browser`, a
85
+ * container — passes false and skips a bind that would only ever time out.
86
+ */
87
+ readonly loopback?: boolean;
88
+ }
89
+ /**
90
+ * Begin a login. Returns the page to open plus the two ways to finish it.
91
+ *
92
+ * Deliberately does NOT open a browser. Which of `open`, `xdg-open` or a
93
+ * Windows protocol handler to spawn — and whether spawning anything is even
94
+ * appropriate — is a property of the surface the operator is sitting in front
95
+ * of, not of the protocol. A module that reached for a browser would be
96
+ * unusable from the one place that most needs it: a machine that has none.
97
+ */
98
+ export declare function beginSubscriptionLogin(options?: SubscriptionLoginOptions): Promise<SubscriptionLogin>;
99
+ /**
100
+ * The detected-provider record a completed sign-in produces.
101
+ *
102
+ * Shaped exactly like a discovered one — the same shape `discoverProviders`
103
+ * would return on the NEXT launch, having read the file this login just
104
+ * wrote — so every downstream path treats it identically and nothing has to
105
+ * learn that a session began with a login. It carries `origin: 'stored'`, so
106
+ * the between-turn refresh writes back to the file rather than to a Keychain
107
+ * this machine may not have.
108
+ */
109
+ export declare function subscriptionDetectedProvider(credential: AgentOAuthCredential, storedAt: string): DetectedProvider;
110
+ /**
111
+ * Pull a code (and a state, when there is one) out of whatever got pasted.
112
+ *
113
+ * Four spellings reach this in practice: the full redirect address, its query
114
+ * string alone, the `code#state` pair a consent screen offers for copying,
115
+ * and a bare code. Anything else yields no code and is refused by the caller
116
+ * — guessing at a fifth shape would send an arbitrary string to a token
117
+ * endpoint as if it were an authorization code.
118
+ */
119
+ export declare function parsePastedInput(input: string): {
120
+ code?: string;
121
+ state?: string;
122
+ };
123
+ //# sourceMappingURL=subscription-login.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscription-login.d.ts","sourceRoot":"","sources":["../../../src/integrations/providers/subscription-login.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAMH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AASrD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAA;AAGzD,6EAA6E;AAC7E,MAAM,MAAM,YAAY,GACrB;IACA,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAA;IACjB,kFAAkF;IAClF,QAAQ,CAAC,UAAU,EAAE,oBAAoB,CAAA;IACzC,0EAA0E;IAC1E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CACxB,GACD;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAElD,MAAM,WAAW,iBAAiB;IACjC,gFAAgF;IAChF,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;IAC1B;;;;;;;OAOG;IACH,eAAe,IAAI,OAAO,CAAC,YAAY,CAAC,GAAG,IAAI,CAAA;IAC/C,uEAAuE;IACvE,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;IAC5D,gFAAgF;IAChF,MAAM,IAAI,IAAI,CAAA;CACd;AAED,MAAM,WAAW,wBAAwB;IACxC,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,mEAAmE;IACnE,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAC3B;AAED;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,CAC3C,OAAO,GAAE,wBAA6B,GACpC,OAAO,CAAC,iBAAiB,CAAC,CA6D5B;AAED;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAC3C,UAAU,EAAE,oBAAoB,EAChC,QAAQ,EAAE,MAAM,GACd,gBAAgB,CAYlB;AA4KD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CA+BjF"}
@@ -0,0 +1,348 @@
1
+ /**
2
+ * Obtaining a subscription credential from inside namzu.
3
+ *
4
+ * namzu could already USE and REFRESH a plan-backed credential; it could not
5
+ * get one. The only way in was to install another product, sign in there, and
6
+ * let discovery borrow what that product had stored — which works on exactly
7
+ * one operating system and requires the other product. This is the missing
8
+ * half: an authorization-code exchange, run by namzu, that ends with a
9
+ * credential in namzu's own store (`credential-store.ts`).
10
+ *
11
+ * ## The flow
12
+ *
13
+ * Authorization code with PKCE (RFC 7636, S256), redirected to a loopback
14
+ * address (RFC 8252 §7.3). Two ways to finish, and BOTH are always offered:
15
+ *
16
+ * 1. **Loopback.** A one-request server on `127.0.0.1` at the port the
17
+ * redirect is registered under. The browser lands on it and the code
18
+ * never touches a screen.
19
+ * 2. **Paste.** The operator copies the code — or the whole redirect URL —
20
+ * out of a browser and hands it back.
21
+ *
22
+ * The second is not a consolation prize. A container, a remote shell over
23
+ * SSH, a machine with no graphical session: none of them can be redirected
24
+ * to, and on all of them the loopback server is either unreachable from
25
+ * wherever the browser actually is or cannot bind at all. `start()` says
26
+ * which of the two it managed to arrange (`loopback`), and the paste path
27
+ * works whether or not it did.
28
+ *
29
+ * ## What never happens here
30
+ *
31
+ * No token is logged, printed, or put in an error. Failures name the status,
32
+ * the endpoint and the stage; they never carry the response body, because a
33
+ * token endpoint's body is where the token is. Nothing is written to the
34
+ * store until an exchange has fully succeeded, so an abandoned or rejected
35
+ * login leaves no partial credential behind.
36
+ */
37
+ import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
38
+ import { createServer } from 'node:http';
39
+ import { writeStoredSubscriptionCredential } from './credential-store.js';
40
+ import { AUTHORIZE_URL, OAUTH_CLIENT_ID, OAUTH_SCOPES, OAUTH_TOKEN_URL, REDIRECT_URI, SUBSCRIPTION_REDIRECT_PORT, } from './identity.js';
41
+ import { PROVIDER_REGISTRY } from './registry.js';
42
+ /**
43
+ * Begin a login. Returns the page to open plus the two ways to finish it.
44
+ *
45
+ * Deliberately does NOT open a browser. Which of `open`, `xdg-open` or a
46
+ * Windows protocol handler to spawn — and whether spawning anything is even
47
+ * appropriate — is a property of the surface the operator is sitting in front
48
+ * of, not of the protocol. A module that reached for a browser would be
49
+ * unusable from the one place that most needs it: a machine that has none.
50
+ */
51
+ export async function beginSubscriptionLogin(options = {}) {
52
+ const verifier = base64Url(randomBytes(32));
53
+ const challenge = base64Url(createHash('sha256').update(verifier).digest());
54
+ // An independent value, not the verifier reused.
55
+ //
56
+ // Reusing the verifier as `state` is a thing implementations do, and it
57
+ // undoes PKCE: `state` travels in the authorization URL, so the verifier
58
+ // would sit in the address bar, the browser history and any referrer along
59
+ // the way — and the secret PKCE exists to keep out of that URL is exactly
60
+ // the verifier.
61
+ const state = base64Url(randomBytes(16));
62
+ const listener = options.loopback === false ? null : await openLoopback(state);
63
+ let settled = false;
64
+ const exchangeOnce = async (code, seenState) => {
65
+ if (settled)
66
+ return { ok: false, reason: 'This login was already completed.' };
67
+ if (!constantTimeEquals(seenState, state)) {
68
+ return {
69
+ ok: false,
70
+ reason: 'The sign-in did not come back with the value namzu sent, so it cannot be matched to this attempt. Nothing was stored. Start the login again.',
71
+ };
72
+ }
73
+ settled = true;
74
+ return exchange(code, state, verifier, options);
75
+ };
76
+ return {
77
+ url: authorizeUrl(challenge, state),
78
+ redirectUri: REDIRECT_URI,
79
+ loopback: listener !== null,
80
+ waitForCallback: () => listener === null
81
+ ? null
82
+ : listener.received.then((got) => got === null
83
+ ? {
84
+ ok: false,
85
+ reason: 'The sign-in was cancelled before the browser came back.',
86
+ }
87
+ : got.error
88
+ ? {
89
+ ok: false,
90
+ reason: `The sign-in did not complete (${got.error}). Nothing was stored.`,
91
+ }
92
+ : exchangeOnce(got.code, got.state)),
93
+ completeWithPastedCode: async (input) => {
94
+ const parsed = parsePastedInput(input);
95
+ if (!parsed.code) {
96
+ return {
97
+ ok: false,
98
+ reason: 'That does not contain an authorization code. Paste the whole address the browser finished on, or the code it showed you.',
99
+ };
100
+ }
101
+ return exchangeOnce(parsed.code, parsed.state ?? state);
102
+ },
103
+ cancel: () => listener?.close(),
104
+ };
105
+ }
106
+ /**
107
+ * The detected-provider record a completed sign-in produces.
108
+ *
109
+ * Shaped exactly like a discovered one — the same shape `discoverProviders`
110
+ * would return on the NEXT launch, having read the file this login just
111
+ * wrote — so every downstream path treats it identically and nothing has to
112
+ * learn that a session began with a login. It carries `origin: 'stored'`, so
113
+ * the between-turn refresh writes back to the file rather than to a Keychain
114
+ * this machine may not have.
115
+ */
116
+ export function subscriptionDetectedProvider(credential, storedAt) {
117
+ return {
118
+ entry: PROVIDER_REGISTRY.anthropic,
119
+ source: { kind: 'stored', path: storedAt },
120
+ apiKey: credential.accessToken,
121
+ oauth: {
122
+ refreshToken: credential.refreshToken,
123
+ expiresAt: credential.expiresAt,
124
+ origin: 'stored',
125
+ },
126
+ alternatives: [],
127
+ };
128
+ }
129
+ function authorizeUrl(challenge, state) {
130
+ const params = new URLSearchParams({
131
+ code: 'true',
132
+ client_id: OAUTH_CLIENT_ID,
133
+ response_type: 'code',
134
+ redirect_uri: REDIRECT_URI,
135
+ scope: OAUTH_SCOPES,
136
+ code_challenge: challenge,
137
+ code_challenge_method: 'S256',
138
+ state,
139
+ });
140
+ return `${AUTHORIZE_URL}?${params.toString()}`;
141
+ }
142
+ /**
143
+ * Exchange the code, and store the result only once it is whole.
144
+ *
145
+ * The store write is the LAST thing. A response missing an access token, a
146
+ * non-2xx, a transport failure and a malformed body all return before it, so
147
+ * there is no path on which a failed login leaves a file behind.
148
+ */
149
+ async function exchange(code, state, verifier, options) {
150
+ const fetchFn = options.fetch ?? globalThis.fetch;
151
+ let res;
152
+ try {
153
+ res = await fetchFn(OAUTH_TOKEN_URL, {
154
+ method: 'POST',
155
+ headers: { 'content-type': 'application/json', accept: 'application/json' },
156
+ body: JSON.stringify({
157
+ grant_type: 'authorization_code',
158
+ client_id: OAUTH_CLIENT_ID,
159
+ code,
160
+ state,
161
+ redirect_uri: REDIRECT_URI,
162
+ code_verifier: verifier,
163
+ }),
164
+ signal: AbortSignal.timeout(30_000),
165
+ });
166
+ }
167
+ catch (err) {
168
+ // The MESSAGE of a transport error is safe (a hostname, a timeout); the
169
+ // body of a response is not, and there is none here.
170
+ return {
171
+ ok: false,
172
+ reason: `Could not reach the sign-in service (${err instanceof Error ? err.message : String(err)}). Nothing was stored.`,
173
+ };
174
+ }
175
+ if (!res.ok) {
176
+ // The status, never the body. A token endpoint answers failures with a
177
+ // document that can contain the very thing this module must not leak.
178
+ return {
179
+ ok: false,
180
+ reason: `The sign-in service refused the exchange (HTTP ${res.status}). The code may have already been used, or expired. Nothing was stored.`,
181
+ };
182
+ }
183
+ let data;
184
+ try {
185
+ data = (await res.json());
186
+ }
187
+ catch {
188
+ return {
189
+ ok: false,
190
+ reason: 'The sign-in service answered with something that is not a token. Nothing was stored.',
191
+ };
192
+ }
193
+ if (typeof data.access_token !== 'string' || data.access_token.length === 0) {
194
+ return {
195
+ ok: false,
196
+ reason: 'The sign-in service answered without a token. Nothing was stored.',
197
+ };
198
+ }
199
+ const credential = {
200
+ accessToken: data.access_token,
201
+ ...(typeof data.refresh_token === 'string' && data.refresh_token.length > 0
202
+ ? { refreshToken: data.refresh_token }
203
+ : {}),
204
+ ...(typeof data.expires_in === 'number'
205
+ ? { expiresAt: Date.now() + data.expires_in * 1_000 }
206
+ : {}),
207
+ };
208
+ let storedAt;
209
+ try {
210
+ storedAt = writeStoredSubscriptionCredential(credential, options.home);
211
+ }
212
+ catch (err) {
213
+ // The store refused to prove the file private, so there is no file. Say
214
+ // what happened; the credential is dropped with the process.
215
+ return {
216
+ ok: false,
217
+ reason: `Signed in, but the credential could not be stored privately: ${err instanceof Error ? err.message : String(err)}`,
218
+ };
219
+ }
220
+ return { ok: true, credential, storedAt };
221
+ }
222
+ /**
223
+ * A single-purpose listener on the loopback redirect, or `null`.
224
+ *
225
+ * `null` on every failure to bind — the port is in use (a second namzu, or
226
+ * another product's login mid-flight), the sandbox forbids listening, there
227
+ * is no loopback interface. None of those is fatal, because the paste path
228
+ * does not need this.
229
+ */
230
+ async function openLoopback(expectedState) {
231
+ let settle = () => { };
232
+ const received = new Promise((r) => {
233
+ let done = false;
234
+ settle = (v) => {
235
+ if (done)
236
+ return;
237
+ done = true;
238
+ r(v);
239
+ };
240
+ });
241
+ const server = createServer((req, res) => {
242
+ const url = new URL(req.url ?? '/', `http://127.0.0.1:${SUBSCRIPTION_REDIRECT_PORT}`);
243
+ const error = url.searchParams.get('error');
244
+ const code = url.searchParams.get('code');
245
+ const state = url.searchParams.get('state');
246
+ const finish = (status, text) => {
247
+ res.writeHead(status, { 'content-type': 'text/plain; charset=utf-8' });
248
+ res.end(text);
249
+ };
250
+ if (error) {
251
+ finish(400, 'Sign-in did not complete. Return to namzu.');
252
+ settle({ code: '', state: '', error });
253
+ return;
254
+ }
255
+ if (!code || !state) {
256
+ // Not our redirect — a browser probing the port, a favicon request.
257
+ // Answering and NOT settling keeps the listener alive for the real one.
258
+ finish(404, 'Not the sign-in callback.');
259
+ return;
260
+ }
261
+ if (!constantTimeEquals(state, expectedState)) {
262
+ // Refused here as well as at the exchange. This is the request a
263
+ // cross-site attempt would make, and answering it with "done" would
264
+ // tell whoever made it that the port is a live login.
265
+ finish(400, 'Sign-in did not complete. Return to namzu.');
266
+ return;
267
+ }
268
+ finish(200, 'Signed in. You can close this tab and return to namzu.');
269
+ settle({ code, state });
270
+ });
271
+ const bound = await new Promise((resolve) => {
272
+ server.once('error', () => resolve(false));
273
+ server.listen(SUBSCRIPTION_REDIRECT_PORT, '127.0.0.1', () => resolve(true));
274
+ });
275
+ if (!bound) {
276
+ server.close();
277
+ return null;
278
+ }
279
+ return {
280
+ received,
281
+ close: () => {
282
+ settle(null);
283
+ server.close();
284
+ },
285
+ };
286
+ }
287
+ /**
288
+ * Pull a code (and a state, when there is one) out of whatever got pasted.
289
+ *
290
+ * Four spellings reach this in practice: the full redirect address, its query
291
+ * string alone, the `code#state` pair a consent screen offers for copying,
292
+ * and a bare code. Anything else yields no code and is refused by the caller
293
+ * — guessing at a fifth shape would send an arbitrary string to a token
294
+ * endpoint as if it were an authorization code.
295
+ */
296
+ export function parsePastedInput(input) {
297
+ const value = input.trim();
298
+ if (value.length === 0)
299
+ return {};
300
+ if (/^https?:\/\//i.test(value)) {
301
+ try {
302
+ const url = new URL(value);
303
+ return {
304
+ code: url.searchParams.get('code') ?? undefined,
305
+ state: url.searchParams.get('state') ?? undefined,
306
+ };
307
+ }
308
+ catch {
309
+ return {};
310
+ }
311
+ }
312
+ if (value.includes('code=')) {
313
+ const params = new URLSearchParams(value.replace(/^\?/, ''));
314
+ return {
315
+ code: params.get('code') ?? undefined,
316
+ state: params.get('state') ?? undefined,
317
+ };
318
+ }
319
+ if (value.includes('#')) {
320
+ const [code, state] = value.split('#', 2);
321
+ return { code: code || undefined, state: state || undefined };
322
+ }
323
+ // A bare code. Whitespace already ruled out by the trim + the checks above
324
+ // only when it contained a separator, so reject an obviously-wrong paste
325
+ // (a sentence, a wrapped line) rather than posting it.
326
+ if (/\s/.test(value))
327
+ return {};
328
+ return { code: value };
329
+ }
330
+ /**
331
+ * Compare two opaque values without leaking their relationship through timing.
332
+ *
333
+ * `state` is compared, not a secret, so this is belt-and-braces — but the
334
+ * cheap version of this comparison is `===`, and `===` on strings of equal
335
+ * length is the shape that teaches the next person to write it for the
336
+ * verifier too.
337
+ */
338
+ function constantTimeEquals(a, b) {
339
+ const left = Buffer.from(a, 'utf8');
340
+ const right = Buffer.from(b, 'utf8');
341
+ if (left.length !== right.length)
342
+ return false;
343
+ return timingSafeEqual(left, right);
344
+ }
345
+ function base64Url(bytes) {
346
+ return bytes.toString('base64url');
347
+ }
348
+ //# sourceMappingURL=subscription-login.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscription-login.js","sourceRoot":"","sources":["../../../src/integrations/providers/subscription-login.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../../src/tui/App.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AA0DH,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE/D,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAA;CACxB;AAWD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,SAAS,iBAAiB,EAAE,GAAG,SAAS,MAAM,EAAE,CAY1F;AAGD,wBAAgB,GAAG,CAAC,EAAE,GAAG,EAAE,EAAE,QAAQ,2CAq3CpC"}
1
+ {"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../../src/tui/App.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAiEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE/D,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAA;CACxB;AAWD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,SAAS,iBAAiB,EAAE,GAAG,SAAS,MAAM,EAAE,CAY1F;AAGD,wBAAgB,GAAG,CAAC,EAAE,GAAG,EAAE,EAAE,QAAQ,2CAo+CpC"}
package/dist/tui/App.js CHANGED
@@ -2,7 +2,9 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { Box, Text, useApp, useInput } from 'ink';
3
3
  import { relative } from 'node:path';
4
4
  import { useCallback, useEffect, useRef, useState } from 'react';
5
- import { primaryProvider, writePreferences, } from '../integrations/providers/index.js';
5
+ import { beginSubscriptionLogin, clearStoredSubscriptionCredential, credentialsPath, primaryProvider, readStoredSubscriptionCredential, writePreferences, } from '../integrations/providers/index.js';
6
+ import { describeLoginOutcome, describeLoginStart, describeLogout } from './login-prompt.js';
7
+ import { openInBrowser } from './open-browser.js';
6
8
  import { isTrusted, trustDir } from '../integrations/trust/store.js';
7
9
  import { appendMemory, composeMemoryPrompt, readMemory } from '../memory/store.js';
8
10
  import { composeSkillsPrompt, discoverSkills, loadSkillBody } from '../skills/store.js';
@@ -105,6 +107,15 @@ export function App({ ctx }) {
105
107
  const [selectedResume, setSelectedResume] = useState(0);
106
108
  const exitArmedRef = useRef(false);
107
109
  const abortRef = useRef(null);
110
+ /**
111
+ * The sign-in attempt awaiting its authorization code, if any.
112
+ *
113
+ * A ref and not state: nothing renders from it, and a re-render between
114
+ * `/login` and `/login <address>` must not lose the verifier — without
115
+ * which the code that comes back cannot be exchanged for anything.
116
+ */
117
+ const loginRef = useRef(null);
118
+ const runProbeRef = useRef(null);
108
119
  /**
109
120
  * The session currently holding resources, so a re-hydration can release the
110
121
  * one it replaces. A `/model` switch builds a new session, and a tool
@@ -302,6 +313,77 @@ export function App({ ctx }) {
302
313
  pushMessage('system', s.errorHint);
303
314
  }
304
315
  }, [ctx.cwd, ctx.rules, pushMessage]);
316
+ /**
317
+ * Sign in to a subscription without leaving namzu.
318
+ *
319
+ * Two calls, one command. Bare `/login` starts an attempt and parks it in
320
+ * `loginRef`; `/login <address>` finishes that one. Nothing here parses the
321
+ * argument — `slashCommands` already decided which of the two this is.
322
+ *
323
+ * ## Why it re-probes instead of installing the credential directly
324
+ *
325
+ * The login writes to the same file `discoverProviders` reads, so re-running
326
+ * the probe is not a lazy substitute for wiring the credential in: it is the
327
+ * only version that cannot disagree with a cold start. Building a session
328
+ * from the returned credential here, in parallel, would be a SECOND way to
329
+ * arrive at a provider — and the day the two differ, the operator's session
330
+ * works until they restart, which is the worst possible moment to find out.
331
+ *
332
+ * It also gets the no-preferences case right for free: a first-run operator
333
+ * who signs in lands in the picker, which is exactly where someone with a
334
+ * working credential and no chosen provider should be.
335
+ */
336
+ const startOrFinishLogin = useCallback(async (pasted) => {
337
+ if (pasted !== undefined) {
338
+ const pending = loginRef.current;
339
+ if (!pending) {
340
+ pushMessage('system', 'There is no sign-in waiting to be finished. Run /login on its own to start one.');
341
+ return;
342
+ }
343
+ const outcome = await pending.completeWithPastedCode(pasted);
344
+ pending.cancel();
345
+ loginRef.current = null;
346
+ pushMessage('system', describeLoginOutcome(outcome));
347
+ if (outcome.ok)
348
+ await runProbeRef.current?.();
349
+ return;
350
+ }
351
+ loginRef.current?.cancel();
352
+ loginRef.current = null;
353
+ let start;
354
+ try {
355
+ start = await beginSubscriptionLogin();
356
+ }
357
+ catch (err) {
358
+ pushMessage('system', `Could not start a sign-in: ${err instanceof Error ? err.message : String(err)}`);
359
+ return;
360
+ }
361
+ loginRef.current = start;
362
+ pushMessage('system', describeLoginStart({
363
+ url: start.url,
364
+ loopback: start.loopback,
365
+ browserOpened: openInBrowser(start.url),
366
+ }));
367
+ // The automatic half, when there is one. Not awaited by the caller:
368
+ // the composer stays live throughout, so the operator can paste
369
+ // instead — or type anything else — while this waits.
370
+ const waiting = start.waitForCallback();
371
+ if (!waiting)
372
+ return;
373
+ const outcome = await waiting;
374
+ if (loginRef.current !== start)
375
+ return; // superseded, or finished by paste
376
+ loginRef.current = null;
377
+ start.cancel();
378
+ // A cancelled listener resolves to a refusal, and saying "sign-in
379
+ // failed" when the operator finished it another way would be a lie
380
+ // about their own successful login.
381
+ if (!outcome.ok && outcome.reason.includes('cancelled'))
382
+ return;
383
+ pushMessage('system', describeLoginOutcome(outcome));
384
+ if (outcome.ok)
385
+ await runProbeRef.current?.();
386
+ }, [pushMessage]);
305
387
  const runProbe = useCallback(async () => {
306
388
  try {
307
389
  const probe = await probeAgentSession();
@@ -340,6 +422,10 @@ export function App({ ctx }) {
340
422
  pushMessage('system', `Failed to probe agents: ${err instanceof Error ? err.message : String(err)}`);
341
423
  }
342
424
  }, [hydrateSession, pushMessage]);
425
+ // `startOrFinishLogin` is declared above `runProbe` and calls it, so it
426
+ // reads the current one through a ref rather than closing over a stale
427
+ // binding or forcing the two into a declaration order that reads backwards.
428
+ runProbeRef.current = runProbe;
343
429
  // Trust gate runs first: don't touch the folder until the user trusts it.
344
430
  useEffect(() => {
345
431
  if (isTrusted(ctx.cwd)) {
@@ -822,6 +908,22 @@ export function App({ ctx }) {
822
908
  setKeyEntryFor(null);
823
909
  setPhase('picker');
824
910
  return;
911
+ case 'login':
912
+ void startOrFinishLogin(slash.pasted);
913
+ return;
914
+ case 'logout': {
915
+ const path = credentialsPath();
916
+ const had = readStoredSubscriptionCredential() !== null;
917
+ try {
918
+ clearStoredSubscriptionCredential();
919
+ }
920
+ catch (err) {
921
+ pushMessage('system', `Could not remove ${path}: ${err instanceof Error ? err.message : String(err)}`);
922
+ return;
923
+ }
924
+ pushMessage('system', describeLogout(path, had));
925
+ return;
926
+ }
825
927
  case 'remember':
826
928
  try {
827
929
  appendMemory(slash.text);