@djangocfg/api 2.1.455 → 2.1.457

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 (89) hide show
  1. package/dist/auth.cjs +1611 -2148
  2. package/dist/auth.cjs.map +1 -1
  3. package/dist/auth.d.cts +37 -82
  4. package/dist/auth.d.ts +37 -82
  5. package/dist/auth.mjs +1581 -2118
  6. package/dist/auth.mjs.map +1 -1
  7. package/dist/chunk-2G67QRNU.mjs +2096 -0
  8. package/dist/chunk-2G67QRNU.mjs.map +1 -0
  9. package/dist/chunk-32SRQGAC.mjs +2109 -0
  10. package/dist/chunk-32SRQGAC.mjs.map +1 -0
  11. package/dist/chunk-4BPRCONN.mjs +2098 -0
  12. package/dist/chunk-4BPRCONN.mjs.map +1 -0
  13. package/dist/chunk-5UZ2Z323.mjs +2021 -0
  14. package/dist/chunk-5UZ2Z323.mjs.map +1 -0
  15. package/dist/chunk-7KFTJXNM.mjs +2097 -0
  16. package/dist/chunk-7KFTJXNM.mjs.map +1 -0
  17. package/dist/chunk-BK4K5CVT.mjs +2019 -0
  18. package/dist/chunk-BK4K5CVT.mjs.map +1 -0
  19. package/dist/chunk-JLPJZ6WB.mjs +2020 -0
  20. package/dist/chunk-JLPJZ6WB.mjs.map +1 -0
  21. package/dist/chunk-TVU6PYJH.mjs +2032 -0
  22. package/dist/chunk-TVU6PYJH.mjs.map +1 -0
  23. package/dist/clients.cjs +292 -47
  24. package/dist/clients.cjs.map +1 -1
  25. package/dist/clients.d.cts +23 -0
  26. package/dist/clients.d.ts +23 -0
  27. package/dist/clients.mjs +292 -47
  28. package/dist/clients.mjs.map +1 -1
  29. package/dist/hooks.cjs +200 -32
  30. package/dist/hooks.cjs.map +1 -1
  31. package/dist/hooks.mjs +200 -32
  32. package/dist/hooks.mjs.map +1 -1
  33. package/dist/index.cjs +292 -47
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.d.cts +81 -4
  36. package/dist/index.d.ts +81 -4
  37. package/dist/index.mjs +292 -47
  38. package/dist/index.mjs.map +1 -1
  39. package/dist/sdk.gen-2SQOPTWJ.mjs +26 -0
  40. package/dist/sdk.gen-2SQOPTWJ.mjs.map +1 -0
  41. package/dist/sdk.gen-HSGK4C5S.mjs +26 -0
  42. package/dist/sdk.gen-HSGK4C5S.mjs.map +1 -0
  43. package/dist/sdk.gen-NUK2VGHO.mjs +25 -0
  44. package/dist/sdk.gen-NUK2VGHO.mjs.map +1 -0
  45. package/dist/sdk.gen-O4KAQUZB.mjs +26 -0
  46. package/dist/sdk.gen-O4KAQUZB.mjs.map +1 -0
  47. package/dist/sdk.gen-PPAVSBNT.mjs +26 -0
  48. package/dist/sdk.gen-PPAVSBNT.mjs.map +1 -0
  49. package/dist/sdk.gen-XLHLOCJ2.mjs +25 -0
  50. package/dist/sdk.gen-XLHLOCJ2.mjs.map +1 -0
  51. package/dist/sdk.gen-ZOE6NQAL.mjs +26 -0
  52. package/dist/sdk.gen-ZOE6NQAL.mjs.map +1 -0
  53. package/dist/sdk.gen-ZT7LGJVO.mjs +26 -0
  54. package/dist/sdk.gen-ZT7LGJVO.mjs.map +1 -0
  55. package/package.json +2 -2
  56. package/src/_api/generated/_cfg_accounts/hooks/index.ts +1 -0
  57. package/src/_api/generated/_cfg_accounts/hooks/useCfgAccountsTokenBlacklistCreate.ts +28 -0
  58. package/src/_api/generated/_cfg_accounts/openapi.json +99 -0
  59. package/src/_api/generated/_cfg_accounts/schemas/TokenBlacklistRequest.ts +11 -0
  60. package/src/_api/generated/_cfg_accounts/schemas/index.ts +1 -0
  61. package/src/_api/generated/_cfg_centrifugo/openapi.json +9 -0
  62. package/src/_api/generated/_cfg_totp/openapi.json +33 -0
  63. package/src/_api/generated/helpers/auth.ts +288 -31
  64. package/src/_api/generated/openapi.json +129 -0
  65. package/src/_api/generated/sdk.gen.ts +94 -16
  66. package/src/_api/generated/types.gen.ts +18 -0
  67. package/src/auth/__tests__/guard.test.ts +0 -31
  68. package/src/auth/constants.ts +6 -0
  69. package/src/auth/context/AccountsContext.tsx +14 -24
  70. package/src/auth/context/AuthContext.tsx +263 -607
  71. package/src/auth/context/types.ts +13 -0
  72. package/src/auth/hooks/index.ts +8 -5
  73. package/src/auth/hooks/useAuthRedirect.ts +17 -0
  74. package/src/auth/hooks/useGithubAuth.ts +3 -3
  75. package/src/auth/hooks/useSession.ts +22 -0
  76. package/src/auth/hooks/useTwoFactor.ts +8 -6
  77. package/src/auth/middlewares/index.ts +5 -6
  78. package/src/auth/utils/guard.ts +0 -22
  79. package/src/auth/utils/index.ts +0 -2
  80. package/src/_api/generated/_cfg_accounts/events.ts +0 -198
  81. package/src/_api/generated/_cfg_centrifugo/events.ts +0 -198
  82. package/src/_api/generated/_cfg_totp/events.ts +0 -198
  83. package/src/auth/__tests__/jwt.test.ts +0 -119
  84. package/src/auth/__tests__/sessionBootstrap.test.ts +0 -111
  85. package/src/auth/__tests__/useTokenRefresh.dom.test.tsx +0 -167
  86. package/src/auth/hooks/useAuthGuard.ts +0 -35
  87. package/src/auth/hooks/useTokenRefresh.ts +0 -131
  88. package/src/auth/refreshHandler.ts +0 -79
  89. package/src/auth/utils/jwt.ts +0 -66
