@voyant-travel/storefront 0.120.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +231 -0
  3. package/dist/booking-intents.d.ts +42 -0
  4. package/dist/booking-intents.d.ts.map +1 -0
  5. package/dist/booking-intents.js +83 -0
  6. package/dist/customer-portal/index.d.ts +16 -0
  7. package/dist/customer-portal/index.d.ts.map +1 -0
  8. package/dist/customer-portal/index.js +23 -0
  9. package/dist/customer-portal/route-runtime.d.ts +16 -0
  10. package/dist/customer-portal/route-runtime.d.ts.map +1 -0
  11. package/dist/customer-portal/route-runtime.js +27 -0
  12. package/dist/customer-portal/routes-public.d.ts +1936 -0
  13. package/dist/customer-portal/routes-public.d.ts.map +1 -0
  14. package/dist/customer-portal/routes-public.js +165 -0
  15. package/dist/customer-portal/routes.d.ts +43 -0
  16. package/dist/customer-portal/routes.d.ts.map +1 -0
  17. package/dist/customer-portal/routes.js +17 -0
  18. package/dist/customer-portal/service-public-impl.d.ts +138 -0
  19. package/dist/customer-portal/service-public-impl.d.ts.map +1 -0
  20. package/dist/customer-portal/service-public-impl.js +1808 -0
  21. package/dist/customer-portal/service-public.d.ts +2 -0
  22. package/dist/customer-portal/service-public.d.ts.map +1 -0
  23. package/dist/customer-portal/service-public.js +1 -0
  24. package/dist/customer-portal/validation-public/bookings.d.ts +551 -0
  25. package/dist/customer-portal/validation-public/bookings.d.ts.map +1 -0
  26. package/dist/customer-portal/validation-public/bookings.js +132 -0
  27. package/dist/customer-portal/validation-public/common.d.ts +162 -0
  28. package/dist/customer-portal/validation-public/common.d.ts.map +1 -0
  29. package/dist/customer-portal/validation-public/common.js +139 -0
  30. package/dist/customer-portal/validation-public/profile.d.ts +749 -0
  31. package/dist/customer-portal/validation-public/profile.d.ts.map +1 -0
  32. package/dist/customer-portal/validation-public/profile.js +308 -0
  33. package/dist/customer-portal/validation-public.d.ts +3 -0
  34. package/dist/customer-portal/validation-public.d.ts.map +1 -0
  35. package/dist/customer-portal/validation-public.js +2 -0
  36. package/dist/guest-booking-guard.d.ts +24 -0
  37. package/dist/guest-booking-guard.d.ts.map +1 -0
  38. package/dist/guest-booking-guard.js +55 -0
  39. package/dist/index.d.ts +23 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +41 -0
  42. package/dist/product-extra-ref.d.ts +238 -0
  43. package/dist/product-extra-ref.d.ts.map +1 -0
  44. package/dist/product-extra-ref.js +22 -0
  45. package/dist/routes-admin.d.ts +220 -0
  46. package/dist/routes-admin.d.ts.map +1 -0
  47. package/dist/routes-admin.js +28 -0
  48. package/dist/routes-public.d.ts +1475 -0
  49. package/dist/routes-public.d.ts.map +1 -0
  50. package/dist/routes-public.js +362 -0
  51. package/dist/service-booking-session-bootstrap.d.ts +227 -0
  52. package/dist/service-booking-session-bootstrap.d.ts.map +1 -0
  53. package/dist/service-booking-session-bootstrap.js +287 -0
  54. package/dist/service-boundary-resource-sql.d.ts +18 -0
  55. package/dist/service-boundary-resource-sql.d.ts.map +1 -0
  56. package/dist/service-boundary-resource-sql.js +73 -0
  57. package/dist/service-boundary-sql.d.ts +103 -0
  58. package/dist/service-boundary-sql.d.ts.map +1 -0
  59. package/dist/service-boundary-sql.js +307 -0
  60. package/dist/service-departures-core.d.ts +41 -0
  61. package/dist/service-departures-core.d.ts.map +1 -0
  62. package/dist/service-departures-core.js +92 -0
  63. package/dist/service-departures-extensions.d.ts +46 -0
  64. package/dist/service-departures-extensions.d.ts.map +1 -0
  65. package/dist/service-departures-extensions.js +81 -0
  66. package/dist/service-departures-offers.d.ts +220 -0
  67. package/dist/service-departures-offers.d.ts.map +1 -0
  68. package/dist/service-departures-offers.js +177 -0
  69. package/dist/service-departures-price-preview.d.ts +306 -0
  70. package/dist/service-departures-price-preview.d.ts.map +1 -0
  71. package/dist/service-departures-price-preview.js +383 -0
  72. package/dist/service-departures-pricing-context.d.ts +115 -0
  73. package/dist/service-departures-pricing-context.d.ts.map +1 -0
  74. package/dist/service-departures-pricing-context.js +237 -0
  75. package/dist/service-departures-pricing.d.ts +5 -0
  76. package/dist/service-departures-pricing.d.ts.map +1 -0
  77. package/dist/service-departures-pricing.js +4 -0
  78. package/dist/service-departures.d.ts +192 -0
  79. package/dist/service-departures.d.ts.map +1 -0
  80. package/dist/service-departures.js +213 -0
  81. package/dist/service-intake.d.ts +130 -0
  82. package/dist/service-intake.d.ts.map +1 -0
  83. package/dist/service-intake.js +274 -0
  84. package/dist/service-transport-eligibility.d.ts +10 -0
  85. package/dist/service-transport-eligibility.d.ts.map +1 -0
  86. package/dist/service-transport-eligibility.js +198 -0
  87. package/dist/service.d.ts +1062 -0
  88. package/dist/service.d.ts.map +1 -0
  89. package/dist/service.js +332 -0
  90. package/dist/transport-eligibility.d.ts +4 -0
  91. package/dist/transport-eligibility.d.ts.map +1 -0
  92. package/dist/transport-eligibility.js +2 -0
  93. package/dist/validation/departures.d.ts +1669 -0
  94. package/dist/validation/departures.d.ts.map +1 -0
  95. package/dist/validation/departures.js +397 -0
  96. package/dist/validation/intake.d.ts +147 -0
  97. package/dist/validation/intake.d.ts.map +1 -0
  98. package/dist/validation/intake.js +69 -0
  99. package/dist/validation/offers.d.ts +340 -0
  100. package/dist/validation/offers.d.ts.map +1 -0
  101. package/dist/validation/offers.js +117 -0
  102. package/dist/validation-settings.d.ts +609 -0
  103. package/dist/validation-settings.d.ts.map +1 -0
  104. package/dist/validation-settings.js +235 -0
  105. package/dist/validation-transport-eligibility.d.ts +314 -0
  106. package/dist/validation-transport-eligibility.d.ts.map +1 -0
  107. package/dist/validation-transport-eligibility.js +97 -0
  108. package/dist/validation.d.ts +6 -0
  109. package/dist/validation.d.ts.map +1 -0
  110. package/dist/validation.js +4 -0
  111. package/dist/verification/index.d.ts +12 -0
  112. package/dist/verification/index.d.ts.map +1 -0
  113. package/dist/verification/index.js +18 -0
  114. package/dist/verification/routes-public.d.ts +121 -0
  115. package/dist/verification/routes-public.d.ts.map +1 -0
  116. package/dist/verification/routes-public.js +125 -0
  117. package/dist/verification/schema.d.ts +273 -0
  118. package/dist/verification/schema.d.ts.map +1 -0
  119. package/dist/verification/schema.js +50 -0
  120. package/dist/verification/service.d.ts +114 -0
  121. package/dist/verification/service.d.ts.map +1 -0
  122. package/dist/verification/service.js +283 -0
  123. package/dist/verification/validation.d.ts +98 -0
  124. package/dist/verification/validation.d.ts.map +1 -0
  125. package/dist/verification/validation.js +54 -0
  126. package/package.json +148 -0
