@doswiftly/storefront-sdk 4.3.0 → 4.5.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.
Files changed (96) hide show
  1. package/README.md +6 -14
  2. package/dist/core/cart/types.d.ts +53 -20
  3. package/dist/core/cart/types.d.ts.map +1 -1
  4. package/dist/core/cart/types.js +3 -0
  5. package/dist/core/image.d.ts +4 -46
  6. package/dist/core/image.d.ts.map +1 -1
  7. package/dist/core/image.js +4 -65
  8. package/dist/core/index.d.ts +1 -1
  9. package/dist/core/index.d.ts.map +1 -1
  10. package/dist/core/index.js +0 -2
  11. package/dist/core/operations/cart.d.ts +15 -9
  12. package/dist/core/operations/cart.d.ts.map +1 -1
  13. package/dist/core/operations/cart.js +130 -58
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.js +1 -1
  16. package/package.json +9 -4
  17. package/src/__tests__/contract/storefront-api.contract.test.ts +0 -450
  18. package/src/__tests__/unit/auth-client.test.ts +0 -210
  19. package/src/__tests__/unit/bot-protection.test.ts +0 -461
  20. package/src/__tests__/unit/cart-client.test.ts +0 -233
  21. package/src/__tests__/unit/cart-store.test.ts +0 -349
  22. package/src/__tests__/unit/create-client.test.ts +0 -356
  23. package/src/__tests__/unit/helpers.test.ts +0 -377
  24. package/src/__tests__/unit/middleware.test.ts +0 -374
  25. package/src/__tests__/unit/test-helpers.ts +0 -103
  26. package/src/core/auth/auth-client.ts +0 -123
  27. package/src/core/auth/cookie-config.ts +0 -23
  28. package/src/core/auth/handlers.ts +0 -168
  29. package/src/core/auth/routes.ts +0 -26
  30. package/src/core/auth/token-client.ts +0 -51
  31. package/src/core/auth/types.ts +0 -54
  32. package/src/core/bot-protection/abstract-manager.ts +0 -185
  33. package/src/core/bot-protection/create-manager.ts +0 -37
  34. package/src/core/bot-protection/eucaptcha-manager.ts +0 -88
  35. package/src/core/bot-protection/fallback-manager.ts +0 -43
  36. package/src/core/bot-protection/turnstile-manager.ts +0 -92
  37. package/src/core/bot-protection/types/eucaptcha.d.ts +0 -28
  38. package/src/core/bot-protection/types/turnstile.d.ts +0 -33
  39. package/src/core/cache.ts +0 -102
  40. package/src/core/cart/cart-client.ts +0 -150
  41. package/src/core/cart/cookie-config.ts +0 -13
  42. package/src/core/cart/types.ts +0 -104
  43. package/src/core/client/compose.ts +0 -15
  44. package/src/core/client/create-client.ts +0 -129
  45. package/src/core/client/dedupe.ts +0 -19
  46. package/src/core/client/execute.ts +0 -70
  47. package/src/core/client/hash.ts +0 -21
  48. package/src/core/client/operation-name.ts +0 -12
  49. package/src/core/client/types.ts +0 -171
  50. package/src/core/currency/cookie-config.ts +0 -13
  51. package/src/core/errors.ts +0 -67
  52. package/src/core/format.ts +0 -254
  53. package/src/core/helpers/assert-no-user-errors.ts +0 -21
  54. package/src/core/helpers/normalize-connection.ts +0 -48
  55. package/src/core/helpers/sanitize-html.ts +0 -42
  56. package/src/core/image.ts +0 -103
  57. package/src/core/index.ts +0 -180
  58. package/src/core/language/cookie-config.ts +0 -13
  59. package/src/core/middleware/auth.ts +0 -27
  60. package/src/core/middleware/bot-protection.ts +0 -140
  61. package/src/core/middleware/currency.ts +0 -27
  62. package/src/core/middleware/errors.ts +0 -86
  63. package/src/core/middleware/language.ts +0 -30
  64. package/src/core/middleware/retry.ts +0 -75
  65. package/src/core/middleware/timeout.ts +0 -61
  66. package/src/core/operations/auth.ts +0 -123
  67. package/src/core/operations/cart.ts +0 -185
  68. package/src/index.ts +0 -25
  69. package/src/react/bot-protection/bot-protection-context.ts +0 -17
  70. package/src/react/bot-protection/bot-protection-widget.tsx +0 -46
  71. package/src/react/cookies.ts +0 -89
  72. package/src/react/helpers/create-store-context.ts +0 -56
  73. package/src/react/hooks/use-auth.ts +0 -218
  74. package/src/react/hooks/use-bot-protection.ts +0 -31
  75. package/src/react/hooks/use-cart-manager.ts +0 -236
  76. package/src/react/hooks/use-currency.ts +0 -23
  77. package/src/react/hooks/use-debounced-value.ts +0 -30
  78. package/src/react/hooks/use-hydrated.ts +0 -20
  79. package/src/react/hooks/use-storefront-client.ts +0 -12
  80. package/src/react/index.ts +0 -71
  81. package/src/react/providers/currency-provider.tsx +0 -30
  82. package/src/react/providers/language-provider.tsx +0 -34
  83. package/src/react/providers/storefront-client-provider.tsx +0 -107
  84. package/src/react/providers/storefront-provider.tsx +0 -99
  85. package/src/react/server/get-storefront-client.ts +0 -60
  86. package/src/react/server/index.ts +0 -1
  87. package/src/react/stores/auth.store.ts +0 -112
  88. package/src/react/stores/cart.context.ts +0 -10
  89. package/src/react/stores/cart.store.ts +0 -254
  90. package/src/react/stores/currency.store.ts +0 -93
  91. package/src/react/stores/index.ts +0 -17
  92. package/src/react/stores/language.store.ts +0 -90
  93. package/src/react/stores/store-context.tsx +0 -103
  94. package/src/react/types/shop-config.ts +0 -22
  95. package/tsconfig.json +0 -20
  96. package/vitest.config.ts +0 -14
