@hexclave/shared 1.0.2 → 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 +217 -30
- 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 +63 -63
- 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 +216 -31
- 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 +63 -63
- 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 +8 -8
- 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 +32 -32
- package/dist/esm/interface/crud/current-user.d.ts +9 -9
- package/dist/esm/interface/crud/email-outbox.d.ts +200 -200
- package/dist/esm/interface/crud/products.d.ts +13 -13
- package/dist/esm/interface/crud/products.d.ts.map +1 -1
- package/dist/esm/interface/crud/project-api-keys.d.ts +4 -4
- 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 +36 -36
- package/dist/esm/interface/crud/team-member-profiles.d.ts +18 -18
- 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/transactions.d.ts +6 -6
- package/dist/esm/interface/crud/transactions.d.ts.map +1 -1
- package/dist/esm/interface/crud/users.d.ts +12 -12
- 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 +2 -2
- package/dist/esm/known-errors.d.ts +2 -2
- package/dist/esm/known-errors.js +4 -4
- package/dist/esm/known-errors.js.map +1 -1
- package/dist/esm/schema-fields.d.ts +17 -7
- 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/sessions.d.ts +7 -7
- 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 +8 -8
- 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 +32 -32
- package/dist/interface/crud/current-user.d.ts +9 -9
- package/dist/interface/crud/email-outbox.d.ts +200 -200
- package/dist/interface/crud/products.d.ts +13 -13
- package/dist/interface/crud/products.d.ts.map +1 -1
- package/dist/interface/crud/project-api-keys.d.ts +4 -4
- 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 +36 -36
- package/dist/interface/crud/team-member-profiles.d.ts +18 -18
- package/dist/interface/crud/team-permissions.js +5 -5
- package/dist/interface/crud/team-permissions.js.map +1 -1
- package/dist/interface/crud/transactions.d.ts +6 -6
- package/dist/interface/crud/transactions.d.ts.map +1 -1
- package/dist/interface/crud/users.d.ts +12 -12
- 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 +2 -2
- package/dist/known-errors.d.ts +2 -2
- package/dist/known-errors.js +4 -4
- package/dist/known-errors.js.map +1 -1
- package/dist/schema-fields.d.ts +17 -7
- 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/sessions.d.ts +7 -7
- 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,92 @@
|
|
|
1
|
+
import { signUpAuthMethodValues } from "./auth-methods";
|
|
2
|
+
import { standardProviders } from "./oauth";
|
|
3
|
+
|
|
4
|
+
// ── Types ──────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
export type ConditionField =
|
|
7
|
+
| 'email'
|
|
8
|
+
| 'countryCode'
|
|
9
|
+
| 'emailDomain'
|
|
10
|
+
| 'authMethod'
|
|
11
|
+
| 'oauthProvider'
|
|
12
|
+
| 'riskScores.bot'
|
|
13
|
+
| 'riskScores.free_trial_abuse';
|
|
14
|
+
|
|
15
|
+
export type ConditionOperator =
|
|
16
|
+
| 'equals'
|
|
17
|
+
| 'not_equals'
|
|
18
|
+
| 'greater_than'
|
|
19
|
+
| 'greater_or_equal'
|
|
20
|
+
| 'less_than'
|
|
21
|
+
| 'less_or_equal'
|
|
22
|
+
| 'matches'
|
|
23
|
+
| 'ends_with'
|
|
24
|
+
| 'starts_with'
|
|
25
|
+
| 'contains'
|
|
26
|
+
| 'in_list';
|
|
27
|
+
|
|
28
|
+
// ── Helpers ────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
export function isNumericField(field: ConditionField): boolean {
|
|
31
|
+
return field === 'riskScores.bot' || field === 'riskScores.free_trial_abuse';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Validates a numeric field value is a finite integer within [0, 100].
|
|
36
|
+
* Returns null if valid, or an error message string if invalid.
|
|
37
|
+
*/
|
|
38
|
+
export function validateNumericFieldValue(field: string, value: string | number): string | null {
|
|
39
|
+
const num = typeof value === 'number' ? value : Number(value);
|
|
40
|
+
if (!Number.isFinite(num)) {
|
|
41
|
+
return `Expected a finite number for field "${field}", got "${String(value)}"`;
|
|
42
|
+
}
|
|
43
|
+
if (!Number.isInteger(num)) {
|
|
44
|
+
return `Expected an integer for field "${field}", got "${String(value)}"`;
|
|
45
|
+
}
|
|
46
|
+
if (num < 0 || num > 100) {
|
|
47
|
+
return `Value for field "${field}" must be between 0 and 100, got ${num}`;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function escapeCelString(value: string): string {
|
|
53
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function unescapeCelString(value: string): string {
|
|
57
|
+
return value.replace(/\\\\/g, '\\').replace(/\\"/g, '"');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Field metadata ─────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
const numericOperators: ConditionOperator[] = ['equals', 'not_equals', 'greater_than', 'greater_or_equal', 'less_than', 'less_or_equal'];
|
|
63
|
+
const enumOperators: ConditionOperator[] = ['equals', 'not_equals', 'in_list'];
|
|
64
|
+
const stringOperators: ConditionOperator[] = ['equals', 'not_equals', 'contains', 'starts_with', 'ends_with', 'matches', 'in_list'];
|
|
65
|
+
|
|
66
|
+
export type FieldMetadataEntry = {
|
|
67
|
+
label: string,
|
|
68
|
+
operators: ConditionOperator[],
|
|
69
|
+
predefinedValues?: string[],
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const fieldMetadata: Record<ConditionField, FieldMetadataEntry> = {
|
|
73
|
+
email: { label: 'Email', operators: stringOperators },
|
|
74
|
+
countryCode: { label: 'Country Code', operators: enumOperators },
|
|
75
|
+
emailDomain: { label: 'Email Domain', operators: stringOperators },
|
|
76
|
+
authMethod: { label: 'Auth Method', operators: enumOperators, predefinedValues: [...signUpAuthMethodValues] },
|
|
77
|
+
oauthProvider: { label: 'OAuth Provider', operators: enumOperators, predefinedValues: [...standardProviders] },
|
|
78
|
+
'riskScores.bot': { label: 'Risk Score: Bot', operators: numericOperators },
|
|
79
|
+
'riskScores.free_trial_abuse': { label: 'Risk Score: Free Trial Abuse', operators: numericOperators },
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const conditionFields = Object.keys(fieldMetadata) as ConditionField[];
|
|
83
|
+
|
|
84
|
+
export const conditionOperators: ConditionOperator[] = [
|
|
85
|
+
'equals', 'not_equals', 'greater_than', 'greater_or_equal',
|
|
86
|
+
'less_than', 'less_or_equal', 'matches', 'ends_with',
|
|
87
|
+
'starts_with', 'contains', 'in_list',
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
export function getOperatorsForField(field: ConditionField): ConditionOperator[] {
|
|
91
|
+
return fieldMetadata[field].operators;
|
|
92
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the stack-auth ↔ hexclave host pairs that the
|
|
3
|
+
* cloud deployment treats as equivalent siblings. Each
|
|
4
|
+
* `[stackAuthHost, hexclaveHost]` pair is consumed in a few places:
|
|
5
|
+
*
|
|
6
|
+
* 1. `CLOUD_API_HOST_BY_REQUEST_HOST` in the backend's `request-api-url.ts` —
|
|
7
|
+
* the allowlist of request hosts we resolve into a JWT `iss` claim.
|
|
8
|
+
* 2. `issuerHostAliases` in `tokens.tsx` — the bidirectional validator alias
|
|
9
|
+
* map, so a token issued under either host validates against the other.
|
|
10
|
+
* 3. `getCloudApiUrlSiblings` below — resolves the branded base URLs used for
|
|
11
|
+
* OAuth `redirect_uri` callbacks (both at runtime and in the dashboard).
|
|
12
|
+
*
|
|
13
|
+
* Deriving all of these from this one list prevents drift.
|
|
14
|
+
*/
|
|
15
|
+
export const CLOUD_HOST_PAIRS: ReadonlyArray<readonly [string, string]> = [
|
|
16
|
+
["api.stack-auth.com", "api.hexclave.com"],
|
|
17
|
+
["api.dev.stack-auth.com", "api.dev.hexclave.com"],
|
|
18
|
+
["api.staging.stack-auth.com", "api.staging.hexclave.com"],
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function hostFromApiUrlOrHost(input: string): string | undefined {
|
|
22
|
+
try {
|
|
23
|
+
return new URL(input).host.split(":")[0].toLowerCase();
|
|
24
|
+
} catch {
|
|
25
|
+
const firstHost = input.split(",")[0]?.trim();
|
|
26
|
+
if (!firstHost) return undefined;
|
|
27
|
+
return firstHost.split(":")[0].toLowerCase();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Given an API URL (or bare host), if its host belongs to a known
|
|
33
|
+
* stack-auth ↔ hexclave cloud pair, return both branded base URLs. Returns
|
|
34
|
+
* null for unknown hosts (localhost, vercel previews, self-host custom
|
|
35
|
+
* domains).
|
|
36
|
+
*/
|
|
37
|
+
export function getCloudApiUrlSiblings(apiUrlOrHost: string | undefined | null): { stackAuth: string, hexclave: string } | null {
|
|
38
|
+
if (!apiUrlOrHost) return null;
|
|
39
|
+
const host = hostFromApiUrlOrHost(apiUrlOrHost);
|
|
40
|
+
if (!host) return null;
|
|
41
|
+
for (const [stackAuthHost, hexclaveHost] of CLOUD_HOST_PAIRS) {
|
|
42
|
+
if (host === stackAuthHost || host === hexclaveHost) {
|
|
43
|
+
return { stackAuth: `https://${stackAuthHost}`, hexclave: `https://${hexclaveHost}` };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The stack-auth-branded base URL for the given deployment API URL. Used as the
|
|
51
|
+
* OAuth `redirect_uri` base for shared providers and for custom providers that
|
|
52
|
+
* predate `customCallbackUrl` (so existing flows keep hitting
|
|
53
|
+
* `api.stack-auth.com`). Unknown hosts fall back to the input unchanged.
|
|
54
|
+
*/
|
|
55
|
+
export function getStackAuthApiBaseUrl(apiUrl: string): string {
|
|
56
|
+
return getCloudApiUrlSiblings(apiUrl)?.stackAuth ?? apiUrl;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The hexclave-branded base URL for the given deployment API URL. Used when a
|
|
61
|
+
* new custom OAuth provider is set up, so its `customCallbackUrl` points at the
|
|
62
|
+
* hexclave brand. Unknown hosts fall back to the input unchanged.
|
|
63
|
+
*/
|
|
64
|
+
export function getHexclaveApiBaseUrl(apiUrl: string): string {
|
|
65
|
+
return getCloudApiUrlSiblings(apiUrl)?.hexclave ?? apiUrl;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
import.meta.vitest?.test("getCloudApiUrlSiblings maps both sides of each cloud pair", ({ expect }) => {
|
|
69
|
+
for (const [stackAuthHost, hexclaveHost] of CLOUD_HOST_PAIRS) {
|
|
70
|
+
const expected = { stackAuth: `https://${stackAuthHost}`, hexclave: `https://${hexclaveHost}` };
|
|
71
|
+
for (const host of [stackAuthHost, hexclaveHost]) {
|
|
72
|
+
expect(getCloudApiUrlSiblings(host)).toEqual(expected);
|
|
73
|
+
expect(getCloudApiUrlSiblings(`https://${host}`)).toEqual(expected);
|
|
74
|
+
expect(getCloudApiUrlSiblings(`https://${host.toUpperCase()}:443`)).toEqual(expected);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
import.meta.vitest?.test("getCloudApiUrlSiblings returns null for unknown hosts", ({ expect }) => {
|
|
80
|
+
expect(getCloudApiUrlSiblings("http://localhost:8102")).toBeNull();
|
|
81
|
+
expect(getCloudApiUrlSiblings("https://my-app.vercel.app")).toBeNull();
|
|
82
|
+
expect(getCloudApiUrlSiblings(undefined)).toBeNull();
|
|
83
|
+
expect(getCloudApiUrlSiblings("")).toBeNull();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
import.meta.vitest?.test("getStackAuthApiBaseUrl / getHexclaveApiBaseUrl resolve brands and fall back", ({ expect }) => {
|
|
87
|
+
expect(getStackAuthApiBaseUrl("https://api.hexclave.com")).toBe("https://api.stack-auth.com");
|
|
88
|
+
expect(getHexclaveApiBaseUrl("https://api.stack-auth.com")).toBe("https://api.hexclave.com");
|
|
89
|
+
expect(getStackAuthApiBaseUrl("https://api.dev.stack-auth.com")).toBe("https://api.dev.stack-auth.com");
|
|
90
|
+
expect(getHexclaveApiBaseUrl("http://localhost:8102")).toBe("http://localhost:8102");
|
|
91
|
+
expect(getStackAuthApiBaseUrl("http://localhost:8102")).toBe("http://localhost:8102");
|
|
92
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the first argument passed to it, but compilers won't be able to optimize it out. This is useful in some
|
|
3
|
+
* cases where compiler warnings go awry; for example, when importing things that may not exist (but are guaranteed
|
|
4
|
+
* to exist at runtime).
|
|
5
|
+
*/
|
|
6
|
+
export function scrambleDuringCompileTime<T>(t: T): T {
|
|
7
|
+
if (Math.random() < 0.00001 && Math.random() > 0.99999 && Math.random() < 0.00001 && Math.random() > 0.99999) {
|
|
8
|
+
return "this will never happen" as any;
|
|
9
|
+
}
|
|
10
|
+
return t;
|
|
11
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export function normalizeCountryCode(countryCode: string): string {
|
|
2
|
+
return countryCode.trim().toUpperCase();
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function isValidCountryCode(countryCode: string): boolean {
|
|
6
|
+
const normalized = normalizeCountryCode(countryCode);
|
|
7
|
+
return /^[A-Z]{2}$/.test(normalized);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Validates and normalizes a country code value (single string or array).
|
|
12
|
+
* Returns null if valid, or an error message string if invalid.
|
|
13
|
+
*/
|
|
14
|
+
export function validateCountryCode(value: string | string[]): string | null {
|
|
15
|
+
const values = Array.isArray(value) ? value : [value];
|
|
16
|
+
if (values.length === 0) {
|
|
17
|
+
return "At least one country code is required";
|
|
18
|
+
}
|
|
19
|
+
return values.every(v => isValidCountryCode(v))
|
|
20
|
+
? null
|
|
21
|
+
: "Country code must be a 2-letter code";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
import.meta.vitest?.test("country codes", ({ expect }) => {
|
|
25
|
+
expect(normalizeCountryCode(" us ")).toBe("US");
|
|
26
|
+
expect(isValidCountryCode("us")).toBe(true);
|
|
27
|
+
expect(isValidCountryCode("US")).toBe(true);
|
|
28
|
+
expect(isValidCountryCode("ZZ")).toBe(true);
|
|
29
|
+
expect(isValidCountryCode("usa")).toBe(false);
|
|
30
|
+
expect(isValidCountryCode("a")).toBe(false);
|
|
31
|
+
expect(isValidCountryCode("")).toBe(false);
|
|
32
|
+
expect(isValidCountryCode("12")).toBe(false);
|
|
33
|
+
|
|
34
|
+
expect(validateCountryCode("US")).toBeNull();
|
|
35
|
+
expect(validateCountryCode("zz")).toBeNull();
|
|
36
|
+
expect(validateCountryCode(["US", "CA"])).toBeNull();
|
|
37
|
+
expect(validateCountryCode([])).toBe("At least one country code is required");
|
|
38
|
+
expect(validateCountryCode(["US", "123"])).toBe("Country code must be a 2-letter code");
|
|
39
|
+
});
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { encodeBase32, encodeBase64 } from "./bytes";
|
|
2
|
+
import { HexclaveAssertionError } from "./errors";
|
|
3
|
+
import { globalVar } from "./globals";
|
|
4
|
+
import { Result } from "./results";
|
|
5
|
+
import { toArrayBufferBacked } from "./typed-arrays";
|
|
6
|
+
|
|
7
|
+
export function generateRandomValues(array: Uint8Array): typeof array {
|
|
8
|
+
if (!globalVar.crypto) {
|
|
9
|
+
throw new HexclaveAssertionError("Crypto API is not available in this environment. Are you using an old browser?");
|
|
10
|
+
}
|
|
11
|
+
if (!globalVar.crypto.getRandomValues) {
|
|
12
|
+
throw new HexclaveAssertionError("crypto.getRandomValues is not available in this environment. Are you using an old browser?");
|
|
13
|
+
}
|
|
14
|
+
return globalVar.crypto.getRandomValues(array);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generates a secure alphanumeric string using the system's cryptographically secure
|
|
19
|
+
* random number generator.
|
|
20
|
+
*/
|
|
21
|
+
export function generateSecureRandomString(minBitsOfEntropy: number = 224) {
|
|
22
|
+
const base32CharactersCount = Math.ceil(minBitsOfEntropy / 5);
|
|
23
|
+
const bytesCount = Math.ceil(base32CharactersCount * 5 / 8);
|
|
24
|
+
const randomBytes = generateRandomValues(new Uint8Array(bytesCount));
|
|
25
|
+
const str = encodeBase32(randomBytes);
|
|
26
|
+
return str.slice(str.length - base32CharactersCount).toLowerCase();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function getDerivedSymmetricKey(purpose: string, secret: string | Uint8Array, salt: Uint8Array) {
|
|
30
|
+
const secretBytes = typeof secret === "string" ? new TextEncoder().encode(secret) : toArrayBufferBacked(secret);
|
|
31
|
+
const originalSecretKey = await crypto.subtle.importKey("raw", secretBytes, "HKDF", false, ["deriveKey"]);
|
|
32
|
+
return await crypto.subtle.deriveKey(
|
|
33
|
+
{
|
|
34
|
+
name: "HKDF",
|
|
35
|
+
salt: toArrayBufferBacked(salt),
|
|
36
|
+
hash: "SHA-256",
|
|
37
|
+
// NOTE (Hexclave rebrand): do NOT rename this "stack-*" literal. It is a domain-separation
|
|
38
|
+
// tag baked into HKDF key derivation; changing it would derive different keys and make all
|
|
39
|
+
// previously-encrypted data undecryptable. It is an internal constant, never user-visible.
|
|
40
|
+
info: new TextEncoder().encode(JSON.stringify([
|
|
41
|
+
"stack-crypto-helper-derived-symmetric-key",
|
|
42
|
+
purpose,
|
|
43
|
+
typeof secret === "string" ? "string-key" : "binary-key",
|
|
44
|
+
encodeBase64(salt),
|
|
45
|
+
])),
|
|
46
|
+
},
|
|
47
|
+
originalSecretKey,
|
|
48
|
+
{ name: "AES-GCM", length: 256 },
|
|
49
|
+
false,
|
|
50
|
+
["encrypt", "decrypt"]
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function encrypt({ purpose, secret, value }: { purpose: string, secret: string | Uint8Array, value: Uint8Array }) {
|
|
55
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
56
|
+
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
57
|
+
const derivedSecretKey = await getDerivedSymmetricKey(purpose, secret, salt);
|
|
58
|
+
|
|
59
|
+
const cipher = await crypto.subtle.encrypt({
|
|
60
|
+
name: "AES-GCM",
|
|
61
|
+
iv,
|
|
62
|
+
}, derivedSecretKey, toArrayBufferBacked(value));
|
|
63
|
+
|
|
64
|
+
const version = [0x01, 0x00];
|
|
65
|
+
return new Uint8Array([...version, ...salt, ...iv, ...new Uint8Array(cipher)]);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function decrypt({ purpose, secret, cipher }: { purpose: string, secret: string | Uint8Array, cipher: Uint8Array }) {
|
|
69
|
+
const version = cipher.slice(0, 2);
|
|
70
|
+
if (version[0] !== 0x01 || version[1] !== 0x00) throw new HexclaveAssertionError("Invalid ciphertext version in decrypt(...); expected 0x0100", { purpose });
|
|
71
|
+
const salt = cipher.slice(2, 18);
|
|
72
|
+
const iv = cipher.slice(18, 30);
|
|
73
|
+
const cipherBytes = cipher.slice(30);
|
|
74
|
+
const derivedSecretKey = await getDerivedSymmetricKey(purpose, secret, salt);
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const plaintext = await crypto.subtle.decrypt({
|
|
78
|
+
name: "AES-GCM",
|
|
79
|
+
iv,
|
|
80
|
+
}, derivedSecretKey, cipherBytes);
|
|
81
|
+
return Result.ok(new Uint8Array(plaintext));
|
|
82
|
+
} catch (e) {
|
|
83
|
+
if (e instanceof DOMException && e.name === "OperationError") {
|
|
84
|
+
return Result.error(new Error("Invalid ciphertext or secret when decrypting encrypted value", { cause: e }));
|
|
85
|
+
}
|
|
86
|
+
throw e;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
import.meta.vitest?.test("encrypt & decrypt", async ({ expect }) => {
|
|
91
|
+
const encryptAndDecrypt = async (encryptPurpose: string, decryptPurpose: string, encryptSecret: string | Uint8Array, decryptSecret: string | Uint8Array, value: Uint8Array) => {
|
|
92
|
+
const encrypted = await encrypt({ purpose: encryptPurpose, secret: encryptSecret, value });
|
|
93
|
+
const decrypted = await decrypt({ purpose: decryptPurpose, secret: decryptSecret, cipher: encrypted });
|
|
94
|
+
return decrypted;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const exampleBytes = new TextEncoder().encode("hello");
|
|
98
|
+
|
|
99
|
+
const exampleKey1 = crypto.getRandomValues(new Uint8Array(32));
|
|
100
|
+
const exampleKey2 = crypto.getRandomValues(new Uint8Array(32));
|
|
101
|
+
|
|
102
|
+
expect(await encryptAndDecrypt("p", "p", "secret", "secret", exampleBytes)).toEqual(Result.ok(exampleBytes));
|
|
103
|
+
expect(await encryptAndDecrypt("p", "p", exampleKey1, exampleKey1, exampleBytes)).toEqual(Result.ok(exampleBytes));
|
|
104
|
+
expect(await encryptAndDecrypt("p", "p", exampleKey1, "secret", exampleBytes)).toEqual(Result.error(expect.objectContaining({ message: "Invalid ciphertext or secret when decrypting encrypted value" })));
|
|
105
|
+
expect(await encryptAndDecrypt("p", "p", exampleKey1, exampleKey2, exampleBytes)).toEqual(Result.error(expect.objectContaining({ message: "Invalid ciphertext or secret when decrypting encrypted value" })));
|
|
106
|
+
expect(await encryptAndDecrypt("p", "not-p", exampleKey1, exampleKey1, exampleBytes)).toEqual(Result.error(expect.objectContaining({ message: "Invalid ciphertext or secret when decrypting encrypted value" })));
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
export type HashOptions = {
|
|
110
|
+
purpose: string,
|
|
111
|
+
salt?: string | Uint8Array,
|
|
112
|
+
extra?: string | Uint8Array,
|
|
113
|
+
value: string | Uint8Array,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export async function hash(options: HashOptions) {
|
|
117
|
+
return await iteratedHash({ ...options, iterations: 1 });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function iteratedHash(options: HashOptions & { iterations: number }) {
|
|
121
|
+
const stringOrUint8ArrayToUint8Array = (value: string | Uint8Array) => typeof value === "string" ? new TextEncoder().encode(value) : toArrayBufferBacked(value);
|
|
122
|
+
const stringOrUint8ArrayToBase64 = (value: string | Uint8Array) => encodeBase64(stringOrUint8ArrayToUint8Array(value));
|
|
123
|
+
const input = await crypto.subtle.importKey(
|
|
124
|
+
"raw",
|
|
125
|
+
stringOrUint8ArrayToUint8Array(options.value),
|
|
126
|
+
"PBKDF2",
|
|
127
|
+
false,
|
|
128
|
+
["deriveBits"]
|
|
129
|
+
);
|
|
130
|
+
return new Uint8Array(await crypto.subtle.deriveBits({
|
|
131
|
+
name: "PBKDF2",
|
|
132
|
+
// NOTE (Hexclave rebrand): do NOT rename this "stack-*" literal. It is a domain-separation tag
|
|
133
|
+
// mixed into the PBKDF2 salt; changing it would change every hash output and break verification
|
|
134
|
+
// of all previously-hashed values. It is an internal constant, never user-visible.
|
|
135
|
+
salt: new TextEncoder().encode(JSON.stringify([
|
|
136
|
+
"stack-crypto-helper-iterated-hash",
|
|
137
|
+
options.purpose,
|
|
138
|
+
stringOrUint8ArrayToBase64(options.salt ?? ""),
|
|
139
|
+
stringOrUint8ArrayToBase64(options.extra ?? ""),
|
|
140
|
+
])),
|
|
141
|
+
iterations: options.iterations,
|
|
142
|
+
hash: "SHA-256",
|
|
143
|
+
}, input, 256));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
import.meta.vitest?.test("iteratedHash", async ({ expect }) => {
|
|
147
|
+
const valueBytes = new TextEncoder().encode("hello");
|
|
148
|
+
const incrementBytes = new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10]);
|
|
149
|
+
|
|
150
|
+
const hash = await iteratedHash({ purpose: "purpose", value: valueBytes, iterations: 100_000 });
|
|
151
|
+
const hash2 = await iteratedHash({ purpose: "purpose", value: valueBytes, iterations: 100_000 });
|
|
152
|
+
const hashWithDifferentPurpose = await iteratedHash({ purpose: "different-purpose", value: valueBytes, iterations: 100_000 });
|
|
153
|
+
const hashWithEmptySalt = await iteratedHash({ purpose: "purpose", value: valueBytes, salt: new Uint8Array(0), iterations: 100_000 });
|
|
154
|
+
const hashWithDifferentSalt = await iteratedHash({ purpose: "purpose", value: valueBytes, salt: incrementBytes, iterations: 100_000 });
|
|
155
|
+
const hashWithEmptyExtra = await iteratedHash({ purpose: "purpose", value: valueBytes, extra: new Uint8Array(0), iterations: 100_000 });
|
|
156
|
+
const hashWithDifferentExtra = await iteratedHash({ purpose: "purpose", value: valueBytes, extra: incrementBytes, iterations: 100_000 });
|
|
157
|
+
const hashWithDifferentValue = await iteratedHash({ purpose: "purpose", value: new TextEncoder().encode("hello2"), iterations: 100_000 });
|
|
158
|
+
const hashWithDifferentSaltAndExtra = await iteratedHash({ purpose: "purpose", value: valueBytes, salt: incrementBytes, extra: incrementBytes, iterations: 100_000 });
|
|
159
|
+
const hashWithDifferentIterations = await iteratedHash({ purpose: "purpose", value: valueBytes, iterations: 100_001 });
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
expect(hash).toEqual(hash2);
|
|
163
|
+
expect(hash).not.toEqual(hashWithDifferentPurpose);
|
|
164
|
+
expect(hash).toEqual(hashWithEmptySalt);
|
|
165
|
+
expect(hash).not.toEqual(hashWithDifferentSalt);
|
|
166
|
+
expect(hash).toEqual(hashWithEmptyExtra);
|
|
167
|
+
expect(hash).not.toEqual(hashWithDifferentExtra);
|
|
168
|
+
expect(hash).not.toEqual(hashWithDifferentValue);
|
|
169
|
+
expect(hash).not.toEqual(hashWithDifferentIterations);
|
|
170
|
+
|
|
171
|
+
expect(hashWithDifferentSalt).not.toEqual(hashWithDifferentExtra);
|
|
172
|
+
expect(hashWithDifferentSalt).not.toEqual(hashWithDifferentSaltAndExtra);
|
|
173
|
+
expect(hashWithDifferentExtra).not.toEqual(hashWithDifferentSaltAndExtra);
|
|
174
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { moneyAmountSchema } from "../schema-fields";
|
|
2
|
+
import { SUPPORTED_CURRENCIES, type Currency, type MoneyAmount } from "./currency-constants";
|
|
3
|
+
import { HexclaveAssertionError } from "./errors";
|
|
4
|
+
|
|
5
|
+
export type SupportedCurrency = (typeof SUPPORTED_CURRENCIES)[number];
|
|
6
|
+
|
|
7
|
+
export function moneyAmountToStripeUnits(amount: MoneyAmount, currency: Currency): number {
|
|
8
|
+
const validated = moneyAmountSchema(currency).defined().validateSync(amount);
|
|
9
|
+
if (currency.stripeDecimals !== currency.decimals) {
|
|
10
|
+
throw new HexclaveAssertionError("unimplemented: TODO support different decimal configurations");
|
|
11
|
+
}
|
|
12
|
+
// `moneyAmountSchema` accepts strings with 0..currency.decimals fractional
|
|
13
|
+
// digits, so we must right-pad the fractional part before stripping the dot.
|
|
14
|
+
// A naïve `replace('.', '')` underweights inputs like "5" → 5 (should be 500
|
|
15
|
+
// for USD) and "0.5" → 5 (should be 50).
|
|
16
|
+
const [whole, fractional = ""] = validated.split(".");
|
|
17
|
+
const paddedFractional = fractional.padEnd(currency.decimals, "0");
|
|
18
|
+
return Number.parseInt(whole + paddedFractional, 10);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
import.meta.vitest?.describe("moneyAmountToStripeUnits", (test) => {
|
|
22
|
+
const USD = SUPPORTED_CURRENCIES.find((c) => c.code === "USD")!;
|
|
23
|
+
|
|
24
|
+
test("converts fully-padded USD amounts", ({ expect }) => {
|
|
25
|
+
expect(moneyAmountToStripeUnits("5.00" as MoneyAmount, USD)).toBe(500);
|
|
26
|
+
expect(moneyAmountToStripeUnits("0.01" as MoneyAmount, USD)).toBe(1);
|
|
27
|
+
expect(moneyAmountToStripeUnits("12.34" as MoneyAmount, USD)).toBe(1234);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("converts whole-number USD amounts (no decimals)", ({ expect }) => {
|
|
31
|
+
expect(moneyAmountToStripeUnits("5" as MoneyAmount, USD)).toBe(500);
|
|
32
|
+
expect(moneyAmountToStripeUnits("10" as MoneyAmount, USD)).toBe(1000);
|
|
33
|
+
expect(moneyAmountToStripeUnits("0" as MoneyAmount, USD)).toBe(0);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("converts USD amounts with one decimal digit", ({ expect }) => {
|
|
37
|
+
expect(moneyAmountToStripeUnits("5.5" as MoneyAmount, USD)).toBe(550);
|
|
38
|
+
expect(moneyAmountToStripeUnits("0.5" as MoneyAmount, USD)).toBe(50);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("matches stripeUnitsToMoneyAmount round-trip", ({ expect }) => {
|
|
42
|
+
for (const cents of [0, 1, 5, 50, 99, 100, 1234, 100000]) {
|
|
43
|
+
const padded = `${Math.floor(cents / 100)}.${(cents % 100).toString().padStart(2, "0")}` as MoneyAmount;
|
|
44
|
+
expect(moneyAmountToStripeUnits(padded, USD)).toBe(cents);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("rejects invalid money strings via schema", ({ expect }) => {
|
|
49
|
+
expect(() => moneyAmountToStripeUnits("abc" as MoneyAmount, USD)).toThrow();
|
|
50
|
+
expect(() => moneyAmountToStripeUnits("5.555" as MoneyAmount, USD)).toThrow();
|
|
51
|
+
expect(() => moneyAmountToStripeUnits("05" as MoneyAmount, USD)).toThrow();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A decimal string representing a monetary amount, e.g. `"9.99"`, `"0.01"`, or `"1000"`.
|
|
3
|
+
*
|
|
4
|
+
* This is NOT an integer in cents/minor units — it is always a human-readable decimal string.
|
|
5
|
+
* For example, nine dollars and ninety-nine cents is `"9.99"`, not `999`.
|
|
6
|
+
*/
|
|
7
|
+
export type MoneyAmount = `${number}` | `${number}.${number}`;
|
|
8
|
+
|
|
9
|
+
export type Currency = {
|
|
10
|
+
code: Uppercase<string>,
|
|
11
|
+
decimals: number,
|
|
12
|
+
stripeDecimals: number,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const SUPPORTED_CURRENCIES = [
|
|
16
|
+
{
|
|
17
|
+
code: 'USD',
|
|
18
|
+
decimals: 2,
|
|
19
|
+
stripeDecimals: 2,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
code: 'EUR',
|
|
23
|
+
decimals: 2,
|
|
24
|
+
stripeDecimals: 2,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
code: 'GBP',
|
|
28
|
+
decimals: 2,
|
|
29
|
+
stripeDecimals: 2,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
code: 'JPY',
|
|
33
|
+
decimals: 0,
|
|
34
|
+
stripeDecimals: 0,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
code: 'INR',
|
|
38
|
+
decimals: 2,
|
|
39
|
+
stripeDecimals: 2,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
code: 'AUD',
|
|
43
|
+
decimals: 2,
|
|
44
|
+
stripeDecimals: 2,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
code: 'CAD',
|
|
48
|
+
decimals: 2,
|
|
49
|
+
stripeDecimals: 2,
|
|
50
|
+
},
|
|
51
|
+
] as const satisfies Currency[];
|