@doswiftly/storefront-sdk 4.0.0 → 4.1.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 +33 -6
- package/dist/core/bot-protection/abstract-manager.d.ts +57 -0
- package/dist/core/bot-protection/abstract-manager.d.ts.map +1 -0
- package/dist/core/bot-protection/abstract-manager.js +144 -0
- package/dist/core/bot-protection/create-manager.d.ts +15 -0
- package/dist/core/bot-protection/create-manager.d.ts.map +1 -0
- package/dist/core/bot-protection/create-manager.js +33 -0
- package/dist/core/bot-protection/eucaptcha-manager.d.ts +15 -0
- package/dist/core/bot-protection/eucaptcha-manager.d.ts.map +1 -0
- package/dist/core/bot-protection/eucaptcha-manager.js +76 -0
- package/dist/core/bot-protection/fallback-manager.d.ts +18 -0
- package/dist/core/bot-protection/fallback-manager.d.ts.map +1 -0
- package/dist/core/bot-protection/fallback-manager.js +42 -0
- package/dist/core/bot-protection/turnstile-manager.d.ts +15 -0
- package/dist/core/bot-protection/turnstile-manager.d.ts.map +1 -0
- package/dist/core/bot-protection/turnstile-manager.js +78 -0
- package/dist/core/cart/cookie-config.d.ts +14 -0
- package/dist/core/cart/cookie-config.d.ts.map +1 -0
- package/dist/core/cart/cookie-config.js +13 -0
- package/dist/core/currency/cookie-config.d.ts +14 -0
- package/dist/core/currency/cookie-config.d.ts.map +1 -0
- package/dist/core/currency/cookie-config.js +13 -0
- package/dist/core/index.d.ts +7 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +11 -0
- package/dist/core/language/cookie-config.d.ts +14 -0
- package/dist/core/language/cookie-config.d.ts.map +1 -0
- package/dist/core/language/cookie-config.js +13 -0
- package/dist/core/middleware/bot-protection.d.ts +71 -0
- package/dist/core/middleware/bot-protection.d.ts.map +1 -0
- package/dist/core/middleware/bot-protection.js +63 -0
- package/dist/core/middleware/currency.d.ts.map +1 -1
- package/dist/core/middleware/currency.js +2 -1
- package/dist/core/middleware/language.d.ts +18 -0
- package/dist/core/middleware/language.d.ts.map +1 -0
- package/dist/core/middleware/language.js +25 -0
- package/dist/react/bot-protection/bot-protection-context.d.ts +12 -0
- package/dist/react/bot-protection/bot-protection-context.d.ts.map +1 -0
- package/dist/react/bot-protection/bot-protection-context.js +9 -0
- package/dist/react/bot-protection/bot-protection-widget.d.ts +13 -0
- package/dist/react/bot-protection/bot-protection-widget.d.ts.map +1 -0
- package/dist/react/bot-protection/bot-protection-widget.js +34 -0
- package/dist/react/cookies.d.ts +17 -0
- package/dist/react/cookies.d.ts.map +1 -1
- package/dist/react/cookies.js +36 -3
- package/dist/react/hooks/use-bot-protection.d.ts +16 -0
- package/dist/react/hooks/use-bot-protection.d.ts.map +1 -0
- package/dist/react/hooks/use-bot-protection.js +24 -0
- package/dist/react/index.d.ts +10 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +9 -1
- package/dist/react/providers/language-provider.d.ts +18 -0
- package/dist/react/providers/language-provider.d.ts.map +1 -0
- package/dist/react/providers/language-provider.js +24 -0
- package/dist/react/providers/storefront-client-provider.d.ts +7 -2
- package/dist/react/providers/storefront-client-provider.d.ts.map +1 -1
- package/dist/react/providers/storefront-client-provider.js +14 -3
- package/dist/react/providers/storefront-provider.d.ts +7 -1
- package/dist/react/providers/storefront-provider.d.ts.map +1 -1
- package/dist/react/providers/storefront-provider.js +11 -4
- package/dist/react/stores/cart.context.d.ts +12 -0
- package/dist/react/stores/cart.context.d.ts.map +1 -0
- package/dist/react/stores/cart.context.js +3 -0
- package/dist/react/stores/cart.store.d.ts +71 -0
- package/dist/react/stores/cart.store.d.ts.map +1 -0
- package/dist/react/stores/cart.store.js +166 -0
- package/dist/react/stores/currency.store.d.ts +6 -9
- package/dist/react/stores/currency.store.d.ts.map +1 -1
- package/dist/react/stores/currency.store.js +5 -22
- package/dist/react/stores/language.store.d.ts +33 -0
- package/dist/react/stores/language.store.d.ts.map +1 -0
- package/dist/react/stores/language.store.js +67 -0
- package/dist/react/stores/store-context.d.ts +5 -0
- package/dist/react/stores/store-context.d.ts.map +1 -1
- package/dist/react/stores/store-context.js +14 -0
- package/dist/react/types/shop-config.d.ts +19 -0
- package/dist/react/types/shop-config.d.ts.map +1 -0
- package/dist/react/types/shop-config.js +7 -0
- package/package.json +1 -1
- package/src/__tests__/unit/bot-protection.test.ts +461 -0
- package/src/__tests__/unit/cart-store.test.ts +349 -0
- package/src/core/bot-protection/abstract-manager.ts +185 -0
- package/src/core/bot-protection/create-manager.ts +37 -0
- package/src/core/bot-protection/eucaptcha-manager.ts +88 -0
- package/src/core/bot-protection/fallback-manager.ts +43 -0
- package/src/core/bot-protection/turnstile-manager.ts +92 -0
- package/src/core/bot-protection/types/eucaptcha.d.ts +28 -0
- package/src/core/bot-protection/types/turnstile.d.ts +33 -0
- package/src/core/cart/cookie-config.ts +13 -0
- package/src/core/currency/cookie-config.ts +13 -0
- package/src/core/index.ts +23 -0
- package/src/core/language/cookie-config.ts +13 -0
- package/src/core/middleware/bot-protection.ts +140 -0
- package/src/core/middleware/currency.ts +2 -1
- package/src/core/middleware/language.ts +30 -0
- package/src/react/bot-protection/bot-protection-context.ts +17 -0
- package/src/react/bot-protection/bot-protection-widget.tsx +46 -0
- package/src/react/cookies.ts +39 -4
- package/src/react/hooks/use-bot-protection.ts +31 -0
- package/src/react/index.ts +27 -1
- package/src/react/providers/language-provider.tsx +34 -0
- package/src/react/providers/storefront-client-provider.tsx +20 -3
- package/src/react/providers/storefront-provider.tsx +34 -6
- package/src/react/stores/cart.context.ts +10 -0
- package/src/react/stores/cart.store.ts +254 -0
- package/src/react/stores/currency.store.ts +12 -32
- package/src/react/stores/language.store.ts +90 -0
- package/src/react/stores/store-context.tsx +21 -0
- package/src/react/types/shop-config.ts +22 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TurnstileManager — Cloudflare Turnstile bot protection provider.
|
|
3
|
+
*
|
|
4
|
+
* Implements BotProtectionTokenProvider via AbstractBotProtectionManager.
|
|
5
|
+
* Invisible widget, execution-mode challenge (no user interaction).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/// <reference path="./types/turnstile.d.ts" />
|
|
9
|
+
import { AbstractBotProtectionManager } from './abstract-manager';
|
|
10
|
+
|
|
11
|
+
export class TurnstileManager extends AbstractBotProtectionManager {
|
|
12
|
+
private callbackName = `onloadTurnstileCallback_${Math.random().toString(36).slice(2)}`;
|
|
13
|
+
|
|
14
|
+
protected _waitForApi(): Promise<void> {
|
|
15
|
+
if (typeof window !== 'undefined' && window.turnstile) {
|
|
16
|
+
return Promise.resolve();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return new Promise<void>((resolve) => {
|
|
20
|
+
const check = setInterval(() => {
|
|
21
|
+
if (typeof window !== 'undefined' && window.turnstile) {
|
|
22
|
+
clearInterval(check);
|
|
23
|
+
resolve();
|
|
24
|
+
}
|
|
25
|
+
}, 50);
|
|
26
|
+
|
|
27
|
+
// Safety timeout — don't poll forever
|
|
28
|
+
setTimeout(() => {
|
|
29
|
+
clearInterval(check);
|
|
30
|
+
resolve(); // Resolve anyway — execute will fail gracefully
|
|
31
|
+
}, 15000);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
protected _renderWidget(container: HTMLElement, action?: string): string {
|
|
36
|
+
if (!window.turnstile) {
|
|
37
|
+
throw new Error('Turnstile API not loaded');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return window.turnstile.render(container, {
|
|
41
|
+
sitekey: this.siteKey,
|
|
42
|
+
size: 'invisible',
|
|
43
|
+
execution: 'execute',
|
|
44
|
+
action,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
protected _executeChallenge(widgetId: string, action?: string): Promise<string | null> {
|
|
49
|
+
return new Promise<string | null>((resolve) => {
|
|
50
|
+
if (!window.turnstile) {
|
|
51
|
+
resolve(null);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Reset to get a fresh token (tokens are single-use)
|
|
56
|
+
window.turnstile.reset(widgetId);
|
|
57
|
+
|
|
58
|
+
// Remove old widget and re-render with callback
|
|
59
|
+
window.turnstile.remove(widgetId);
|
|
60
|
+
|
|
61
|
+
if (!this.container) {
|
|
62
|
+
resolve(null);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.widgetId = window.turnstile.render(this.container, {
|
|
67
|
+
sitekey: this.siteKey,
|
|
68
|
+
size: 'invisible',
|
|
69
|
+
execution: 'execute',
|
|
70
|
+
action,
|
|
71
|
+
callback: (token: string) => {
|
|
72
|
+
resolve(token);
|
|
73
|
+
},
|
|
74
|
+
'error-callback': () => {
|
|
75
|
+
resolve(null);
|
|
76
|
+
},
|
|
77
|
+
'expired-callback': () => {
|
|
78
|
+
resolve(null);
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Trigger execution
|
|
83
|
+
window.turnstile.execute(this.widgetId, { action });
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
protected _destroyWidget(widgetId: string): void {
|
|
88
|
+
if (window.turnstile) {
|
|
89
|
+
window.turnstile.remove(widgetId);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EU CAPTCHA (Myra Security) global type declarations.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
interface EuCaptchaRenderOptions {
|
|
6
|
+
sitekey: string;
|
|
7
|
+
callback?: (token: string) => void;
|
|
8
|
+
'error-callback'?: (error: unknown) => void;
|
|
9
|
+
'expired-callback'?: () => void;
|
|
10
|
+
size?: 'normal' | 'compact' | 'invisible';
|
|
11
|
+
action?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface EuCaptchaApi {
|
|
15
|
+
render(container: string | HTMLElement, options: EuCaptchaRenderOptions): string;
|
|
16
|
+
execute(widgetId: string): void;
|
|
17
|
+
reset(widgetId: string): void;
|
|
18
|
+
getResponse(widgetId: string): string | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
declare global {
|
|
22
|
+
interface Window {
|
|
23
|
+
eucaptcha?: EuCaptchaApi;
|
|
24
|
+
onloadEuCaptchaCallback?: () => void;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Turnstile global type declarations.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
interface TurnstileRenderOptions {
|
|
6
|
+
sitekey: string;
|
|
7
|
+
callback?: (token: string) => void;
|
|
8
|
+
'error-callback'?: (error: unknown) => void;
|
|
9
|
+
'expired-callback'?: () => void;
|
|
10
|
+
size?: 'normal' | 'compact' | 'invisible';
|
|
11
|
+
theme?: 'light' | 'dark' | 'auto';
|
|
12
|
+
execution?: 'render' | 'execute';
|
|
13
|
+
action?: string;
|
|
14
|
+
cData?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface TurnstileApi {
|
|
18
|
+
render(container: string | HTMLElement, options: TurnstileRenderOptions): string;
|
|
19
|
+
execute(widgetId: string, options?: { action?: string }): void;
|
|
20
|
+
reset(widgetId: string): void;
|
|
21
|
+
remove(widgetId: string): void;
|
|
22
|
+
getResponse(widgetId: string): string | undefined;
|
|
23
|
+
isExpired(widgetId: string): boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
declare global {
|
|
27
|
+
interface Window {
|
|
28
|
+
turnstile?: TurnstileApi;
|
|
29
|
+
onloadTurnstileCallback?: () => void;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cart cookie configuration — platform contract.
|
|
3
|
+
*
|
|
4
|
+
* Used by:
|
|
5
|
+
* - SDK cart store (client-side cookie read/write for cartId)
|
|
6
|
+
* - Server-side cart prefetching (SSR cart badge, middleware)
|
|
7
|
+
* - proxy.ts (edge cart ID detection)
|
|
8
|
+
*
|
|
9
|
+
* Single cookie for cart ID persistence. Value is a plain cart ID string
|
|
10
|
+
* (not JSON) — server can read it directly via cookies().get('cart-id').
|
|
11
|
+
*/
|
|
12
|
+
export const CART_COOKIE_NAME = 'cart-id';
|
|
13
|
+
export const CART_COOKIE_MAX_AGE = 30 * 24 * 60 * 60; // 30 days
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Currency configuration constants — platform contract.
|
|
3
|
+
*
|
|
4
|
+
* Used by:
|
|
5
|
+
* - SDK currency store (client-side cookie read/write)
|
|
6
|
+
* - SDK currency middleware (X-Preferred-Currency header)
|
|
7
|
+
* - Server-side currency detection (SSR helpers)
|
|
8
|
+
*
|
|
9
|
+
* Single cookie for preferred currency persistence.
|
|
10
|
+
*/
|
|
11
|
+
export const CURRENCY_COOKIE_NAME = 'preferred-currency';
|
|
12
|
+
export const CURRENCY_COOKIE_MAX_AGE = 365 * 24 * 60 * 60; // 1 year
|
|
13
|
+
export const CURRENCY_HEADER_NAME = 'X-Preferred-Currency';
|
package/src/core/index.ts
CHANGED
|
@@ -55,10 +55,24 @@ export type {
|
|
|
55
55
|
// Middleware
|
|
56
56
|
export { authMiddleware } from './middleware/auth';
|
|
57
57
|
export { currencyMiddleware } from './middleware/currency';
|
|
58
|
+
export { languageMiddleware } from './middleware/language';
|
|
59
|
+
export {
|
|
60
|
+
botProtectionMiddleware,
|
|
61
|
+
BOT_PROTECTION_HEADER,
|
|
62
|
+
type BotProtectionTokenProvider,
|
|
63
|
+
type BotProtectionConfig,
|
|
64
|
+
type BotProtectionProviderConfig,
|
|
65
|
+
type BotProtectionMiddlewareOptions,
|
|
66
|
+
type FailStrategy,
|
|
67
|
+
} from './middleware/bot-protection';
|
|
58
68
|
export { retryMiddleware, type RetryOptions } from './middleware/retry';
|
|
59
69
|
export { timeoutMiddleware, type TimeoutOptions } from './middleware/timeout';
|
|
60
70
|
export { errorMiddleware } from './middleware/errors';
|
|
61
71
|
|
|
72
|
+
// Bot protection managers (framework-agnostic, DOM APIs)
|
|
73
|
+
export { createBotProtectionManager } from './bot-protection/create-manager';
|
|
74
|
+
export { FallbackBotProtectionManager } from './bot-protection/fallback-manager';
|
|
75
|
+
|
|
62
76
|
// Errors
|
|
63
77
|
export { StorefrontError, ErrorCodes, type StorefrontErrorOptions } from './errors';
|
|
64
78
|
|
|
@@ -134,6 +148,15 @@ export {
|
|
|
134
148
|
type AuthCookieConfig,
|
|
135
149
|
} from './auth/cookie-config';
|
|
136
150
|
|
|
151
|
+
// Language config
|
|
152
|
+
export { LANGUAGE_COOKIE_NAME, LANGUAGE_COOKIE_MAX_AGE, LANGUAGE_HEADER_NAME } from './language/cookie-config';
|
|
153
|
+
|
|
154
|
+
// Currency config
|
|
155
|
+
export { CURRENCY_COOKIE_NAME, CURRENCY_COOKIE_MAX_AGE, CURRENCY_HEADER_NAME } from './currency/cookie-config';
|
|
156
|
+
|
|
157
|
+
// Cart cookie config
|
|
158
|
+
export { CART_COOKIE_NAME, CART_COOKIE_MAX_AGE } from './cart/cookie-config';
|
|
159
|
+
|
|
137
160
|
// Auth route matching
|
|
138
161
|
export { matchesRoute, type RouteProtectionConfig } from './auth/routes';
|
|
139
162
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language configuration constants — platform contract.
|
|
3
|
+
*
|
|
4
|
+
* Used by:
|
|
5
|
+
* - SDK language store (client-side cookie read/write)
|
|
6
|
+
* - SDK language middleware (X-Lang header)
|
|
7
|
+
* - next-intl middleware `localeCookie.name` (server-side locale detection)
|
|
8
|
+
*
|
|
9
|
+
* Single cookie replaces both next-intl's NEXT_LOCALE and SDK's preferred-language.
|
|
10
|
+
*/
|
|
11
|
+
export const LANGUAGE_COOKIE_NAME = 'preferred-language';
|
|
12
|
+
export const LANGUAGE_COOKIE_MAX_AGE = 365 * 24 * 60 * 60; // 1 year
|
|
13
|
+
export const LANGUAGE_HEADER_NAME = 'X-Lang';
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bot protection middleware — vendor-agnostic token injection.
|
|
3
|
+
*
|
|
4
|
+
* Intercepts protected mutations and adds a bot verification token
|
|
5
|
+
* via the X-Bot-Protection-Token header. Token acquisition is delegated
|
|
6
|
+
* to a BotProtectionTokenProvider (EU CAPTCHA, Turnstile, etc.).
|
|
7
|
+
*
|
|
8
|
+
* Fail strategy is per-operation: 'open' = continue without token,
|
|
9
|
+
* 'closed' = throw if token unavailable.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Middleware } from '../client/types';
|
|
13
|
+
import { StorefrontError } from '../errors';
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Token provider interface (implemented by managers)
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
/** Vendor-agnostic token provider interface */
|
|
20
|
+
export interface BotProtectionTokenProvider {
|
|
21
|
+
/**
|
|
22
|
+
* Get a fresh bot protection token.
|
|
23
|
+
* Handles: in-flight deduplication, timeout, singleton script loading.
|
|
24
|
+
*
|
|
25
|
+
* @param options.action - Operation name (e.g. 'CustomerCreate') for action validation
|
|
26
|
+
* @param options.timeoutMs - Timeout in ms (default: 10000)
|
|
27
|
+
* @returns Token string or null if unavailable (adblock, timeout, error)
|
|
28
|
+
*/
|
|
29
|
+
execute(options?: { action?: string; timeoutMs?: number }): Promise<string | null>;
|
|
30
|
+
|
|
31
|
+
/** Cleanup widget and script resources */
|
|
32
|
+
destroy(): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Configuration types
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
/** Single provider config (from shop query) */
|
|
40
|
+
export interface BotProtectionProviderConfig {
|
|
41
|
+
/** Provider identifier: 'eucaptcha' | 'turnstile' | 'recaptcha' | extensible */
|
|
42
|
+
provider: string;
|
|
43
|
+
/** Site key for the bot protection widget */
|
|
44
|
+
siteKey: string;
|
|
45
|
+
/** Script URL — configurable for region-specific / enterprise overrides */
|
|
46
|
+
scriptUrl: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Bot protection configuration from shop query */
|
|
50
|
+
export interface BotProtectionConfig {
|
|
51
|
+
/** Primary provider */
|
|
52
|
+
primary: BotProtectionProviderConfig;
|
|
53
|
+
/** Fallback provider (runtime: if primary fails -> fallback -> fail-open) */
|
|
54
|
+
fallback?: BotProtectionProviderConfig | null;
|
|
55
|
+
/** GraphQL operation names requiring verification */
|
|
56
|
+
protectedOperations: string[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Fail strategy per request */
|
|
60
|
+
export type FailStrategy = 'open' | 'closed';
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Middleware options
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
export interface BotProtectionMiddlewareOptions {
|
|
67
|
+
/** Token provider instance (manager or fallback chain) */
|
|
68
|
+
tokenProvider: BotProtectionTokenProvider;
|
|
69
|
+
/** GraphQL operation names that require bot protection */
|
|
70
|
+
protectedOperations: string[];
|
|
71
|
+
/** Default fail strategy when token acquisition fails (default: 'open') */
|
|
72
|
+
defaultFailStrategy?: FailStrategy;
|
|
73
|
+
/** Override fail strategy per operation (e.g. CheckoutComplete = closed) */
|
|
74
|
+
failStrategyOverrides?: Record<string, FailStrategy>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Constants
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
/** Header name for bot protection token */
|
|
82
|
+
export const BOT_PROTECTION_HEADER = 'X-Bot-Protection-Token';
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Middleware factory
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Creates bot protection middleware for the SDK pipeline.
|
|
90
|
+
*
|
|
91
|
+
* Only intercepts mutations whose operationName is in protectedOperations.
|
|
92
|
+
* Token is fetched fresh on every call (single-use tokens, retry-safe).
|
|
93
|
+
*
|
|
94
|
+
* Pipeline position: AFTER auth/currency, BEFORE retry/timeout/errors.
|
|
95
|
+
* Retry middleware re-executes the full chain, so each attempt gets a fresh token.
|
|
96
|
+
*/
|
|
97
|
+
export function botProtectionMiddleware(options: BotProtectionMiddlewareOptions): Middleware {
|
|
98
|
+
const {
|
|
99
|
+
tokenProvider,
|
|
100
|
+
protectedOperations,
|
|
101
|
+
defaultFailStrategy = 'open',
|
|
102
|
+
failStrategyOverrides = {},
|
|
103
|
+
} = options;
|
|
104
|
+
|
|
105
|
+
const protectedSet = new Set(protectedOperations);
|
|
106
|
+
|
|
107
|
+
return async (request, next) => {
|
|
108
|
+
// Only intercept mutations
|
|
109
|
+
if (!request.isMutation) return next(request);
|
|
110
|
+
|
|
111
|
+
// Only intercept protected operations
|
|
112
|
+
const opName = request.operationName;
|
|
113
|
+
if (!opName || !protectedSet.has(opName)) {
|
|
114
|
+
return next(request);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Fetch fresh token — action param enables backend action validation
|
|
118
|
+
const token = await tokenProvider.execute({
|
|
119
|
+
action: opName,
|
|
120
|
+
timeoutMs: 10000,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (token) {
|
|
124
|
+
request.headers[BOT_PROTECTION_HEADER] = token;
|
|
125
|
+
} else {
|
|
126
|
+
// Fail strategy: open = continue without token, closed = throw
|
|
127
|
+
const strategy = failStrategyOverrides[opName] ?? defaultFailStrategy;
|
|
128
|
+
if (strategy === 'closed') {
|
|
129
|
+
throw new StorefrontError({
|
|
130
|
+
code: 'BOT_PROTECTION_REQUIRED',
|
|
131
|
+
message: 'Bot protection verification failed. Please try again.',
|
|
132
|
+
status: 0,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
// fail-open: continue without token — backend guard may still reject
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return next(request);
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import type { Middleware } from '../client/types';
|
|
15
|
+
import { CURRENCY_HEADER_NAME } from '../currency/cookie-config';
|
|
15
16
|
|
|
16
17
|
export function currencyMiddleware(
|
|
17
18
|
getCurrency: () => string | null | undefined,
|
|
@@ -19,7 +20,7 @@ export function currencyMiddleware(
|
|
|
19
20
|
return (request, next) => {
|
|
20
21
|
const currency = getCurrency();
|
|
21
22
|
if (currency) {
|
|
22
|
-
request.headers[
|
|
23
|
+
request.headers[CURRENCY_HEADER_NAME] = currency;
|
|
23
24
|
}
|
|
24
25
|
return next(request);
|
|
25
26
|
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language middleware — adds X-Lang header.
|
|
3
|
+
*
|
|
4
|
+
* Language is resolved lazily so language switches are reflected immediately.
|
|
5
|
+
* CRITICAL: does NOT send header when language=null (store not yet initialized)
|
|
6
|
+
* — without this, backend returns default language, React Query caches it,
|
|
7
|
+
* and after setLanguage() the cache is stale.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { languageMiddleware } from '@doswiftly/storefront-sdk';
|
|
12
|
+
*
|
|
13
|
+
* client.use(languageMiddleware(() => languageStore.getState().language));
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { Middleware } from '../client/types';
|
|
18
|
+
import { LANGUAGE_HEADER_NAME } from '../language/cookie-config';
|
|
19
|
+
|
|
20
|
+
export function languageMiddleware(
|
|
21
|
+
getLanguage: () => string | null | undefined,
|
|
22
|
+
): Middleware {
|
|
23
|
+
return (request, next) => {
|
|
24
|
+
const language = getLanguage();
|
|
25
|
+
if (language) {
|
|
26
|
+
request.headers[LANGUAGE_HEADER_NAME] = language;
|
|
27
|
+
}
|
|
28
|
+
return next(request);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bot protection React context.
|
|
3
|
+
*
|
|
4
|
+
* Provides access to the BotProtectionTokenProvider manager
|
|
5
|
+
* for the useBotProtection() escape hatch hook.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use client';
|
|
9
|
+
|
|
10
|
+
import { createContext } from 'react';
|
|
11
|
+
import type { BotProtectionTokenProvider } from '../../core/middleware/bot-protection';
|
|
12
|
+
|
|
13
|
+
export interface BotProtectionContextValue {
|
|
14
|
+
manager: BotProtectionTokenProvider | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const BotProtectionContext = createContext<BotProtectionContextValue | null>(null);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BotProtectionWidget — invisible React wrapper that mounts the bot protection manager.
|
|
3
|
+
*
|
|
4
|
+
* Renders a hidden div and mounts/unmounts the manager lifecycle.
|
|
5
|
+
* Triggers lazy script preload on mount.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use client';
|
|
9
|
+
|
|
10
|
+
import React, { useEffect, useRef } from 'react';
|
|
11
|
+
import type { BotProtectionTokenProvider } from '../../core/middleware/bot-protection';
|
|
12
|
+
import type { AbstractBotProtectionManager } from '../../core/bot-protection/abstract-manager';
|
|
13
|
+
|
|
14
|
+
interface BotProtectionWidgetProps {
|
|
15
|
+
manager: BotProtectionTokenProvider | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function BotProtectionWidget({ manager }: BotProtectionWidgetProps) {
|
|
19
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
20
|
+
const mountedRef = useRef(false);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!manager || !containerRef.current || mountedRef.current) return;
|
|
24
|
+
|
|
25
|
+
mountedRef.current = true;
|
|
26
|
+
|
|
27
|
+
// Trigger lazy script preload + mount container
|
|
28
|
+
const mgr = manager as AbstractBotProtectionManager;
|
|
29
|
+
if (typeof mgr.loadScript === 'function') {
|
|
30
|
+
mgr.loadScript().catch(() => {
|
|
31
|
+
// Script load failure is handled gracefully in execute()
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
if (typeof mgr.mount === 'function') {
|
|
35
|
+
mgr.mount(containerRef.current);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return () => {
|
|
39
|
+
mountedRef.current = false;
|
|
40
|
+
};
|
|
41
|
+
}, [manager]);
|
|
42
|
+
|
|
43
|
+
if (!manager) return null;
|
|
44
|
+
|
|
45
|
+
return <div ref={containerRef} style={{ display: 'none' }} aria-hidden="true" />;
|
|
46
|
+
}
|
package/src/react/cookies.ts
CHANGED
|
@@ -16,15 +16,20 @@ export function getCookie(name: string): string | null {
|
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Set cookie (client-side only).
|
|
19
|
+
*
|
|
20
|
+
* `secure` defaults to auto-detect from `location.protocol` (true on HTTPS).
|
|
19
21
|
*/
|
|
20
22
|
export function setCookie(
|
|
21
23
|
name: string,
|
|
22
24
|
value: string,
|
|
23
|
-
options: { maxAge?: number; path?: string; sameSite?: string } = {},
|
|
25
|
+
options: { maxAge?: number; path?: string; sameSite?: string; secure?: boolean } = {},
|
|
24
26
|
): void {
|
|
25
27
|
if (typeof document === 'undefined') return;
|
|
26
28
|
const { maxAge = 365 * 24 * 60 * 60, path = '/', sameSite = 'lax' } = options;
|
|
27
|
-
|
|
29
|
+
const secure = options.secure ??
|
|
30
|
+
(typeof location !== 'undefined' && location.protocol === 'https:');
|
|
31
|
+
const securePart = secure ? ';secure' : '';
|
|
32
|
+
document.cookie = `${name}=${encodeURIComponent(value)};max-age=${maxAge};path=${path};samesite=${sameSite}${securePart}`;
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
/**
|
|
@@ -35,6 +40,9 @@ export function deleteCookie(name: string, path = '/'): void {
|
|
|
35
40
|
document.cookie = `${name}=;max-age=0;path=${path}`;
|
|
36
41
|
}
|
|
37
42
|
|
|
43
|
+
import { CURRENCY_COOKIE_NAME } from '../core/currency/cookie-config';
|
|
44
|
+
import { CART_COOKIE_NAME } from '../core/cart/cookie-config';
|
|
45
|
+
|
|
38
46
|
/**
|
|
39
47
|
* Get preferred currency from cookie (async — works with Next.js cookies()).
|
|
40
48
|
* Falls back to document.cookie on client.
|
|
@@ -44,11 +52,38 @@ export async function getCurrencyFromCookieAsync(): Promise<string | null> {
|
|
|
44
52
|
try {
|
|
45
53
|
const { cookies } = await import('next/headers');
|
|
46
54
|
const cookieStore = await cookies();
|
|
47
|
-
return cookieStore.get(
|
|
55
|
+
return cookieStore.get(CURRENCY_COOKIE_NAME)?.value ?? null;
|
|
56
|
+
} catch {
|
|
57
|
+
// Not in a server context or next not available
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Client-side fallback
|
|
61
|
+
return getCookie(CURRENCY_COOKIE_NAME);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get cart ID from cookie (async — works with Next.js cookies()).
|
|
66
|
+
* Falls back to document.cookie on client.
|
|
67
|
+
*
|
|
68
|
+
* Use in Server Components for SSR cart badge:
|
|
69
|
+
* ```typescript
|
|
70
|
+
* const cartId = await getCartIdFromCookieAsync();
|
|
71
|
+
* if (cartId) {
|
|
72
|
+
* const cart = await fetchCart(cartId);
|
|
73
|
+
* // Render cart badge with real totalQuantity — no skeleton needed
|
|
74
|
+
* }
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export async function getCartIdFromCookieAsync(): Promise<string | null> {
|
|
78
|
+
// Server-side: try Next.js cookies()
|
|
79
|
+
try {
|
|
80
|
+
const { cookies } = await import('next/headers');
|
|
81
|
+
const cookieStore = await cookies();
|
|
82
|
+
return cookieStore.get(CART_COOKIE_NAME)?.value ?? null;
|
|
48
83
|
} catch {
|
|
49
84
|
// Not in a server context or next not available
|
|
50
85
|
}
|
|
51
86
|
|
|
52
87
|
// Client-side fallback
|
|
53
|
-
return getCookie(
|
|
88
|
+
return getCookie(CART_COOKIE_NAME);
|
|
54
89
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useBotProtection — escape hatch hook for custom bot protection use cases.
|
|
3
|
+
*
|
|
4
|
+
* Normally the middleware handles token injection automatically.
|
|
5
|
+
* Use this hook only when you need manual control (e.g. custom forms).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use client';
|
|
9
|
+
|
|
10
|
+
import { useContext, useCallback } from 'react';
|
|
11
|
+
import { BotProtectionContext } from '../bot-protection/bot-protection-context';
|
|
12
|
+
|
|
13
|
+
export function useBotProtection() {
|
|
14
|
+
const ctx = useContext(BotProtectionContext);
|
|
15
|
+
const manager = ctx?.manager ?? null;
|
|
16
|
+
|
|
17
|
+
const execute = useCallback(
|
|
18
|
+
(options?: { action?: string; timeoutMs?: number }) => {
|
|
19
|
+
if (!manager) return Promise.resolve(null);
|
|
20
|
+
return manager.execute(options);
|
|
21
|
+
},
|
|
22
|
+
[manager],
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
/** Execute a bot protection challenge and get a token */
|
|
27
|
+
execute,
|
|
28
|
+
/** Whether bot protection is configured and available */
|
|
29
|
+
isAvailable: !!manager,
|
|
30
|
+
};
|
|
31
|
+
}
|
package/src/react/index.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
export { StorefrontProvider, type StorefrontProviderProps } from './providers/storefront-provider';
|
|
17
17
|
export { StorefrontClientProvider, type StorefrontClientProviderProps } from './providers/storefront-client-provider';
|
|
18
18
|
export { CurrencyProvider, type CurrencyProviderProps } from './providers/currency-provider';
|
|
19
|
+
export { LanguageProvider, type LanguageProviderProps } from './providers/language-provider';
|
|
19
20
|
|
|
20
21
|
// Hooks
|
|
21
22
|
export { useAuth, type UseAuthOptions, type LoginResult, type LogoutResult, type TokenRenewResult } from './hooks/use-auth';
|
|
@@ -26,20 +27,45 @@ export { useCurrency } from './hooks/use-currency';
|
|
|
26
27
|
// Store hooks (Context-based)
|
|
27
28
|
export { useAuthStore, useAuthStoreApi, useAuthHydrated } from './stores/store-context';
|
|
28
29
|
export { useCurrencyStore, useCurrencyStoreApi } from './stores/store-context';
|
|
30
|
+
export { useLanguageStore, useLanguageStoreApi } from './stores/store-context';
|
|
29
31
|
|
|
30
32
|
// Store types
|
|
31
33
|
export type { AuthStore, CustomerInfo } from './stores/auth.store';
|
|
32
34
|
export type { CurrencyStore, ShopCurrencyData } from './stores/currency.store';
|
|
35
|
+
export type { LanguageStore } from './stores/language.store';
|
|
36
|
+
export type { ShopConfig } from './types/shop-config';
|
|
33
37
|
|
|
34
38
|
// Selectors
|
|
35
39
|
export { selectCurrency, selectBaseCurrency, selectSupportedCurrencies, selectIsLoaded } from './stores/currency.store';
|
|
40
|
+
export { selectLanguage, selectDefaultLanguage, selectSupportedLanguages, selectLanguageIsLoaded } from './stores/language.store';
|
|
36
41
|
|
|
37
42
|
// Cookie utilities
|
|
38
|
-
export { getCookie, setCookie, deleteCookie, getCurrencyFromCookieAsync } from './cookies';
|
|
43
|
+
export { getCookie, setCookie, deleteCookie, getCurrencyFromCookieAsync, getCartIdFromCookieAsync } from './cookies';
|
|
44
|
+
|
|
45
|
+
// Bot protection
|
|
46
|
+
export { useBotProtection } from './hooks/use-bot-protection';
|
|
39
47
|
|
|
40
48
|
// Generic hooks
|
|
41
49
|
export { useHydrated } from './hooks/use-hydrated';
|
|
42
50
|
export { useDebouncedValue } from './hooks/use-debounced-value';
|
|
43
51
|
|
|
52
|
+
// Cart store (DI-based)
|
|
53
|
+
export {
|
|
54
|
+
createCartStore,
|
|
55
|
+
selectCartId,
|
|
56
|
+
selectIsCartOpen,
|
|
57
|
+
selectCartIsLoading,
|
|
58
|
+
} from './stores/cart.store';
|
|
59
|
+
export type {
|
|
60
|
+
CartState,
|
|
61
|
+
CartStoreConfig,
|
|
62
|
+
CartActions,
|
|
63
|
+
CartData,
|
|
64
|
+
CartMutationAction,
|
|
65
|
+
CartLineInput,
|
|
66
|
+
CartLineUpdateInput,
|
|
67
|
+
} from './stores/cart.store';
|
|
68
|
+
export { CartProvider, useCartStore, useCartStoreApi } from './stores/cart.context';
|
|
69
|
+
|
|
44
70
|
// Store context helper
|
|
45
71
|
export { createStoreContext } from './helpers/create-store-context';
|