@wix/himalaya-cli 0.870.0 → 0.872.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.
@@ -9,6 +9,7 @@ export interface WixGatewayState {
9
9
  seq: number;
10
10
  ruleSeq: number;
11
11
  }
12
+ export declare const SECRET_BODY_KEYS: Set<string>;
12
13
  /** Scrub secrets from a request/response body before it is stored/streamed to the
13
14
  * admin inspector. JSON bodies are redacted structurally by key; other bodies
14
15
  * get form/bearer token scrubbing. Never throws. */
@@ -18,6 +18,13 @@ export interface DeviceAuthResult {
18
18
  tokens?: DeviceAuthTokens;
19
19
  error?: string;
20
20
  }
21
+ /** A device-authorization request that failed before a QR could be shown. Typed
22
+ * so the caller can render it, rather than the raw `SyntaxError` an HTML gateway
23
+ * page used to produce out of `res.json()`. */
24
+ export declare class DeviceAuthError extends Error {
25
+ readonly status?: number;
26
+ constructor(message: string, status?: number);
27
+ }
21
28
  export interface DeviceAuthConfig {
22
29
  clientId: string;
23
30
  baseURL?: string;
@@ -7,7 +7,7 @@ export { REDACTED, WIX_SENSITIVE_KEYS, isSensitiveKey, normalizeKey, redactHeade
7
7
  export { SessionIDService, InMemorySessionIDStore, bsiShouldRotate, localDayIndex, BSI_ROTATE_INTERVAL_MS, type SessionIDRecord, type SessionIDStore, } from "./session-id.js";
8
8
  export { policyFor as wixRequestPolicy, isFirstPartyWixHost, isSignedUploadTarget, type MobileRequestKind, type RequestPolicy, } from "./request-policy.js";
9
9
  export { METASITE_HEADER, SIGNED_INSTANCE_EXP_HEADER, AUTH_PROXY_PREFIX, authProxyHeaders, authProxyPseudoInstance, isAuthProxyInstance, authProxyBusinessId, parseSignedInstanceExp, isSignedInstanceExpired, } from "./auth-proxy.js";
10
- export { DeviceAuthClient, parseDeviceQr, type DeviceAuthorization, type DeviceAuthTokens, type DeviceAuthResult, type DeviceAuthConfig, type DeviceAuthTerminal, } from "./device-auth.js";
10
+ export { DeviceAuthClient, DeviceAuthError, parseDeviceQr, type DeviceAuthorization, type DeviceAuthTokens, type DeviceAuthResult, type DeviceAuthConfig, type DeviceAuthTerminal, } from "./device-auth.js";
11
11
  export { buildRegistrationOptions, buildAuthenticationOptions, authenticatorDataCounter, assertSignCountAdvanced, PasskeyCloneError, type RegistrationOptions, type AuthenticationOptions, } from "./passkey-ceremony.js";
12
12
  export { WixAuthModalities, classifyAuthErrorCode, type PushApprovalState, type AuthModalitiesConfig, } from "./auth-modalities.js";
13
13
  export { shouldEscalateCaptcha, captchaResponseFields, CaptchaEscalation, CAPTCHA_ESCALATION_CODES, type CaptchaTier, } from "./captcha-escalation.js";
@@ -3,6 +3,12 @@ export interface StoredCredential {
3
3
  credentialId: string;
4
4
  publicKeySpki: string;
5
5
  alg: number;
6
+ /** The authenticator's signature counter, for clone detection (WebAuthn §7.2
7
+ * step 17). `verifyAuthentication` does NOT read or advance this — step 17 is
8
+ * `assertSignCountAdvanced` in `passkey-ceremony.ts`, which the host calls with
9
+ * `authenticatorDataCounter(authenticatorData)` and then PERSISTS the returned
10
+ * value. It is split out because only the host owns the store; do not read a
11
+ * non-zero value here as proof the check ran. */
6
12
  signCount: number;
7
13
  }
8
14
  export interface ClientData {
@@ -33,6 +39,16 @@ export interface AuthenticationInput {
33
39
  stored: StoredCredential;
34
40
  expectedChallenge: string;
35
41
  expectedOrigin: string;
42
+ /** The relying-party id this assertion must be bound to — the SAME value the
43
+ * page passed as `rpId` in `buildAuthenticationOptions` (a registrable domain
44
+ * suffix of the origin, e.g. `"localhost"` or `"wix.com"`).
45
+ *
46
+ * REQUIRED, not optional, and deliberately so: a security check the caller can
47
+ * forget is a security check that is off in production. WebAuthn §7.2 step 15
48
+ * is the only thing binding an assertion to its relying party, and without it
49
+ * `expectedOrigin` alone leaves nothing checking what the *authenticator*
50
+ * signed over. */
51
+ expectedRpId: string;
36
52
  }
37
53
  /** Verify an assertion against a stored credential. Throws PasskeyError on any
38
54
  * mismatch / bad signature; returns true on success. */
@@ -7,6 +7,26 @@ export declare const WIX_SENSITIVE_KEYS: readonly string[];
7
7
  export declare function normalizeKey(key: string): string;
8
8
  /** Whether a header/field/cookie name names a Wix secret. */
9
9
  export declare function isSensitiveKey(key: string): boolean;
10
+ /** Whether a `name=` token seen in a FREE-TEXT body (form-encoded, query string,
11
+ * raw dump) names a Wix secret.
12
+ *
13
+ * Looser than `isSensitiveKey` on purpose, in exactly two ways, because a
14
+ * free-text scrubber sees names the structural paths never do:
15
+ *
16
+ * 1. **Internal separators.** The vocabulary is stored separator-stripped
17
+ * (`accesstoken`), so a literal alternation built from it can never match the
18
+ * snake_case OAuth spellings `access_token` / `refresh_token` / `id_token`
19
+ * — which is precisely the shape an OAuth form body uses. We therefore split
20
+ * the name on separators and normalize before comparing.
21
+ * 2. **Prefix/suffix tolerance.** A name whose leading segments are a secret
22
+ * name counts (`accessTokenV2`, `x-access-token`), matching the loose
23
+ * `\b<key>[-_]?[a-z0-9]*=` shape this replaced. Over-redaction is the safe
24
+ * direction for a log scrubber; under-redaction is a leak.
25
+ *
26
+ * Checking every segment-suffix (not just the whole name) reproduces the old
27
+ * `\b` anchoring: `x-access-token` matches on its `access-token` tail, while
28
+ * `myaccesstoken` — which has no word boundary before the secret — does not. */
29
+ export declare function isSensitiveFreeTextName(name: string): boolean;
10
30
  /** Redact a header map (values only; keys preserved). Case-insensitive. */
11
31
  export declare function redactHeaders(headers: Record<string, string | string[] | undefined>): Record<string, string>;
12
32
  /** Redact a `Cookie:` / `Set-Cookie:` header string, keeping cookie names but
@@ -57,6 +57,9 @@ export declare class SignedInstanceResolver {
57
57
  /** Resolve the signed instance for `businessId`, using (and populating) the
58
58
  * caches. Concurrent calls for the same key share a single fetch. */
59
59
  resolve(businessId: string, signedInstanceType?: SignedInstanceType, forceRefresh?: boolean): Promise<string | undefined>;
60
+ /** Whether `task` is still the in-flight fetch registered for `key` — false once
61
+ * `remove`/`reset` has evicted it, or a later resolve has replaced it. */
62
+ private isCurrent;
60
63
  /** Evict everything for a business (e.g. on leaving / switching it). */
61
64
  remove(businessId: string): void;
62
65
  /** Wipe all caches (e.g. on sign-out). */
@@ -3,7 +3,7 @@ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { en
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
4
 
5
5
  // ../../stdlib/auth-providers/wix/src/passkey-server.ts
6
- import { createHash, createPublicKey, verify as cryptoVerify, randomBytes } from "node:crypto";
6
+ import { createHash, createPublicKey, verify as cryptoVerify, randomBytes, timingSafeEqual } from "node:crypto";
7
7
  function newChallenge() {
8
8
  return randomBytes(32).toString("base64url");
9
9
  }
@@ -34,11 +34,17 @@ function verifyRegistration(input) {
34
34
  signCount: 0
35
35
  };
36
36
  }
37
+ var FLAG_USER_PRESENT = 1;
37
38
  function verifyAuthentication(input) {
38
39
  const client = parseClientData(input.clientDataJSON);
39
40
  assertCeremony(client, "webauthn.get", input.expectedChallenge, input.expectedOrigin);
40
41
  const authData = Buffer.from(input.authenticatorData, "base64url");
41
42
  if (authData.length < 37) throw new PasskeyError("authenticatorData too short");
43
+ const expectedRpIdHash = createHash("sha256").update(input.expectedRpId).digest();
44
+ if (!timingSafeEqual(authData.subarray(0, 32), expectedRpIdHash)) {
45
+ throw new PasskeyError("rpIdHash mismatch");
46
+ }
47
+ if ((authData[32] & FLAG_USER_PRESENT) === 0) throw new PasskeyError("user not present");
42
48
  const clientHash = createHash("sha256").update(Buffer.from(input.clientDataJSON, "base64url")).digest();
43
49
  const signedData = Buffer.concat([authData, clientHash]);
44
50
  let key;
@@ -53,7 +59,7 @@ function verifyAuthentication(input) {
53
59
  }
54
60
 
55
61
  // ../../stdlib/auth-providers/wix/src/jot-deeplink.ts
56
- import { createHmac, timingSafeEqual } from "node:crypto";
62
+ import { createHmac, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
57
63
  var JOT_DLA_HOST = "invite.mobileappinvite.com";
58
64
  function signJotPayload(payload, secret) {
59
65
  return createHmac("sha256", secret).update(payload).digest("base64url");
@@ -62,7 +68,7 @@ function verifyJotSignature(payload, signature, secret) {
62
68
  const expected = signJotPayload(payload, secret);
63
69
  const a = Buffer.from(signature);
64
70
  const b = Buffer.from(expected);
65
- return a.length === b.length && timingSafeEqual(a, b);
71
+ return a.length === b.length && timingSafeEqual2(a, b);
66
72
  }
67
73
  function parseUniversalLink(url, appId) {
68
74
  let u;
@@ -4,6 +4,21 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
4
4
 
5
5
  // ../../stdlib/auth-providers/wix/src/device-auth.ts
6
6
  var DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
7
+ var DeviceAuthError = class extends Error {
8
+ constructor(message, status) {
9
+ super(message);
10
+ this.status = status;
11
+ this.name = "DeviceAuthError";
12
+ }
13
+ };
14
+ async function readJsonSafely(res) {
15
+ try {
16
+ const value = await res.json();
17
+ return value && typeof value === "object" ? { json: value, parsed: true } : { json: {}, parsed: false };
18
+ } catch {
19
+ return { json: {}, parsed: false };
20
+ }
21
+ }
7
22
  function parseDeviceQr(scanned) {
8
23
  const s = scanned.trim();
9
24
  if (!s) return {};
@@ -52,7 +67,13 @@ var DeviceAuthClient = class {
52
67
  headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
53
68
  body: `client_id=${encodeURIComponent(this.clientId)}&scope=${encodeURIComponent(scope)}`
54
69
  });
55
- const j = await res.json();
70
+ const { json: j } = await readJsonSafely(res);
71
+ if (!res.ok || !j.device_code) {
72
+ throw new DeviceAuthError(
73
+ j.error_description ?? j.error ?? `Could not start device sign-in (HTTP ${res.status}).`,
74
+ res.status
75
+ );
76
+ }
56
77
  return {
57
78
  deviceCode: j.device_code,
58
79
  userCode: j.user_code,
@@ -65,15 +86,21 @@ var DeviceAuthClient = class {
65
86
  /** One poll step against the token endpoint. */
66
87
  async pollOnce(auth) {
67
88
  if (this.devMode) return { terminal: { state: "authorized", tokens: { accessToken: "dev-device-token" } } };
68
- const res = await this.fetchImpl(`${this.baseURL}/oauth2/token`, {
69
- method: "POST",
70
- headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
71
- body: `grant_type=${encodeURIComponent(DEVICE_GRANT)}&device_code=${encodeURIComponent(auth.deviceCode)}&client_id=${encodeURIComponent(this.clientId)}`
72
- });
73
- const j = await res.json();
89
+ let res;
90
+ try {
91
+ res = await this.fetchImpl(`${this.baseURL}/oauth2/token`, {
92
+ method: "POST",
93
+ headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
94
+ body: `grant_type=${encodeURIComponent(DEVICE_GRANT)}&device_code=${encodeURIComponent(auth.deviceCode)}&client_id=${encodeURIComponent(this.clientId)}`
95
+ });
96
+ } catch {
97
+ return {};
98
+ }
99
+ const { json: j, parsed } = await readJsonSafely(res);
74
100
  if (j.access_token) {
75
101
  return { terminal: { state: "authorized", tokens: { accessToken: j.access_token, refreshToken: j.refresh_token } } };
76
102
  }
103
+ if (!parsed) return {};
77
104
  switch (j.error) {
78
105
  case "authorization_pending":
79
106
  return {};
@@ -83,8 +110,11 @@ var DeviceAuthClient = class {
83
110
  return { terminal: { state: "denied", error: "The sign-in request was denied." } };
84
111
  case "expired_token":
85
112
  return { terminal: { state: "expired", error: "The code expired. Try again." } };
113
+ // `message` is read too so Wix's own error envelope, which carries no
114
+ // `error`, still reaches the user with its actual reason rather than the
115
+ // generic fallback.
86
116
  default:
87
- return { terminal: { state: "error", error: j.error_description ?? j.error ?? "Device authorization failed." } };
117
+ return { terminal: { state: "error", error: j.error_description ?? j.error ?? j.message ?? "Device authorization failed." } };
88
118
  }
89
119
  }
90
120
  /** Run the full poll loop until a terminal state or the code expires. Honors
@@ -152,7 +182,15 @@ var WixNativeAuthEngine = class {
152
182
  } catch (e) {
153
183
  throw new NativeAuthError("network", e.message);
154
184
  }
155
- const json = await res.json().catch(() => ({}));
185
+ if (!res.ok) {
186
+ throw new NativeAuthError("network", `Account status lookup failed (HTTP ${res.status}).`);
187
+ }
188
+ let json;
189
+ try {
190
+ json = await res.json();
191
+ } catch {
192
+ throw new NativeAuthError("network", "Account status response was not readable JSON.");
193
+ }
156
194
  const status = json?.payload?.userAuthStatus ?? json?.userAuthStatus ?? {};
157
195
  if (!status.userId) return "newUser";
158
196
  return status.hasPassword ? "existingPassword" : "passwordless";
@@ -671,6 +709,14 @@ function normalizeKey(key) {
671
709
  function isSensitiveKey(key) {
672
710
  return SENSITIVE.has(normalizeKey(key));
673
711
  }
712
+ function isSensitiveFreeTextName(name) {
713
+ const segments = name.split(/[^A-Za-z0-9]+/).filter(Boolean);
714
+ for (let i = 0; i < segments.length; i++) {
715
+ const tail = normalizeKey(segments.slice(i).join(""));
716
+ for (const k of WIX_SENSITIVE_KEYS) if (tail.startsWith(k)) return true;
717
+ }
718
+ return false;
719
+ }
674
720
  function redactHeaders(headers) {
675
721
  const out = {};
676
722
  for (const [k, v] of Object.entries(headers)) {
@@ -699,10 +745,9 @@ function redactJson(value) {
699
745
  }
700
746
  return value;
701
747
  }
702
- var FORM_TOKEN_RE = new RegExp(
703
- `\\b(${WIX_SENSITIVE_KEYS.map((k) => k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})([-_]?[a-z0-9]*)?=([^&;\\s]+)`,
704
- "gi"
705
- );
748
+ var FORM_ASSIGNMENT_RE = /([A-Za-z0-9][A-Za-z0-9_.-]*)=([^&;\s]+)/g;
749
+ var JSON_FIELD_RE = /"([^"\\]+)"(\s*:\s*)"((?:\\.|[^"\\])*)"/g;
750
+ var JSON_FIELD_UNTERMINATED_RE = /"([^"\\]+)"(\s*:\s*)"(?:\\.|[^"\\])*$/;
706
751
  var BEARER_RE = /\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi;
707
752
  function redactBody(body, maxLen = 4096) {
708
753
  if (!body) return body ?? "";
@@ -720,7 +765,7 @@ function redactBody(body, maxLen = 4096) {
720
765
  return out.length > maxLen ? out.slice(0, maxLen) + "\u2026(truncated)" : out;
721
766
  }
722
767
  function scrubText(s) {
723
- return s.replace(FORM_TOKEN_RE, (_m, name) => `${name}=${REDACTED}`).replace(BEARER_RE, (_m, scheme) => `${scheme} ${REDACTED}`);
768
+ return s.replace(FORM_ASSIGNMENT_RE, (m, name) => isSensitiveFreeTextName(name) ? `${name}=${REDACTED}` : m).replace(JSON_FIELD_RE, (m, name, sep) => isSensitiveFreeTextName(name) ? `"${name}"${sep}"${REDACTED}"` : m).replace(JSON_FIELD_UNTERMINATED_RE, (m, name, sep) => isSensitiveFreeTextName(name) ? `"${name}"${sep}"${REDACTED}` : m).replace(BEARER_RE, (_m, scheme) => `${scheme} ${REDACTED}`);
724
769
  }
725
770
 
726
771
  // ../../stdlib/auth-providers/wix/src/request-policy.ts
@@ -915,24 +960,32 @@ var SignedInstanceResolver = class {
915
960
  this.pending.set(key, task);
916
961
  try {
917
962
  const record = await task;
918
- if (record) {
963
+ if (record && this.isCurrent(key, task)) {
919
964
  this.memoryCache.set(key, record);
920
965
  if (!this.isExpired(record)) this.storage.save(businessId, signedInstanceType, record);
921
966
  }
922
967
  return record?.signedInstance;
923
968
  } catch (error) {
924
969
  const sie = error instanceof SignedInstanceError ? error : new SignedInstanceError("Signed-instance fetch failed", "fetchFailed", error);
925
- this.failureCache.set(key, { error: sie, expiry: this.now() + this.failureCacheTtlMs });
970
+ if (this.isCurrent(key, task)) {
971
+ this.failureCache.set(key, { error: sie, expiry: this.now() + this.failureCacheTtlMs });
972
+ }
926
973
  this.report(sie);
927
974
  throw sie;
928
975
  } finally {
929
- this.pending.delete(key);
976
+ if (this.isCurrent(key, task)) this.pending.delete(key);
930
977
  }
931
978
  }
979
+ /** Whether `task` is still the in-flight fetch registered for `key` — false once
980
+ * `remove`/`reset` has evicted it, or a later resolve has replaced it. */
981
+ isCurrent(key, task) {
982
+ return this.pending.get(key) === task;
983
+ }
932
984
  /** Evict everything for a business (e.g. on leaving / switching it). */
933
985
  remove(businessId) {
934
986
  for (const k of [...this.memoryCache.keys()]) if (k.startsWith(`${businessId}:`)) this.memoryCache.delete(k);
935
987
  for (const k of [...this.failureCache.keys()]) if (k.startsWith(`${businessId}:`)) this.failureCache.delete(k);
988
+ for (const k of [...this.pending.keys()]) if (k.startsWith(`${businessId}:`)) this.pending.delete(k);
936
989
  this.storage.deleteBusiness(businessId);
937
990
  }
938
991
  /** Wipe all caches (e.g. on sign-out). */
@@ -958,6 +1011,7 @@ export {
958
1011
  CAPTCHA_ESCALATION_CODES,
959
1012
  CaptchaEscalation,
960
1013
  DeviceAuthClient,
1014
+ DeviceAuthError,
961
1015
  InMemorySessionIDStore,
962
1016
  InMemorySignedInstanceStorage,
963
1017
  METASITE_HEADER,
@@ -988,6 +1042,7 @@ export {
988
1042
  dualDomainAuthorizeUrls,
989
1043
  isAuthProxyInstance,
990
1044
  isFirstPartyWixHost,
1045
+ isSensitiveFreeTextName,
991
1046
  isSensitiveKey,
992
1047
  isSignedInstanceExpired,
993
1048
  isSignedUploadTarget,
@@ -70,7 +70,15 @@ var WixNativeAuthEngine = class {
70
70
  } catch (e) {
71
71
  throw new NativeAuthError("network", e.message);
72
72
  }
73
- const json = await res.json().catch(() => ({}));
73
+ if (!res.ok) {
74
+ throw new NativeAuthError("network", `Account status lookup failed (HTTP ${res.status}).`);
75
+ }
76
+ let json;
77
+ try {
78
+ json = await res.json();
79
+ } catch {
80
+ throw new NativeAuthError("network", "Account status response was not readable JSON.");
81
+ }
74
82
  const status = json?.payload?.userAuthStatus ?? json?.userAuthStatus ?? {};
75
83
  if (!status.userId) return "newUser";
76
84
  return status.hasPassword ? "existingPassword" : "passwordless";
@@ -402,24 +410,32 @@ var SignedInstanceResolver = class {
402
410
  this.pending.set(key, task);
403
411
  try {
404
412
  const record = await task;
405
- if (record) {
413
+ if (record && this.isCurrent(key, task)) {
406
414
  this.memoryCache.set(key, record);
407
415
  if (!this.isExpired(record)) this.storage.save(businessId, signedInstanceType, record);
408
416
  }
409
417
  return record?.signedInstance;
410
418
  } catch (error) {
411
419
  const sie = error instanceof SignedInstanceError ? error : new SignedInstanceError("Signed-instance fetch failed", "fetchFailed", error);
412
- this.failureCache.set(key, { error: sie, expiry: this.now() + this.failureCacheTtlMs });
420
+ if (this.isCurrent(key, task)) {
421
+ this.failureCache.set(key, { error: sie, expiry: this.now() + this.failureCacheTtlMs });
422
+ }
413
423
  this.report(sie);
414
424
  throw sie;
415
425
  } finally {
416
- this.pending.delete(key);
426
+ if (this.isCurrent(key, task)) this.pending.delete(key);
417
427
  }
418
428
  }
429
+ /** Whether `task` is still the in-flight fetch registered for `key` — false once
430
+ * `remove`/`reset` has evicted it, or a later resolve has replaced it. */
431
+ isCurrent(key, task) {
432
+ return this.pending.get(key) === task;
433
+ }
419
434
  /** Evict everything for a business (e.g. on leaving / switching it). */
420
435
  remove(businessId) {
421
436
  for (const k of [...this.memoryCache.keys()]) if (k.startsWith(`${businessId}:`)) this.memoryCache.delete(k);
422
437
  for (const k of [...this.failureCache.keys()]) if (k.startsWith(`${businessId}:`)) this.failureCache.delete(k);
438
+ for (const k of [...this.pending.keys()]) if (k.startsWith(`${businessId}:`)) this.pending.delete(k);
423
439
  this.storage.deleteBusiness(businessId);
424
440
  }
425
441
  /** Wipe all caches (e.g. on sign-out). */
@@ -477,6 +493,14 @@ function normalizeKey(key) {
477
493
  function isSensitiveKey(key) {
478
494
  return SENSITIVE.has(normalizeKey(key));
479
495
  }
496
+ function isSensitiveFreeTextName(name) {
497
+ const segments = name.split(/[^A-Za-z0-9]+/).filter(Boolean);
498
+ for (let i = 0; i < segments.length; i++) {
499
+ const tail = normalizeKey(segments.slice(i).join(""));
500
+ for (const k of WIX_SENSITIVE_KEYS) if (tail.startsWith(k)) return true;
501
+ }
502
+ return false;
503
+ }
480
504
  function redactHeaders(headers) {
481
505
  const out = {};
482
506
  for (const [k, v] of Object.entries(headers)) {
@@ -505,10 +529,9 @@ function redactJson(value) {
505
529
  }
506
530
  return value;
507
531
  }
508
- var FORM_TOKEN_RE = new RegExp(
509
- `\\b(${WIX_SENSITIVE_KEYS.map((k) => k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})([-_]?[a-z0-9]*)?=([^&;\\s]+)`,
510
- "gi"
511
- );
532
+ var FORM_ASSIGNMENT_RE = /([A-Za-z0-9][A-Za-z0-9_.-]*)=([^&;\s]+)/g;
533
+ var JSON_FIELD_RE = /"([^"\\]+)"(\s*:\s*)"((?:\\.|[^"\\])*)"/g;
534
+ var JSON_FIELD_UNTERMINATED_RE = /"([^"\\]+)"(\s*:\s*)"(?:\\.|[^"\\])*$/;
512
535
  var BEARER_RE = /\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi;
513
536
  function redactBody(body, maxLen = 4096) {
514
537
  if (!body) return body ?? "";
@@ -526,7 +549,7 @@ function redactBody(body, maxLen = 4096) {
526
549
  return out.length > maxLen ? out.slice(0, maxLen) + "\u2026(truncated)" : out;
527
550
  }
528
551
  function scrubText(s) {
529
- return s.replace(FORM_TOKEN_RE, (_m, name) => `${name}=${REDACTED}`).replace(BEARER_RE, (_m, scheme) => `${scheme} ${REDACTED}`);
552
+ return s.replace(FORM_ASSIGNMENT_RE, (m, name) => isSensitiveFreeTextName(name) ? `${name}=${REDACTED}` : m).replace(JSON_FIELD_RE, (m, name, sep) => isSensitiveFreeTextName(name) ? `"${name}"${sep}"${REDACTED}"` : m).replace(JSON_FIELD_UNTERMINATED_RE, (m, name, sep) => isSensitiveFreeTextName(name) ? `"${name}"${sep}"${REDACTED}` : m).replace(BEARER_RE, (_m, scheme) => `${scheme} ${REDACTED}`);
530
553
  }
531
554
 
532
555
  // ../../stdlib/auth-providers/wix/src/session-id.ts
@@ -661,6 +684,21 @@ function pickHeader(headers, name) {
661
684
 
662
685
  // ../../stdlib/auth-providers/wix/src/device-auth.ts
663
686
  var DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
687
+ var DeviceAuthError = class extends Error {
688
+ constructor(message, status) {
689
+ super(message);
690
+ this.status = status;
691
+ this.name = "DeviceAuthError";
692
+ }
693
+ };
694
+ async function readJsonSafely(res) {
695
+ try {
696
+ const value = await res.json();
697
+ return value && typeof value === "object" ? { json: value, parsed: true } : { json: {}, parsed: false };
698
+ } catch {
699
+ return { json: {}, parsed: false };
700
+ }
701
+ }
664
702
  function parseDeviceQr(scanned) {
665
703
  const s = scanned.trim();
666
704
  if (!s) return {};
@@ -709,7 +747,13 @@ var DeviceAuthClient = class {
709
747
  headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
710
748
  body: `client_id=${encodeURIComponent(this.clientId)}&scope=${encodeURIComponent(scope)}`
711
749
  });
712
- const j = await res.json();
750
+ const { json: j } = await readJsonSafely(res);
751
+ if (!res.ok || !j.device_code) {
752
+ throw new DeviceAuthError(
753
+ j.error_description ?? j.error ?? `Could not start device sign-in (HTTP ${res.status}).`,
754
+ res.status
755
+ );
756
+ }
713
757
  return {
714
758
  deviceCode: j.device_code,
715
759
  userCode: j.user_code,
@@ -722,15 +766,21 @@ var DeviceAuthClient = class {
722
766
  /** One poll step against the token endpoint. */
723
767
  async pollOnce(auth) {
724
768
  if (this.devMode) return { terminal: { state: "authorized", tokens: { accessToken: "dev-device-token" } } };
725
- const res = await this.fetchImpl(`${this.baseURL}/oauth2/token`, {
726
- method: "POST",
727
- headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
728
- body: `grant_type=${encodeURIComponent(DEVICE_GRANT)}&device_code=${encodeURIComponent(auth.deviceCode)}&client_id=${encodeURIComponent(this.clientId)}`
729
- });
730
- const j = await res.json();
769
+ let res;
770
+ try {
771
+ res = await this.fetchImpl(`${this.baseURL}/oauth2/token`, {
772
+ method: "POST",
773
+ headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
774
+ body: `grant_type=${encodeURIComponent(DEVICE_GRANT)}&device_code=${encodeURIComponent(auth.deviceCode)}&client_id=${encodeURIComponent(this.clientId)}`
775
+ });
776
+ } catch {
777
+ return {};
778
+ }
779
+ const { json: j, parsed } = await readJsonSafely(res);
731
780
  if (j.access_token) {
732
781
  return { terminal: { state: "authorized", tokens: { accessToken: j.access_token, refreshToken: j.refresh_token } } };
733
782
  }
783
+ if (!parsed) return {};
734
784
  switch (j.error) {
735
785
  case "authorization_pending":
736
786
  return {};
@@ -740,8 +790,11 @@ var DeviceAuthClient = class {
740
790
  return { terminal: { state: "denied", error: "The sign-in request was denied." } };
741
791
  case "expired_token":
742
792
  return { terminal: { state: "expired", error: "The code expired. Try again." } };
793
+ // `message` is read too so Wix's own error envelope, which carries no
794
+ // `error`, still reaches the user with its actual reason rather than the
795
+ // generic fallback.
743
796
  default:
744
- return { terminal: { state: "error", error: j.error_description ?? j.error ?? "Device authorization failed." } };
797
+ return { terminal: { state: "error", error: j.error_description ?? j.error ?? j.message ?? "Device authorization failed." } };
745
798
  }
746
799
  }
747
800
  /** Run the full poll loop until a terminal state or the code expires. Honors
@@ -1113,6 +1166,7 @@ export {
1113
1166
  CLIENT_SITE_KEY_OPTIONS,
1114
1167
  CaptchaEscalation,
1115
1168
  DeviceAuthClient,
1169
+ DeviceAuthError,
1116
1170
  InMemorySessionIDStore,
1117
1171
  InMemorySignedInstanceStorage,
1118
1172
  MEMBER_SITE_SETTINGS_URL,