@@ -0,0 +1,114 @@
1
+ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
2
+ import type { ConfirmEmailVerificationChallengeInput, ConfirmSmsVerificationChallengeInput, StartEmailVerificationChallengeInput, StartSmsVerificationChallengeInput } from "./validation.js";
3
+ export interface StorefrontVerificationServiceOptions {
4
+ codeLength?: number;
5
+ expiresInSeconds?: number;
6
+ maxAttempts?: number;
7
+ now?: () => Date;
8
+ }
9
+ export interface StorefrontVerificationDeliveryResult {
10
+ id?: string;
11
+ provider?: string;
12
+ }
13
+ export type StorefrontVerificationNotificationChannel = "email" | "sms" | (string & {});
14
+ export interface StorefrontVerificationNotificationPayload {
15
+ to: string;
16
+ channel: StorefrontVerificationNotificationChannel;
17
+ provider?: string;
18
+ template: string;
19
+ data?: unknown;
20
+ subject?: string;
21
+ text?: string;
22
+ }
23
+ export interface StorefrontVerificationNotificationResult {
24
+ id?: string;
25
+ provider: string;
26
+ }
27
+ export interface StorefrontVerificationNotificationProvider {
28
+ readonly name: string;
29
+ readonly channels: ReadonlyArray<StorefrontVerificationNotificationChannel>;
30
+ send(payload: StorefrontVerificationNotificationPayload): Promise<StorefrontVerificationNotificationResult>;
31
+ }
32
+ export interface StorefrontVerificationEmailSendInput {
33
+ email: string;
34
+ code: string;
35
+ purpose: string;
36
+ locale?: string | null;
37
+ expiresAt: Date;
38
+ metadata?: Record<string, unknown> | null;
39
+ }
40
+ export interface StorefrontVerificationSmsSendInput {
41
+ phone: string;
42
+ code: string;
43
+ purpose: string;
44
+ locale?: string | null;
45
+ expiresAt: Date;
46
+ metadata?: Record<string, unknown> | null;
47
+ }
48
+ export interface StorefrontVerificationSenders {
49
+ sendEmailChallenge?: (input: StorefrontVerificationEmailSendInput) => Promise<StorefrontVerificationDeliveryResult | undefined>;
50
+ sendSmsChallenge?: (input: StorefrontVerificationSmsSendInput) => Promise<StorefrontVerificationDeliveryResult | undefined>;
51
+ }
52
+ export interface StorefrontVerificationProviderOptions {
53
+ email?: {
54
+ provider?: string;
55
+ template?: string;
56
+ subject?: string | ((input: StorefrontVerificationEmailSendInput) => string);
57
+ };
58
+ sms?: {
59
+ provider?: string;
60
+ template?: string;
61
+ };
62
+ }
63
+ export declare class StorefrontVerificationError extends Error {
64
+ readonly code: "sender_not_configured" | "challenge_not_found" | "challenge_expired" | "challenge_invalid" | "challenge_failed";
65
+ constructor(message: string, code: "sender_not_configured" | "challenge_not_found" | "challenge_expired" | "challenge_invalid" | "challenge_failed");
66
+ }
67
+ export declare function createStorefrontVerificationSendersFromProviders(providers: ReadonlyArray<StorefrontVerificationNotificationProvider>, options?: StorefrontVerificationProviderOptions): StorefrontVerificationSenders;
68
+ export declare function createStorefrontVerificationService(options?: StorefrontVerificationServiceOptions): {
69
+ startEmailChallenge(db: PostgresJsDatabase, input: StartEmailVerificationChallengeInput, senders: StorefrontVerificationSenders): Promise<{
70
+ id: string;
71
+ channel: "email" | "sms";
72
+ destination: string;
73
+ purpose: string;
74
+ status: "failed" | "pending" | "cancelled" | "expired" | "verified";
75
+ expiresAt: Date;
76
+ verifiedAt: Date | null;
77
+ createdAt: Date;
78
+ updatedAt: Date;
79
+ }>;
80
+ startSmsChallenge(db: PostgresJsDatabase, input: StartSmsVerificationChallengeInput, senders: StorefrontVerificationSenders): Promise<{
81
+ id: string;
82
+ channel: "email" | "sms";
83
+ destination: string;
84
+ purpose: string;
85
+ status: "failed" | "pending" | "cancelled" | "expired" | "verified";
86
+ expiresAt: Date;
87
+ verifiedAt: Date | null;
88
+ createdAt: Date;
89
+ updatedAt: Date;
90
+ }>;
91
+ confirmEmailChallenge(db: PostgresJsDatabase, input: ConfirmEmailVerificationChallengeInput): Promise<{
92
+ id: string;
93
+ channel: "email" | "sms";
94
+ destination: string;
95
+ purpose: string;
96
+ status: "failed" | "pending" | "cancelled" | "expired" | "verified";
97
+ expiresAt: Date;
98
+ verifiedAt: Date | null;
99
+ createdAt: Date;
100
+ updatedAt: Date;
101
+ }>;
102
+ confirmSmsChallenge(db: PostgresJsDatabase, input: ConfirmSmsVerificationChallengeInput): Promise<{
103
+ id: string;
104
+ channel: "email" | "sms";
105
+ destination: string;
106
+ purpose: string;
107
+ status: "failed" | "pending" | "cancelled" | "expired" | "verified";
108
+ expiresAt: Date;
109
+ verifiedAt: Date | null;
110
+ createdAt: Date;
111
+ updatedAt: Date;
112
+ }>;
113
+ };
114
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/verification/service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,283 @@
1
+ import { and, desc, eq } from "drizzle-orm";
2
+ import { storefrontVerificationChallenges } from "./schema.js";
3
+ export class StorefrontVerificationError extends Error {
4
+ code;
5
+ constructor(message, code) {
6
+ super(message);
7
+ this.code = code;
8
+ this.name = "StorefrontVerificationError";
9
+ }
10
+ }
11
+ function normalizeEmail(value) {
12
+ return value.trim().toLowerCase();
13
+ }
14
+ function normalizePhone(value) {
15
+ return value.trim();
16
+ }
17
+ function generateVerificationCode(length) {
18
+ const chars = "0123456789";
19
+ const bytes = new Uint8Array(length);
20
+ crypto.getRandomValues(bytes);
21
+ return Array.from(bytes, (byte) => chars[byte % chars.length]).join("");
22
+ }
23
+ async function hashVerificationCode(code) {
24
+ const bytes = new TextEncoder().encode(code);
25
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
26
+ return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
27
+ }
28
+ function toChallengeRecord(row) {
29
+ return {
30
+ id: row.id,
31
+ channel: row.channel,
32
+ destination: row.destination,
33
+ purpose: row.purpose,
34
+ status: row.status,
35
+ expiresAt: row.expiresAt,
36
+ verifiedAt: row.verifiedAt ?? null,
37
+ createdAt: row.createdAt,
38
+ updatedAt: row.updatedAt,
39
+ };
40
+ }
41
+ function requireChallengeRow(row, operation) {
42
+ if (!row) {
43
+ throw new Error(`Storefront verification ${operation} did not return a challenge row`);
44
+ }
45
+ return row;
46
+ }
47
+ async function getLatestChallenge(db, channel, destination, purpose) {
48
+ const [row] = await db
49
+ .select()
50
+ .from(storefrontVerificationChallenges)
51
+ .where(and(eq(storefrontVerificationChallenges.channel, channel), eq(storefrontVerificationChallenges.destination, destination), eq(storefrontVerificationChallenges.purpose, purpose)))
52
+ .orderBy(desc(storefrontVerificationChallenges.updatedAt), desc(storefrontVerificationChallenges.createdAt))
53
+ .limit(1);
54
+ return row ?? null;
55
+ }
56
+ async function startChallenge(db, channel, destination, purpose, metadata, options) {
57
+ const now = options?.now?.() ?? new Date();
58
+ const codeLength = Math.max(4, Math.min(8, options?.codeLength ?? 6));
59
+ const maxAttempts = Math.max(1, options?.maxAttempts ?? 5);
60
+ const expiresInSeconds = Math.max(60, options?.expiresInSeconds ?? 600);
61
+ const expiresAt = new Date(now.getTime() + expiresInSeconds * 1000);
62
+ const code = generateVerificationCode(codeLength);
63
+ const codeHash = await hashVerificationCode(code);
64
+ const existing = await getLatestChallenge(db, channel, destination, purpose);
65
+ if (existing && existing.status === "pending" && existing.expiresAt > now) {
66
+ const [updated] = await db
67
+ .update(storefrontVerificationChallenges)
68
+ .set({
69
+ codeHash,
70
+ attemptCount: 0,
71
+ maxAttempts,
72
+ expiresAt,
73
+ lastSentAt: now,
74
+ failedAt: null,
75
+ verifiedAt: null,
76
+ metadata: metadata ?? null,
77
+ updatedAt: now,
78
+ })
79
+ .where(eq(storefrontVerificationChallenges.id, existing.id))
80
+ .returning();
81
+ return { challenge: requireChallengeRow(updated, "update"), code };
82
+ }
83
+ if (existing && existing.status === "pending" && existing.expiresAt <= now) {
84
+ await db
85
+ .update(storefrontVerificationChallenges)
86
+ .set({
87
+ status: "expired",
88
+ failedAt: now,
89
+ updatedAt: now,
90
+ })
91
+ .where(eq(storefrontVerificationChallenges.id, existing.id));
92
+ }
93
+ const [created] = await db
94
+ .insert(storefrontVerificationChallenges)
95
+ .values({
96
+ channel,
97
+ destination,
98
+ purpose,
99
+ codeHash,
100
+ status: "pending",
101
+ attemptCount: 0,
102
+ maxAttempts,
103
+ expiresAt,
104
+ lastSentAt: now,
105
+ metadata: metadata ?? null,
106
+ createdAt: now,
107
+ updatedAt: now,
108
+ })
109
+ .returning();
110
+ return { challenge: requireChallengeRow(created, "insert"), code };
111
+ }
112
+ async function confirmChallenge(db, channel, destination, purpose, code, options) {
113
+ const now = options?.now?.() ?? new Date();
114
+ const row = await getLatestChallenge(db, channel, destination, purpose);
115
+ if (!row || row.status !== "pending") {
116
+ throw new StorefrontVerificationError("Verification challenge not found", "challenge_not_found");
117
+ }
118
+ if (row.expiresAt <= now) {
119
+ await db
120
+ .update(storefrontVerificationChallenges)
121
+ .set({
122
+ status: "expired",
123
+ failedAt: now,
124
+ updatedAt: now,
125
+ })
126
+ .where(eq(storefrontVerificationChallenges.id, row.id));
127
+ throw new StorefrontVerificationError("Verification challenge expired", "challenge_expired");
128
+ }
129
+ if (row.codeHash !== (await hashVerificationCode(code))) {
130
+ const nextAttemptCount = row.attemptCount + 1;
131
+ const terminal = nextAttemptCount >= row.maxAttempts;
132
+ await db
133
+ .update(storefrontVerificationChallenges)
134
+ .set({
135
+ attemptCount: nextAttemptCount,
136
+ status: terminal ? "failed" : row.status,
137
+ failedAt: terminal ? now : row.failedAt,
138
+ updatedAt: now,
139
+ })
140
+ .where(eq(storefrontVerificationChallenges.id, row.id));
141
+ throw new StorefrontVerificationError(terminal ? "Verification challenge failed" : "Invalid verification code", terminal ? "challenge_failed" : "challenge_invalid");
142
+ }
143
+ const [verified] = await db
144
+ .update(storefrontVerificationChallenges)
145
+ .set({
146
+ status: "verified",
147
+ verifiedAt: now,
148
+ updatedAt: now,
149
+ })
150
+ .where(eq(storefrontVerificationChallenges.id, row.id))
151
+ .returning();
152
+ return requireChallengeRow(verified, "confirm");
153
+ }
154
+ export function createStorefrontVerificationSendersFromProviders(providers, options = {}) {
155
+ const byChannel = new Map();
156
+ const byName = new Map();
157
+ for (const provider of providers) {
158
+ byName.set(provider.name, provider);
159
+ for (const channel of provider.channels) {
160
+ byChannel.set(channel, provider);
161
+ }
162
+ }
163
+ async function send(payload) {
164
+ const provider = payload.provider
165
+ ? byName.get(payload.provider)
166
+ : byChannel.get(payload.channel);
167
+ if (!provider) {
168
+ throw new StorefrontVerificationError(`No verification notification provider registered for channel "${payload.channel}"`, "sender_not_configured");
169
+ }
170
+ return provider.send(payload);
171
+ }
172
+ return {
173
+ async sendEmailChallenge(input) {
174
+ const subject = typeof options.email?.subject === "function"
175
+ ? options.email.subject(input)
176
+ : options.email?.subject;
177
+ const result = await send({
178
+ to: input.email,
179
+ channel: "email",
180
+ provider: options.email?.provider,
181
+ template: options.email?.template ?? "storefront-verification-email",
182
+ subject,
183
+ data: {
184
+ code: input.code,
185
+ purpose: input.purpose,
186
+ locale: input.locale ?? null,
187
+ expiresAt: input.expiresAt.toISOString(),
188
+ metadata: input.metadata ?? null,
189
+ },
190
+ });
191
+ return { id: result.id, provider: result.provider };
192
+ },
193
+ async sendSmsChallenge(input) {
194
+ const result = await send({
195
+ to: input.phone,
196
+ channel: "sms",
197
+ provider: options.sms?.provider,
198
+ template: options.sms?.template ?? "storefront-verification-sms",
199
+ data: {
200
+ code: input.code,
201
+ purpose: input.purpose,
202
+ locale: input.locale ?? null,
203
+ expiresAt: input.expiresAt.toISOString(),
204
+ metadata: input.metadata ?? null,
205
+ },
206
+ text: `${input.code} is your verification code.`,
207
+ });
208
+ return { id: result.id, provider: result.provider };
209
+ },
210
+ };
211
+ }
212
+ export function createStorefrontVerificationService(options) {
213
+ return {
214
+ async startEmailChallenge(db, input, senders) {
215
+ const email = normalizeEmail(input.email);
216
+ const { challenge, code } = await startChallenge(db, "email", email, input.purpose, input.metadata, options);
217
+ if (!senders.sendEmailChallenge) {
218
+ throw new StorefrontVerificationError("Email verification sender not configured", "sender_not_configured");
219
+ }
220
+ try {
221
+ await senders.sendEmailChallenge({
222
+ email,
223
+ code,
224
+ purpose: input.purpose,
225
+ locale: input.locale ?? null,
226
+ expiresAt: challenge.expiresAt,
227
+ metadata: input.metadata,
228
+ });
229
+ }
230
+ catch (error) {
231
+ const now = options?.now?.() ?? new Date();
232
+ await db
233
+ .update(storefrontVerificationChallenges)
234
+ .set({
235
+ status: "failed",
236
+ failedAt: now,
237
+ updatedAt: now,
238
+ })
239
+ .where(eq(storefrontVerificationChallenges.id, challenge.id));
240
+ throw error;
241
+ }
242
+ return toChallengeRecord(challenge);
243
+ },
244
+ async startSmsChallenge(db, input, senders) {
245
+ const phone = normalizePhone(input.phone);
246
+ const { challenge, code } = await startChallenge(db, "sms", phone, input.purpose, input.metadata, options);
247
+ if (!senders.sendSmsChallenge) {
248
+ throw new StorefrontVerificationError("SMS verification sender not configured", "sender_not_configured");
249
+ }
250
+ try {
251
+ await senders.sendSmsChallenge({
252
+ phone,
253
+ code,
254
+ purpose: input.purpose,
255
+ locale: input.locale ?? null,
256
+ expiresAt: challenge.expiresAt,
257
+ metadata: input.metadata,
258
+ });
259
+ }
260
+ catch (error) {
261
+ const now = options?.now?.() ?? new Date();
262
+ await db
263
+ .update(storefrontVerificationChallenges)
264
+ .set({
265
+ status: "failed",
266
+ failedAt: now,
267
+ updatedAt: now,
268
+ })
269
+ .where(eq(storefrontVerificationChallenges.id, challenge.id));
270
+ throw error;
271
+ }
272
+ return toChallengeRecord(challenge);
273
+ },
274
+ async confirmEmailChallenge(db, input) {
275
+ const verified = await confirmChallenge(db, "email", normalizeEmail(input.email), input.purpose, input.code, options);
276
+ return toChallengeRecord(verified);
277
+ },
278
+ async confirmSmsChallenge(db, input) {
279
+ const verified = await confirmChallenge(db, "sms", normalizePhone(input.phone), input.purpose, input.code, options);
280
+ return toChallengeRecord(verified);
281
+ },
282
+ };
283
+ }
@@ -0,0 +1,98 @@
1
+ import { z } from "zod";
2
+ export declare const storefrontVerificationChannelSchema: z.ZodEnum<{
3
+ email: "email";
4
+ sms: "sms";
5
+ }>;
6
+ export declare const storefrontVerificationStatusSchema: z.ZodEnum<{
7
+ failed: "failed";
8
+ pending: "pending";
9
+ cancelled: "cancelled";
10
+ expired: "expired";
11
+ verified: "verified";
12
+ }>;
13
+ export declare const startEmailVerificationChallengeSchema: z.ZodObject<{
14
+ email: z.ZodEmail;
15
+ purpose: z.ZodDefault<z.ZodString>;
16
+ locale: z.ZodNullable<z.ZodOptional<z.ZodString>>;
17
+ metadata: z.ZodNullable<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
18
+ }, z.core.$strip>;
19
+ export declare const startSmsVerificationChallengeSchema: z.ZodObject<{
20
+ phone: z.ZodString;
21
+ purpose: z.ZodDefault<z.ZodString>;
22
+ locale: z.ZodNullable<z.ZodOptional<z.ZodString>>;
23
+ metadata: z.ZodNullable<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
24
+ }, z.core.$strip>;
25
+ export declare const confirmEmailVerificationChallengeSchema: z.ZodObject<{
26
+ email: z.ZodEmail;
27
+ code: z.ZodString;
28
+ purpose: z.ZodDefault<z.ZodString>;
29
+ }, z.core.$strip>;
30
+ export declare const confirmSmsVerificationChallengeSchema: z.ZodObject<{
31
+ phone: z.ZodString;
32
+ code: z.ZodString;
33
+ purpose: z.ZodDefault<z.ZodString>;
34
+ }, z.core.$strip>;
35
+ export declare const storefrontVerificationChallengeRecordSchema: z.ZodObject<{
36
+ id: z.ZodString;
37
+ channel: z.ZodEnum<{
38
+ email: "email";
39
+ sms: "sms";
40
+ }>;
41
+ destination: z.ZodString;
42
+ purpose: z.ZodString;
43
+ status: z.ZodEnum<{
44
+ failed: "failed";
45
+ pending: "pending";
46
+ cancelled: "cancelled";
47
+ expired: "expired";
48
+ verified: "verified";
49
+ }>;
50
+ expiresAt: z.ZodDate;
51
+ verifiedAt: z.ZodNullable<z.ZodDate>;
52
+ createdAt: z.ZodDate;
53
+ updatedAt: z.ZodDate;
54
+ }, z.core.$strip>;
55
+ export declare const storefrontVerificationStartResultSchema: z.ZodObject<{
56
+ id: z.ZodString;
57
+ channel: z.ZodEnum<{
58
+ email: "email";
59
+ sms: "sms";
60
+ }>;
61
+ destination: z.ZodString;
62
+ purpose: z.ZodString;
63
+ status: z.ZodEnum<{
64
+ failed: "failed";
65
+ pending: "pending";
66
+ cancelled: "cancelled";
67
+ expired: "expired";
68
+ verified: "verified";
69
+ }>;
70
+ expiresAt: z.ZodDate;
71
+ verifiedAt: z.ZodNullable<z.ZodDate>;
72
+ createdAt: z.ZodDate;
73
+ updatedAt: z.ZodDate;
74
+ }, z.core.$strip>;
75
+ export declare const storefrontVerificationConfirmResultSchema: z.ZodObject<{
76
+ id: z.ZodString;
77
+ channel: z.ZodEnum<{
78
+ email: "email";
79
+ sms: "sms";
80
+ }>;
81
+ destination: z.ZodString;
82
+ purpose: z.ZodString;
83
+ expiresAt: z.ZodDate;
84
+ verifiedAt: z.ZodNullable<z.ZodDate>;
85
+ createdAt: z.ZodDate;
86
+ updatedAt: z.ZodDate;
87
+ status: z.ZodLiteral<"verified">;
88
+ }, z.core.$strip>;
89
+ export type StorefrontVerificationChannel = z.infer<typeof storefrontVerificationChannelSchema>;
90
+ export type StorefrontVerificationStatus = z.infer<typeof storefrontVerificationStatusSchema>;
91
+ export type StartEmailVerificationChallengeInput = z.infer<typeof startEmailVerificationChallengeSchema>;
92
+ export type StartSmsVerificationChallengeInput = z.infer<typeof startSmsVerificationChallengeSchema>;
93
+ export type ConfirmEmailVerificationChallengeInput = z.infer<typeof confirmEmailVerificationChallengeSchema>;
94
+ export type ConfirmSmsVerificationChallengeInput = z.infer<typeof confirmSmsVerificationChallengeSchema>;
95
+ export type StorefrontVerificationChallengeRecord = z.infer<typeof storefrontVerificationChallengeRecordSchema>;
96
+ export type StorefrontVerificationStartResult = z.infer<typeof storefrontVerificationStartResultSchema>;
97
+ export type StorefrontVerificationConfirmResult = z.infer<typeof storefrontVerificationConfirmResultSchema>;
98
+ //# sourceMappingURL=validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/verification/validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,eAAO,MAAM,mCAAmC;;;EAA2B,CAAA;AAC3E,eAAO,MAAM,kCAAkC;;;;;;EAM7C,CAAA;AAKF,eAAO,MAAM,qCAAqC;;;;;iBAKhD,CAAA;AAEF,eAAO,MAAM,mCAAmC;;;;;iBAK9C,CAAA;AAEF,eAAO,MAAM,uCAAuC;;;;iBAOlD,CAAA;AAEF,eAAO,MAAM,qCAAqC;;;;iBAOhD,CAAA;AAEF,eAAO,MAAM,2CAA2C;;;;;;;;;;;;;;;;;;;iBAUtD,CAAA;AAEF,eAAO,MAAM,uCAAuC;;;;;;;;;;;;;;;;;;;iBAA8C,CAAA;AAElG,eAAO,MAAM,yCAAyC;;;;;;;;;;;;;iBAGlD,CAAA;AAEJ,MAAM,MAAM,6BAA6B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mCAAmC,CAAC,CAAA;AAC/F,MAAM,MAAM,4BAA4B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kCAAkC,CAAC,CAAA;AAC7F,MAAM,MAAM,oCAAoC,GAAG,CAAC,CAAC,KAAK,CACxD,OAAO,qCAAqC,CAC7C,CAAA;AACD,MAAM,MAAM,kCAAkC,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mCAAmC,CAAC,CAAA;AACpG,MAAM,MAAM,sCAAsC,GAAG,CAAC,CAAC,KAAK,CAC1D,OAAO,uCAAuC,CAC/C,CAAA;AACD,MAAM,MAAM,oCAAoC,GAAG,CAAC,CAAC,KAAK,CACxD,OAAO,qCAAqC,CAC7C,CAAA;AACD,MAAM,MAAM,qCAAqC,GAAG,CAAC,CAAC,KAAK,CACzD,OAAO,2CAA2C,CACnD,CAAA;AACD,MAAM,MAAM,iCAAiC,GAAG,CAAC,CAAC,KAAK,CACrD,OAAO,uCAAuC,CAC/C,CAAA;AACD,MAAM,MAAM,mCAAmC,GAAG,CAAC,CAAC,KAAK,CACvD,OAAO,yCAAyC,CACjD,CAAA"}
@@ -0,0 +1,54 @@
1
+ import { z } from "zod";
2
+ export const storefrontVerificationChannelSchema = z.enum(["email", "sms"]);
3
+ export const storefrontVerificationStatusSchema = z.enum([
4
+ "pending",
5
+ "verified",
6
+ "expired",
7
+ "failed",
8
+ "cancelled",
9
+ ]);
10
+ const purposeSchema = z.string().trim().min(1).max(100).default("contact_confirmation");
11
+ const metadataSchema = z.record(z.string(), z.unknown()).optional().nullable();
12
+ export const startEmailVerificationChallengeSchema = z.object({
13
+ email: z.email(),
14
+ purpose: purposeSchema,
15
+ locale: z.string().trim().min(2).max(16).optional().nullable(),
16
+ metadata: metadataSchema,
17
+ });
18
+ export const startSmsVerificationChallengeSchema = z.object({
19
+ phone: z.string().trim().min(6).max(32),
20
+ purpose: purposeSchema,
21
+ locale: z.string().trim().min(2).max(16).optional().nullable(),
22
+ metadata: metadataSchema,
23
+ });
24
+ export const confirmEmailVerificationChallengeSchema = z.object({
25
+ email: z.email(),
26
+ code: z
27
+ .string()
28
+ .trim()
29
+ .regex(/^\d{4,8}$/),
30
+ purpose: purposeSchema,
31
+ });
32
+ export const confirmSmsVerificationChallengeSchema = z.object({
33
+ phone: z.string().trim().min(6).max(32),
34
+ code: z
35
+ .string()
36
+ .trim()
37
+ .regex(/^\d{4,8}$/),
38
+ purpose: purposeSchema,
39
+ });
40
+ export const storefrontVerificationChallengeRecordSchema = z.object({
41
+ id: z.string(),
42
+ channel: storefrontVerificationChannelSchema,
43
+ destination: z.string(),
44
+ purpose: z.string(),
45
+ status: storefrontVerificationStatusSchema,
46
+ expiresAt: z.date(),
47
+ verifiedAt: z.date().nullable(),
48
+ createdAt: z.date(),
49
+ updatedAt: z.date(),
50
+ });
51
+ export const storefrontVerificationStartResultSchema = storefrontVerificationChallengeRecordSchema;
52
+ export const storefrontVerificationConfirmResultSchema = storefrontVerificationChallengeRecordSchema.extend({
53
+ status: z.literal("verified"),
54
+ });