@hexclave/shared 1.0.3 → 1.0.5
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/dist/ai/unified-prompts/reminders.js +4 -3
- package/dist/ai/unified-prompts/reminders.js.map +1 -1
- package/dist/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.d.ts +3 -1
- package/dist/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.d.ts.map +1 -1
- package/dist/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.js +216 -29
- package/dist/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.js.map +1 -1
- package/dist/ai/unified-prompts/skill-site-prompt-parts/docs-index.d.ts.map +1 -1
- package/dist/ai/unified-prompts/skill-site-prompt-parts/docs-index.js +1 -0
- package/dist/ai/unified-prompts/skill-site-prompt-parts/docs-index.js.map +1 -1
- package/dist/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.d.ts +18 -6
- package/dist/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.js +23 -8
- package/dist/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.js.map +1 -1
- package/dist/ai/unified-prompts/skill-site-prompt-parts/openapi-specs.js +1 -1
- package/dist/ai/unified-prompts/skill-site-prompt-parts/openapi-specs.js.map +1 -1
- package/dist/config/schema.d.ts +71 -71
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +5 -5
- package/dist/config/schema.js.map +1 -1
- package/dist/config-authoring.d.ts +4 -4
- package/dist/config-authoring.d.ts.map +1 -1
- package/dist/config-authoring.js +2 -2
- package/dist/config-authoring.js.map +1 -1
- package/dist/config-rendering.d.ts +4 -4
- package/dist/config-rendering.d.ts.map +1 -1
- package/dist/config-rendering.js +19 -18
- package/dist/config-rendering.js.map +1 -1
- package/dist/esm/ai/unified-prompts/reminders.js +4 -3
- package/dist/esm/ai/unified-prompts/reminders.js.map +1 -1
- package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.d.ts +3 -1
- package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.d.ts.map +1 -1
- package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.js +215 -30
- package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.js.map +1 -1
- package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/docs-index.d.ts.map +1 -1
- package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/docs-index.js +1 -0
- package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/docs-index.js.map +1 -1
- package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.d.ts +18 -6
- package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.js +23 -8
- package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.js.map +1 -1
- package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/openapi-specs.js +1 -1
- package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/openapi-specs.js.map +1 -1
- package/dist/esm/config/schema.d.ts +71 -71
- package/dist/esm/config/schema.d.ts.map +1 -1
- package/dist/esm/config/schema.js +5 -5
- package/dist/esm/config/schema.js.map +1 -1
- package/dist/esm/config-authoring.d.ts +4 -4
- package/dist/esm/config-authoring.d.ts.map +1 -1
- package/dist/esm/config-authoring.js +2 -2
- package/dist/esm/config-authoring.js.map +1 -1
- package/dist/esm/config-rendering.d.ts +4 -4
- package/dist/esm/config-rendering.d.ts.map +1 -1
- package/dist/esm/config-rendering.js +15 -14
- package/dist/esm/config-rendering.js.map +1 -1
- package/dist/esm/helpers/vault/client-side.d.ts.map +1 -1
- package/dist/esm/helpers/vault/client-side.js.map +1 -1
- package/dist/esm/helpers/vault/server-side.d.ts.map +1 -1
- package/dist/esm/helpers/vault/server-side.js.map +1 -1
- package/dist/esm/hexclave-config-file.d.ts +15 -0
- package/dist/esm/hexclave-config-file.d.ts.map +1 -0
- package/dist/esm/{stack-config-file.js → hexclave-config-file.js} +7 -7
- package/dist/esm/hexclave-config-file.js.map +1 -0
- package/dist/esm/hooks/use-async-callback.js +1 -1
- package/dist/esm/hooks/use-async-external-store.js +1 -1
- package/dist/esm/hooks/use-strict-memo.js +1 -1
- package/dist/esm/interface/admin-metrics.d.ts +4 -4
- package/dist/esm/interface/admin-metrics.js.map +1 -1
- package/dist/esm/interface/client-interface.js +2 -2
- package/dist/esm/interface/client-interface.js.map +1 -1
- package/dist/esm/interface/conversations.d.ts +13 -13
- package/dist/esm/interface/crud/current-user.d.ts +9 -9
- package/dist/esm/interface/crud/email-outbox.d.ts +112 -112
- package/dist/esm/interface/crud/products.d.ts +1 -1
- package/dist/esm/interface/crud/project-api-keys.d.ts +2 -2
- package/dist/esm/interface/crud/project-permissions.js +5 -5
- package/dist/esm/interface/crud/project-permissions.js.map +1 -1
- package/dist/esm/interface/crud/projects.d.ts +28 -28
- package/dist/esm/interface/crud/team-member-profiles.d.ts +22 -22
- package/dist/esm/interface/crud/team-permissions.js +5 -5
- package/dist/esm/interface/crud/team-permissions.js.map +1 -1
- package/dist/esm/interface/crud/users.d.ts +14 -14
- package/dist/esm/interface/page-component-versions.js +107 -107
- package/dist/esm/interface/page-component-versions.js.map +1 -1
- package/dist/esm/interface/webhooks.d.ts +4 -4
- package/dist/esm/known-errors.js +4 -4
- package/dist/esm/known-errors.js.map +1 -1
- package/dist/esm/schema-fields.d.ts +15 -5
- package/dist/esm/schema-fields.d.ts.map +1 -1
- package/dist/esm/schema-fields.js +26 -16
- package/dist/esm/schema-fields.js.map +1 -1
- package/dist/esm/utils/crypto.d.ts.map +1 -1
- package/dist/esm/utils/crypto.js.map +1 -1
- package/dist/esm/utils/currency-constants.d.ts +6 -0
- package/dist/esm/utils/currency-constants.d.ts.map +1 -1
- package/dist/esm/utils/currency-constants.js.map +1 -1
- package/dist/esm/utils/dev-env-state-path.d.ts +3 -3
- package/dist/esm/utils/dev-env-state-path.d.ts.map +1 -1
- package/dist/esm/utils/dev-env-state-path.js +4 -4
- package/dist/esm/utils/dev-env-state-path.js.map +1 -1
- package/dist/esm/utils/errors.js +2 -2
- package/dist/esm/utils/errors.js.map +1 -1
- package/dist/esm/utils/esbuild.d.ts.map +1 -1
- package/dist/esm/utils/esbuild.js +0 -1
- package/dist/esm/utils/esbuild.js.map +1 -1
- package/dist/esm/utils/featurebase.d.ts +3 -3
- package/dist/esm/utils/featurebase.d.ts.map +1 -1
- package/dist/esm/utils/featurebase.js +15 -15
- package/dist/esm/utils/featurebase.js.map +1 -1
- package/dist/esm/utils/fs.js +6 -6
- package/dist/esm/utils/fs.js.map +1 -1
- package/dist/esm/utils/globals.d.ts.map +1 -1
- package/dist/esm/utils/globals.js +10 -10
- package/dist/esm/utils/globals.js.map +1 -1
- package/dist/esm/utils/jwt.d.ts.map +1 -1
- package/dist/esm/utils/jwt.js +3 -3
- package/dist/esm/utils/jwt.js.map +1 -1
- package/dist/esm/utils/redirect-urls.d.ts +3 -3
- package/dist/esm/utils/redirect-urls.d.ts.map +1 -1
- package/dist/esm/utils/redirect-urls.js +5 -5
- package/dist/esm/utils/redirect-urls.js.map +1 -1
- package/dist/esm/utils/urls.js.map +1 -1
- package/dist/helpers/vault/client-side.d.ts.map +1 -1
- package/dist/helpers/vault/client-side.js.map +1 -1
- package/dist/helpers/vault/server-side.d.ts.map +1 -1
- package/dist/helpers/vault/server-side.js.map +1 -1
- package/dist/hexclave-config-file.d.ts +15 -0
- package/dist/hexclave-config-file.d.ts.map +1 -0
- package/dist/{stack-config-file.js → hexclave-config-file.js} +8 -8
- package/dist/hexclave-config-file.js.map +1 -0
- package/dist/hooks/use-async-callback.js +1 -1
- package/dist/hooks/use-async-external-store.js +1 -1
- package/dist/hooks/use-strict-memo.js +1 -1
- package/dist/interface/admin-metrics.d.ts +4 -4
- package/dist/interface/admin-metrics.js.map +1 -1
- package/dist/interface/client-interface.js +2 -2
- package/dist/interface/client-interface.js.map +1 -1
- package/dist/interface/conversations.d.ts +13 -13
- package/dist/interface/crud/current-user.d.ts +9 -9
- package/dist/interface/crud/email-outbox.d.ts +112 -112
- package/dist/interface/crud/products.d.ts +1 -1
- package/dist/interface/crud/project-api-keys.d.ts +2 -2
- package/dist/interface/crud/project-permissions.js +5 -5
- package/dist/interface/crud/project-permissions.js.map +1 -1
- package/dist/interface/crud/projects.d.ts +28 -28
- package/dist/interface/crud/team-member-profiles.d.ts +22 -22
- package/dist/interface/crud/team-permissions.js +5 -5
- package/dist/interface/crud/team-permissions.js.map +1 -1
- package/dist/interface/crud/users.d.ts +14 -14
- package/dist/interface/page-component-versions.js +107 -107
- package/dist/interface/page-component-versions.js.map +1 -1
- package/dist/interface/webhooks.d.ts +4 -4
- package/dist/known-errors.js +4 -4
- package/dist/known-errors.js.map +1 -1
- package/dist/schema-fields.d.ts +15 -5
- package/dist/schema-fields.d.ts.map +1 -1
- package/dist/schema-fields.js +26 -16
- package/dist/schema-fields.js.map +1 -1
- package/dist/utils/crypto.d.ts.map +1 -1
- package/dist/utils/crypto.js.map +1 -1
- package/dist/utils/currency-constants.d.ts +6 -0
- package/dist/utils/currency-constants.d.ts.map +1 -1
- package/dist/utils/currency-constants.js.map +1 -1
- package/dist/utils/dev-env-state-path.d.ts +3 -3
- package/dist/utils/dev-env-state-path.d.ts.map +1 -1
- package/dist/utils/dev-env-state-path.js +5 -5
- package/dist/utils/dev-env-state-path.js.map +1 -1
- package/dist/utils/errors.js +2 -2
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/esbuild.d.ts.map +1 -1
- package/dist/utils/esbuild.js +0 -1
- package/dist/utils/esbuild.js.map +1 -1
- package/dist/utils/featurebase.d.ts +3 -3
- package/dist/utils/featurebase.d.ts.map +1 -1
- package/dist/utils/featurebase.js +15 -15
- package/dist/utils/featurebase.js.map +1 -1
- package/dist/utils/fs.js.map +1 -1
- package/dist/utils/globals.d.ts.map +1 -1
- package/dist/utils/globals.js +10 -10
- package/dist/utils/globals.js.map +1 -1
- package/dist/utils/jwt.d.ts.map +1 -1
- package/dist/utils/jwt.js +3 -3
- package/dist/utils/jwt.js.map +1 -1
- package/dist/utils/redirect-urls.d.ts +3 -3
- package/dist/utils/redirect-urls.d.ts.map +1 -1
- package/dist/utils/redirect-urls.js +5 -5
- package/dist/utils/redirect-urls.js.map +1 -1
- package/dist/utils/urls.js.map +1 -1
- package/package.json +2 -1
- package/src/ai/image-limits.ts +62 -0
- package/src/ai/llms/llms.ts +36 -0
- package/src/ai/unified-prompts/reminders.ts +27 -0
- package/src/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.ts +1047 -0
- package/src/ai/unified-prompts/skill-site-prompt-parts/brain.ts +5 -0
- package/src/ai/unified-prompts/skill-site-prompt-parts/cli-help.ts +5 -0
- package/src/ai/unified-prompts/skill-site-prompt-parts/config-docs.ts +5 -0
- package/src/ai/unified-prompts/skill-site-prompt-parts/custom-components-instructions.ts +5 -0
- package/src/ai/unified-prompts/skill-site-prompt-parts/dashboard-instructions.ts +5 -0
- package/src/ai/unified-prompts/skill-site-prompt-parts/docs-index.ts +80 -0
- package/src/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.ts +322 -0
- package/src/ai/unified-prompts/skill-site-prompt-parts/openapi-specs.ts +5 -0
- package/src/ai/unified-prompts/skill-site-prompt-parts/sdk-interface-source.ts +5 -0
- package/src/ai/unified-prompts/skill-site-prompt.ts +72 -0
- package/src/apps/apps-config.ts +177 -0
- package/src/apps/apps-ui.tsx +112 -0
- package/src/config/README.md +154 -0
- package/src/config/db-sync-mappings.ts +2293 -0
- package/src/config/format.ts +388 -0
- package/src/config/migrate-catalogs-to-product-lines.ts +475 -0
- package/src/config/schema-fuzzer.test.ts +377 -0
- package/src/config/schema.ts +1507 -0
- package/src/config-authoring.test.ts +52 -0
- package/src/config-authoring.ts +37 -0
- package/src/config-rendering.ts +142 -0
- package/src/crud.tsx +275 -0
- package/src/global.d.ts +1 -0
- package/src/helpers/emails.ts +250 -0
- package/src/helpers/init-prompt.ts +39 -0
- package/src/helpers/password.ts +16 -0
- package/src/helpers/production-mode.ts +58 -0
- package/src/helpers/support-sla.test.ts +124 -0
- package/src/helpers/support-sla.ts +93 -0
- package/src/helpers/vault/client-side.ts +92 -0
- package/src/helpers/vault/server-side.ts +153 -0
- package/src/hexclave-config-file.ts +120 -0
- package/src/hooks/use-async-callback.tsx +43 -0
- package/src/hooks/use-async-external-store.tsx +24 -0
- package/src/hooks/use-hash.tsx +14 -0
- package/src/hooks/use-hover.tsx +88 -0
- package/src/hooks/use-strict-memo.tsx +157 -0
- package/src/index.ts +17 -0
- package/src/interface/admin-interface.ts +1043 -0
- package/src/interface/admin-metrics.ts +214 -0
- package/src/interface/client-interface.test.ts +765 -0
- package/src/interface/client-interface.ts +2586 -0
- package/src/interface/conversations.ts +109 -0
- package/src/interface/crud/analytics.ts +11 -0
- package/src/interface/crud/connected-accounts.ts +52 -0
- package/src/interface/crud/contact-channels.ts +74 -0
- package/src/interface/crud/current-user.ts +72 -0
- package/src/interface/crud/email-outbox.ts +263 -0
- package/src/interface/crud/email-templates.ts +50 -0
- package/src/interface/crud/emails.ts +22 -0
- package/src/interface/crud/internal-api-keys.ts +69 -0
- package/src/interface/crud/invoices.ts +36 -0
- package/src/interface/crud/items.ts +24 -0
- package/src/interface/crud/notification-preferences.ts +33 -0
- package/src/interface/crud/oauth-providers.ts +91 -0
- package/src/interface/crud/products.ts +42 -0
- package/src/interface/crud/project-api-keys.ts +100 -0
- package/src/interface/crud/project-permissions.ts +118 -0
- package/src/interface/crud/projects.ts +229 -0
- package/src/interface/crud/session-replays.ts +85 -0
- package/src/interface/crud/sessions.ts +66 -0
- package/src/interface/crud/sign-up-rules.ts +12 -0
- package/src/interface/crud/svix-token.ts +20 -0
- package/src/interface/crud/team-invitation-details.ts +22 -0
- package/src/interface/crud/team-invitation.ts +35 -0
- package/src/interface/crud/team-member-profiles.ts +61 -0
- package/src/interface/crud/team-memberships.ts +56 -0
- package/src/interface/crud/team-permissions.ts +119 -0
- package/src/interface/crud/teams.ts +142 -0
- package/src/interface/crud/transactions.ts +131 -0
- package/src/interface/crud/users.ts +185 -0
- package/src/interface/handler-urls.ts +74 -0
- package/src/interface/page-component-versions.ts +1702 -0
- package/src/interface/server-interface.ts +1146 -0
- package/src/interface/webhooks.ts +28 -0
- package/src/known-errors.tsx +2031 -0
- package/src/local-emulator.ts +5 -0
- package/src/payments/payment-methods.ts +167 -0
- package/src/payments/stripe-limits.ts +18 -0
- package/src/plans.ts +107 -0
- package/src/schema-fields.ts +944 -0
- package/src/sessions.ts +310 -0
- package/src/utils/api-keys.tsx +122 -0
- package/src/utils/arrays.tsx +248 -0
- package/src/utils/auth-methods.ts +8 -0
- package/src/utils/base64.tsx +24 -0
- package/src/utils/booleans.tsx +34 -0
- package/src/utils/browser-compat.tsx +17 -0
- package/src/utils/bytes.tsx +280 -0
- package/src/utils/caches.tsx +280 -0
- package/src/utils/cel-fields.ts +92 -0
- package/src/utils/cloud-hosts.tsx +92 -0
- package/src/utils/compile-time.tsx +11 -0
- package/src/utils/country-codes.ts +39 -0
- package/src/utils/crypto.tsx +174 -0
- package/src/utils/currencies.tsx +53 -0
- package/src/utils/currency-constants.tsx +51 -0
- package/src/utils/dates.tsx +246 -0
- package/src/utils/dev-env-state-path.ts +14 -0
- package/src/utils/dom.tsx +7 -0
- package/src/utils/env.tsx +117 -0
- package/src/utils/errors.tsx +284 -0
- package/src/utils/esbuild.tsx +235 -0
- package/src/utils/featurebase.tsx +247 -0
- package/src/utils/fs.tsx +33 -0
- package/src/utils/functions.tsx +22 -0
- package/src/utils/geo.tsx +16 -0
- package/src/utils/globals.tsx +48 -0
- package/src/utils/hashes.tsx +52 -0
- package/src/utils/html.tsx +60 -0
- package/src/utils/http.tsx +91 -0
- package/src/utils/ips.tsx +47 -0
- package/src/utils/json.tsx +200 -0
- package/src/utils/jsx-editable-transpiler.test.ts +156 -0
- package/src/utils/jsx-editable-transpiler.tsx +401 -0
- package/src/utils/jwt.tsx +170 -0
- package/src/utils/locks.tsx +71 -0
- package/src/utils/maps.tsx +407 -0
- package/src/utils/math.tsx +18 -0
- package/src/utils/monkey-patch.tsx +29 -0
- package/src/utils/node-http.tsx +47 -0
- package/src/utils/numbers.tsx +82 -0
- package/src/utils/oauth.tsx +9 -0
- package/src/utils/objects.tsx +581 -0
- package/src/utils/paginated-lists.test.ts +1039 -0
- package/src/utils/paginated-lists.tsx +479 -0
- package/src/utils/passkey.tsx +1 -0
- package/src/utils/promises.tsx +540 -0
- package/src/utils/proxies.tsx +212 -0
- package/src/utils/react.tsx +308 -0
- package/src/utils/redirect-urls.tsx +299 -0
- package/src/utils/regex.tsx +10 -0
- package/src/utils/results.tsx +411 -0
- package/src/utils/sentry.tsx +20 -0
- package/src/utils/stores.tsx +238 -0
- package/src/utils/strings.nicify.test.ts +253 -0
- package/src/utils/strings.tsx +723 -0
- package/src/utils/telemetry.tsx +37 -0
- package/src/utils/turnstile-browser.ts +106 -0
- package/src/utils/turnstile-flow.test.ts +101 -0
- package/src/utils/turnstile-flow.ts +266 -0
- package/src/utils/turnstile.ts +34 -0
- package/src/utils/typed-arrays.tsx +17 -0
- package/src/utils/types.tsx +208 -0
- package/src/utils/unicode.tsx +24 -0
- package/src/utils/urls.tsx +411 -0
- package/src/utils/uuids.tsx +47 -0
- package/dist/esm/stack-config-file.d.ts +0 -15
- package/dist/esm/stack-config-file.d.ts.map +0 -1
- package/dist/esm/stack-config-file.js.map +0 -1
- package/dist/stack-config-file.d.ts +0 -15
- package/dist/stack-config-file.d.ts.map +0 -1
- package/dist/stack-config-file.js.map +0 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Attributes, AttributeValue, Span, trace } from "@opentelemetry/api";
|
|
2
|
+
import { getEnvVariable } from "./env";
|
|
3
|
+
import { HexclaveAssertionError } from "./errors";
|
|
4
|
+
|
|
5
|
+
const tracer = trace.getTracer('stack-tracer');
|
|
6
|
+
|
|
7
|
+
export function withTraceSpan<P extends any[], T>(optionsOrDescription: string | { description: string, attributes?: Record<string, AttributeValue> }, fn: (...args: P) => Promise<T>): (...args: P) => Promise<T> {
|
|
8
|
+
return async (...args: P) => {
|
|
9
|
+
return await traceSpan(optionsOrDescription, (span) => fn(...args));
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function traceSpan<T>(optionsOrDescription: string | { description: string, attributes?: Record<string, AttributeValue> }, fn: (span: Span) => Promise<T>): Promise<T> {
|
|
14
|
+
let options = typeof optionsOrDescription === 'string' ? { description: optionsOrDescription } : optionsOrDescription;
|
|
15
|
+
return await tracer.startActiveSpan(`STACK: ${options.description}`, async (span) => {
|
|
16
|
+
if (options.attributes) {
|
|
17
|
+
for (const [key, value] of Object.entries(options.attributes)) {
|
|
18
|
+
span.setAttribute(key, value);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
return await fn(span);
|
|
23
|
+
} finally {
|
|
24
|
+
span.end();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function log(message: string, attributes: Attributes) {
|
|
30
|
+
const span = trace.getActiveSpan();
|
|
31
|
+
if (span) {
|
|
32
|
+
span.addEvent(message, attributes);
|
|
33
|
+
// Telemetry is not initialized while seeding, so we don't want to throw an error
|
|
34
|
+
} else if (getEnvVariable('STACK_SEED_MODE', 'false') !== 'true') {
|
|
35
|
+
throw new HexclaveAssertionError('No active span found');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { HexclaveAssertionError } from "./errors";
|
|
2
|
+
import { TurnstileAction } from "./turnstile";
|
|
3
|
+
|
|
4
|
+
export type TurnstileWidgetId = string;
|
|
5
|
+
export type TurnstileTheme = "auto" | "light" | "dark";
|
|
6
|
+
export type TurnstileAppearance = "always" | "execute" | "interaction-only";
|
|
7
|
+
export type TurnstileExecution = "render" | "execute";
|
|
8
|
+
export type TurnstileSize = "invisible" | "flexible" | "normal" | "compact";
|
|
9
|
+
|
|
10
|
+
export type TurnstileConfig = {
|
|
11
|
+
sitekey: string,
|
|
12
|
+
action: TurnstileAction,
|
|
13
|
+
theme?: TurnstileTheme,
|
|
14
|
+
appearance?: TurnstileAppearance,
|
|
15
|
+
execution?: TurnstileExecution,
|
|
16
|
+
size?: TurnstileSize,
|
|
17
|
+
callback: (token: string) => void,
|
|
18
|
+
"error-callback": (errorCode?: string) => void,
|
|
19
|
+
"expired-callback": () => void,
|
|
20
|
+
"timeout-callback"?: () => void,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type TurnstileApi = {
|
|
24
|
+
render: (container: HTMLElement, config: TurnstileConfig) => TurnstileWidgetId,
|
|
25
|
+
execute?: (widgetId: TurnstileWidgetId) => void,
|
|
26
|
+
remove: (widgetId: TurnstileWidgetId) => void,
|
|
27
|
+
reset?: (widgetId: TurnstileWidgetId) => void,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const TURNSTILE_SCRIPT_BASE_URL = "https://challenges.cloudflare.com/turnstile/v0/api.js";
|
|
31
|
+
const TURNSTILE_SCRIPT_LOAD_TIMEOUT_MS = 30_000;
|
|
32
|
+
|
|
33
|
+
export function isTurnstileApi(value: unknown): value is TurnstileApi {
|
|
34
|
+
return typeof value === "object"
|
|
35
|
+
&& value !== null
|
|
36
|
+
&& "render" in value
|
|
37
|
+
&& "remove" in value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getTurnstileApi(): TurnstileApi | undefined {
|
|
41
|
+
if (typeof window === "undefined") {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const maybeTurnstile = Reflect.get(window, "turnstile");
|
|
46
|
+
return isTurnstileApi(maybeTurnstile) ? maybeTurnstile : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let turnstileScriptPromise: Promise<void> | null = null;
|
|
50
|
+
|
|
51
|
+
export function loadTurnstileScript(): Promise<void> {
|
|
52
|
+
if (typeof window === "undefined") {
|
|
53
|
+
return Promise.reject(new HexclaveAssertionError("Turnstile can only be loaded in the browser"));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (getTurnstileApi()) {
|
|
57
|
+
return Promise.resolve();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
turnstileScriptPromise ??= new Promise<void>((resolve, reject) => {
|
|
61
|
+
const rejectAndReset = (err: Error) => {
|
|
62
|
+
turnstileScriptPromise = null;
|
|
63
|
+
reject(err);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const timeout = setTimeout(() => {
|
|
67
|
+
rejectAndReset(new Error("Turnstile script load timed out"));
|
|
68
|
+
}, TURNSTILE_SCRIPT_LOAD_TIMEOUT_MS);
|
|
69
|
+
|
|
70
|
+
const resolveAndClearTimeout = () => {
|
|
71
|
+
clearTimeout(timeout);
|
|
72
|
+
resolve();
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const existingScript = document.querySelector<HTMLScriptElement>(`script[src^="${TURNSTILE_SCRIPT_BASE_URL}"]`);
|
|
76
|
+
if (existingScript) {
|
|
77
|
+
// If the Turnstile API is already available (script loaded before our loader ran),
|
|
78
|
+
// resolve immediately — the load event may have already fired.
|
|
79
|
+
if (getTurnstileApi()) {
|
|
80
|
+
resolveAndClearTimeout();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
existingScript.addEventListener("load", () => resolveAndClearTimeout(), { once: true });
|
|
84
|
+
existingScript.addEventListener("error", () => {
|
|
85
|
+
existingScript.remove();
|
|
86
|
+
clearTimeout(timeout);
|
|
87
|
+
rejectAndReset(new Error("Failed to load Turnstile"));
|
|
88
|
+
}, { once: true });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const script = document.createElement("script");
|
|
93
|
+
script.src = `${TURNSTILE_SCRIPT_BASE_URL}?render=explicit`;
|
|
94
|
+
script.async = true;
|
|
95
|
+
script.defer = true;
|
|
96
|
+
script.onload = () => resolveAndClearTimeout();
|
|
97
|
+
script.onerror = () => {
|
|
98
|
+
script.remove();
|
|
99
|
+
clearTimeout(timeout);
|
|
100
|
+
rejectAndReset(new Error("Failed to load Turnstile"));
|
|
101
|
+
};
|
|
102
|
+
document.head.append(script);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return turnstileScriptPromise;
|
|
106
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
const loadTurnstileScriptMock = vi.fn(() => Promise.resolve());
|
|
6
|
+
const renderMock = vi.fn();
|
|
7
|
+
const executeMock = vi.fn();
|
|
8
|
+
const removeMock = vi.fn();
|
|
9
|
+
const captureErrorMock = vi.fn();
|
|
10
|
+
|
|
11
|
+
vi.mock("./turnstile-browser", () => ({
|
|
12
|
+
loadTurnstileScript: loadTurnstileScriptMock,
|
|
13
|
+
getTurnstileApi: () => ({
|
|
14
|
+
render: renderMock,
|
|
15
|
+
execute: executeMock,
|
|
16
|
+
remove: removeMock,
|
|
17
|
+
}),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock("./errors", async () => {
|
|
21
|
+
const actual = await vi.importActual<typeof import("./errors")>("./errors");
|
|
22
|
+
return {
|
|
23
|
+
...actual,
|
|
24
|
+
captureError: captureErrorMock,
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("withBotChallengeFlow", () => {
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
vi.clearAllMocks();
|
|
31
|
+
loadTurnstileScriptMock.mockResolvedValue(undefined);
|
|
32
|
+
renderMock.mockImplementation((_container, config: {
|
|
33
|
+
callback: (token: string) => void,
|
|
34
|
+
}) => {
|
|
35
|
+
config.callback("invisible-token");
|
|
36
|
+
return "widget-id";
|
|
37
|
+
});
|
|
38
|
+
executeMock.mockImplementation(() => {});
|
|
39
|
+
removeMock.mockImplementation(() => {});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("throws a bot challenge execution error when the phase-2 visible challenge fails", async () => {
|
|
43
|
+
const { BotChallengeExecutionFailedError, withBotChallengeFlow } = await import("./turnstile-flow");
|
|
44
|
+
|
|
45
|
+
loadTurnstileScriptMock
|
|
46
|
+
.mockResolvedValueOnce(undefined)
|
|
47
|
+
.mockRejectedValueOnce(new Error("cloudflare unavailable"));
|
|
48
|
+
|
|
49
|
+
const execute = vi.fn(async ({ token, phase }: { token?: string, phase?: "invisible" | "visible" }) => {
|
|
50
|
+
if (token === "invisible-token" && phase === "invisible") {
|
|
51
|
+
return { requiresChallenge: true };
|
|
52
|
+
}
|
|
53
|
+
return { requiresChallenge: false };
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
await expect(withBotChallengeFlow({
|
|
57
|
+
visibleSiteKey: "visible-site-key",
|
|
58
|
+
invisibleSiteKey: "invisible-site-key",
|
|
59
|
+
action: "sign_up_with_credential",
|
|
60
|
+
execute,
|
|
61
|
+
isChallengeRequired: (result) => result.requiresChallenge,
|
|
62
|
+
})).rejects.toBeInstanceOf(BotChallengeExecutionFailedError);
|
|
63
|
+
|
|
64
|
+
expect(execute).toHaveBeenCalledTimes(1);
|
|
65
|
+
expect(execute).toHaveBeenCalledWith({
|
|
66
|
+
token: "invisible-token",
|
|
67
|
+
phase: "invisible",
|
|
68
|
+
});
|
|
69
|
+
expect(captureErrorMock).toHaveBeenCalledWith(
|
|
70
|
+
"turnstile-flow-visible-challenge-failed",
|
|
71
|
+
expect.any(Error),
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("marks the challenge as unavailable when both phase-1 challenge attempts fail", async () => {
|
|
76
|
+
const { withBotChallengeFlow } = await import("./turnstile-flow");
|
|
77
|
+
|
|
78
|
+
loadTurnstileScriptMock
|
|
79
|
+
.mockRejectedValueOnce(new Error("invisible unavailable"))
|
|
80
|
+
.mockRejectedValueOnce(new Error("visible unavailable"));
|
|
81
|
+
|
|
82
|
+
const execute = vi.fn(async ({ unavailable }: { unavailable?: true }) => ({
|
|
83
|
+
unavailable,
|
|
84
|
+
}));
|
|
85
|
+
|
|
86
|
+
await expect(withBotChallengeFlow({
|
|
87
|
+
visibleSiteKey: "visible-site-key",
|
|
88
|
+
invisibleSiteKey: "invisible-site-key",
|
|
89
|
+
action: "sign_up_with_credential",
|
|
90
|
+
execute,
|
|
91
|
+
isChallengeRequired: () => false,
|
|
92
|
+
})).resolves.toEqual({ unavailable: true });
|
|
93
|
+
|
|
94
|
+
expect(execute).toHaveBeenCalledTimes(1);
|
|
95
|
+
expect(execute).toHaveBeenCalledWith({ unavailable: true });
|
|
96
|
+
expect(captureErrorMock).toHaveBeenCalledWith(
|
|
97
|
+
"turnstile-flow-all-challenges-failed",
|
|
98
|
+
expect.any(Error),
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { HexclaveAssertionError, captureError } from "./errors";
|
|
2
|
+
import { loadTurnstileScript, getTurnstileApi } from "./turnstile-browser";
|
|
3
|
+
import type { TurnstileAction } from "./turnstile";
|
|
4
|
+
|
|
5
|
+
export class BotChallengeUserCancelledError extends Error {
|
|
6
|
+
constructor() {
|
|
7
|
+
super("User cancelled the bot challenge");
|
|
8
|
+
this.name = "BotChallengeUserCancelledError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class BotChallengeExecutionFailedError extends Error {
|
|
13
|
+
constructor(message = "Bot challenge could not be completed", options?: { cause?: unknown }) {
|
|
14
|
+
super(message, options);
|
|
15
|
+
this.name = "BotChallengeExecutionFailedError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
// ── Invisible challenge ────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
const INVISIBLE_TIMEOUT_MS = 30_000;
|
|
23
|
+
|
|
24
|
+
export async function executeTurnstileInvisible(siteKey: string, action: TurnstileAction): Promise<string> {
|
|
25
|
+
await loadTurnstileScript();
|
|
26
|
+
const api = getTurnstileApi();
|
|
27
|
+
if (!api) throw new HexclaveAssertionError("Turnstile API not available after loadTurnstileScript() resolved");
|
|
28
|
+
|
|
29
|
+
const container = document.createElement("div");
|
|
30
|
+
Object.assign(container.style, { position: "fixed", left: "-9999px", top: "-9999px" });
|
|
31
|
+
document.body.appendChild(container);
|
|
32
|
+
|
|
33
|
+
let widgetId: string | undefined;
|
|
34
|
+
try {
|
|
35
|
+
return await new Promise<string>((resolve, reject) => {
|
|
36
|
+
const timeout = setTimeout(
|
|
37
|
+
() => reject(new Error("Turnstile invisible challenge timed out")),
|
|
38
|
+
INVISIBLE_TIMEOUT_MS,
|
|
39
|
+
);
|
|
40
|
+
const settle = (fn: () => void) => {
|
|
41
|
+
clearTimeout(timeout);
|
|
42
|
+
fn();
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
widgetId = api.render(container, {
|
|
46
|
+
sitekey: siteKey,
|
|
47
|
+
action,
|
|
48
|
+
size: "invisible",
|
|
49
|
+
execution: "execute",
|
|
50
|
+
appearance: "execute",
|
|
51
|
+
callback: (t) => settle(() => resolve(t)),
|
|
52
|
+
"error-callback": () => settle(() => reject(new Error("Turnstile invisible verification failed"))),
|
|
53
|
+
"expired-callback": () => settle(() => reject(new Error("Turnstile token expired"))),
|
|
54
|
+
"timeout-callback": () => settle(() => reject(new Error("Turnstile challenge timed out"))),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
api.execute?.(widgetId);
|
|
58
|
+
});
|
|
59
|
+
} finally {
|
|
60
|
+
if (widgetId != null) {
|
|
61
|
+
try {
|
|
62
|
+
api.remove(widgetId);
|
|
63
|
+
} catch (e) {
|
|
64
|
+
captureError("turnstile-widget-remove", e instanceof Error ? e : new HexclaveAssertionError("Non-Error thrown during Turnstile widget removal", { cause: e }));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
container.remove();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
// ── Visible challenge overlay ──────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
const VISIBLE_TIMEOUT_MS = 120_000;
|
|
75
|
+
const OVERLAY_Z_INDEX = "999999";
|
|
76
|
+
|
|
77
|
+
// Module-level singleton: only one visible overlay can be active at a time.
|
|
78
|
+
// If a second challenge is requested while one is showing (e.g. user clicks another
|
|
79
|
+
// auth flow), the previous overlay is cancelled with BotChallengeUserCancelledError
|
|
80
|
+
// and cleaned up before the new one renders.
|
|
81
|
+
let activeOverlay: { cleanup: () => void, reject: (err: Error) => void } | null = null;
|
|
82
|
+
|
|
83
|
+
function el<K extends keyof HTMLElementTagNameMap>(
|
|
84
|
+
tag: K,
|
|
85
|
+
style: Partial<CSSStyleDeclaration>,
|
|
86
|
+
props?: Record<string, string>,
|
|
87
|
+
): HTMLElementTagNameMap[K] {
|
|
88
|
+
const element = document.createElement(tag);
|
|
89
|
+
Object.assign(element.style, style);
|
|
90
|
+
if (props) {
|
|
91
|
+
for (const [k, v] of Object.entries(props)) {
|
|
92
|
+
element.setAttribute(k, v);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return element;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function showTurnstileVisibleChallenge(siteKey: string, action: TurnstileAction): Promise<string> {
|
|
99
|
+
if (activeOverlay) {
|
|
100
|
+
activeOverlay.reject(new BotChallengeUserCancelledError());
|
|
101
|
+
activeOverlay.cleanup();
|
|
102
|
+
activeOverlay = null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return new Promise<string>((resolve, reject) => {
|
|
106
|
+
const timeout = setTimeout(() => {
|
|
107
|
+
cleanup();
|
|
108
|
+
reject(new Error("Visible Turnstile challenge timed out"));
|
|
109
|
+
}, VISIBLE_TIMEOUT_MS);
|
|
110
|
+
|
|
111
|
+
const overlay = el("div", {
|
|
112
|
+
position: "fixed", inset: "0", zIndex: OVERLAY_Z_INDEX,
|
|
113
|
+
display: "flex", alignItems: "center", justifyContent: "center",
|
|
114
|
+
background: "rgba(0,0,0,0.5)", backdropFilter: "blur(2px)",
|
|
115
|
+
}, { "data-stack-turnstile-overlay": "true" });
|
|
116
|
+
|
|
117
|
+
const card = el("div", {
|
|
118
|
+
background: "white", borderRadius: "12px", padding: "24px",
|
|
119
|
+
maxWidth: "400px", width: "90%", textAlign: "center",
|
|
120
|
+
boxShadow: "0 4px 24px rgba(0,0,0,0.18)",
|
|
121
|
+
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const title = el("p", { margin: "0 0 16px", fontSize: "16px", fontWeight: "600", color: "#333" });
|
|
125
|
+
title.textContent = "Please complete the security check";
|
|
126
|
+
|
|
127
|
+
const widgetContainer = el("div", { display: "flex", justifyContent: "center", minHeight: "65px" });
|
|
128
|
+
|
|
129
|
+
const errorText = el("p", { margin: "8px 0 0", fontSize: "14px", color: "#dc2626", display: "none" });
|
|
130
|
+
|
|
131
|
+
const cancelBtn = el("button", {
|
|
132
|
+
marginTop: "16px", padding: "8px 20px", border: "1px solid #ddd",
|
|
133
|
+
borderRadius: "6px", background: "transparent", cursor: "pointer",
|
|
134
|
+
fontSize: "14px", color: "#666",
|
|
135
|
+
});
|
|
136
|
+
cancelBtn.textContent = "Cancel";
|
|
137
|
+
cancelBtn.onmouseover = () => {
|
|
138
|
+
cancelBtn.style.background = "#f5f5f5";
|
|
139
|
+
};
|
|
140
|
+
cancelBtn.onmouseout = () => {
|
|
141
|
+
cancelBtn.style.background = "transparent";
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
card.append(title, widgetContainer, errorText, cancelBtn);
|
|
145
|
+
overlay.appendChild(card);
|
|
146
|
+
document.body.appendChild(overlay);
|
|
147
|
+
|
|
148
|
+
function cleanup() {
|
|
149
|
+
clearTimeout(timeout);
|
|
150
|
+
overlay.remove();
|
|
151
|
+
if (activeOverlay?.cleanup === cleanup) {
|
|
152
|
+
activeOverlay = null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
activeOverlay = { cleanup, reject };
|
|
157
|
+
cancelBtn.onclick = () => {
|
|
158
|
+
cleanup();
|
|
159
|
+
reject(new BotChallengeUserCancelledError());
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
loadTurnstileScript().then(() => {
|
|
163
|
+
const api = getTurnstileApi();
|
|
164
|
+
if (!api) {
|
|
165
|
+
cleanup();
|
|
166
|
+
reject(new HexclaveAssertionError("Turnstile API not available after loadTurnstileScript() resolved"));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
api.render(widgetContainer, {
|
|
171
|
+
sitekey: siteKey,
|
|
172
|
+
action,
|
|
173
|
+
appearance: "always",
|
|
174
|
+
execution: "render",
|
|
175
|
+
size: "flexible",
|
|
176
|
+
callback: (token) => {
|
|
177
|
+
cleanup();
|
|
178
|
+
resolve(token);
|
|
179
|
+
},
|
|
180
|
+
"error-callback": (errorCode) => {
|
|
181
|
+
errorText.textContent = errorCode ? `Verification error: ${errorCode}` : "Verification failed. Please try again.";
|
|
182
|
+
errorText.style.display = "block";
|
|
183
|
+
},
|
|
184
|
+
"expired-callback": () => {
|
|
185
|
+
errorText.textContent = "Challenge expired. Please solve it again.";
|
|
186
|
+
errorText.style.display = "block";
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
}).catch((err) => {
|
|
190
|
+
cleanup();
|
|
191
|
+
reject(err);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
// ── Flow orchestrator ──────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
export type BotChallengeExecuteParams = {
|
|
200
|
+
token?: string,
|
|
201
|
+
phase?: "invisible" | "visible",
|
|
202
|
+
unavailable?: true,
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
export type WithBotChallengeFlowOptions<T> = {
|
|
206
|
+
visibleSiteKey: string,
|
|
207
|
+
invisibleSiteKey: string,
|
|
208
|
+
action: TurnstileAction,
|
|
209
|
+
execute: (challenge: BotChallengeExecuteParams) => Promise<T>,
|
|
210
|
+
isChallengeRequired: (result: T) => boolean,
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// We use separate invisible + visible flows (rather than Turnstile's "managed" mode) because:
|
|
214
|
+
// 1. Managed mode auto-decides visibility, but we need deterministic server-side logic:
|
|
215
|
+
// invisible-fail → require visible challenge → fail = block.
|
|
216
|
+
// 2. Invisible + visible use different site keys so the server can tell which phase passed.
|
|
217
|
+
// 3. Managed mode doesn't expose an API to programmatically trigger a retry with a
|
|
218
|
+
// different widget type, which our two-phase challenge escalation requires.
|
|
219
|
+
export async function withBotChallengeFlow<T>(options: WithBotChallengeFlowOptions<T>): Promise<T> {
|
|
220
|
+
// Server safe: no Turnstile in SSR — just call execute with no turnstile params
|
|
221
|
+
if (typeof window === "undefined") {
|
|
222
|
+
return await options.execute({});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Phase 1: invisible token
|
|
226
|
+
let invisibleToken: string | undefined;
|
|
227
|
+
let usedVisibleFallback = false;
|
|
228
|
+
try {
|
|
229
|
+
invisibleToken = await executeTurnstileInvisible(options.invisibleSiteKey, options.action);
|
|
230
|
+
} catch {
|
|
231
|
+
try {
|
|
232
|
+
invisibleToken = await showTurnstileVisibleChallenge(options.visibleSiteKey, options.action);
|
|
233
|
+
usedVisibleFallback = true;
|
|
234
|
+
} catch (e) {
|
|
235
|
+
if (e instanceof BotChallengeUserCancelledError) throw e;
|
|
236
|
+
// Both challenges failed (for example Cloudflare is unreachable) — tell the
|
|
237
|
+
// server explicitly so it can distinguish challenge infra outages from a
|
|
238
|
+
// user submitting an invalid visible challenge.
|
|
239
|
+
captureError("turnstile-flow-all-challenges-failed", e instanceof Error ? e : new HexclaveAssertionError("Non-Error thrown during Turnstile challenge", { cause: e }));
|
|
240
|
+
return await options.execute({ unavailable: true });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const firstResult = await options.execute({
|
|
245
|
+
token: invisibleToken,
|
|
246
|
+
phase: invisibleToken ? (usedVisibleFallback ? "visible" : "invisible") : undefined,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
if (!options.isChallengeRequired(firstResult)) {
|
|
250
|
+
return firstResult;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Phase 2: visible challenge (single retry)
|
|
254
|
+
let visibleToken: string | undefined;
|
|
255
|
+
try {
|
|
256
|
+
visibleToken = await showTurnstileVisibleChallenge(options.visibleSiteKey, options.action);
|
|
257
|
+
} catch (e) {
|
|
258
|
+
if (e instanceof BotChallengeUserCancelledError) throw e;
|
|
259
|
+
captureError("turnstile-flow-visible-challenge-failed", e instanceof Error ? e : new HexclaveAssertionError("Non-Error thrown during visible Turnstile challenge", { cause: e }));
|
|
260
|
+
throw new BotChallengeExecutionFailedError("Visible bot challenge could not be completed", {
|
|
261
|
+
cause: e,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return await options.execute({ token: visibleToken, phase: "visible" });
|
|
266
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const turnstileActionValues = [
|
|
2
|
+
"sign_up_with_credential",
|
|
3
|
+
"send_magic_link_email",
|
|
4
|
+
"oauth_authenticate",
|
|
5
|
+
] as const;
|
|
6
|
+
|
|
7
|
+
export type TurnstileAction = typeof turnstileActionValues[number];
|
|
8
|
+
|
|
9
|
+
export const turnstilePhaseValues = [
|
|
10
|
+
"invisible",
|
|
11
|
+
"visible",
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
export type TurnstilePhase = typeof turnstilePhaseValues[number];
|
|
15
|
+
|
|
16
|
+
export const turnstileResultValues = [
|
|
17
|
+
"ok",
|
|
18
|
+
"invalid",
|
|
19
|
+
"error",
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
export type TurnstileResult = typeof turnstileResultValues[number];
|
|
23
|
+
|
|
24
|
+
export const turnstileDevelopmentKeys = {
|
|
25
|
+
visibleSiteKey: "1x00000000000000000000AA",
|
|
26
|
+
invisibleSiteKey: "1x00000000000000000000BB",
|
|
27
|
+
secretKey: "1x0000000000000000000000000000000AA",
|
|
28
|
+
forcedChallengeSiteKey: "3x00000000000000000000FF",
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
export function isTurnstileResult(value: unknown): value is TurnstileResult {
|
|
33
|
+
return typeof value === "string" && turnstileResultValues.some((status) => status === value);
|
|
34
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { HexclaveAssertionError } from "./errors";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ensures a Uint8Array is backed by a regular ArrayBuffer (not SharedArrayBuffer).
|
|
5
|
+
*
|
|
6
|
+
* TypeScript 5.7+ made typed arrays generic over their buffer type. Bare `Uint8Array`
|
|
7
|
+
* defaults to `Uint8Array<ArrayBufferLike>`, which includes SharedArrayBuffer. Web Crypto
|
|
8
|
+
* APIs require `BufferSource` which only accepts `ArrayBufferView<ArrayBuffer>`. This
|
|
9
|
+
* function narrows the type using an instanceof guard, creating a same-buffer view
|
|
10
|
+
* (zero-copy) when the buffer is already an ArrayBuffer.
|
|
11
|
+
*/
|
|
12
|
+
export function toArrayBufferBacked(arr: Uint8Array): Uint8Array<ArrayBuffer> {
|
|
13
|
+
if (arr.buffer instanceof SharedArrayBuffer) {
|
|
14
|
+
throw new HexclaveAssertionError("SharedArrayBuffer-backed Uint8Arrays are not supported in this context");
|
|
15
|
+
}
|
|
16
|
+
return new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength);
|
|
17
|
+
}
|