@autumnsgrove/groveengine 0.9.96 → 0.9.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 (65) hide show
  1. package/dist/config/petal.d.ts +23 -0
  2. package/dist/config/petal.js +26 -0
  3. package/dist/curios/timeline/index.js +31 -3
  4. package/dist/curios/timeline/voices/presets/casual.js +4 -1
  5. package/dist/curios/timeline/voices/presets/minimal.js +4 -1
  6. package/dist/curios/timeline/voices/presets/poetic.js +4 -1
  7. package/dist/curios/timeline/voices/presets/professional.js +4 -1
  8. package/dist/curios/timeline/voices/presets/quest.js +4 -1
  9. package/dist/grafts/login/LoginGraft.svelte +1 -0
  10. package/dist/grafts/login/config.d.ts +3 -1
  11. package/dist/grafts/login/config.js +3 -1
  12. package/dist/grafts/login/server/callback.js +20 -28
  13. package/dist/groveauth/client.js +16 -10
  14. package/dist/groveauth/errors.d.ts +165 -0
  15. package/dist/groveauth/errors.js +217 -0
  16. package/dist/groveauth/index.d.ts +13 -11
  17. package/dist/groveauth/index.js +9 -7
  18. package/dist/lumen/index.d.ts +1 -1
  19. package/dist/server/encryption.d.ts +24 -0
  20. package/dist/server/encryption.js +168 -0
  21. package/dist/server/petal/index.d.ts +2 -1
  22. package/dist/server/petal/index.js +5 -1
  23. package/dist/server/petal/layer1-csam.d.ts +22 -25
  24. package/dist/server/petal/layer1-csam.js +99 -43
  25. package/dist/server/petal/photodna-client.d.ts +84 -0
  26. package/dist/server/petal/photodna-client.js +150 -0
  27. package/dist/server/petal/types.d.ts +7 -1
  28. package/dist/server/rate-limits/config.d.ts +4 -0
  29. package/dist/server/rate-limits/config.js +3 -0
  30. package/dist/server/secrets-manager.d.ts +114 -0
  31. package/dist/server/secrets-manager.js +303 -0
  32. package/dist/server/secrets.d.ts +27 -0
  33. package/dist/server/secrets.js +28 -0
  34. package/dist/server/services/index.d.ts +16 -14
  35. package/dist/server/services/index.js +21 -14
  36. package/dist/server/services/storage.js +16 -1
  37. package/dist/server/services/trace-email.d.ts +24 -0
  38. package/dist/server/services/trace-email.js +113 -0
  39. package/dist/styles/content.css +4 -2
  40. package/dist/styles/fonts-optional.css +90 -0
  41. package/dist/ui/components/feedback/Trace.svelte +315 -0
  42. package/dist/ui/components/feedback/Trace.svelte.d.ts +15 -0
  43. package/dist/ui/components/feedback/index.d.ts +2 -0
  44. package/dist/ui/components/feedback/index.js +13 -0
  45. package/dist/ui/components/ui/Accordion.svelte +9 -3
  46. package/dist/ui/components/ui/GlassButton.svelte +51 -67
  47. package/dist/ui/components/ui/GlassButton.svelte.d.ts +4 -3
  48. package/dist/ui/components/ui/GlassCard.svelte +19 -3
  49. package/dist/ui/components/ui/GlassCard.svelte.d.ts +19 -3
  50. package/dist/ui/components/ui/Input.svelte +1 -2
  51. package/dist/ui/components/ui/Input.svelte.d.ts +1 -1
  52. package/dist/ui/components/ui/Select.svelte +7 -5
  53. package/dist/ui/components/ui/Select.svelte.d.ts +5 -2
  54. package/dist/ui/components/ui/index.d.ts +1 -1
  55. package/dist/ui/components/ui/index.js +1 -1
  56. package/dist/ui/index.d.ts +1 -0
  57. package/dist/ui/index.js +1 -0
  58. package/dist/utils/index.d.ts +1 -0
  59. package/dist/utils/index.js +1 -0
  60. package/dist/utils/markdown.d.ts +12 -2
  61. package/dist/utils/sanitize.d.ts +14 -0
  62. package/dist/utils/sanitize.js +67 -0
  63. package/dist/utils/trace-path.d.ts +36 -0
  64. package/dist/utils/trace-path.js +56 -0
  65. package/package.json +9 -3