@@ -17,12 +17,25 @@ export interface AuthConfig {
17
17
  onLogout?: () => void;
18
18
  }
19
19
 
20
+ /**
21
+ * AuthConfig.routes resolved against the framework conventions — no field is
22
+ * optional here. This is what components consume (via `useAuth().routes`)
23
+ * instead of defaulting to route constants themselves.
24
+ */
25
+ export interface ResolvedAuthRoutes {
26
+ auth: string;
27
+ defaultCallback: string;
28
+ defaultAuthCallback: string;
29
+ }
30
+
20
31
  // Auth context interface
21
32
  export interface AuthContextType {
22
33
  user: UserProfile | null;
23
34
  isLoading: boolean;
24
35
  isAuthenticated: boolean;
25
36
  isAdminUser: boolean;
37
+ /** Resolved route conventions (config override or framework default). */
38
+ routes: ResolvedAuthRoutes;
26
39
  loadCurrentProfile: (callerId?: string) => Promise<void>;
27
40
  checkAuthAndRedirect: () => Promise<void>;
28
41
 
@@ -1,5 +1,8 @@
1
1
  'use client';
2
2
 
3
+ // Session (reactive snapshot from the generated auth store)
4
+ export { useSession, type SessionSnapshot, type SessionStatus } from './useSession';
5
+
3
6
  // Router hooks
4
7
  export { useCfgRouter } from './useCfgRouter';
5
8
  export { useQueryParams } from './useQueryParams';
@@ -32,8 +35,11 @@ export {
32
35
  } from './useTwoFactorStatus';
33
36
 
34
37
  // Auth guards and redirects
35
- export { useAuthRedirectManager } from './useAuthRedirect';
36
- export { useAuthGuard } from './useAuthGuard';
38
+ export {
39
+ consumeSavedRedirect,
40
+ peekSavedRedirect,
41
+ useAuthRedirectManager,
42
+ } from './useAuthRedirect';
37
43
 
38
44
  // Storage hooks
39
45
  export { useSessionStorage } from './useSessionStorage';
@@ -50,9 +56,6 @@ export {
50
56
  type ProfileCacheOptions,
51
57
  } from './useProfileCache';
52
58
 
53
- // Token refresh
54
- export { useTokenRefresh } from './useTokenRefresh';
55
-
56
59
  // Account deletion
57
60
  export {
58
61
  useDeleteAccount,
@@ -34,6 +34,23 @@ const clearRedirectFromStorage = (): void => {
34
34
  }
35
35
  };
36
36
 
37
+ /**
38
+ * Read the saved back-url without clearing it. Null when nothing saved —
39
+ * unlike getFinalRedirectUrl, never substitutes a fallback.
40
+ */
41
+ export const peekSavedRedirect = (): string | null => getRedirectFromStorage() || null;
42
+
43
+ /**
44
+ * Read AND clear the saved back-url (single-use intent). Null when nothing
45
+ * saved. This is the one primitive every post-auth navigator uses to honor
46
+ * "return the user to where the guard bounced them from".
47
+ */
48
+ export const consumeSavedRedirect = (): string | null => {
49
+ const stored = getRedirectFromStorage();
50
+ if (stored) clearRedirectFromStorage();
51
+ return stored || null;
52
+ };
53
+
37
54
  export const useAuthRedirectManager = (options: AuthRedirectOptions = {}) => {
38
55
  const { fallbackUrl = '/dashboard', clearOnUse = true } = options;
39
56
  const [redirectUrl, setRedirectUrl, removeRedirectUrl] = useSessionStorage<string>(AUTH_REDIRECT_KEY, '');
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useCallback, useState } from 'react';
4
4
 
5
- import { apiAccounts } from '../../clients';
5
+ import { auth } from '../../_api/generated/helpers/auth';
6
6
  import { CfgAccountsOauth } from '../../_api/generated/sdk.gen';
7
7
  import { Analytics, AnalyticsCategory, AnalyticsEvent } from '../utils/analytics';
8
8
  import { authLogger } from '../utils/logger';
@@ -158,8 +158,8 @@ export const useGithubAuth = (options: UseGithubAuthOptions = {}): UseGithubAuth
158
158
 
159
159
  authLogger.info('GitHub OAuth successful, user:', response.user);
160
160
 
161
- apiAccounts.setToken(response.access);
162
- apiAccounts.setRefreshToken(response.refresh);
161
+ // The ONE write path — atomic pair + session notification.
162
+ auth.setSession({ access: response.access, refresh: response.refresh });
163
163
 
164
164
  // Track successful OAuth
165
165
  Analytics.event(AnalyticsEvent.AUTH_LOGIN_SUCCESS, {
@@ -0,0 +1,22 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * useSession — reactive session snapshot from the generated auth store.
5
+ *
6
+ * The ONE way to answer "am I logged in" in React. The store owns the
7
+ * semantics (JWT `exp`-aware, refresh-token-aware) and pushes changes on
8
+ * token writes, refresh success/failure, expiry (internal timer), and
9
+ * cross-tab / cross-store sync. No polling, no ticks.
10
+ */
11
+
12
+ import { useSyncExternalStore } from 'react';
13
+
14
+ import { auth } from '../../_api/generated/helpers/auth';
15
+
16
+ import type { SessionSnapshot, SessionStatus } from '../../_api/generated/helpers/auth';
17
+
18
+ export type { SessionSnapshot, SessionStatus };
19
+
20
+ export function useSession(): SessionSnapshot {
21
+ return useSyncExternalStore(auth.subscribe, auth.getSnapshot, auth.getServerSnapshot);
22
+ }
@@ -2,11 +2,13 @@
2
2
 
3
3
  import { useCallback, useState } from 'react';
4
4
 
5
- import { apiAccounts, apiTotp } from '../../clients';
5
+ import { apiTotp } from '../../clients';
6
+ import { auth } from '../../_api/generated/helpers/auth';
6
7
  import { APIError } from '../../_api/generated/helpers';
7
8
  import { CfgTotp, CfgTotpVerify } from '../../_api/generated/sdk.gen';
8
9
  import { Analytics, AnalyticsCategory, AnalyticsEvent } from '../utils/analytics';
9
10
  import { authLogger } from '../utils/logger';
11
+ import { consumeSavedRedirect } from './useAuthRedirect';
10
12
  import { useCfgRouter } from './useCfgRouter';
11
13
 
12
14
  export interface UseTwoFactorOptions {
@@ -88,9 +90,8 @@ export const useTwoFactor = (options: UseTwoFactorOptions = {}): UseTwoFactorRet
88
90
  warning?: string;
89
91
  remaining_backup_codes?: number;
90
92
  }) => {
91
- // Save tokens
92
- apiAccounts.setToken(response.access_token);
93
- apiAccounts.setRefreshToken(response.refresh_token);
93
+ // Save tokens — the ONE write path (atomic pair + session notification).
94
+ auth.setSession({ access: response.access_token, refresh: response.refresh_token });
94
95
 
95
96
  // Set warning if any
96
97
  if (response.warning) {
@@ -116,9 +117,10 @@ export const useTwoFactor = (options: UseTwoFactorOptions = {}): UseTwoFactorRet
116
117
  // Call success callback
117
118
  onSuccess?.(response.user);
118
119
 
119
- // Redirect (unless skipRedirect is true)
120
+ // Redirect (unless skipRedirect is true).
121
+ // Priority: saved back-url (guard intent) > explicit option > default.
120
122
  if (!skipRedirect) {
121
- const finalRedirectUrl = redirectUrl || '/dashboard';
123
+ const finalRedirectUrl = consumeSavedRedirect() || redirectUrl || '/';
122
124
  authLogger.info('2FA successful, redirecting to:', finalRedirectUrl);
123
125
  router.hardPush(finalRedirectUrl);
124
126
  }
@@ -1,8 +1,7 @@
1
1
  export { proxyMiddleware, proxyMiddlewareConfig } from './proxy';
2
2
 
3
- // NOTE: The former `tokenRefresh` middleware (createAutoRefreshFetch /
4
- // refreshAccessToken / refreshIfExpiringSoon) was removed. Automatic 401
5
- // recovery and proactive refresh now live in a single implementation: the
6
- // generated client interceptor (helpers/auth.ts `installAuthOnClient`) plus the
7
- // canonical strategy in `auth/refreshHandler.ts`. Do not reintroduce a parallel
8
- // refresh path here — see auth/refreshHandler.ts for the rationale.
3
+ // NOTE: Automatic 401 recovery and proactive refresh live in ONE place: the
4
+ // generated store (`_api/generated/helpers/auth.ts`) interceptor, Web-Locks
5
+ // single-flight, and an auto-registered refresh handler. Do not reintroduce a
6
+ // parallel refresh path here (a former `tokenRefresh` middleware was removed
7
+ // for exactly that reason).
@@ -55,25 +55,3 @@ export function resolveGuardIsAuthenticated(s: Pick<GuardInput, 'requireAuth' |
55
55
  return !s.requireAuth || s.isAuthenticated;
56
56
  }
57
57
 
58
- /**
59
- * Delay (ms from `now`) until `isAuthenticated` should next be re-evaluated,
60
- * given the access/refresh expiry timestamps (ms epoch, or null if absent).
61
- *
62
- * `isAuthenticated` is derived from token `exp`, so it can change with no event
63
- * to trigger a recompute — a tab left open silently crosses an expiry boundary.
64
- * We schedule a wake-up at the SOONEST future expiry (the next moment the answer
65
- * could flip), plus a 1s cushion so we re-check strictly after the inclusive
66
- * boundary. Returns null when nothing live remains to expire (no timer needed).
67
- * Callers should clamp the result to the platform setTimeout ceiling.
68
- */
69
- export function nextAuthEvaluationDelay(
70
- accessExp: number | null,
71
- refreshExp: number | null,
72
- now: number,
73
- ): number | null {
74
- const future = [accessExp, refreshExp].filter(
75
- (d): d is number => d !== null && d > now,
76
- );
77
- if (future.length === 0) return null;
78
- return Math.max(0, Math.min(...future) - now) + 1000;
79
- }
@@ -4,10 +4,8 @@ export {
4
4
  resolveGuardIsLoading,
5
5
  shouldRedirectToAuth,
6
6
  resolveGuardIsAuthenticated,
7
- nextAuthEvaluationDelay,
8
7
  type GuardInput,
9
8
  } from './guard';
10
- export { getTokenExpiry, isTokenExpired, isTokenExpiringSoon } from './jwt';
11
9
  export { normalizePath, isAllowedAuthPath } from './path';
12
10
  export { logger, authLogger } from './logger';
13
11
  export {
@@ -1,198 +0,0 @@
1
- // AUTO-GENERATED by cmdop_server / devtools.generator.ts_extras.events
2
- // Hooks Hey API client interceptors → typed CustomEvents on `window`.
3
- // DO NOT EDIT — re-run `make gen-clients`.
4
- //
5
- // Usage:
6
- // import "./generated/events" // side-effect: registers interceptors
7
- //
8
- // window.addEventListener("cmdop:network-error", (e) => {
9
- // console.log(e.detail.status, e.detail.method, e.detail.url);
10
- // });
11
-
12
- import { client } from "../client.gen";
13
-
14
- // ─────────────────────────────────────────────────────────────────────────────
15
- // Event names + detail types
16
- // ─────────────────────────────────────────────────────────────────────────────
17
-
18
- export const NETWORK_ERROR_EVENT = "cmdop:network-error" as const;
19
- export const VALIDATION_ERROR_EVENT = "cmdop:validation-error" as const;
20
- export const RUNTIME_ERROR_EVENT = "cmdop:runtime-error" as const;
21
-
22
- export interface NetworkErrorDetail {
23
- /** HTTP method (GET/POST/...). */
24
- method: string;
25
- /** Request URL. */
26
- url: string;
27
- /** HTTP status if response arrived; null on transport failure. */
28
- status: number | null;
29
- /** Response body parsed as text (best-effort, may be empty). */
30
- body?: string;
31
- /** Underlying error message (network failures, abort, etc.). */
32
- error?: string;
33
- /** Heuristic: did the request likely fail due to CORS? */
34
- possiblyCors: boolean;
35
- timestamp: Date;
36
- }
37
-
38
- export interface ValidationErrorDetail {
39
- /** Operation ID this validation applies to (from the OpenAPI spec). */
40
- operation: string;
41
- /** Schema name that failed (if known). */
42
- schema?: string;
43
- /** Issues array — shape mirrors zod's ZodIssue but kept opaque here to
44
- * avoid forcing a hard zod dep on consumers that don't use schemas. */
45
- issues: unknown;
46
- /** Raw response data that failed validation. */
47
- data: unknown;
48
- timestamp: Date;
49
- }
50
-
51
- export interface RuntimeErrorDetail {
52
- /** Free-form source identifier — component name, hook name, etc. */
53
- source: string;
54
- message: string;
55
- /** Original Error object (may be undefined when caller lacks one). */
56
- error?: Error;
57
- /** Arbitrary context payload from the caller. */
58
- context?: Record<string, unknown>;
59
- timestamp: Date;
60
- }
61
-
62
- // Augment the global WindowEventMap so addEventListener gets typed details.
63
- declare global {
64
- interface WindowEventMap {
65
- "cmdop:network-error": CustomEvent<NetworkErrorDetail>;
66
- "cmdop:validation-error": CustomEvent<ValidationErrorDetail>;
67
- "cmdop:runtime-error": CustomEvent<RuntimeErrorDetail>;
68
- }
69
- }
70
-
71
- // ─────────────────────────────────────────────────────────────────────────────
72
- // Dispatch helpers
73
- // ─────────────────────────────────────────────────────────────────────────────
74
-
75
- function _dispatch<T>(name: string, detail: T): void {
76
- if (typeof window === "undefined") return;
77
- try {
78
- window.dispatchEvent(new CustomEvent(name, { detail, bubbles: true }));
79
- } catch {
80
- // Event dispatch must never crash the app.
81
- }
82
- }
83
-
84
- /** Emit a runtime error from anywhere in app code. */
85
- export function emitRuntimeError(
86
- source: string,
87
- message: string,
88
- error?: Error,
89
- context?: Record<string, unknown>,
90
- ): void {
91
- _dispatch<RuntimeErrorDetail>(RUNTIME_ERROR_EVENT, {
92
- source,
93
- message,
94
- error,
95
- context,
96
- timestamp: new Date(),
97
- });
98
- }
99
-
100
- /** Emit a zod validation failure. Called by hooks when `validate` is provided. */
101
- export function dispatchValidationError(
102
- operation: string,
103
- data: unknown,
104
- issues: unknown,
105
- schema?: string,
106
- ): void {
107
- _dispatch<ValidationErrorDetail>(VALIDATION_ERROR_EVENT, {
108
- operation,
109
- schema,
110
- issues,
111
- data,
112
- timestamp: new Date(),
113
- });
114
- }
115
-
116
- // ─────────────────────────────────────────────────────────────────────────────
117
- // Listen helpers — typed wrappers over addEventListener, return cleanup fn.
118
- // ─────────────────────────────────────────────────────────────────────────────
119
-
120
- export function onNetworkError(
121
- cb: (detail: NetworkErrorDetail) => void,
122
- ): () => void {
123
- if (typeof window === "undefined") return () => {};
124
- const handler = (e: CustomEvent<NetworkErrorDetail>) => cb(e.detail);
125
- window.addEventListener(NETWORK_ERROR_EVENT, handler);
126
- return () => window.removeEventListener(NETWORK_ERROR_EVENT, handler);
127
- }
128
-
129
- export function onValidationError(
130
- cb: (detail: ValidationErrorDetail) => void,
131
- ): () => void {
132
- if (typeof window === "undefined") return () => {};
133
- const handler = (e: CustomEvent<ValidationErrorDetail>) => cb(e.detail);
134
- window.addEventListener(VALIDATION_ERROR_EVENT, handler);
135
- return () => window.removeEventListener(VALIDATION_ERROR_EVENT, handler);
136
- }
137
-
138
- export function onRuntimeError(
139
- cb: (detail: RuntimeErrorDetail) => void,
140
- ): () => void {
141
- if (typeof window === "undefined") return () => {};
142
- const handler = (e: CustomEvent<RuntimeErrorDetail>) => cb(e.detail);
143
- window.addEventListener(RUNTIME_ERROR_EVENT, handler);
144
- return () => window.removeEventListener(RUNTIME_ERROR_EVENT, handler);
145
- }
146
-
147
- // ─────────────────────────────────────────────────────────────────────────────
148
- // Hey API interceptors — wire once, emit on response/error.
149
- // ─────────────────────────────────────────────────────────────────────────────
150
-
151
- let _wired = false;
152
-
153
- function _wire(): void {
154
- if (_wired) return;
155
- _wired = true;
156
-
157
- // response interceptor: every response, OK or not, lets us see status.
158
- // We only emit for non-2xx — successful requests are silent.
159
- client.interceptors.response.use(async (response, request) => {
160
- if (!response.ok) {
161
- let body: string | undefined;
162
- try {
163
- body = await response.clone().text();
164
- if (body.length > 4000) body = body.slice(0, 4000) + "…";
165
- } catch {
166
- // ignore — body inaccessible
167
- }
168
- _dispatch<NetworkErrorDetail>(NETWORK_ERROR_EVENT, {
169
- method: request.method,
170
- url: request.url,
171
- status: response.status,
172
- body,
173
- possiblyCors: false,
174
- timestamp: new Date(),
175
- });
176
- }
177
- return response;
178
- });
179
-
180
- // error interceptor: transport-level failures (abort, DNS, CORS, offline).
181
- client.interceptors.error.use((error, _response, request) => {
182
- const msg = error instanceof Error ? error.message : String(error);
183
- // Heuristic: CORS-blocked fetches usually surface as "Failed to fetch"
184
- // without a status. Not perfect, but catches the common case.
185
- const possiblyCors = /failed to fetch|network error|cors/i.test(msg);
186
- _dispatch<NetworkErrorDetail>(NETWORK_ERROR_EVENT, {
187
- method: request?.method ?? "UNKNOWN",
188
- url: request?.url ?? "",
189
- status: null,
190
- error: msg,
191
- possiblyCors,
192
- timestamp: new Date(),
193
- });
194
- return error;
195
- });
196
- }
197
-
198
- _wire();
@@ -1,198 +0,0 @@
1
- // AUTO-GENERATED by cmdop_server / devtools.generator.ts_extras.events
2
- // Hooks Hey API client interceptors → typed CustomEvents on `window`.
3
- // DO NOT EDIT — re-run `make gen-clients`.
4
- //
5
- // Usage:
6
- // import "./generated/events" // side-effect: registers interceptors
7
- //
8
- // window.addEventListener("cmdop:network-error", (e) => {
9
- // console.log(e.detail.status, e.detail.method, e.detail.url);
10
- // });
11
-
12
- import { client } from "../client.gen";
13
-
14
- // ─────────────────────────────────────────────────────────────────────────────
15
- // Event names + detail types
16
- // ─────────────────────────────────────────────────────────────────────────────
17
-
18
- export const NETWORK_ERROR_EVENT = "cmdop:network-error" as const;
19
- export const VALIDATION_ERROR_EVENT = "cmdop:validation-error" as const;
20
- export const RUNTIME_ERROR_EVENT = "cmdop:runtime-error" as const;
21
-
22
- export interface NetworkErrorDetail {
23
- /** HTTP method (GET/POST/...). */
24
- method: string;
25
- /** Request URL. */
26
- url: string;
27
- /** HTTP status if response arrived; null on transport failure. */
28
- status: number | null;
29
- /** Response body parsed as text (best-effort, may be empty). */
30
- body?: string;
31
- /** Underlying error message (network failures, abort, etc.). */
32
- error?: string;
33
- /** Heuristic: did the request likely fail due to CORS? */
34
- possiblyCors: boolean;
35
- timestamp: Date;
36
- }
37
-
38
- export interface ValidationErrorDetail {
39
- /** Operation ID this validation applies to (from the OpenAPI spec). */
40
- operation: string;
41
- /** Schema name that failed (if known). */
42
- schema?: string;
43
- /** Issues array — shape mirrors zod's ZodIssue but kept opaque here to
44
- * avoid forcing a hard zod dep on consumers that don't use schemas. */
45
- issues: unknown;
46
- /** Raw response data that failed validation. */
47
- data: unknown;
48
- timestamp: Date;
49
- }
50
-
51
- export interface RuntimeErrorDetail {
52
- /** Free-form source identifier — component name, hook name, etc. */
53
- source: string;
54
- message: string;
55
- /** Original Error object (may be undefined when caller lacks one). */
56
- error?: Error;
57
- /** Arbitrary context payload from the caller. */
58
- context?: Record<string, unknown>;
59
- timestamp: Date;
60
- }
61
-
62
- // Augment the global WindowEventMap so addEventListener gets typed details.
63
- declare global {
64
- interface WindowEventMap {
65
- "cmdop:network-error": CustomEvent<NetworkErrorDetail>;
66
- "cmdop:validation-error": CustomEvent<ValidationErrorDetail>;
67
- "cmdop:runtime-error": CustomEvent<RuntimeErrorDetail>;
68
- }
69
- }
70
-
71
- // ─────────────────────────────────────────────────────────────────────────────
72
- // Dispatch helpers
73
- // ─────────────────────────────────────────────────────────────────────────────
74
-
75
- function _dispatch<T>(name: string, detail: T): void {
76
- if (typeof window === "undefined") return;
77
- try {
78
- window.dispatchEvent(new CustomEvent(name, { detail, bubbles: true }));
79
- } catch {
80
- // Event dispatch must never crash the app.
81
- }
82
- }
83
-
84
- /** Emit a runtime error from anywhere in app code. */
85
- export function emitRuntimeError(
86
- source: string,
87
- message: string,
88
- error?: Error,
89
- context?: Record<string, unknown>,
90
- ): void {
91
- _dispatch<RuntimeErrorDetail>(RUNTIME_ERROR_EVENT, {
92
- source,
93
- message,
94
- error,
95
- context,
96
- timestamp: new Date(),
97
- });
98
- }
99
-
100
- /** Emit a zod validation failure. Called by hooks when `validate` is provided. */
101
- export function dispatchValidationError(
102
- operation: string,
103
- data: unknown,
104
- issues: unknown,
105
- schema?: string,
106
- ): void {
107
- _dispatch<ValidationErrorDetail>(VALIDATION_ERROR_EVENT, {
108
- operation,
109
- schema,
110
- issues,
111
- data,
112
- timestamp: new Date(),
113
- });
114
- }
115
-
116
- // ─────────────────────────────────────────────────────────────────────────────
117
- // Listen helpers — typed wrappers over addEventListener, return cleanup fn.
118
- // ─────────────────────────────────────────────────────────────────────────────
119
-
120
- export function onNetworkError(
121
- cb: (detail: NetworkErrorDetail) => void,
122
- ): () => void {
123
- if (typeof window === "undefined") return () => {};
124
- const handler = (e: CustomEvent<NetworkErrorDetail>) => cb(e.detail);
125
- window.addEventListener(NETWORK_ERROR_EVENT, handler);
126
- return () => window.removeEventListener(NETWORK_ERROR_EVENT, handler);
127
- }
128
-
129
- export function onValidationError(
130
- cb: (detail: ValidationErrorDetail) => void,
131
- ): () => void {
132
- if (typeof window === "undefined") return () => {};
133
- const handler = (e: CustomEvent<ValidationErrorDetail>) => cb(e.detail);
134
- window.addEventListener(VALIDATION_ERROR_EVENT, handler);
135
- return () => window.removeEventListener(VALIDATION_ERROR_EVENT, handler);
136
- }
137
-
138
- export function onRuntimeError(
139
- cb: (detail: RuntimeErrorDetail) => void,
140
- ): () => void {
141
- if (typeof window === "undefined") return () => {};
142
- const handler = (e: CustomEvent<RuntimeErrorDetail>) => cb(e.detail);
143
- window.addEventListener(RUNTIME_ERROR_EVENT, handler);
144
- return () => window.removeEventListener(RUNTIME_ERROR_EVENT, handler);
145
- }
146
-
147
- // ─────────────────────────────────────────────────────────────────────────────
148
- // Hey API interceptors — wire once, emit on response/error.
149
- // ─────────────────────────────────────────────────────────────────────────────
150
-
151
- let _wired = false;
152
-
153
- function _wire(): void {
154
- if (_wired) return;
155
- _wired = true;
156
-
157
- // response interceptor: every response, OK or not, lets us see status.
158
- // We only emit for non-2xx — successful requests are silent.
159
- client.interceptors.response.use(async (response, request) => {
160
- if (!response.ok) {
161
- let body: string | undefined;
162
- try {
163
- body = await response.clone().text();
164
- if (body.length > 4000) body = body.slice(0, 4000) + "…";
165
- } catch {
166
- // ignore — body inaccessible
167
- }
168
- _dispatch<NetworkErrorDetail>(NETWORK_ERROR_EVENT, {
169
- method: request.method,
170
- url: request.url,
171
- status: response.status,
172
- body,
173
- possiblyCors: false,
174
- timestamp: new Date(),
175
- });
176
- }
177
- return response;
178
- });
179
-
180
- // error interceptor: transport-level failures (abort, DNS, CORS, offline).
181
- client.interceptors.error.use((error, _response, request) => {
182
- const msg = error instanceof Error ? error.message : String(error);
183
- // Heuristic: CORS-blocked fetches usually surface as "Failed to fetch"
184
- // without a status. Not perfect, but catches the common case.
185
- const possiblyCors = /failed to fetch|network error|cors/i.test(msg);
186
- _dispatch<NetworkErrorDetail>(NETWORK_ERROR_EVENT, {
187
- method: request?.method ?? "UNKNOWN",
188
- url: request?.url ?? "",
189
- status: null,
190
- error: msg,
191
- possiblyCors,
192
- timestamp: new Date(),
193
- });
194
- return error;
195
- });
196
- }
197
-
198
- _wire();