@inerrata-corporation/errata 2.0.0-dev.96 → 2.0.0-dev.97

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/errata.mjs +196 -6
  2. package/package.json +1 -1
package/errata.mjs CHANGED
@@ -51471,7 +51471,7 @@ function startLoopLagMonitor(thresholdMs = 1e3) {
51471
51471
  }
51472
51472
 
51473
51473
  // src/engine.ts
51474
- var DAEMON_VERSION = true ? "2.0.0-dev.96" : "2.0.0-alpha.0";
51474
+ var DAEMON_VERSION = true ? "2.0.0-dev.97" : "2.0.0-alpha.0";
51475
51475
  var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
51476
51476
  var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
51477
51477
  var GIT_OP_MUTE_MS = 4e3;
@@ -54752,6 +54752,193 @@ async function loginOAuthLoopback(opts) {
54752
54752
  return { tokens, tokenEndpoint: endpoints.tokenEndpoint, clientId };
54753
54753
  }
54754
54754
 
54755
+ // src/device-login.ts
54756
+ init_src6();
54757
+ var DEFAULT_SCOPE = "openid profile graph:read graph:write mcp:tools";
54758
+ var BRIDGE_REDIRECT_URI = "http://127.0.0.1:1/device-bridge-callback";
54759
+ var strip2 = (u) => u.replace(/\/+$/, "");
54760
+ async function discoverDeviceEndpoint(cloudUrl, fetchFn = fetch) {
54761
+ try {
54762
+ const res = await fetchFn(`${strip2(cloudUrl)}/.well-known/oauth-authorization-server`);
54763
+ if (res.ok) {
54764
+ const j = await res.json();
54765
+ if (j.device_authorization_endpoint) return j.device_authorization_endpoint;
54766
+ }
54767
+ } catch {
54768
+ }
54769
+ return `${strip2(cloudUrl)}/device`;
54770
+ }
54771
+ async function requestDeviceAuthorization(fetchFn, deviceEndpoint, p) {
54772
+ const res = await fetchFn(deviceEndpoint, {
54773
+ method: "POST",
54774
+ headers: { "content-type": "application/json", accept: "application/json" },
54775
+ body: JSON.stringify({ client_id: p.clientId, scope: p.scope })
54776
+ });
54777
+ if (!res.ok) throw new Error(`device authorization failed: HTTP ${res.status}`);
54778
+ const j = await res.json();
54779
+ if (!j.device_code || !j.user_code || !j.verification_uri) {
54780
+ throw new Error("device authorization response missing device_code/user_code/verification_uri");
54781
+ }
54782
+ return {
54783
+ deviceCode: j.device_code,
54784
+ userCode: j.user_code,
54785
+ verificationUri: j.verification_uri,
54786
+ ...j.verification_uri_complete ? { verificationUriComplete: j.verification_uri_complete } : {},
54787
+ expiresInSeconds: j.expires_in ?? 1800,
54788
+ intervalSeconds: j.interval ?? 5
54789
+ };
54790
+ }
54791
+ async function pollDeviceApproval(fetchFn, deviceEndpoint, p, sleep = (ms) => new Promise((r) => setTimeout(r, ms))) {
54792
+ const deadline = Date.now() + p.timeoutMs;
54793
+ let intervalMs = Math.max(1, p.intervalSeconds) * 1e3;
54794
+ for (; ; ) {
54795
+ if (Date.now() >= deadline) throw new Error("device approval timed out \u2014 run `errata login` again");
54796
+ await sleep(intervalMs);
54797
+ const res = await fetchFn(`${strip2(deviceEndpoint)}/token`, {
54798
+ method: "POST",
54799
+ headers: { "content-type": "application/json", accept: "application/json" },
54800
+ body: JSON.stringify({
54801
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
54802
+ device_code: p.deviceCode,
54803
+ client_id: p.clientId
54804
+ })
54805
+ });
54806
+ const j = await res.json().catch(() => ({}));
54807
+ if (res.ok && j.access_token) return j.access_token;
54808
+ if (j.error === "authorization_pending") continue;
54809
+ if (j.error === "slow_down") {
54810
+ intervalMs += 5e3;
54811
+ continue;
54812
+ }
54813
+ throw new Error(`device approval failed: ${j.error ?? `HTTP ${res.status}`}`);
54814
+ }
54815
+ }
54816
+ async function authorizeWithSession(fetchFn, p) {
54817
+ const u = new URL(p.authorizationEndpoint);
54818
+ u.searchParams.set("response_type", "code");
54819
+ u.searchParams.set("client_id", p.clientId);
54820
+ u.searchParams.set("redirect_uri", BRIDGE_REDIRECT_URI);
54821
+ u.searchParams.set("scope", p.scope);
54822
+ u.searchParams.set("state", p.state);
54823
+ u.searchParams.set("code_challenge", p.challenge);
54824
+ u.searchParams.set("code_challenge_method", p.method);
54825
+ const res = await fetchFn(u.toString(), {
54826
+ method: "GET",
54827
+ headers: { authorization: `Bearer ${p.sessionToken}`, accept: "application/json" },
54828
+ redirect: "manual"
54829
+ });
54830
+ const location = res.headers.get("location");
54831
+ if (!location) throw new Error(`authorize did not redirect (HTTP ${res.status}) \u2014 is the session valid?`);
54832
+ const codeFromLocation = extractCode(location, p.state);
54833
+ if (codeFromLocation) return { code: codeFromLocation };
54834
+ const consentUrl = new URL(location);
54835
+ const consentCode = consentUrl.searchParams.get("consent_code");
54836
+ if (!consentCode) throw new Error(`authorize redirected without code or consent_code: ${consentUrl.pathname}`);
54837
+ const pagePath = "/oauth/consent";
54838
+ const prefix = consentUrl.pathname.endsWith(pagePath) ? consentUrl.pathname.slice(0, -pagePath.length) : "";
54839
+ const consentApi = `${consentUrl.origin}${prefix}/api/auth/oauth2/consent`;
54840
+ const consentRes = await fetchFn(consentApi, {
54841
+ method: "POST",
54842
+ headers: {
54843
+ "content-type": "application/json",
54844
+ accept: "application/json",
54845
+ authorization: `Bearer ${p.sessionToken}`
54846
+ },
54847
+ body: JSON.stringify({ accept: true, consent_code: consentCode })
54848
+ });
54849
+ if (!consentRes.ok) throw new Error(`consent approval failed: HTTP ${consentRes.status}`);
54850
+ const consentBody = await consentRes.json();
54851
+ const code = consentBody.redirectURI ? extractCode(consentBody.redirectURI, p.state) : null;
54852
+ if (!code) throw new Error("consent approved but no authorization code returned");
54853
+ return { code };
54854
+ }
54855
+ function extractCode(redirectUrl, expectedState) {
54856
+ try {
54857
+ const u = new URL(redirectUrl);
54858
+ const code = u.searchParams.get("code");
54859
+ const state = u.searchParams.get("state");
54860
+ if (!code) return null;
54861
+ if (state !== expectedState) throw new Error("state mismatch in authorization redirect");
54862
+ return code;
54863
+ } catch (err2) {
54864
+ if (err2 instanceof Error && err2.message.includes("state mismatch")) throw err2;
54865
+ return null;
54866
+ }
54867
+ }
54868
+ async function loginViaDeviceBridge(opts) {
54869
+ const fetchFn = opts.fetchFn ?? fetch;
54870
+ const log = opts.log ?? console.log;
54871
+ const scope = opts.scope ?? DEFAULT_SCOPE;
54872
+ const timeoutMs = opts.timeoutMs ?? 15 * 6e4;
54873
+ const endpoints = await discoverEndpoints(opts.cloudUrl, fetchFn);
54874
+ const deviceEndpoint = await discoverDeviceEndpoint(opts.cloudUrl, fetchFn);
54875
+ if (!endpoints.registrationEndpoint) {
54876
+ throw new Error("cloud does not advertise dynamic client registration \u2014 try `errata login --browser`");
54877
+ }
54878
+ const { clientId } = await registerOAuthClient(fetchFn, endpoints.registrationEndpoint, {
54879
+ redirectUri: BRIDGE_REDIRECT_URI,
54880
+ scope,
54881
+ clientName: "errata daemon"
54882
+ });
54883
+ const grant = await requestDeviceAuthorization(fetchFn, deviceEndpoint, { clientId, scope });
54884
+ log(`approve this login:`);
54885
+ log(` visit ${grant.verificationUri}`);
54886
+ log(` code ${grant.userCode}`);
54887
+ if (grant.verificationUriComplete && opts.open) opts.open(grant.verificationUriComplete);
54888
+ const sessionToken = await pollDeviceApproval(fetchFn, deviceEndpoint, {
54889
+ clientId,
54890
+ deviceCode: grant.deviceCode,
54891
+ intervalSeconds: grant.intervalSeconds,
54892
+ timeoutMs
54893
+ });
54894
+ const pkce = generatePkce();
54895
+ const state = randomState();
54896
+ const { code } = await authorizeWithSession(fetchFn, {
54897
+ authorizationEndpoint: endpoints.authorizationEndpoint,
54898
+ sessionToken,
54899
+ clientId,
54900
+ scope,
54901
+ state,
54902
+ challenge: pkce.challenge,
54903
+ method: pkce.method
54904
+ });
54905
+ const tokens = await exchangeAuthorizationCode(fetchFn, endpoints.tokenEndpoint, {
54906
+ code,
54907
+ codeVerifier: pkce.verifier,
54908
+ redirectUri: BRIDGE_REDIRECT_URI,
54909
+ clientId
54910
+ });
54911
+ try {
54912
+ const consoleAuthBase = deriveAuthBase(grant.verificationUri);
54913
+ if (consoleAuthBase) {
54914
+ await fetchFn(`${consoleAuthBase}/sign-out`, {
54915
+ method: "POST",
54916
+ headers: { authorization: `Bearer ${sessionToken}`, "content-type": "application/json" },
54917
+ body: "{}"
54918
+ });
54919
+ }
54920
+ } catch {
54921
+ }
54922
+ return { tokens, tokenEndpoint: endpoints.tokenEndpoint, clientId };
54923
+ }
54924
+ function deriveAuthBase(verificationUri) {
54925
+ try {
54926
+ const origin = new URL(verificationUri).origin;
54927
+ return `${origin}/newapp/api/auth`;
54928
+ } catch {
54929
+ return null;
54930
+ }
54931
+ }
54932
+ function openInBrowser(url2) {
54933
+ try {
54934
+ void import("node:child_process").then(({ spawn: spawn4 }) => {
54935
+ const [cmd2, args2] = process.platform === "win32" ? ["cmd", ["/c", "start", "", url2]] : process.platform === "darwin" ? ["open", [url2]] : ["xdg-open", [url2]];
54936
+ spawn4(cmd2, args2, { detached: true, stdio: "ignore" }).unref();
54937
+ });
54938
+ } catch {
54939
+ }
54940
+ }
54941
+
54755
54942
  // src/cli.ts
