@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.
- package/README.md +6 -14
- package/dist/core/cart/types.d.ts +53 -20
- package/dist/core/cart/types.d.ts.map +1 -1
- package/dist/core/cart/types.js +3 -0
- package/dist/core/image.d.ts +4 -46
- package/dist/core/image.d.ts.map +1 -1
- package/dist/core/image.js +4 -65
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +0 -2
- package/dist/core/operations/cart.d.ts +15 -9
- package/dist/core/operations/cart.d.ts.map +1 -1
- package/dist/core/operations/cart.js +130 -58
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +9 -4
- package/src/__tests__/contract/storefront-api.contract.test.ts +0 -450
- package/src/__tests__/unit/auth-client.test.ts +0 -210
- package/src/__tests__/unit/bot-protection.test.ts +0 -461
- package/src/__tests__/unit/cart-client.test.ts +0 -233
- package/src/__tests__/unit/cart-store.test.ts +0 -349
- package/src/__tests__/unit/create-client.test.ts +0 -356
- package/src/__tests__/unit/helpers.test.ts +0 -377
- package/src/__tests__/unit/middleware.test.ts +0 -374
- package/src/__tests__/unit/test-helpers.ts +0 -103
- package/src/core/auth/auth-client.ts +0 -123
- package/src/core/auth/cookie-config.ts +0 -23
- package/src/core/auth/handlers.ts +0 -168
- package/src/core/auth/routes.ts +0 -26
- package/src/core/auth/token-client.ts +0 -51
- package/src/core/auth/types.ts +0 -54
- package/src/core/bot-protection/abstract-manager.ts +0 -185
- package/src/core/bot-protection/create-manager.ts +0 -37
- package/src/core/bot-protection/eucaptcha-manager.ts +0 -88
- package/src/core/bot-protection/fallback-manager.ts +0 -43
- package/src/core/bot-protection/turnstile-manager.ts +0 -92
- package/src/core/bot-protection/types/eucaptcha.d.ts +0 -28
- package/src/core/bot-protection/types/turnstile.d.ts +0 -33
- package/src/core/cache.ts +0 -102
- package/src/core/cart/cart-client.ts +0 -150
- package/src/core/cart/cookie-config.ts +0 -13
- package/src/core/cart/types.ts +0 -104
- package/src/core/client/compose.ts +0 -15
- package/src/core/client/create-client.ts +0 -129
- package/src/core/client/dedupe.ts +0 -19
- package/src/core/client/execute.ts +0 -70
- package/src/core/client/hash.ts +0 -21
- package/src/core/client/operation-name.ts +0 -12
- package/src/core/client/types.ts +0 -171
- package/src/core/currency/cookie-config.ts +0 -13
- package/src/core/errors.ts +0 -67
- package/src/core/format.ts +0 -254
- package/src/core/helpers/assert-no-user-errors.ts +0 -21
- package/src/core/helpers/normalize-connection.ts +0 -48
- package/src/core/helpers/sanitize-html.ts +0 -42
- package/src/core/image.ts +0 -103
- package/src/core/index.ts +0 -180
- package/src/core/language/cookie-config.ts +0 -13
- package/src/core/middleware/auth.ts +0 -27
- package/src/core/middleware/bot-protection.ts +0 -140
- package/src/core/middleware/currency.ts +0 -27
- package/src/core/middleware/errors.ts +0 -86
- package/src/core/middleware/language.ts +0 -30
- package/src/core/middleware/retry.ts +0 -75
- package/src/core/middleware/timeout.ts +0 -61
- package/src/core/operations/auth.ts +0 -123
- package/src/core/operations/cart.ts +0 -185
- package/src/index.ts +0 -25
- package/src/react/bot-protection/bot-protection-context.ts +0 -17
- package/src/react/bot-protection/bot-protection-widget.tsx +0 -46
- package/src/react/cookies.ts +0 -89
- package/src/react/helpers/create-store-context.ts +0 -56
- package/src/react/hooks/use-auth.ts +0 -218
- package/src/react/hooks/use-bot-protection.ts +0 -31
- package/src/react/hooks/use-cart-manager.ts +0 -236
- package/src/react/hooks/use-currency.ts +0 -23
- package/src/react/hooks/use-debounced-value.ts +0 -30
- package/src/react/hooks/use-hydrated.ts +0 -20
- package/src/react/hooks/use-storefront-client.ts +0 -12
- package/src/react/index.ts +0 -71
- package/src/react/providers/currency-provider.tsx +0 -30
- package/src/react/providers/language-provider.tsx +0 -34
- package/src/react/providers/storefront-client-provider.tsx +0 -107
- package/src/react/providers/storefront-provider.tsx +0 -99
- package/src/react/server/get-storefront-client.ts +0 -60
- package/src/react/server/index.ts +0 -1
- package/src/react/stores/auth.store.ts +0 -112
- package/src/react/stores/cart.context.ts +0 -10
- package/src/react/stores/cart.store.ts +0 -254
- package/src/react/stores/currency.store.ts +0 -93
- package/src/react/stores/index.ts +0 -17
- package/src/react/stores/language.store.ts +0 -90
- package/src/react/stores/store-context.tsx +0 -103
- package/src/react/types/shop-config.ts +0 -22
- package/tsconfig.json +0 -20
- 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
|
-
}
|
package/src/core/auth/routes.ts
DELETED
|
@@ -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
|
-
}
|
package/src/core/auth/types.ts
DELETED
|
@@ -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
|
-
}
|