@@ -1,168 +0,0 @@
1
- /**
2
- * Auth cookie handlers — factory functions for API routes.
3
- *
4
- * 0 deps, pure Web API (Request/Response). Framework-agnostic.
5
- * Creates set-token and clear-token handlers that any storefront
6
- * can use as 2-line API routes.
7
- *
8
- * @example
9
- * ```ts
10
- * // app/api/auth/set-token/route.ts
11
- * import { createSetTokenHandler } from '@doswiftly/storefront-sdk';
12
- * export const POST = createSetTokenHandler();
13
- * ```
14
- */
15
-
16
- import { AUTH_COOKIE_NAME, AUTH_COOKIE_DEFAULTS } from './cookie-config';
17
-
18
- interface SetTokenHandlerOptions {
19
- maxAge?: number;
20
- }
21
-
22
- function serializeCookie(
23
- name: string,
24
- value: string,
25
- options: {
26
- maxAge?: number;
27
- path?: string;
28
- sameSite?: string;
29
- secure?: boolean;
30
- httpOnly?: boolean;
31
- },
32
- ): string {
33
- const parts = [`${name}=${value}`];
34
- if (options.maxAge != null) parts.push(`Max-Age=${options.maxAge}`);
35
- if (options.path) parts.push(`Path=${options.path}`);
36
- if (options.sameSite) parts.push(`SameSite=${options.sameSite}`);
37
- if (options.secure) parts.push('Secure');
38
- if (options.httpOnly) parts.push('HttpOnly');
39
- return parts.join('; ');
40
- }
41
-
42
- function validateOrigin(request: Request): Response | null {
43
- const origin = request.headers.get('origin');
44
- const host = request.headers.get('host');
45
-
46
- if (origin && !origin.includes(host || '')) {
47
- return new Response(JSON.stringify({ error: 'Invalid origin' }), {
48
- status: 403,
49
- headers: { 'Content-Type': 'application/json' },
50
- });
51
- }
52
- return null;
53
- }
54
-
55
- /**
56
- * Create a POST handler that sets the auth token in an httpOnly cookie.
57
- *
58
- * Security: origin validation, Content-Type check, CSRF (SameSite=Lax),
59
- * httpOnly (XSS protection), Secure in production.
60
- */
61
- export function createSetTokenHandler(
62
- overrides?: SetTokenHandlerOptions,
63
- ): (request: Request) => Promise<Response> {
64
- const maxAge = overrides?.maxAge ?? AUTH_COOKIE_DEFAULTS.maxAge;
65
-
66
- return async (request: Request): Promise<Response> => {
67
- try {
68
- // 1. CSRF: validate origin
69
- const originError = validateOrigin(request);
70
- if (originError) return originError;
71
-
72
- // 2. Validate Content-Type
73
- const contentType = request.headers.get('content-type');
74
- if (!contentType?.includes('application/json')) {
75
- return new Response(
76
- JSON.stringify({ error: 'Content-Type must be application/json' }),
77
- { status: 400, headers: { 'Content-Type': 'application/json' } },
78
- );
79
- }
80
-
81
- // 3. Parse body
82
- let body: { token?: string };
83
- try {
84
- body = await request.json();
85
- } catch {
86
- return new Response(
87
- JSON.stringify({ error: 'Invalid JSON body' }),
88
- { status: 400, headers: { 'Content-Type': 'application/json' } },
89
- );
90
- }
91
-
92
- const { token } = body;
93
- if (!token || typeof token !== 'string' || token.trim() === '') {
94
- return new Response(
95
- JSON.stringify({ error: 'Token is required and must be a non-empty string' }),
96
- { status: 400, headers: { 'Content-Type': 'application/json' } },
97
- );
98
- }
99
-
100
- // 4. Set httpOnly cookie
101
- const cookie = serializeCookie(AUTH_COOKIE_NAME, token, {
102
- maxAge,
103
- path: AUTH_COOKIE_DEFAULTS.path,
104
- sameSite: AUTH_COOKIE_DEFAULTS.sameSite,
105
- secure: AUTH_COOKIE_DEFAULTS.secure,
106
- httpOnly: AUTH_COOKIE_DEFAULTS.httpOnly,
107
- });
108
-
109
- return new Response(
110
- JSON.stringify({ success: true, message: 'Token set successfully' }),
111
- {
112
- status: 200,
113
- headers: {
114
- 'Content-Type': 'application/json',
115
- 'Set-Cookie': cookie,
116
- },
117
- },
118
- );
119
- } catch (error) {
120
- console.error('Error setting auth token:', error);
121
- return new Response(
122
- JSON.stringify({ error: 'Internal server error' }),
123
- { status: 500, headers: { 'Content-Type': 'application/json' } },
124
- );
125
- }
126
- };
127
- }
128
-
129
- /**
130
- * Create a POST handler that clears the auth token cookie.
131
- *
132
- * Security: origin validation, immediate expiration (maxAge=0).
133
- */
134
- export function createClearTokenHandler(): (request: Request) => Promise<Response> {
135
- return async (request: Request): Promise<Response> => {
136
- try {
137
- // 1. CSRF: validate origin
138
- const originError = validateOrigin(request);
139
- if (originError) return originError;
140
-
141
- // 2. Clear cookie (maxAge=0)
142
- const cookie = serializeCookie(AUTH_COOKIE_NAME, '', {
143
- maxAge: 0,
144
- path: AUTH_COOKIE_DEFAULTS.path,
145
- sameSite: AUTH_COOKIE_DEFAULTS.sameSite,
146
- secure: AUTH_COOKIE_DEFAULTS.secure,
147
- httpOnly: AUTH_COOKIE_DEFAULTS.httpOnly,
148
- });
149
-
150
- return new Response(
151
- JSON.stringify({ success: true, message: 'Token cleared successfully' }),
152
- {
153
- status: 200,
154
- headers: {
155
- 'Content-Type': 'application/json',
156
- 'Set-Cookie': cookie,
157
- },
158
- },
159
- );
160
- } catch (error) {
161
- console.error('Error clearing auth token:', error);
162
- return new Response(
163
- JSON.stringify({ error: 'Internal server error' }),
164
- { status: 500, headers: { 'Content-Type': 'application/json' } },
165
- );
166
- }
167
- };
168
- }
@@ -1,26 +0,0 @@
1
- /**
2
- * Route matching utility for auth redirects.
3
- *
4
- * Pure function — route lists stay in the template (SSOT config),
5
- * but matching logic lives in the SDK (reusable across templates).
6
- */
7
-
8
- export interface RouteProtectionConfig {
9
- protectedRoutes: string[];
10
- guestOnlyRoutes: string[];
11
- redirects: {
12
- unauthenticated: string;
13
- authenticated: string;
14
- };
15
- }
16
-
17
- /**
18
- * Check if a pathname matches any route in the list.
19
- * Supports both exact matches and prefix matches
20
- * (e.g., "/account" matches "/account/orders").
21
- */
22
- export function matchesRoute(pathname: string, routes: string[]): boolean {
23
- return routes.some(
24
- (route) => pathname === route || pathname.startsWith(`${route}/`),
25
- );
26
- }
@@ -1,51 +0,0 @@
1
- /**
2
- * Client-side auth token helpers — fetch-based, 0 deps.
3
- *
4
- * Calls API routes to set/clear the httpOnly auth cookie.
5
- * Used by template hooks (use-auth.ts, use-auth-sync.ts, register-form.tsx).
6
- *
7
- * @example
8
- * ```ts
9
- * import { createAuthTokenClient } from '@doswiftly/storefront-sdk';
10
- * const { setToken, clearToken } = createAuthTokenClient();
11
- *
12
- * await setToken(accessToken);
13
- * await clearToken();
14
- * ```
15
- */
16
-
17
- export interface AuthTokenClient {
18
- setToken: (token: string) => Promise<void>;
19
- clearToken: () => Promise<void>;
20
- }
21
-
22
- /**
23
- * Create a client for managing auth tokens via API routes.
24
- *
25
- * @param basePath - Base path for API routes (default: "/api/auth")
26
- */
27
- export function createAuthTokenClient(basePath = '/api/auth'): AuthTokenClient {
28
- return {
29
- async setToken(token: string): Promise<void> {
30
- const response = await fetch(`${basePath}/set-token`, {
31
- method: 'POST',
32
- headers: { 'Content-Type': 'application/json' },
33
- body: JSON.stringify({ token }),
34
- });
35
-
36
- if (!response.ok) {
37
- throw new Error('Failed to set authentication token');
38
- }
39
- },
40
-
41
- async clearToken(): Promise<void> {
42
- const response = await fetch(`${basePath}/clear-token`, {
43
- method: 'POST',
44
- });
45
-
46
- if (!response.ok) {
47
- throw new Error('Failed to clear authentication token');
48
- }
49
- },
50
- };
51
- }
@@ -1,54 +0,0 @@
1
- /**
2
- * Auth types — manual (no codegen).
3
- */
4
-
5
- export interface CustomerAccessToken {
6
- accessToken: string;
7
- expiresAt: string;
8
- }
9
-
10
- export interface Customer {
11
- id: string;
12
- email: string;
13
- firstName: string | null;
14
- lastName: string | null;
15
- displayName: string | null;
16
- phone: string | null;
17
- emailVerified: boolean;
18
- emailMarketingState: string | null;
19
- defaultAddress: MailingAddress | null;
20
- ordersCount: number;
21
- totalSpent: { amount: string; currencyCode: string } | null;
22
- createdAt: string;
23
- updatedAt: string;
24
- }
25
-
26
- export interface MailingAddress {
27
- id: string;
28
- address1: string | null;
29
- address2: string | null;
30
- city: string | null;
31
- company: string | null;
32
- country: string | null;
33
- countryCode: string | null;
34
- firstName: string | null;
35
- lastName: string | null;
36
- phone: string | null;
37
- province: string | null;
38
- provinceCode: string | null;
39
- zip: string | null;
40
- isDefault: boolean;
41
- }
42
-
43
- export interface AuthResult {
44
- accessToken: string;
45
- expiresAt: string;
46
- customer?: Customer;
47
- }
48
-
49
- export interface CustomerCreateInput {
50
- email: string;
51
- password: string;
52
- firstName?: string;
53
- lastName?: string;
54
- }
@@ -1,185 +0,0 @@
1
- /**
2
- * AbstractBotProtectionManager — base class for all bot protection providers.
3
- *
4
- * DRY: singleton script loading, lazy mount, in-flight deduplication, timeout.
5
- * Subclasses override only: _loadScript(), _renderWidget(), _executeChallenge(), _destroyWidget().
6
- *
7
- * Framework-agnostic (DOM APIs only) — works in React, Vue, Svelte, vanilla JS.
8
- */
9
-
10
- import type { BotProtectionTokenProvider } from '../middleware/bot-protection';
11
-
12
- export abstract class AbstractBotProtectionManager implements BotProtectionTokenProvider {
13
- protected readonly siteKey: string;
14
- protected readonly scriptUrl: string;
15
-
16
- /** Singleton script loading promise — one load per provider globally */
17
- private scriptPromise: Promise<void> | null = null;
18
- /** In-flight deduplication — prevents double-submit */
19
- private inFlight: Promise<string | null> | null = null;
20
- /** Whether the widget has been mounted */
21
- protected widgetId: string | null = null;
22
- /** Container element for the widget */
23
- protected container: HTMLElement | null = null;
24
-
25
- constructor(siteKey: string, scriptUrl: string) {
26
- this.siteKey = siteKey;
27
- this.scriptUrl = scriptUrl;
28
- }
29
-
30
- /**
31
- * Load the provider's script tag. Singleton — only loads once.
32
- * Exposed publicly so React wrapper can trigger preload.
33
- */
34
- loadScript(): Promise<void> {
35
- if (this.scriptPromise) return this.scriptPromise;
36
-
37
- this.scriptPromise = new Promise<void>((resolve, reject) => {
38
- // Skip in SSR
39
- if (typeof document === 'undefined') {
40
- resolve();
41
- return;
42
- }
43
-
44
- // Check if script already exists
45
- const existing = document.querySelector(`script[src="${this.scriptUrl}"]`);
46
- if (existing) {
47
- // Script tag exists — wait for API to be available
48
- this._waitForApi().then(resolve).catch(reject);
49
- return;
50
- }
51
-
52
- const script = document.createElement('script');
53
- script.src = this.scriptUrl;
54
- script.async = true;
55
- script.defer = true;
56
-
57
- // Set up onload callback if provider supports it
58
- this._setupOnloadCallback(resolve);
59
-
60
- script.onload = () => {
61
- this._waitForApi().then(resolve).catch(reject);
62
- };
63
-
64
- script.onerror = () => {
65
- this.scriptPromise = null; // Allow retry
66
- reject(new Error(`Failed to load bot protection script: ${this.scriptUrl}`));
67
- };
68
-
69
- document.head.appendChild(script);
70
- });
71
-
72
- return this.scriptPromise;
73
- }
74
-
75
- /**
76
- * Mount the invisible widget into a container element.
77
- * Lazy — called automatically on first execute() if not mounted.
78
- */
79
- mount(container: HTMLElement): void {
80
- this.container = container;
81
- }
82
-
83
- /**
84
- * Execute challenge and get a fresh token.
85
- * In-flight dedup + timeout + lazy mount + lazy script load.
86
- */
87
- async execute(options?: { action?: string; timeoutMs?: number }): Promise<string | null> {
88
- // In-flight deduplication — return existing promise if one is running
89
- if (this.inFlight) return this.inFlight;
90
-
91
- const timeoutMs = options?.timeoutMs ?? 10000;
92
-
93
- this.inFlight = this._doExecute(options?.action, timeoutMs).finally(() => {
94
- this.inFlight = null;
95
- });
96
-
97
- return this.inFlight;
98
- }
99
-
100
- /**
101
- * Cleanup widget and resources.
102
- */
103
- destroy(): void {
104
- try {
105
- if (this.widgetId) {
106
- this._destroyWidget(this.widgetId);
107
- }
108
- } catch {
109
- // Ignore cleanup errors
110
- }
111
- this.widgetId = null;
112
- this.container = null;
113
- }
114
-
115
- // ---------------------------------------------------------------------------
116
- // Private execution pipeline
117
- // ---------------------------------------------------------------------------
118
-
119
- private async _doExecute(action: string | undefined, timeoutMs: number): Promise<string | null> {
120
- try {
121
- // 1. Lazy script load
122
- await this.loadScript();
123
-
124
- // 2. Lazy mount — create container if needed
125
- if (!this.widgetId) {
126
- await this._ensureMounted(action);
127
- }
128
-
129
- if (!this.widgetId) {
130
- return null; // Mount failed — fail-open
131
- }
132
-
133
- // 3. Execute challenge with timeout
134
- const tokenPromise = this._executeChallenge(this.widgetId, action);
135
-
136
- const timeoutPromise = new Promise<null>((resolve) => {
137
- setTimeout(() => resolve(null), timeoutMs);
138
- });
139
-
140
- return await Promise.race([tokenPromise, timeoutPromise]);
141
- } catch {
142
- // Any error → return null (fail-open at middleware level)
143
- return null;
144
- }
145
- }
146
-
147
- private async _ensureMounted(action?: string): Promise<void> {
148
- // Create an invisible container if none provided
149
- if (!this.container && typeof document !== 'undefined') {
150
- this.container = document.createElement('div');
151
- this.container.style.display = 'none';
152
- this.container.setAttribute('data-bot-protection', 'true');
153
- document.body.appendChild(this.container);
154
- }
155
-
156
- if (!this.container) return;
157
-
158
- try {
159
- this.widgetId = this._renderWidget(this.container, action);
160
- } catch {
161
- this.widgetId = null;
162
- }
163
- }
164
-
165
- // ---------------------------------------------------------------------------
166
- // Abstract methods — subclass responsibility
167
- // ---------------------------------------------------------------------------
168
-
169
- /** Wait for the provider API to be available on window */
170
- protected abstract _waitForApi(): Promise<void>;
171
-
172
- /** Optional: setup global onload callback for the script */
173
- protected _setupOnloadCallback(_resolve: () => void): void {
174
- // Default: no-op. Override if provider uses a callback pattern.
175
- }
176
-
177
- /** Render the invisible widget. Return widget ID. */
178
- protected abstract _renderWidget(container: HTMLElement, action?: string): string;
179
-
180
- /** Execute the challenge and return a token. Called after mount. */
181
- protected abstract _executeChallenge(widgetId: string, action?: string): Promise<string | null>;
182
-
183
- /** Destroy/remove the widget. */
184
- protected abstract _destroyWidget(widgetId: string): void;
185
- }
@@ -1,37 +0,0 @@
1
- /**
2
- * Bot protection manager factory.
3
- *
4
- * Creates the appropriate manager based on provider config from shop query.
5
- * Supports runtime fallback chain via FallbackBotProtectionManager.
6
- */
7
-
8
- import type { BotProtectionTokenProvider, BotProtectionConfig, BotProtectionProviderConfig } from '../middleware/bot-protection';
9
- import { EuCaptchaManager } from './eucaptcha-manager';
10
- import { TurnstileManager } from './turnstile-manager';
11
- import { FallbackBotProtectionManager } from './fallback-manager';
12
-
13
- /**
14
- * Create a single provider manager instance.
15
- */
16
- function createSingleManager(config: BotProtectionProviderConfig): BotProtectionTokenProvider {
17
- switch (config.provider) {
18
- case 'eucaptcha':
19
- return new EuCaptchaManager(config.siteKey, config.scriptUrl);
20
- case 'turnstile':
21
- return new TurnstileManager(config.siteKey, config.scriptUrl);
22
- default:
23
- throw new Error(`Unknown bot protection provider: ${config.provider}`);
24
- }
25
- }
26
-
27
- /**
28
- * Create a bot protection manager with optional fallback chain.
29
- *
30
- * @param config - Bot protection configuration from shop query
31
- * @returns BotProtectionTokenProvider (FallbackBotProtectionManager if fallback configured)
32
- */
33
- export function createBotProtectionManager(config: BotProtectionConfig): BotProtectionTokenProvider {
34
- const primary = createSingleManager(config.primary);
35
- const fallback = config.fallback ? createSingleManager(config.fallback) : null;
36
- return new FallbackBotProtectionManager(primary, fallback);
37
- }
@@ -1,88 +0,0 @@
1
- /**
2
- * EuCaptchaManager — EU CAPTCHA (Myra Security) bot protection provider.
3
- *
4
- * Default provider — GDPR by design, EU-hosted (Germany), privacy-first.
5
- * Implements BotProtectionTokenProvider via AbstractBotProtectionManager.
6
- * Invisible behavioral analysis, zero user interaction.
7
- */
8
-
9
- /// <reference path="./types/eucaptcha.d.ts" />
10
- import { AbstractBotProtectionManager } from './abstract-manager';
11
-
12
- export class EuCaptchaManager extends AbstractBotProtectionManager {
13
- protected _waitForApi(): Promise<void> {
14
- if (typeof window !== 'undefined' && window.eucaptcha) {
15
- return Promise.resolve();
16
- }
17
-
18
- return new Promise<void>((resolve) => {
19
- const check = setInterval(() => {
20
- if (typeof window !== 'undefined' && window.eucaptcha) {
21
- clearInterval(check);
22
- resolve();
23
- }
24
- }, 50);
25
-
26
- // Safety timeout
27
- setTimeout(() => {
28
- clearInterval(check);
29
- resolve();
30
- }, 15000);
31
- });
32
- }
33
-
34
- protected _renderWidget(container: HTMLElement, action?: string): string {
35
- if (!window.eucaptcha) {
36
- throw new Error('EU CAPTCHA API not loaded');
37
- }
38
-
39
- return window.eucaptcha.render(container, {
40
- sitekey: this.siteKey,
41
- size: 'invisible',
42
- action,
43
- });
44
- }
45
-
46
- protected _executeChallenge(widgetId: string, action?: string): Promise<string | null> {
47
- return new Promise<string | null>((resolve) => {
48
- if (!window.eucaptcha) {
49
- resolve(null);
50
- return;
51
- }
52
-
53
- // Reset for fresh token
54
- window.eucaptcha.reset(widgetId);
55
-
56
- // Remove old widget and re-render with callback
57
- if (!this.container) {
58
- resolve(null);
59
- return;
60
- }
61
-
62
- // Re-render with callback to capture token
63
- this.widgetId = window.eucaptcha.render(this.container, {
64
- sitekey: this.siteKey,
65
- size: 'invisible',
66
- action,
67
- callback: (token: string) => {
68
- resolve(token);
69
- },
70
- 'error-callback': () => {
71
- resolve(null);
72
- },
73
- 'expired-callback': () => {
74
- resolve(null);
75
- },
76
- });
77
-
78
- // Trigger execution
79
- window.eucaptcha.execute(this.widgetId);
80
- });
81
- }
82
-
83
- protected _destroyWidget(widgetId: string): void {
84
- if (window.eucaptcha) {
85
- window.eucaptcha.reset(widgetId);
86
- }
87
- }
88
- }
@@ -1,43 +0,0 @@
1
- /**
2
- * FallbackBotProtectionManager — runtime fallback chain.
3
- *
4
- * Tries primary provider, falls back to secondary, ultimately returns null (fail-open).
5
- * RULE: NEVER block the customer — fail-open is the ultimate fallback.
6
- */
7
-
8
- import type { BotProtectionTokenProvider } from '../middleware/bot-protection';
9
-
10
- export class FallbackBotProtectionManager implements BotProtectionTokenProvider {
11
- constructor(
12
- private readonly primary: BotProtectionTokenProvider,
13
- private readonly fallback: BotProtectionTokenProvider | null,
14
- ) {}
15
-
16
- async execute(options?: { action?: string; timeoutMs?: number }): Promise<string | null> {
17
- // 1. Try primary
18
- try {
19
- const token = await this.primary.execute(options);
20
- if (token) return token;
21
- } catch {
22
- // Primary failed — try fallback
23
- }
24
-
25
- // 2. Try fallback
26
- if (this.fallback) {
27
- try {
28
- const token = await this.fallback.execute(options);
29
- if (token) return token;
30
- } catch {
31
- // Fallback also failed
32
- }
33
- }
34
-
35
- // 3. Ultimate fail-open: return null -> middleware decides per-operation strategy
36
- return null;
37
- }
38
-
39
- destroy(): void {
40
- this.primary.destroy();
41
- this.fallback?.destroy();
42
- }
43
- }