54756
54943
  init_paths();
54757
54944
 
@@ -55004,7 +55191,9 @@ Commands:
55004
55191
  Flags: --port N (default 7891)
55005
55192
  mcp Run the MCP stdio server \u2014 the agent's full errata tool
55006
55193
  surface (navigation, problems, claims, burst, health)
55007
- login Sign in with the cloud (OAuth by default; --device for legacy device-code)
55194
+ login Sign in with the cloud. Default: short verification URL +
55195
+ typeable code (device-bridged OAuth). Flags: --browser
55196
+ (loopback code flow) \xB7 --token <key> \xB7 --device (legacy v1)
55008
55197
  logout Clear local cloud credentials
55009
55198
  link Corrective project link (ambient linking covers the happy path).
55010
55199
  Flags: --project <id> adopt an existing project (fork\u2192upstream);
@@ -55360,6 +55549,7 @@ function parseFlags(args2) {
55360
55549
  else if (a.startsWith("--token=")) out2.token = a.slice("--token=".length);
55361
55550
  else if (a === "--oauth") out2.oauth = true;
55362
55551
  else if (a === "--device") out2.device = true;
55552
+ else if (a === "--browser") out2.browser = true;
55363
55553
  else if (!a.startsWith("--")) out2._.push(a);
55364
55554
  }