@@ -99,6 +99,29 @@ export declare const FAILOVER_CONFIG: {
99
99
  /** Sanity check timeout (ms) */
100
100
  readonly sanityTimeoutMs: 10000;
101
101
  };
102
+ /**
103
+ * PhotoDNA Cloud Service configuration
104
+ *
105
+ * PhotoDNA is Microsoft's industry-standard perceptual hash system for
106
+ * detecting known CSAM content against the NCMEC database.
107
+ *
108
+ * INTEGRATION STATUS: Awaiting Microsoft approval
109
+ * Once approved, add PHOTODNA_SUBSCRIPTION_KEY to Cloudflare secrets.
110
+ *
111
+ * @see https://www.microsoft.com/en-us/photodna
112
+ */
113
+ export declare const PHOTODNA_CONFIG: {
114
+ /** PhotoDNA Cloud Service endpoint */
115
+ readonly endpoint: "https://api.microsoftmoderator.com/photodna";
116
+ /** Request timeout in milliseconds */
117
+ readonly timeoutMs: 5000;
118
+ /** Environment variable name for subscription key */
119
+ readonly subscriptionKeyEnvVar: "PHOTODNA_SUBSCRIPTION_KEY";
120
+ /** Whether PhotoDNA is the primary CSAM detection method */
121
+ readonly isPrimary: true;
122
+ /** Fall back to vision model if PhotoDNA unavailable */
123
+ readonly fallbackToVision: true;
124
+ };
102
125
  /**
103
126
  * Approximate cost per image (in USD)
104
127
  * Based on Workers AI pricing: $0.27/M input, $0.85/M output
@@ -198,6 +198,32 @@ export const FAILOVER_CONFIG = {
198
198
  sanityTimeoutMs: 10_000,
199
199
  };
200
200
  // ============================================================================
201
+ // PhotoDNA Configuration
202
+ // ============================================================================
203
+ /**
204
+ * PhotoDNA Cloud Service configuration
205
+ *
206
+ * PhotoDNA is Microsoft's industry-standard perceptual hash system for
207
+ * detecting known CSAM content against the NCMEC database.
208
+ *
209
+ * INTEGRATION STATUS: Awaiting Microsoft approval
210
+ * Once approved, add PHOTODNA_SUBSCRIPTION_KEY to Cloudflare secrets.
211
+ *
212
+ * @see https://www.microsoft.com/en-us/photodna
213
+ */
214
+ export const PHOTODNA_CONFIG = {
215
+ /** PhotoDNA Cloud Service endpoint */
216
+ endpoint: "https://api.microsoftmoderator.com/photodna",
217
+ /** Request timeout in milliseconds */
218
+ timeoutMs: 5_000,
219
+ /** Environment variable name for subscription key */
220
+ subscriptionKeyEnvVar: "PHOTODNA_SUBSCRIPTION_KEY",
221
+ /** Whether PhotoDNA is the primary CSAM detection method */
222
+ isPrimary: true,
223
+ /** Fall back to vision model if PhotoDNA unavailable */
224
+ fallbackToVision: true,
225
+ };
226
+ // ============================================================================
201
227
  // Pricing (for cost tracking)
202
228
  // ============================================================================
203
229
  /**
@@ -50,6 +50,31 @@ formatHistoricalContextForPrompt, formatContinuationForPrompt, buildPromptContex
50
50
  // =============================================================================
51
51
  // Utility Functions
52
52
  // =============================================================================
53
+ /**
54
+ * Strip hallucinated GitHub links from AI output.
55
+ *
56
+ * The AI sometimes invents URLs for section headers like:
57
+ * [Sentinel Stress Testing System](https://github.com/AutumnsGrove/Sentinel Stress Testing System)
58
+ *
59
+ * This function strips fake links while preserving real commit/PR links.
60
+ * Real links have patterns like /commit/abc123 or /pull/123
61
+ */
62
+ function stripHallucinatedLinks(text) {
63
+ // Match markdown links to github.com/AutumnsGrove/...
64
+ const githubLinkPattern = /\[([^\]]+)\]\((https?:\/\/github\.com\/AutumnsGrove\/[^)]+)\)/g;
65
+ return text.replace(githubLinkPattern, (match, linkText, url) => {
66
+ // Real links contain: /commit/, /pull/, /issues/, /blob/, /tree/
67
+ const isRealLink = /\/(commit|pull|issues|blob|tree|compare)\//.test(url) &&
68
+ // And shouldn't have spaces in the URL (hallucinated links often do)
69
+ !/ /.test(url);
70
+ if (isRealLink) {
71
+ return match; // Keep valid links
72
+ }
73
+ // Strip fake links, keep just the text
74
+ console.warn(`[Timeline] Stripped hallucinated link: ${url}`);
75
+ return linkText;
76
+ });
77
+ }
53
78
  /**
54
79
  * Parse AI response into structured summary data
55
80
  */
