@actionway/cli 0.18.15 → 0.18.17

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/index.js +105 -10
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -12573,11 +12573,22 @@ function deviceErrorDetail(value) {
12573
12573
  const message = typeof error?.message === "string" ? error.message : "";
12574
12574
  return [code, message].filter(Boolean).join(": ");
12575
12575
  }
12576
- async function startDevicePairing(gateway, codeChallenge, fetchImpl = fetch) {
12576
+ async function startDevicePairing(gateway, codeChallenge, fetchImpl = fetch, context = {}) {
12577
12577
  const response = await fetchImpl(`${stripSlash(gateway)}/api/auth/cli-device/start`, {
12578
12578
  method: "POST",
12579
- headers: { Accept: "application/json", "Content-Type": "application/json" },
12580
- body: JSON.stringify({ code_challenge: codeChallenge }),
12579
+ headers: {
12580
+ Accept: "application/json",
12581
+ "Content-Type": "application/json",
12582
+ "x-actionway-cli-version": getCliVersion(),
12583
+ ...context.clientContext ? localClientContextHeaders(context.clientContext) : {}
12584
+ },
12585
+ body: JSON.stringify({
12586
+ code_challenge: codeChallenge,
12587
+ ...context.installAttribution ? {
12588
+ install_session_id: context.installAttribution.installSessionId,
12589
+ install_source: context.installAttribution.installSource
12590
+ } : {}
12591
+ }),
12581
12592
  signal: AbortSignal.timeout(DEVICE_START_TIMEOUT_MS)
12582
12593
  });
12583
12594
  const value = await readDeviceJson(response);
@@ -12750,6 +12761,18 @@ async function login(options = {}) {
12750
12761
  const sleep = options.sleepImpl ?? defaultSleep;
12751
12762
  const onNotice = options.onNotice ?? ((message) => process.stderr.write(`${message}
12752
12763
  `));
12764
+ let failureReported = false;
12765
+ const reportInstallEvent = async (event) => {
12766
+ if (!options.installAttribution || !options.onInstallEvent) return;
12767
+ try {
12768
+ await options.onInstallEvent(event);
12769
+ } catch {
12770
+ }
12771
+ };
12772
+ const reportInstallFailure = async (failureStage2, failureReason) => {
12773
+ failureReported = true;
12774
+ await reportInstallEvent({ event: "install_failed", failureStage: failureStage2, failureReason });
12775
+ };
12753
12776
  const gateway = resolveGatewayUrl(options.gateway, env);
12754
12777
  const gatewaySource = options.gateway?.trim() ? "flag" : (env.ACTIONWAY_GATEWAY_URL ?? "").trim() ? "env" : "default";
12755
12778
  let clientId = options.clientId?.trim() || (env.ACTIONWAY_OAUTH_CLIENT_ID ?? "").trim();
@@ -12771,6 +12794,7 @@ async function login(options = {}) {
12771
12794
  throw error;
12772
12795
  };
12773
12796
  if (!!clientId !== !!issuer) {
12797
+ await reportInstallFailure("configuration", "configuration_invalid");
12774
12798
  throw new CliError(
12775
12799
  "E_SCHEMA",
12776
12800
  "client_id and issuer overrides must be provided together",
@@ -12783,6 +12807,7 @@ async function login(options = {}) {
12783
12807
  try {
12784
12808
  discovered = await fetchCliOAuthConfig(gateway, fetchImpl);
12785
12809
  } catch (error) {
12810
+ await reportInstallFailure("configuration", "configuration_unavailable");
12786
12811
  const message = error instanceof Error ? error.message : String(error);
12787
12812
  return withAuthContext(new CliError(
12788
12813
  "E_BACKEND",
@@ -12794,9 +12819,13 @@ async function login(options = {}) {
12794
12819
  issuer ||= discovered.issuer;
12795
12820
  }
12796
12821
  const timeoutMs = options.timeoutSeconds !== void 0 && Number.isFinite(options.timeoutSeconds) && options.timeoutSeconds > 0 ? options.timeoutSeconds * 1e3 : DEFAULT_LOGIN_TIMEOUT_MS;
12822
+ let failureStage = "pairing";
12797
12823
  try {
12798
12824
  const verifier = randomBase64Url();
12799
- const pairing = await startDevicePairing(gateway, pkceChallenge(verifier), fetchImpl);
12825
+ const pairing = await startDevicePairing(gateway, pkceChallenge(verifier), fetchImpl, {
12826
+ ...options.installAttribution ? { installAttribution: options.installAttribution } : {},
12827
+ ...options.clientContext ? { clientContext: options.clientContext } : {}
12828
+ });
12800
12829
  onNotice(
12801
12830
  `Sign in to Actionway on any device:
12802
12831
  ${pairing.verificationUrl}
@@ -12805,6 +12834,7 @@ async function login(options = {}) {
12805
12834
  if (options.noBrowser) onNotice(`Open this URL to approve the sign-in:
12806
12835
  ${pairing.verificationUrlComplete}`);
12807
12836
  else openBrowser(pairing.verificationUrlComplete, onNotice);
12837
+ failureStage = "authorization";
12808
12838
  const deadline = Date.now() + Math.min(timeoutMs, pairing.expiresInS * 1e3);
12809
12839
  let approvalCode = null;
12810
12840
  for (; ; ) {
@@ -12814,6 +12844,7 @@ ${pairing.verificationUrlComplete}`);
12814
12844
  break;
12815
12845
  }
12816
12846
  if (poll.status === "denied") {
12847
+ await reportInstallFailure("authorization", "authorization_denied");
12817
12848
  throw new CliError(
12818
12849
  "E_BACKEND",
12819
12850
  "the sign-in request was denied in the browser",
@@ -12821,6 +12852,7 @@ ${pairing.verificationUrlComplete}`);
12821
12852
  );
12822
12853
  }
12823
12854
  if (poll.status === "expired") {
12855
+ await reportInstallFailure("authorization", "authorization_expired");
12824
12856
  throw new CliError(
12825
12857
  "E_BACKEND",
12826
12858
  "the sign-in code expired before it was approved",
@@ -12828,6 +12860,7 @@ ${pairing.verificationUrlComplete}`);
12828
12860
  );
12829
12861
  }
12830
12862
  if (Date.now() >= deadline) {
12863
+ await reportInstallFailure("authorization", "authorization_timeout");
12831
12864
  throw new CliError(
12832
12865
  "E_BACKEND",
12833
12866
  `login timed out after ${Math.round(timeoutMs / 1e3)} seconds`,
@@ -12836,6 +12869,7 @@ ${pairing.verificationUrlComplete}`);
12836
12869
  }
12837
12870
  await sleep(pairing.intervalS * 1e3);
12838
12871
  }
12872
+ failureStage = "token_exchange";
12839
12873
  const tokens = await tokenRequest(
12840
12874
  issuer,
12841
12875
  new URLSearchParams({
@@ -12847,6 +12881,8 @@ ${pairing.verificationUrlComplete}`);
12847
12881
  }),
12848
12882
  fetchImpl
12849
12883
  );
12884
+ await reportInstallEvent({ event: "install_token_exchanged" });
12885
+ failureStage = "account_connection";
12850
12886
  const account = await fetchCliSession(gateway, tokens.accessToken, fetchImpl, {
12851
12887
  ...options.installAttribution ? { installAttribution: options.installAttribution } : {},
12852
12888
  ...options.clientContext ? { clientContext: options.clientContext } : {}
@@ -12874,6 +12910,11 @@ ${pairing.verificationUrlComplete}`);
12874
12910
  saveCredentials(credentials, env);
12875
12911
  return { credentials, account };
12876
12912
  } catch (error) {
12913
+ if (!failureReported) {
12914
+ const stage = typeof failureStage === "string" ? failureStage : "pairing";
12915
+ const reason = stage === "pairing" ? "pairing_unavailable" : stage === "authorization" ? "authorization_unavailable" : stage === "token_exchange" ? "token_exchange_failed" : "account_connection_failed";
12916
+ await reportInstallFailure(stage, reason);
12917
+ }
12877
12918
  return withAuthContext(error);
12878
12919
  }
12879
12920
  }
@@ -13367,6 +13408,36 @@ async function captureInstallStarted(input) {
13367
13408
  return false;
13368
13409
  }
13369
13410
  }
13411
+ async function captureInstallAuthEvent(input) {
13412
+ try {
13413
+ const response = await fetchWithTimeout(
13414
+ `${stripSlash2(input.gateway)}/api/analytics/install-auth`,
13415
+ {
13416
+ method: "POST",
13417
+ headers: {
13418
+ Accept: "application/json",
13419
+ "Content-Type": "application/json",
13420
+ "x-actionway-cli-version": getCliVersion(),
13421
+ ...input.clientContext ? localClientContextHeaders(input.clientContext) : {}
13422
+ },
13423
+ body: JSON.stringify({
13424
+ event: input.event.event,
13425
+ installSessionId: input.installSessionId,
13426
+ installSource: input.installSource,
13427
+ ...input.event.event === "install_failed" ? {
13428
+ failureStage: input.event.failureStage,
13429
+ failureReason: input.event.failureReason
13430
+ } : {}
13431
+ })
13432
+ },
13433
+ ANALYTICS_TIMEOUT_MS,
13434
+ input.fetchImpl ?? fetch
13435
+ );
13436
+ return response.ok;
13437
+ } catch {
13438
+ return false;
13439
+ }
13440
+ }
13370
13441
  async function requestCliInstallSession(input) {
13371
13442
  const url = new URL(`${stripSlash2(input.gateway)}/api/auth/cli-session`);
13372
13443
  url.searchParams.set("install_session_id", input.installSessionId);
@@ -13470,6 +13541,15 @@ function registerInitCommand(program2, getClientContext = () => ({ runtimeOs: "o
13470
13541
  ...opts.browser === false ? { noBrowser: true } : {},
13471
13542
  installAttribution: { installSessionId, installSource },
13472
13543
  clientContext,
13544
+ onInstallEvent: async (event) => {
13545
+ await captureInstallAuthEvent({
13546
+ gateway: initGateway,
13547
+ installSessionId,
13548
+ installSource,
13549
+ event,
13550
+ clientContext
13551
+ });
13552
+ },
13473
13553
  onNotice: (message) => emitEvent({ event: "auth", message })
13474
13554
  });
13475
13555
  account = {
@@ -13478,12 +13558,27 @@ function registerInitCommand(program2, getClientContext = () => ({ runtimeOs: "o
13478
13558
  walletId: loginResult.account.walletId
13479
13559
  };
13480
13560
  } else {
13481
- account = await connectInstallSession({
13482
- ...opts.gateway ? { explicitGateway: opts.gateway } : {},
13483
- installSessionId,
13484
- installSource,
13485
- clientContext
13486
- });
13561
+ try {
13562
+ account = await connectInstallSession({
13563
+ ...opts.gateway ? { explicitGateway: opts.gateway } : {},
13564
+ installSessionId,
13565
+ installSource,
13566
+ clientContext
13567
+ });
13568
+ } catch (error) {
13569
+ await captureInstallAuthEvent({
13570
+ gateway: initGateway,
13571
+ installSessionId,
13572
+ installSource,
13573
+ clientContext,
13574
+ event: {
13575
+ event: "install_failed",
13576
+ failureStage: error instanceof CliError && error.code === "E_NOT_AUTHENTICATED" ? "token_exchange" : "account_connection",
13577
+ failureReason: error instanceof CliError && error.code === "E_NOT_AUTHENTICATED" ? "token_refresh_failed" : "account_connection_failed"
13578
+ }
13579
+ });
13580
+ throw error;
13581
+ }
13487
13582
  }
13488
13583
  const snapshot = whoamiSnapshot();
13489
13584
  ok({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@actionway/cli",
3
- "version": "0.18.15",
3
+ "version": "0.18.17",
4
4
  "description": "actionway CLI 的本地端壳:Clerk OAuth 鉴权 + cli-core 共享命令面 + init / update / skill 物化 / 版本三链路(终端用户本地 Codex / Claude Code / WorkBuddy 使用)",
5
5
  "type": "module",
6
6
  "bin": {