55365
55555
  return out2;
@@ -55441,7 +55631,7 @@ async function warnIfApprovalUnreachable(url2) {
55441
55631
  } catch {
55442
55632
  }
55443
55633
  }
55444
- async function cmdLoginOAuth(cfg) {
55634
+ async function cmdLoginOAuth(cfg, useBrowserLoopback = false) {
55445
55635
  const inspection = await inspectCloudEndpoint(cfg.cloudUrl);
55446
55636
  try {
55447
55637
  assertCloudEndpointAllowed(cfg.cloudUrl, inspection);
@@ -55453,10 +55643,10 @@ async function cmdLoginOAuth(cfg) {
55453
55643
  console.log(`signing in via OAuth at ${cfg.cloudUrl} \u2026`);
55454
55644
  let result;
55455
55645
  try {
55456
- result = await loginOAuthLoopback({ cloudUrl: cfg.cloudUrl, timeoutMs: 5 * 6e4 });
55646
+ result = useBrowserLoopback ? await loginOAuthLoopback({ cloudUrl: cfg.cloudUrl, timeoutMs: 5 * 6e4 }) : await loginViaDeviceBridge({ cloudUrl: cfg.cloudUrl, timeoutMs: 15 * 6e4, open: openInBrowser });
55457
55647
  } catch (err2) {
55458
55648
  console.error(`oauth login failed: ${err2 instanceof Error ? err2.message : err2}`);
55459
- console.error(` fall back to device code: errata login --device`);
55649
+ if (!useBrowserLoopback) console.error(` try the browser flow: errata login --browser`);
55460
55650
  console.error(` or paste a key: errata login --token <key>`);
55461
55651
  process.exitCode = 1;
55462
55652
  return;
@@ -55587,7 +55777,7 @@ async function cmdLogin() {
55587
55777
  return;
55588
55778
  }
55589
55779
  if (shouldUseOAuthLogin(flags2)) {
55590
- await cmdLoginOAuth(cfg);
55780
+ await cmdLoginOAuth(cfg, flags2.browser ?? false);
55591
55781
  return;
55592
55782
  }
55593
55783
  const c = new CloudClient({ baseUrl: cfg.cloudUrl });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inerrata-corporation/errata",
3
- "version": "2.0.0-dev.96",
3
+ "version": "2.0.0-dev.97",
4
4
  "description": "errata - local-first observation engine for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {