osborn 0.8.9 → 0.8.10

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 (2) hide show
  1. package/dist/claude-auth.js +49 -52
  2. package/package.json +1 -1
@@ -147,67 +147,64 @@ export async function checkClaudeAuthStatus() {
147
147
  // ─────────────────────────────────────────
148
148
  /**
149
149
  * Extract OAuth URL from CLI output.
150
- * Strips ALL whitespace first (like vutran1710/claudebox) to handle
151
- * Ink UI wrapping the URL across multiple lines.
152
- * Also cleans trailing "Pastecodehereifprompted" that Ink appends.
153
150
  *
154
- * IMPORTANT: strips the `redirect_uri` query parameter (which points to a
155
- * localhost callback server on the *sprite*, not the user's machine). With
156
- * no redirect_uri, claude.com falls back to showing the auth code in-page,
157
- * which the user pastes back into the modal. This is the only flow that
158
- * works for cloud sandboxes the localhost redirect breaks both on phones
159
- * (no listener) AND on desktops (sprite's localhost is unreachable).
151
+ * Strips ALL whitespace first (like vutran1710/claudebox) to handle Ink UI
152
+ * wrapping the URL across multiple lines. Cleans trailing junk the Ink UI
153
+ * appends (e.g. "Pastecodehereifprompted") and any stray ESC bytes.
154
+ *
155
+ * Note on redirect_uri: we used to strip it hoping claude.ai would fall back
156
+ * to an in-page code display, but claude.ai REQUIRES redirect_uri and returns
157
+ * "Invalid OAuth Request: Missing redirect_uri parameter" when it's missing.
158
+ * The pinned Claude Code client ID (9d1c250a-e61b-44d9-88ed-5944d1962f5e)
159
+ * only has http://localhost:<port>/callback URIs in its whitelist, so we
160
+ * can't rewrite to a public callback either — it would be rejected.
161
+ *
162
+ * Actual flow that works: keep the localhost redirect AS-IS. User clicks the
163
+ * URL on any device, authorizes. claude.ai 302s the browser to the localhost
164
+ * URL (which is unreachable). The browser shows "connection refused" but
165
+ * leaves the full URL in the address bar — including ?code=XXX&state=YYY.
166
+ * The user copies the `code` value from the address bar and pastes it into
167
+ * the modal. This is ugly on mobile but it's the only flow the server
168
+ * accepts.
160
169
  */
161
170
  function extractOAuthUrl(text) {
162
- // Strip ANSI codes
163
- const noAnsi = text.replace(/\x1B\[[0-9;]*[A-Za-z]/g, '')
164
- .replace(/\x1B\][^\x07]*\x07/g, '');
171
+ // Replace ANSI control sequences AND lone ESC bytes with a NUL sentinel.
172
+ // Why NUL and not a space: we need to strip all whitespace next (to
173
+ // unwrap URLs that Ink wrapped across terminal lines), and if we used
174
+ // a space it would be eaten by the whitespace strip — then text on
175
+ // either side of the control sequence would fuse into the URL. NUL
176
+ // survives the whitespace strip and acts as a hard boundary the
177
+ // tail-cut logic below can detect.
178
+ const SENTINEL = '\x00';
179
+ const noAnsi = text
180
+ .replace(/\x1B\[[0-9;]*[A-Za-z]/g, SENTINEL)
181
+ .replace(/\x1B\][^\x07]*\x07/g, SENTINEL)
182
+ .replace(/\x1B/g, SENTINEL);
165
183
  // Strip all whitespace (claudebox pattern: strings.Join(strings.Fields(pane), ""))
184
+ // to unwrap URLs split across terminal lines. NUL sentinels survive.
166
185
  const stripped = noAnsi.replace(/\s+/g, '');
167
186
  const match = stripped.match(URL_REGEX);
168
187
  if (!match)
169
188
  return null;
170
189
  let url = match[0];
171
- // Clean trailing Ink artifacts (claudebox pattern)
172
- const trailingJunk = ['Pastecodehereifprompted', 'Pastecodehereifprompted>'];
173
- for (const junk of trailingJunk) {
174
- const idx = url.indexOf(junk);
175
- if (idx > 0)
176
- url = url.substring(0, idx);
177
- }
178
- // Strip the localhost redirect_uri so claude.com shows a pasteable code
179
- // instead of trying to redirect. URL() can't be used here because it
180
- // re-encodes the path, so we surgically delete the redirect_uri param.
181
- url = stripRedirectUri(url);
182
- return url;
183
- }
184
- /**
185
- * Strip the `redirect_uri` query param from an OAuth URL.
186
- *
187
- * Background: `claude setup-token` spawns a one-shot localhost HTTP server on
188
- * a random port and registers it as the redirect_uri. That works fine when the
189
- * user is on the same machine as the CLI, but on a sprite the URL points to
190
- * the *sprite's* localhost — unreachable from the user's browser regardless
191
- * of whether they open the auth link on their PC or their phone. With no
192
- * redirect_uri at all, claude.ai falls back to its in-page code display
193
- * (the same flow that `claude setup-token`'s "Paste code here if prompted"
194
- * Ink input is built to consume), and the user can paste the code back into
195
- * our modal — which works whether they signed in on phone or desktop.
196
- *
197
- * Done with regex rather than `new URL()` because the URL constructor
198
- * normalizes the path (which can break Claude's strict redirect check)
199
- * and re-encodes spaces/special chars in other params.
200
- */
201
- function stripRedirectUri(url) {
202
- const before = url;
203
- // Three cases: leading param (?redirect_uri=...&), middle/trailing (&redirect_uri=...),
204
- // and only param (?redirect_uri=...). Order matters so cleanup leaves the URL well-formed.
205
- url = url.replace(/&redirect_uri=[^&]*/g, '');
206
- url = url.replace(/\?redirect_uri=[^&]*&/g, '?');
207
- url = url.replace(/\?redirect_uri=[^&]*$/g, '');
208
- if (before !== url) {
209
- console.log('🔑 Stripped localhost redirect_uri from OAuth URL — claude.ai will show a pasteable code instead of redirecting');
210
- }
190
+ // Cut at the first NUL sentinel — that marks where an ANSI control code
191
+ // USED to be, which is a reliable boundary between the URL and adjacent
192
+ // terminal output that got fused by the whitespace strip.
193
+ const sentinelCut = url.indexOf(SENTINEL);
194
+ if (sentinelCut > 0)
195
+ url = url.substring(0, sentinelCut);
196
+ // Cut at the first `paste` (case-insensitive) — Ink always appends a
197
+ // "Paste code here if prompted" input box after the URL, and any case
198
+ // variant of it is junk. None of Claude's query-param values begin
199
+ // with "paste" so this is safe.
200
+ const pasteCut = url.toLowerCase().indexOf('paste');
201
+ if (pasteCut > 0)
202
+ url = url.substring(0, pasteCut);
203
+ // Defensive: cut at any byte outside the URL-valid character set. OAuth
204
+ // URLs use letters, digits, `%`, and URL-safe punctuation only.
205
+ const tailCut = url.search(/[^A-Za-z0-9%._~:/?#\[\]@!$&'()*+,;=\-]/);
206
+ if (tailCut > 0)
207
+ url = url.substring(0, tailCut);
211
208
  return url;
212
209
  }
213
210
  // ─────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osborn",
3
- "version": "0.8.9",
3
+ "version": "0.8.10",
4
4
  "description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
5
5
  "type": "module",
6
6
  "bin": {