@@ -65,14 +90,17 @@ export function parseAIResponse(response) {
65
90
  const validGutter = (parsed.gutter || [])
66
91
  .filter((item) => item.anchor && item.content && typeof item.content === "string")
67
92
  .map((item) => ({
68
- anchor: item.anchor,
93
+ anchor: stripHallucinatedLinks(item.anchor),
69
94
  type: item.type || "comment",
70
95
  content: item.content.trim(),
71
96
  }));
97
+ // Sanitize brief and detailed to remove any hallucinated links
98
+ const brief = stripHallucinatedLinks(parsed.brief || "Worked on a few things today.");
99
+ const detailed = stripHallucinatedLinks(parsed.detailed || "## Projects\n\nSome progress was made.");
72
100
  return {
73
101
  success: true,
74
- brief: parsed.brief || "Worked on a few things today.",
75
- detailed: parsed.detailed || "## Projects\n\nSome progress was made.",
102
+ brief,
103
+ detailed,
76
104
  gutter: validGutter,
77
105
  };
78
106
  }
@@ -83,7 +83,10 @@ REQUIREMENTS:
83
83
  - JSON only, no markdown code blocks
84
84
  - Escape newlines as \\n
85
85
  - Gutter anchors must EXACTLY match "### ProjectName" headers
86
- - Exactly ${gutterCount} gutter comments`;
86
+ - Exactly ${gutterCount} gutter comments
87
+ - NEVER create markdown links for section headers (no [Title](url) format)
88
+ - Only use plain text for headings like "### ProjectName"
89
+ - Do NOT invent URLs or link to non-existent repositories`;
87
90
  },
88
91
  };
89
92
  export default casual;
@@ -84,7 +84,10 @@ REQUIREMENTS:
84
84
  - Escape newlines as \\n
85
85
  - Gutter anchors must EXACTLY match "### ProjectName" headers
86
86
  - Exactly ${gutterCount} gutter comments
87
- - Keep everything as short as possible`;
87
+ - Keep everything as short as possible
88
+ - NEVER create markdown links for section headers (no [Title](url) format)
89
+ - Only use plain text for headings
90
+ - Do NOT invent URLs or link to non-existent repositories`;
88
91
  },
89
92
  };
90
93
  export default minimal;
@@ -84,7 +84,10 @@ REQUIREMENTS:
84
84
  - Escape newlines as \\n
85
85
  - Gutter anchors must EXACTLY match "### ProjectName" headers
86
86
  - Exactly ${gutterCount} gutter comments
