File size: 4,146 Bytes
783fcb6
 
 
7f2cbc9
783fcb6
 
 
 
 
b424a2e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
783fcb6
b424a2e
 
8bcc42c
 
 
b424a2e
783fcb6
 
b424a2e
7f2cbc9
 
b424a2e
8bcc42c
7f2cbc9
b424a2e
7f2cbc9
 
b424a2e
783fcb6
 
7f2cbc9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
783fcb6
 
7f2cbc9
 
783fcb6
 
 
 
 
 
7f2cbc9
 
 
b424a2e
7f2cbc9
 
 
 
 
 
 
 
 
 
b424a2e
7f2cbc9
 
 
 
 
 
 
b424a2e
7f2cbc9
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
export function decodeAuthMessage(raw: string | null): string | null {
  if (!raw) return null;
  try {
    return decodeURIComponent(String(raw).replace(/\+/g, " "));
  } catch {
    return raw;
  }
}

function redactSensitiveAuthText(input: string): string {
  let text = String(input || "");
  // Redact token-like and key-like fragments that should never be displayed to users.
  text = text.replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9._-]+\.[A-Za-z0-9._-]+\b/g, "[redacted-jwt]");
  text = text.replace(/\bsk-[A-Za-z0-9_-]{20,}\b/g, "[redacted-key]");
  text = text.replace(/\b(Bearer)\s+[A-Za-z0-9\-._~+/=]+\b/gi, "$1 [redacted]");
  text = text.replace(
    /\b(code|state|access_token|id_token|refresh_token)=([^&\s]+)/gi,
    (_m, key) => `${String(key)}=[redacted]`
  );
  return text;
}

function clampAuthText(input: string, maxLen: number = 360): string {
  const text = String(input || "");
  if (text.length <= maxLen) return text;
  return `${text.slice(0, maxLen).trimEnd()}...`;
}

export function toUserFacingAuthError(message: string): string {
  const raw = String(message || "").replace(/\s+/g, " ").trim();
  if (!raw) return "Authentication error.";
  if (/service not found:\s*https:\/\/masters-toolkit-api\/?/i.test(raw)) {
    return "Auth0 was asked for legacy audience `https://masters-toolkit-api`, which this app no longer uses. Remove `VITE_AUTH0_AUDIENCE`/`AUTH0_AUDIENCE` unless you have a real Auth0 API Identifier configured.";
  }
  if (/invalid state/i.test(raw)) {
    return "Invalid state. Your login session expired or became stale. Retry login to start a fresh session.";
  }
  if (/^access_denied$/i.test(raw)) {
    return "Access denied by Auth0 policy. Confirm allowed email domains and API/application access, then retry login.";
  }
  if (/^invalid_request$/i.test(raw)) {
    return "Authentication request is invalid. Check callback URL and Auth0 application settings. If this deployment does not use a custom API, leave `VITE_AUTH0_AUDIENCE` unset.";
  }
  if (/^unauthorized_client$/i.test(raw)) {
    return "Auth0 client is not authorized for this request. Verify callback URLs and API Application Access settings.";
  }
  return clampAuthText(redactSensitiveAuthText(raw));
}

function parseAuthCallbackParams(urlValue: string): { err: string | null; desc: string | null } {
  const url = new URL(String(urlValue || ""));
  const errRaw = url.searchParams.get("error");
  const descRaw = url.searchParams.get("error_description");
  if (errRaw || descRaw) {
    return { err: decodeAuthMessage(errRaw), desc: decodeAuthMessage(descRaw) };
  }

  const hash = String(url.hash || "").replace(/^#/, "").trim();
  if (!hash || !hash.includes("=")) {
    return { err: null, desc: null };
  }
  const hp = new URLSearchParams(hash);
  const errHash = hp.get("error");
  const descHash = hp.get("error_description");
  return { err: decodeAuthMessage(errHash), desc: decodeAuthMessage(descHash) };
}

export function getCallbackErrorFromUrl(urlValue: string): string | null {
  try {
    const { err, desc } = parseAuthCallbackParams(urlValue);
    if (!err && !desc) return null;
    const combined = toUserFacingAuthError(String(desc || err || ""));
    return combined || null;
  } catch {
    return null;
  }
}

export function getAuthErrorMessage(error: unknown): string {
  if (!error) return "";
  if (typeof error === "string") return clampAuthText(redactSensitiveAuthText(error));
  const obj = error as Record<string, unknown>;

  const candidates: unknown[] = [
    obj.error_description,
    obj.description,
    obj.message,
    obj.error,
  ];
  for (const c of candidates) {
    const text = String(c || "").trim();
    if (text) return clampAuthText(redactSensitiveAuthText(text));
  }

  // Some wrappers place useful fields under cause.
  const cause = obj.cause as Record<string, unknown> | undefined;
  if (cause && typeof cause === "object") {
    for (const c of [cause.error_description, cause.description, cause.message, cause.error]) {
      const text = String(c || "").trim();
      if (text) return clampAuthText(redactSensitiveAuthText(text));
    }
  }
  return "";
}