87
- - Keep it genuine - find real meaning, don't force purple prose`;
87
+ - Keep it genuine - find real meaning, don't force purple prose
88
+ - NEVER create markdown links for section headers (no [Title](url) format)
89
+ - Only use plain text for headings like "### ProjectName"
90
+ - Do NOT invent URLs or link to non-existent repositories`;
88
91
  },
89
92
  };
90
93
  export default poetic;
@@ -90,7 +90,10 @@ REQUIREMENTS:
90
90
  - JSON only, no markdown code blocks
91
91
  - Escape newlines as \\n
92
92
  - Gutter anchors must EXACTLY match "### ProjectName" headers
93
- - Exactly ${gutterCount} gutter comments`;
93
+ - Exactly ${gutterCount} gutter comments
94
+ - NEVER create markdown links for section headers (no [Title](url) format)
95
+ - Only use plain text for headings like "### ProjectName"
96
+ - Do NOT invent URLs or link to non-existent repositories`;
94
97
  },
95
98
  };
96
99
  export default professional;
@@ -83,7 +83,10 @@ REQUIREMENTS:
83
83
  - Escape newlines as \\n
84
84
  - Gutter anchors must EXACTLY match the "### [ProjectName] Expedition" headers you create
85
85
  - Exactly ${gutterCount} gutter comments
86
- - Keep it fun but still informative about what actually happened`;
86
+ - Keep it fun but still informative about what actually happened
87
+ - NEVER create markdown links for section headers (no [Title](url) format)
88
+ - Only use plain text for headings
89
+ - Do NOT invent URLs or link to non-existent repositories`;
87
90
  },
88
91
  };
89
92
  export default quest;
@@ -95,6 +95,7 @@
95
95
  try {
96
96
  const response = await fetch(GROVEAUTH_URLS.socialSignIn, {
97
97
  method: "POST",
98
+ credentials: "include", // Required for cross-origin cookies
98
99
  headers: {
99
100
  "Content-Type": "application/json",
100
101
  },
@@ -76,8 +76,10 @@ export declare const AUTH_COOKIE_NAMES: {
76
76
  readonly refreshToken: "refresh_token";
77
77
  /** Session ID (legacy) @deprecated */
78
78
  readonly session: "session";
79
- /** Better Auth session token (the new standard) */
79
+ /** Better Auth session token (the new standard) - unprefixed for dev */
80
80
  readonly betterAuthSession: "better-auth.session_token";
81
+ /** Better Auth session token with __Secure- prefix (production HTTPS) */
82
+ readonly betterAuthSessionSecure: "__Secure-better-auth.session_token";
81
83
  };
82
84
  /**
83
85
  * Cookie options for OAuth flow cookies.
@@ -114,8 +114,10 @@ export const AUTH_COOKIE_NAMES = {
114
114
  refreshToken: "refresh_token",
115
115
  /** Session ID (legacy) @deprecated */
116
116
  session: "session",
117
- /** Better Auth session token (the new standard) */
117
+ /** Better Auth session token (the new standard) - unprefixed for dev */
118
118
  betterAuthSession: "better-auth.session_token",
119
+ /** Better Auth session token with __Secure- prefix (production HTTPS) */
120
+ betterAuthSessionSecure: "__Secure-better-auth.session_token",
119
121
  };
120
122
  /**
121
123
  * Cookie options for OAuth flow cookies.
@@ -10,24 +10,7 @@
10
10
  */
11
11
  import { redirect } from "@sveltejs/kit";
12
12
  import { AUTH_COOKIE_NAMES } from "../config.js";
13
- // =============================================================================
14
- // ERROR HANDLING
15
- // =============================================================================
16
- /**
17
- * User-friendly error messages for common OAuth errors.
18
- */
19
- const ERROR_MESSAGES = {
20
- access_denied: "You cancelled the login process",
21
- auth_failed: "Authentication failed, please try again",
22
- no_session: "Session was not created, please try again",
23
- rate_limited: "Too many login attempts. Please wait before trying again.",
24
- };
25
- /**
26
- * Get a user-friendly error message for an error code.
27
- */
28
- function getFriendlyErrorMessage(errorCode) {
29
- return ERROR_MESSAGES[errorCode] || "An error occurred during login";
30
- }
13
+ import { AUTH_ERRORS, getAuthError, logAuthError, buildErrorParams, } from "../../../groveauth/errors.js";
31
14
  // =============================================================================
32
15
  // RATE LIMITING
33
16
  // =============================================================================
@@ -104,10 +87,11 @@ export function createCallbackHandler(config = {}) {
104
87
  const rateLimited = await isRateLimited(kv, `auth-callback:${clientIp}`, 10, // 10 attempts
105
88
  900);
106
89
  if (rateLimited) {
107
- console.warn("[Auth Callback] Rate limited:", { ip: clientIp });
90
+ const authError = AUTH_ERRORS.RATE_LIMITED;
91
+ logAuthError(authError, { ip: clientIp });
108
92
  return new Response(JSON.stringify({
109
- error: "rate_limited",
110
- message: getFriendlyErrorMessage("rate_limited"),
93
+ error: authError.code,
94
+ message: authError.userMessage,
111
95
  }), {
112
96
  status: 429,
113
97
  headers: {
@@ -120,19 +104,27 @@ export function createCallbackHandler(config = {}) {
120
104
  // Check for error from OAuth provider
121
105
  const errorParam = url.searchParams.get("error");
122
106
  if (errorParam) {
123
- console.error("[Auth Callback] Error from provider:", errorParam);
124
- const friendlyMessage = getFriendlyErrorMessage(errorParam === "access_denied" ? "access_denied" : "auth_failed");
125
- redirect(302, `/auth/login?error=${encodeURIComponent(friendlyMessage)}`);
107
+ const authError = getAuthError(errorParam);
108
+ logAuthError(authError, {
109
+ oauthError: errorParam,
110
+ ip: getClientIP(request),
111
+ });
112
+ redirect(302, `/auth/login?${buildErrorParams(authError)}`);
126
113
  }
127
114
  // Get return URL from query params (set by LoginGraft) or use default
128
115
  const returnTo = url.searchParams.get("returnTo") || defaultReturnTo;
129
116
  // Verify Better Auth session cookie was set
130
- // Better Auth sets this cookie during the OAuth callback
131
- const sessionToken = cookies.get(AUTH_COOKIE_NAMES.betterAuthSession);
117
+ // Better Auth uses __Secure- prefix in production (HTTPS)
118
+ // Check both prefixed and unprefixed names for compatibility
119
+ const sessionToken = cookies.get(AUTH_COOKIE_NAMES.betterAuthSessionSecure) ||
120
+ cookies.get(AUTH_COOKIE_NAMES.betterAuthSession);
132
121
  if (!sessionToken) {
133
122
  // No session cookie - auth may have failed silently
134
- console.warn("[Auth Callback] No Better Auth session cookie found");
135
- redirect(302, `/auth/login?error=${encodeURIComponent(getFriendlyErrorMessage("no_session"))}`);
123
+ const authError = AUTH_ERRORS.NO_SESSION;
124
+ logAuthError(authError, {
125
+ ip: getClientIP(request),
126
+ });
127
+ redirect(302, `/auth/login?${buildErrorParams(authError)}`);
136
128
  }
137
129
  // Success! Redirect to the requested destination
138
130
  console.log("[Auth Callback] Success, redirecting to:", returnTo);
@@ -190,9 +190,11 @@ export class GroveAuthClient {
190
190
  code_verifier: codeVerifier,
191
191
  }),
192
192
  });
193
- const data = (await response.json());
193
+ // Parse response body once (streams can only be consumed once)
194
+ const data = await response.json();
194
195
  if (!response.ok) {
195
- throw new GroveAuthError(data.error || "token_error", data.error_description || data.message || "Failed to exchange code", response.status);
196
+ const error = data;
197
+ throw new GroveAuthError(error.error || "token_error", error.error_description || error.message || "Failed to exchange code", response.status);
196
198
  }
197
199
  return data;
198
200
  }
@@ -219,9 +221,13 @@ export class GroveAuthClient {
219
221
  client_secret: this.config.clientSecret,
220
222
  }),
221
223
  });
222
- const data = (await response.json());
224
+ // Parse response body once (streams can only be consumed once)
225
+ const data = await response.json();
223
226
  if (!response.ok) {
224
- throw new GroveAuthError(data.error || "refresh_error", data.error_description || data.message || "Failed to refresh token", response.status);
227
+ const error = data;
228
+ throw new GroveAuthError(error.error || "refresh_error", error.error_description ||
229
+ error.message ||
230
+ "Failed to refresh token", response.status);
225
231
  }
226
232
  return data;
227
233
  }, { maxRetries: options.maxRetries ?? 3 });
@@ -255,8 +261,8 @@ export class GroveAuthClient {
255
261
  }),
256
262
  });
257
263
  if (!response.ok) {
258
- const data = (await response.json());
259
- throw new GroveAuthError(data.error || "revoke_error", data.error_description || data.message || "Failed to revoke token", response.status);
264
+ const error = (await response.json());
265
+ throw new GroveAuthError(error.error || "revoke_error", error.error_description || error.message || "Failed to revoke token", response.status);
260
266
  }
261
267
  }
262
268
  // ===========================================================================
@@ -287,8 +293,8 @@ export class GroveAuthClient {
287
293
  },
288
294
  });
289
295
  if (!response.ok) {
290
- const data = (await response.json());
291
- throw new GroveAuthError(data.error || "userinfo_error", data.error_description || data.message || "Failed to get user info", response.status);
296
+ const error = (await response.json());
297
+ throw new GroveAuthError(error.error || "userinfo_error", error.error_description || error.message || "Failed to get user info", response.status);
292
298
  }
293
299
  return response.json();
294
300
  }
@@ -305,8 +311,8 @@ export class GroveAuthClient {
305
311
  },
306
312
  });
307
313
  if (!response.ok) {
308
- const data = (await response.json());
309
- throw new GroveAuthError(data.error || "subscription_error", data.message || "Failed to get subscription", response.status);
314
+ const error = (await response.json());
315
+ throw new GroveAuthError(error.error || "subscription_error", error.message || "Failed to get subscription", response.status);
310
316
  }
311
317
  return response.json();
312
318
  }
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Heartwood Auth Error System
3
+ *
4
+ * Provides structured error codes for authentication failures.
5
+ * Error codes follow the format: HW-AUTH-XXX
6
+ *
7
+ * Categories:
8
+ * - user: User can fix this themselves (try again, use different method)
9
+ * - admin: User should contact admin/support (config issue, permissions)
10
+ * - bug: Internal error (should not happen, needs investigation)
11
+ *
12
+ * @see https://github.com/AutumnsGrove/GroveEngine/issues/668
13
+ */
14
+ export type ErrorCategory = "user" | "admin" | "bug";
15
+ export interface AuthErrorDef {
16
+ /** Heartwood error code (e.g., HW-AUTH-001) */
17
+ code: string;
18
+ /** Error category for determining how to display */
19
+ category: ErrorCategory;
20
+ /** User-facing message (safe to display) */
21
+ userMessage: string;
22
+ /** Admin-facing message with more detail */
23
+ adminMessage: string;
24
+ }
25
+ /**
26
+ * All Heartwood authentication error codes.
27
+ *
28
+ * Ranges:
29
+ * - 001-019: OAuth provider errors
30
+ * - 020-039: Session/token errors
31
+ * - 040-059: Client configuration errors
32
+ * - 060-079: Rate limiting and security
33
+ * - 080-099: Internal errors
34
+ */
35
+ export declare const AUTH_ERRORS: {
36
+ readonly ACCESS_DENIED: {
37
+ readonly code: "HW-AUTH-001";
38
+ readonly category: "user";
39
+ readonly userMessage: "You cancelled the sign-in process. Try again when ready.";
40
+ readonly adminMessage: "User denied OAuth consent or cancelled the flow.";
41
+ };
42
+ readonly PROVIDER_ERROR: {
43
+ readonly code: "HW-AUTH-002";
44
+ readonly category: "bug";
45
+ readonly userMessage: "The sign-in provider encountered an error. Please try again.";
46
+ readonly adminMessage: "OAuth provider returned an error during authentication.";
47
+ };
48
+ readonly INVALID_SCOPE: {
49
+ readonly code: "HW-AUTH-003";
50
+ readonly category: "admin";
51
+ readonly userMessage: "Sign-in failed due to a configuration issue. Contact support.";
52
+ readonly adminMessage: "OAuth scope is invalid or not permitted for this client.";
53
+ };
54
+ readonly REDIRECT_URI_MISMATCH: {
55
+ readonly code: "HW-AUTH-004";
56
+ readonly category: "admin";
57
+ readonly userMessage: "Sign-in failed due to a configuration issue. Contact support.";
58
+ readonly adminMessage: "The redirect_uri does not match any registered URI for this client.";
59
+ };
60
+ readonly NO_SESSION: {
61
+ readonly code: "HW-AUTH-020";
62
+ readonly category: "user";
63
+ readonly userMessage: "Your session wasn't created. Please try signing in again.";
64
+ readonly adminMessage: "No session cookie was set after OAuth callback. May indicate cookie blocking.";
65
+ };
66
+ readonly SESSION_EXPIRED: {
67
+ readonly code: "HW-AUTH-021";
68
+ readonly category: "user";
69
+ readonly userMessage: "Your session has expired. Please sign in again.";
70
+ readonly adminMessage: "Session token has expired or been invalidated.";
71
+ };
72
+ readonly INVALID_TOKEN: {
73
+ readonly code: "HW-AUTH-022";
74
+ readonly category: "user";
75
+ readonly userMessage: "Your session is invalid. Please sign in again.";
76
+ readonly adminMessage: "Token validation failed - token is malformed or revoked.";
77
+ };
78
+ readonly TOKEN_EXCHANGE_FAILED: {
79
+ readonly code: "HW-AUTH-023";
80
+ readonly category: "bug";
81
+ readonly userMessage: "Sign-in failed. Please try again.";
82
+ readonly adminMessage: "Failed to exchange authorization code for tokens. Check client credentials.";
83
+ };
84
+ readonly LEGACY_SESSION_EXPIRED: {
85
+ readonly code: "HW-AUTH-024";
86
+ readonly category: "user";
87
+ readonly userMessage: "Your old session format has expired. Please sign in with a fresh login.";
88
+ readonly adminMessage: "Legacy session cookie found but migration deadline has passed.";
89
+ };
90
+ readonly UNREGISTERED_CLIENT: {
91
+ readonly code: "HW-AUTH-040";
92
+ readonly category: "admin";
93
+ readonly userMessage: "Sign-in failed due to a configuration issue. Contact support.";
94
+ readonly adminMessage: "Client ID is not registered with GroveAuth or has been revoked.";
95
+ };
96
+ readonly INVALID_CLIENT: {
97
+ readonly code: "HW-AUTH-041";
98
+ readonly category: "admin";
99
+ readonly userMessage: "Sign-in failed due to a configuration issue. Contact support.";
100
+ readonly adminMessage: "Client authentication failed (invalid client_id or secret).";
101
+ };
102
+ readonly ORIGIN_NOT_ALLOWED: {
103
+ readonly code: "HW-AUTH-042";
104
+ readonly category: "admin";
105
+ readonly userMessage: "Sign-in failed due to a configuration issue. Contact support.";
106
+ readonly adminMessage: "The request origin is not in the allowed origins list for this client.";
107
+ };
108
+ readonly RATE_LIMITED: {
109
+ readonly code: "HW-AUTH-060";
110
+ readonly category: "user";
111
+ readonly userMessage: "Too many sign-in attempts. Please wait a few minutes and try again.";
112
+ readonly adminMessage: "Rate limit exceeded for authentication endpoint.";
113
+ };
114
+ readonly CSRF_MISMATCH: {
115
+ readonly code: "HW-AUTH-061";
116
+ readonly category: "user";
117
+ readonly userMessage: "Sign-in failed for security reasons. Please try again.";
118
+ readonly adminMessage: "OAuth state parameter mismatch - possible CSRF attack.";
119
+ };
120
+ readonly INTERNAL_ERROR: {
121
+ readonly code: "HW-AUTH-080";
122
+ readonly category: "bug";
123
+ readonly userMessage: "An unexpected error occurred. Please try again later.";
124
+ readonly adminMessage: "Internal server error during authentication.";
125
+ };
126
+ readonly UNKNOWN_ERROR: {
127
+ readonly code: "HW-AUTH-099";
128
+ readonly category: "bug";
129
+ readonly userMessage: "An unexpected error occurred. Please try again.";
130
+ readonly adminMessage: "Unknown error code received from OAuth flow.";
131
+ };
132
+ };
133
+ export type AuthErrorKey = keyof typeof AUTH_ERRORS;
134
+ /**
135
+ * Get the Heartwood error definition for an OAuth error code.
136
+ *
137
+ * @param oauthError - The error code from OAuth (e.g., "access_denied")
138
+ * @returns The corresponding AuthErrorDef
139
+ */
140
+ export declare function getAuthError(oauthError: string): AuthErrorDef;
141
+ /**
142
+ * Get error by Heartwood code (e.g., "HW-AUTH-001").
143
+ */
144
+ export declare function getAuthErrorByCode(hwCode: string): AuthErrorDef | undefined;
145
+ /**
146
+ * Log an auth error with structured context.
147
+ * Sensitive data (tokens, secrets) is NEVER logged.
148
+ *
149
+ * @param error - The error definition
150
+ * @param context - Additional context (sanitized)
151
+ */
152
+ export declare function logAuthError(error: AuthErrorDef, context?: {
153
+ oauthError?: string;
154
+ ip?: string;
155
+ path?: string;
156
+ userAgent?: string;
157
+ }): void;
158
+ /**
159
+ * Build error URL parameters for redirecting to login page.
160
+ * Includes the error code so the login page can display appropriate messaging.
161
+ *
162
+ * @param error - The error definition
163
+ * @param isAdminFlow - Whether this is an admin-facing flow (shows more detail)
164
+ */
165
+ export declare function buildErrorParams(error: AuthErrorDef, isAdminFlow?: boolean): string;