@voyantjs/storefront-verification 0.24.1 → 0.24.3

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.
@@ -0,0 +1,12 @@
1
+ import type { HonoModule } from "@voyantjs/hono/module";
2
+ import { type StorefrontVerificationRoutesOptions } from "./routes-public.js";
3
+ export type { StorefrontVerificationPublicRoutes, StorefrontVerificationRoutesOptions, } from "./routes-public.js";
4
+ export { buildStorefrontVerificationSenders, createStorefrontVerificationPublicRoutes, STOREFRONT_VERIFICATION_SENDERS_CONTAINER_KEY, } from "./routes-public.js";
5
+ export type { NewStorefrontVerificationChallenge, StorefrontVerificationChallenge, } from "./schema.js";
6
+ export { storefrontVerificationChallenges, storefrontVerificationChannelEnum, storefrontVerificationLinkable, storefrontVerificationModule, storefrontVerificationStatusEnum, } from "./schema.js";
7
+ export type { StorefrontVerificationDeliveryResult, StorefrontVerificationEmailSendInput, StorefrontVerificationProviderOptions, StorefrontVerificationSenders, StorefrontVerificationServiceOptions, StorefrontVerificationSmsSendInput, } from "./service.js";
8
+ export { createStorefrontVerificationSendersFromProviders, createStorefrontVerificationService, StorefrontVerificationError, } from "./service.js";
9
+ export type { ConfirmEmailVerificationChallengeInput, ConfirmSmsVerificationChallengeInput, StartEmailVerificationChallengeInput, StartSmsVerificationChallengeInput, StorefrontVerificationChallengeRecord, StorefrontVerificationChannel, StorefrontVerificationConfirmResult, StorefrontVerificationStartResult, StorefrontVerificationStatus, } from "./validation.js";
10
+ export { confirmEmailVerificationChallengeSchema, confirmSmsVerificationChallengeSchema, startEmailVerificationChallengeSchema, startSmsVerificationChallengeSchema, storefrontVerificationChallengeRecordSchema, storefrontVerificationChannelSchema, storefrontVerificationConfirmResultSchema, storefrontVerificationStartResultSchema, storefrontVerificationStatusSchema, } from "./validation.js";
11
+ export declare function createStorefrontVerificationHonoModule(options?: StorefrontVerificationRoutesOptions): HonoModule;
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;AACvD,OAAO,EAIL,KAAK,mCAAmC,EACzC,MAAM,oBAAoB,CAAA;AAG3B,YAAY,EACV,kCAAkC,EAClC,mCAAmC,GACpC,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACL,kCAAkC,EAClC,wCAAwC,EACxC,6CAA6C,GAC9C,MAAM,oBAAoB,CAAA;AAC3B,YAAY,EACV,kCAAkC,EAClC,+BAA+B,GAChC,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,gCAAgC,EAChC,iCAAiC,EACjC,8BAA8B,EAC9B,4BAA4B,EAC5B,gCAAgC,GACjC,MAAM,aAAa,CAAA;AACpB,YAAY,EACV,oCAAoC,EACpC,oCAAoC,EACpC,qCAAqC,EACrC,6BAA6B,EAC7B,oCAAoC,EACpC,kCAAkC,GACnC,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,gDAAgD,EAChD,mCAAmC,EACnC,2BAA2B,GAC5B,MAAM,cAAc,CAAA;AACrB,YAAY,EACV,sCAAsC,EACtC,oCAAoC,EACpC,oCAAoC,EACpC,kCAAkC,EAClC,qCAAqC,EACrC,6BAA6B,EAC7B,mCAAmC,EACnC,iCAAiC,EACjC,4BAA4B,GAC7B,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,uCAAuC,EACvC,qCAAqC,EACrC,qCAAqC,EACrC,mCAAmC,EACnC,2CAA2C,EAC3C,mCAAmC,EACnC,yCAAyC,EACzC,uCAAuC,EACvC,kCAAkC,GACnC,MAAM,iBAAiB,CAAA;AAExB,wBAAgB,sCAAsC,CACpD,OAAO,CAAC,EAAE,mCAAmC,GAC5C,UAAU,CAeZ"}
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ import { buildStorefrontVerificationSenders, createStorefrontVerificationPublicRoutes, STOREFRONT_VERIFICATION_SENDERS_CONTAINER_KEY, } from "./routes-public.js";
2
+ import { storefrontVerificationModule } from "./schema.js";
3
+ export { buildStorefrontVerificationSenders, createStorefrontVerificationPublicRoutes, STOREFRONT_VERIFICATION_SENDERS_CONTAINER_KEY, } from "./routes-public.js";
4
+ export { storefrontVerificationChallenges, storefrontVerificationChannelEnum, storefrontVerificationLinkable, storefrontVerificationModule, storefrontVerificationStatusEnum, } from "./schema.js";
5
+ export { createStorefrontVerificationSendersFromProviders, createStorefrontVerificationService, StorefrontVerificationError, } from "./service.js";
6
+ export { confirmEmailVerificationChallengeSchema, confirmSmsVerificationChallengeSchema, startEmailVerificationChallengeSchema, startSmsVerificationChallengeSchema, storefrontVerificationChallengeRecordSchema, storefrontVerificationChannelSchema, storefrontVerificationConfirmResultSchema, storefrontVerificationStartResultSchema, storefrontVerificationStatusSchema, } from "./validation.js";
7
+ export function createStorefrontVerificationHonoModule(options) {
8
+ const module = {
9
+ ...storefrontVerificationModule,
10
+ bootstrap: ({ bindings, container }) => {
11
+ container.register(STOREFRONT_VERIFICATION_SENDERS_CONTAINER_KEY, buildStorefrontVerificationSenders(bindings, options));
12
+ },
13
+ };
14
+ return {
15
+ module,
16
+ publicRoutes: createStorefrontVerificationPublicRoutes(options),
17
+ };
18
+ }
@@ -0,0 +1,184 @@
1
+ import type { ModuleContainer } from "@voyantjs/core";
2
+ import type { NotificationProvider } from "@voyantjs/notifications";
3
+ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
4
+ import { type StorefrontVerificationProviderOptions, type StorefrontVerificationSenders, type StorefrontVerificationServiceOptions } from "./service.js";
5
+ type Env = {
6
+ Bindings: Record<string, unknown>;
7
+ Variables: {
8
+ container: ModuleContainer;
9
+ db: PostgresJsDatabase;
10
+ userId?: string;
11
+ };
12
+ };
13
+ export interface StorefrontVerificationRoutesOptions extends StorefrontVerificationServiceOptions, StorefrontVerificationProviderOptions {
14
+ sendEmailChallenge?: StorefrontVerificationSenders["sendEmailChallenge"];
15
+ sendSmsChallenge?: StorefrontVerificationSenders["sendSmsChallenge"];
16
+ providers?: ReadonlyArray<NotificationProvider>;
17
+ resolveProviders?: (bindings: Record<string, unknown>) => ReadonlyArray<NotificationProvider>;
18
+ }
19
+ export declare const STOREFRONT_VERIFICATION_SENDERS_CONTAINER_KEY = "providers.storefrontVerification.senders";
20
+ export declare function buildStorefrontVerificationSenders(bindings: Record<string, unknown>, options?: StorefrontVerificationRoutesOptions): StorefrontVerificationSenders;
21
+ export declare function createStorefrontVerificationPublicRoutes(options?: StorefrontVerificationRoutesOptions): import("hono/hono-base").HonoBase<Env, {
22
+ "/email/start": {
23
+ $post: {
24
+ input: {};
25
+ output: {
26
+ data: {
27
+ id: string;
28
+ channel: "email" | "sms";
29
+ destination: string;
30
+ purpose: string;
31
+ status: "pending" | "verified" | "failed" | "expired" | "cancelled";
32
+ expiresAt: string;
33
+ verifiedAt: string | null;
34
+ createdAt: string;
35
+ updatedAt: string;
36
+ };
37
+ };
38
+ outputFormat: "json";
39
+ status: 201;
40
+ } | {
41
+ input: {};
42
+ output: {
43
+ error: string;
44
+ code: "sender_not_configured";
45
+ } | {
46
+ error: string;
47
+ code: "challenge_not_found";
48
+ } | {
49
+ error: string;
50
+ code: "challenge_expired";
51
+ } | {
52
+ error: string;
53
+ code: "challenge_invalid" | "challenge_failed";
54
+ } | {
55
+ error: string;
56
+ };
57
+ outputFormat: "json";
58
+ status: 400 | 404 | 409 | 410 | 501;
59
+ };
60
+ };
61
+ } & {
62
+ "/email/confirm": {
63
+ $post: {
64
+ input: {};
65
+ output: {
66
+ error: string;
67
+ code: "sender_not_configured";
68
+ } | {
69
+ error: string;
70
+ code: "challenge_not_found";
71
+ } | {
72
+ error: string;
73
+ code: "challenge_expired";
74
+ } | {
75
+ error: string;
76
+ code: "challenge_invalid" | "challenge_failed";
77
+ } | {
78
+ error: string;
79
+ };
80
+ outputFormat: "json";
81
+ status: 400 | 404 | 409 | 410 | 501;
82
+ } | {
83
+ input: {};
84
+ output: {
85
+ data: {
86
+ id: string;
87
+ channel: "email" | "sms";
88
+ destination: string;
89
+ purpose: string;
90
+ status: "pending" | "verified" | "failed" | "expired" | "cancelled";
91
+ expiresAt: string;
92
+ verifiedAt: string | null;
93
+ createdAt: string;
94
+ updatedAt: string;
95
+ };
96
+ };
97
+ outputFormat: "json";
98
+ status: import("hono/utils/http-status").ContentfulStatusCode;
99
+ };
100
+ };
101
+ } & {
102
+ "/sms/start": {
103
+ $post: {
104
+ input: {};
105
+ output: {
106
+ error: string;
107
+ code: "sender_not_configured";
108
+ } | {
109
+ error: string;
110
+ code: "challenge_not_found";
111
+ } | {
112
+ error: string;
113
+ code: "challenge_expired";
114
+ } | {
115
+ error: string;
116
+ code: "challenge_invalid" | "challenge_failed";
117
+ } | {
118
+ error: string;
119
+ };
120
+ outputFormat: "json";
121
+ status: 400 | 404 | 409 | 410 | 501;
122
+ } | {
123
+ input: {};
124
+ output: {
125
+ data: {
126
+ id: string;
127
+ channel: "email" | "sms";
128
+ destination: string;
129
+ purpose: string;
130
+ status: "pending" | "verified" | "failed" | "expired" | "cancelled";
131
+ expiresAt: string;
132
+ verifiedAt: string | null;
133
+ createdAt: string;
134
+ updatedAt: string;
135
+ };
136
+ };
137
+ outputFormat: "json";
138
+ status: 201;
139
+ };
140
+ };
141
+ } & {
142
+ "/sms/confirm": {
143
+ $post: {
144
+ input: {};
145
+ output: {
146
+ error: string;
147
+ code: "sender_not_configured";
148
+ } | {
149
+ error: string;
150
+ code: "challenge_not_found";
151
+ } | {
152
+ error: string;
153
+ code: "challenge_expired";
154
+ } | {
155
+ error: string;
156
+ code: "challenge_invalid" | "challenge_failed";
157
+ } | {
158
+ error: string;
159
+ };
160
+ outputFormat: "json";
161
+ status: 400 | 404 | 409 | 410 | 501;
162
+ } | {
163
+ input: {};
164
+ output: {
165
+ data: {
166
+ id: string;
167
+ channel: "email" | "sms";
168
+ destination: string;
169
+ purpose: string;
170
+ status: "pending" | "verified" | "failed" | "expired" | "cancelled";
171
+ expiresAt: string;
172
+ verifiedAt: string | null;
173
+ createdAt: string;
174
+ updatedAt: string;
175
+ };
176
+ };
177
+ outputFormat: "json";
178
+ status: import("hono/utils/http-status").ContentfulStatusCode;
179
+ };
180
+ };
181
+ }, "/", "/sms/confirm">;
182
+ export type StorefrontVerificationPublicRoutes = ReturnType<typeof createStorefrontVerificationPublicRoutes>;
183
+ export {};
184
+ //# sourceMappingURL=routes-public.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes-public.d.ts","sourceRoot":"","sources":["../src/routes-public.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAErD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAA;AACnE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAEjE,OAAO,EAIL,KAAK,qCAAqC,EAC1C,KAAK,6BAA6B,EAClC,KAAK,oCAAoC,EAC1C,MAAM,cAAc,CAAA;AAQrB,KAAK,GAAG,GAAG;IACT,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACjC,SAAS,EAAE;QACT,SAAS,EAAE,eAAe,CAAA;QAC1B,EAAE,EAAE,kBAAkB,CAAA;QACtB,MAAM,CAAC,EAAE,MAAM,CAAA;KAChB,CAAA;CACF,CAAA;AAED,MAAM,WAAW,mCACf,SAAQ,oCAAoC,EAC1C,qCAAqC;IACvC,kBAAkB,CAAC,EAAE,6BAA6B,CAAC,oBAAoB,CAAC,CAAA;IACxE,gBAAgB,CAAC,EAAE,6BAA6B,CAAC,kBAAkB,CAAC,CAAA;IACpE,SAAS,CAAC,EAAE,aAAa,CAAC,oBAAoB,CAAC,CAAA;IAC/C,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,aAAa,CAAC,oBAAoB,CAAC,CAAA;CAC9F;AAED,eAAO,MAAM,6CAA6C,6CACd,CAAA;AAE5C,wBAAgB,kCAAkC,CAChD,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,OAAO,CAAC,EAAE,mCAAmC,GAC5C,6BAA6B,CAgB/B;AA2CD,wBAAgB,wCAAwC,CACtD,OAAO,CAAC,EAAE,mCAAmC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAuD9C;AAED,MAAM,MAAM,kCAAkC,GAAG,UAAU,CACzD,OAAO,wCAAwC,CAChD,CAAA"}
@@ -0,0 +1,93 @@
1
+ import { parseJsonBody } from "@voyantjs/hono";
2
+ import { Hono } from "hono";
3
+ import { createStorefrontVerificationSendersFromProviders, createStorefrontVerificationService, StorefrontVerificationError, } from "./service.js";
4
+ import { confirmEmailVerificationChallengeSchema, confirmSmsVerificationChallengeSchema, startEmailVerificationChallengeSchema, startSmsVerificationChallengeSchema, } from "./validation.js";
5
+ export const STOREFRONT_VERIFICATION_SENDERS_CONTAINER_KEY = "providers.storefrontVerification.senders";
6
+ export function buildStorefrontVerificationSenders(bindings, options) {
7
+ const senders = {
8
+ sendEmailChallenge: options?.sendEmailChallenge,
9
+ sendSmsChallenge: options?.sendSmsChallenge,
10
+ };
11
+ if (!senders.sendEmailChallenge || !senders.sendSmsChallenge) {
12
+ const providers = options?.resolveProviders?.(bindings) ?? options?.providers;
13
+ if (providers?.length) {
14
+ const providerSenders = createStorefrontVerificationSendersFromProviders(providers, options);
15
+ senders.sendEmailChallenge ??= providerSenders.sendEmailChallenge;
16
+ senders.sendSmsChallenge ??= providerSenders.sendSmsChallenge;
17
+ }
18
+ }
19
+ return senders;
20
+ }
21
+ function getSenders(bindings, options, resolveFromContainer) {
22
+ if (resolveFromContainer) {
23
+ try {
24
+ return resolveFromContainer(STOREFRONT_VERIFICATION_SENDERS_CONTAINER_KEY);
25
+ }
26
+ catch {
27
+ // Fall back to per-request sender construction when bootstrap has not run.
28
+ }
29
+ }
30
+ return buildStorefrontVerificationSenders(bindings, options);
31
+ }
32
+ function errorResponse(error) {
33
+ if (error instanceof StorefrontVerificationError) {
34
+ if (error.code === "sender_not_configured") {
35
+ return { status: 501, body: { error: error.message, code: error.code } };
36
+ }
37
+ if (error.code === "challenge_not_found") {
38
+ return { status: 404, body: { error: error.message, code: error.code } };
39
+ }
40
+ if (error.code === "challenge_expired") {
41
+ return { status: 410, body: { error: error.message, code: error.code } };
42
+ }
43
+ if (error.code === "challenge_invalid" || error.code === "challenge_failed") {
44
+ return { status: 409, body: { error: error.message, code: error.code } };
45
+ }
46
+ }
47
+ const message = error instanceof Error ? error.message : "Verification request failed";
48
+ return { status: 400, body: { error: message } };
49
+ }
50
+ export function createStorefrontVerificationPublicRoutes(options) {
51
+ const service = createStorefrontVerificationService(options);
52
+ return new Hono()
53
+ .post("/email/start", async (c) => {
54
+ try {
55
+ const result = await service.startEmailChallenge(c.get("db"), await parseJsonBody(c, startEmailVerificationChallengeSchema), getSenders(c.env, options, (key) => c.var.container.resolve(key)));
56
+ return c.json({ data: result }, 201);
57
+ }
58
+ catch (error) {
59
+ const response = errorResponse(error);
60
+ return c.json(response.body, response.status);
61
+ }
62
+ })
63
+ .post("/email/confirm", async (c) => {
64
+ try {
65
+ const result = await service.confirmEmailChallenge(c.get("db"), await parseJsonBody(c, confirmEmailVerificationChallengeSchema));
66
+ return c.json({ data: result });
67
+ }
68
+ catch (error) {
69
+ const response = errorResponse(error);
70
+ return c.json(response.body, response.status);
71
+ }
72
+ })
73
+ .post("/sms/start", async (c) => {
74
+ try {
75
+ const result = await service.startSmsChallenge(c.get("db"), await parseJsonBody(c, startSmsVerificationChallengeSchema), getSenders(c.env, options, (key) => c.var.container.resolve(key)));
76
+ return c.json({ data: result }, 201);
77
+ }
78
+ catch (error) {
79
+ const response = errorResponse(error);
80
+ return c.json(response.body, response.status);
81
+ }
82
+ })
83
+ .post("/sms/confirm", async (c) => {
84
+ try {
85
+ const result = await service.confirmSmsChallenge(c.get("db"), await parseJsonBody(c, confirmSmsVerificationChallengeSchema));
86
+ return c.json({ data: result });
87
+ }
88
+ catch (error) {
89
+ const response = errorResponse(error);
90
+ return c.json(response.body, response.status);
91
+ }
92
+ });
93
+ }
@@ -0,0 +1,273 @@
1
+ import type { LinkableDefinition, Module } from "@voyantjs/core";
2
+ export declare const storefrontVerificationChannelEnum: import("drizzle-orm/pg-core").PgEnum<["email", "sms"]>;
3
+ export declare const storefrontVerificationStatusEnum: import("drizzle-orm/pg-core").PgEnum<["pending", "verified", "expired", "failed", "cancelled"]>;
4
+ export declare const storefrontVerificationChallenges: import("drizzle-orm/pg-core").PgTableWithColumns<{
5
+ name: "storefront_verification_challenges";
6
+ schema: undefined;
7
+ columns: {
8
+ id: import("drizzle-orm/pg-core").PgColumn<{
9
+ name: string;
10
+ tableName: "storefront_verification_challenges";
11
+ dataType: "string";
12
+ columnType: "PgText";
13
+ data: string;
14
+ driverParam: string;
15
+ notNull: true;
16
+ hasDefault: true;
17
+ isPrimaryKey: true;
18
+ isAutoincrement: false;
19
+ hasRuntimeDefault: true;
20
+ enumValues: [string, ...string[]];
21
+ baseColumn: never;
22
+ identity: undefined;
23
+ generated: undefined;
24
+ }, {}, {}>;
25
+ channel: import("drizzle-orm/pg-core").PgColumn<{
26
+ name: "channel";
27
+ tableName: "storefront_verification_challenges";
28
+ dataType: "string";
29
+ columnType: "PgEnumColumn";
30
+ data: "email" | "sms";
31
+ driverParam: string;
32
+ notNull: true;
33
+ hasDefault: false;
34
+ isPrimaryKey: false;
35
+ isAutoincrement: false;
36
+ hasRuntimeDefault: false;
37
+ enumValues: ["email", "sms"];
38
+ baseColumn: never;
39
+ identity: undefined;
40
+ generated: undefined;
41
+ }, {}, {}>;
42
+ destination: import("drizzle-orm/pg-core").PgColumn<{
43
+ name: "destination";
44
+ tableName: "storefront_verification_challenges";
45
+ dataType: "string";
46
+ columnType: "PgText";
47
+ data: string;
48
+ driverParam: string;
49
+ notNull: true;
50
+ hasDefault: false;
51
+ isPrimaryKey: false;
52
+ isAutoincrement: false;
53
+ hasRuntimeDefault: false;
54
+ enumValues: [string, ...string[]];
55
+ baseColumn: never;
56
+ identity: undefined;
57
+ generated: undefined;
58
+ }, {}, {}>;
59
+ purpose: import("drizzle-orm/pg-core").PgColumn<{
60
+ name: "purpose";
61
+ tableName: "storefront_verification_challenges";
62
+ dataType: "string";
63
+ columnType: "PgText";
64
+ data: string;
65
+ driverParam: string;
66
+ notNull: true;
67
+ hasDefault: true;
68
+ isPrimaryKey: false;
69
+ isAutoincrement: false;
70
+ hasRuntimeDefault: false;
71
+ enumValues: [string, ...string[]];
72
+ baseColumn: never;
73
+ identity: undefined;
74
+ generated: undefined;
75
+ }, {}, {}>;
76
+ codeHash: import("drizzle-orm/pg-core").PgColumn<{
77
+ name: "code_hash";
78
+ tableName: "storefront_verification_challenges";
79
+ dataType: "string";
80
+ columnType: "PgText";
81
+ data: string;
82
+ driverParam: string;
83
+ notNull: true;
84
+ hasDefault: false;
85
+ isPrimaryKey: false;
86
+ isAutoincrement: false;
87
+ hasRuntimeDefault: false;
88
+ enumValues: [string, ...string[]];
89
+ baseColumn: never;
90
+ identity: undefined;
91
+ generated: undefined;
92
+ }, {}, {}>;
93
+ status: import("drizzle-orm/pg-core").PgColumn<{
94
+ name: "status";
95
+ tableName: "storefront_verification_challenges";
96
+ dataType: "string";
97
+ columnType: "PgEnumColumn";
98
+ data: "pending" | "verified" | "failed" | "expired" | "cancelled";
99
+ driverParam: string;
100
+ notNull: true;
101
+ hasDefault: true;
102
+ isPrimaryKey: false;
103
+ isAutoincrement: false;
104
+ hasRuntimeDefault: false;
105
+ enumValues: ["pending", "verified", "expired", "failed", "cancelled"];
106
+ baseColumn: never;
107
+ identity: undefined;
108
+ generated: undefined;
109
+ }, {}, {}>;
110
+ attemptCount: import("drizzle-orm/pg-core").PgColumn<{
111
+ name: "attempt_count";
112
+ tableName: "storefront_verification_challenges";
113
+ dataType: "number";
114
+ columnType: "PgInteger";
115
+ data: number;
116
+ driverParam: string | number;
117
+ notNull: true;
118
+ hasDefault: true;
119
+ isPrimaryKey: false;
120
+ isAutoincrement: false;
121
+ hasRuntimeDefault: false;
122
+ enumValues: undefined;
123
+ baseColumn: never;
124
+ identity: undefined;
125
+ generated: undefined;
126
+ }, {}, {}>;
127
+ maxAttempts: import("drizzle-orm/pg-core").PgColumn<{
128
+ name: "max_attempts";
129
+ tableName: "storefront_verification_challenges";
130
+ dataType: "number";
131
+ columnType: "PgInteger";
132
+ data: number;
133
+ driverParam: string | number;
134
+ notNull: true;
135
+ hasDefault: true;
136
+ isPrimaryKey: false;
137
+ isAutoincrement: false;
138
+ hasRuntimeDefault: false;
139
+ enumValues: undefined;
140
+ baseColumn: never;
141
+ identity: undefined;
142
+ generated: undefined;
143
+ }, {}, {}>;
144
+ expiresAt: import("drizzle-orm/pg-core").PgColumn<{
145
+ name: "expires_at";
146
+ tableName: "storefront_verification_challenges";
147
+ dataType: "date";
148
+ columnType: "PgTimestamp";
149
+ data: Date;
150
+ driverParam: string;
151
+ notNull: true;
152
+ hasDefault: false;
153
+ isPrimaryKey: false;
154
+ isAutoincrement: false;
155
+ hasRuntimeDefault: false;
156
+ enumValues: undefined;
157
+ baseColumn: never;
158
+ identity: undefined;
159
+ generated: undefined;
160
+ }, {}, {}>;
161
+ lastSentAt: import("drizzle-orm/pg-core").PgColumn<{
162
+ name: "last_sent_at";
163
+ tableName: "storefront_verification_challenges";
164
+ dataType: "date";
165
+ columnType: "PgTimestamp";
166
+ data: Date;
167
+ driverParam: string;
168
+ notNull: true;
169
+ hasDefault: true;
170
+ isPrimaryKey: false;
171
+ isAutoincrement: false;
172
+ hasRuntimeDefault: false;
173
+ enumValues: undefined;
174
+ baseColumn: never;
175
+ identity: undefined;
176
+ generated: undefined;
177
+ }, {}, {}>;
178
+ verifiedAt: import("drizzle-orm/pg-core").PgColumn<{
179
+ name: "verified_at";
180
+ tableName: "storefront_verification_challenges";
181
+ dataType: "date";
182
+ columnType: "PgTimestamp";
183
+ data: Date;
184
+ driverParam: string;
185
+ notNull: false;
186
+ hasDefault: false;
187
+ isPrimaryKey: false;
188
+ isAutoincrement: false;
189
+ hasRuntimeDefault: false;
190
+ enumValues: undefined;
191
+ baseColumn: never;
192
+ identity: undefined;
193
+ generated: undefined;
194
+ }, {}, {}>;
195
+ failedAt: import("drizzle-orm/pg-core").PgColumn<{
196
+ name: "failed_at";
197
+ tableName: "storefront_verification_challenges";
198
+ dataType: "date";
199
+ columnType: "PgTimestamp";
200
+ data: Date;
201
+ driverParam: string;
202
+ notNull: false;
203
+ hasDefault: false;
204
+ isPrimaryKey: false;
205
+ isAutoincrement: false;
206
+ hasRuntimeDefault: false;
207
+ enumValues: undefined;
208
+ baseColumn: never;
209
+ identity: undefined;
210
+ generated: undefined;
211
+ }, {}, {}>;
212
+ metadata: import("drizzle-orm/pg-core").PgColumn<{
213
+ name: "metadata";
214
+ tableName: "storefront_verification_challenges";
215
+ dataType: "json";
216
+ columnType: "PgJsonb";
217
+ data: Record<string, unknown>;
218
+ driverParam: unknown;
219
+ notNull: false;
220
+ hasDefault: false;
221
+ isPrimaryKey: false;
222
+ isAutoincrement: false;
223
+ hasRuntimeDefault: false;
224
+ enumValues: undefined;
225
+ baseColumn: never;
226
+ identity: undefined;
227
+ generated: undefined;
228
+ }, {}, {
229
+ $type: Record<string, unknown>;
230
+ }>;
231
+ createdAt: import("drizzle-orm/pg-core").PgColumn<{
232
+ name: "created_at";
233
+ tableName: "storefront_verification_challenges";
234
+ dataType: "date";
235
+ columnType: "PgTimestamp";
236
+ data: Date;
237
+ driverParam: string;
238
+ notNull: true;
239
+ hasDefault: true;
240
+ isPrimaryKey: false;
241
+ isAutoincrement: false;
242
+ hasRuntimeDefault: false;
243
+ enumValues: undefined;
244
+ baseColumn: never;
245
+ identity: undefined;
246
+ generated: undefined;
247
+ }, {}, {}>;
248
+ updatedAt: import("drizzle-orm/pg-core").PgColumn<{
249
+ name: "updated_at";
250
+ tableName: "storefront_verification_challenges";
251
+ dataType: "date";
252
+ columnType: "PgTimestamp";
253
+ data: Date;
254
+ driverParam: string;
255
+ notNull: true;
256
+ hasDefault: true;
257
+ isPrimaryKey: false;
258
+ isAutoincrement: false;
259
+ hasRuntimeDefault: false;
260
+ enumValues: undefined;
261
+ baseColumn: never;
262
+ identity: undefined;
263
+ generated: undefined;
264
+ }, {}, {}>;
265
+ };
266
+ dialect: "pg";
267
+ }>;
268
+ export declare const storefrontVerificationChallengesRelations: import("drizzle-orm").Relations<"storefront_verification_challenges", {}>;
269
+ export type StorefrontVerificationChallenge = typeof storefrontVerificationChallenges.$inferSelect;
270
+ export type NewStorefrontVerificationChallenge = typeof storefrontVerificationChallenges.$inferInsert;
271
+ export declare const storefrontVerificationLinkable: LinkableDefinition;
272
+ export declare const storefrontVerificationModule: Module;
273
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAKhE,eAAO,MAAM,iCAAiC,wDAG5C,CAAA;AAEF,eAAO,MAAM,gCAAgC,iGAM3C,CAAA;AAEF,eAAO,MAAM,gCAAgC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgC5C,CAAA;AAED,eAAO,MAAM,yCAAyC,2EAGrD,CAAA;AAED,MAAM,MAAM,+BAA+B,GAAG,OAAO,gCAAgC,CAAC,YAAY,CAAA;AAClG,MAAM,MAAM,kCAAkC,GAC5C,OAAO,gCAAgC,CAAC,YAAY,CAAA;AAEtD,eAAO,MAAM,8BAA8B,EAAE,kBAK5C,CAAA;AAED,eAAO,MAAM,4BAA4B,EAAE,MAK1C,CAAA"}
package/dist/schema.js ADDED
@@ -0,0 +1,50 @@
1
+ import { typeId } from "@voyantjs/db/lib/typeid-column";
2
+ import { relations } from "drizzle-orm";
3
+ import { index, integer, jsonb, pgEnum, pgTable, text, timestamp } from "drizzle-orm/pg-core";
4
+ export const storefrontVerificationChannelEnum = pgEnum("storefront_verification_channel", [
5
+ "email",
6
+ "sms",
7
+ ]);
8
+ export const storefrontVerificationStatusEnum = pgEnum("storefront_verification_status", [
9
+ "pending",
10
+ "verified",
11
+ "expired",
12
+ "failed",
13
+ "cancelled",
14
+ ]);
15
+ export const storefrontVerificationChallenges = pgTable("storefront_verification_challenges", {
16
+ id: typeId("storefront_verification_challenges"),
17
+ channel: storefrontVerificationChannelEnum("channel").notNull(),
18
+ destination: text("destination").notNull(),
19
+ purpose: text("purpose").notNull().default("contact_confirmation"),
20
+ codeHash: text("code_hash").notNull(),
21
+ status: storefrontVerificationStatusEnum("status").notNull().default("pending"),
22
+ attemptCount: integer("attempt_count").notNull().default(0),
23
+ maxAttempts: integer("max_attempts").notNull().default(5),
24
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
25
+ lastSentAt: timestamp("last_sent_at", { withTimezone: true }).notNull().defaultNow(),
26
+ verifiedAt: timestamp("verified_at", { withTimezone: true }),
27
+ failedAt: timestamp("failed_at", { withTimezone: true }),
28
+ metadata: jsonb("metadata").$type(),
29
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
30
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
31
+ }, (table) => [
32
+ index("idx_storefront_verification_channel").on(table.channel),
33
+ index("idx_storefront_verification_destination").on(table.destination),
34
+ index("idx_storefront_verification_purpose").on(table.purpose),
35
+ index("idx_storefront_verification_status").on(table.status),
36
+ index("idx_storefront_verification_lookup").on(table.channel, table.destination, table.purpose, table.updatedAt, table.createdAt),
37
+ ]);
38
+ export const storefrontVerificationChallengesRelations = relations(storefrontVerificationChallenges, () => ({}));
39
+ export const storefrontVerificationLinkable = {
40
+ module: "storefront-verification",
41
+ entity: "storefrontVerificationChallenge",
42
+ table: "storefront_verification_challenges",
43
+ idPrefix: "svch",
44
+ };
45
+ export const storefrontVerificationModule = {
46
+ name: "storefront-verification",
47
+ linkable: {
48
+ storefrontVerificationChallenge: storefrontVerificationLinkable,
49
+ },
50
+ };
@@ -0,0 +1,96 @@
1
+ import type { NotificationProvider } from "@voyantjs/notifications";
2
+ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
3
+ import type { ConfirmEmailVerificationChallengeInput, ConfirmSmsVerificationChallengeInput, StartEmailVerificationChallengeInput, StartSmsVerificationChallengeInput } from "./validation.js";
4
+ export interface StorefrontVerificationServiceOptions {
5
+ codeLength?: number;
6
+ expiresInSeconds?: number;
7
+ maxAttempts?: number;
8
+ now?: () => Date;
9
+ }
10
+ export interface StorefrontVerificationDeliveryResult {
11
+ id?: string;
12
+ provider?: string;
13
+ }
14
+ export interface StorefrontVerificationEmailSendInput {
15
+ email: string;
16
+ code: string;
17
+ purpose: string;
18
+ locale?: string | null;
19
+ expiresAt: Date;
20
+ metadata?: Record<string, unknown> | null;
21
+ }
22
+ export interface StorefrontVerificationSmsSendInput {
23
+ phone: string;
24
+ code: string;
25
+ purpose: string;
26
+ locale?: string | null;
27
+ expiresAt: Date;
28
+ metadata?: Record<string, unknown> | null;
29
+ }
30
+ export interface StorefrontVerificationSenders {
31
+ sendEmailChallenge?: (input: StorefrontVerificationEmailSendInput) => Promise<StorefrontVerificationDeliveryResult | undefined>;
32
+ sendSmsChallenge?: (input: StorefrontVerificationSmsSendInput) => Promise<StorefrontVerificationDeliveryResult | undefined>;
33
+ }
34
+ export interface StorefrontVerificationProviderOptions {
35
+ email?: {
36
+ provider?: string;
37
+ template?: string;
38
+ subject?: string | ((input: StorefrontVerificationEmailSendInput) => string);
39
+ };
40
+ sms?: {
41
+ provider?: string;
42
+ template?: string;
43
+ };
44
+ }
45
+ export declare class StorefrontVerificationError extends Error {
46
+ readonly code: "sender_not_configured" | "challenge_not_found" | "challenge_expired" | "challenge_invalid" | "challenge_failed";
47
+ constructor(message: string, code: "sender_not_configured" | "challenge_not_found" | "challenge_expired" | "challenge_invalid" | "challenge_failed");
48
+ }
49
+ export declare function createStorefrontVerificationSendersFromProviders(providers: ReadonlyArray<NotificationProvider>, options?: StorefrontVerificationProviderOptions): StorefrontVerificationSenders;
50
+ export declare function createStorefrontVerificationService(options?: StorefrontVerificationServiceOptions): {
51
+ startEmailChallenge(db: PostgresJsDatabase, input: StartEmailVerificationChallengeInput, senders: StorefrontVerificationSenders): Promise<{
52
+ id: string;
53
+ channel: "email" | "sms";
54
+ destination: string;
55
+ purpose: string;
56
+ status: "pending" | "verified" | "failed" | "expired" | "cancelled";
57
+ expiresAt: Date;
58
+ verifiedAt: Date | null;
59
+ createdAt: Date;
60
+ updatedAt: Date;
61
+ }>;
62
+ startSmsChallenge(db: PostgresJsDatabase, input: StartSmsVerificationChallengeInput, senders: StorefrontVerificationSenders): Promise<{
63
+ id: string;
64
+ channel: "email" | "sms";
65
+ destination: string;
66
+ purpose: string;
67
+ status: "pending" | "verified" | "failed" | "expired" | "cancelled";
68
+ expiresAt: Date;
69
+ verifiedAt: Date | null;
70
+ createdAt: Date;
71
+ updatedAt: Date;
72
+ }>;
73
+ confirmEmailChallenge(db: PostgresJsDatabase, input: ConfirmEmailVerificationChallengeInput): Promise<{
74
+ id: string;
75
+ channel: "email" | "sms";
76
+ destination: string;
77
+ purpose: string;
78
+ status: "pending" | "verified" | "failed" | "expired" | "cancelled";
79
+ expiresAt: Date;
80
+ verifiedAt: Date | null;
81
+ createdAt: Date;
82
+ updatedAt: Date;
83
+ }>;
84
+ confirmSmsChallenge(db: PostgresJsDatabase, input: ConfirmSmsVerificationChallengeInput): Promise<{
85
+ id: string;
86
+ channel: "email" | "sms";
87
+ destination: string;
88
+ purpose: string;
89
+ status: "pending" | "verified" | "failed" | "expired" | "cancelled";
90
+ expiresAt: Date;
91
+ verifiedAt: Date | null;
92
+ createdAt: Date;
93
+ updatedAt: Date;
94
+ }>;
95
+ };
96
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAA;AAGnE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAGjE,OAAO,KAAK,EACV,sCAAsC,EACtC,oCAAoC,EACpC,oCAAoC,EACpC,kCAAkC,EAGnC,MAAM,iBAAiB,CAAA;AAExB,MAAM,WAAW,oCAAoC;IACnD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,GAAG,CAAC,EAAE,MAAM,IAAI,CAAA;CACjB;AAED,MAAM,WAAW,oCAAoC;IACnD,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,oCAAoC;IACnD,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,SAAS,EAAE,IAAI,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;CAC1C;AAED,MAAM,WAAW,kCAAkC;IACjD,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,SAAS,EAAE,IAAI,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;CAC1C;AAED,MAAM,WAAW,6BAA6B;IAC5C,kBAAkB,CAAC,EAAE,CACnB,KAAK,EAAE,oCAAoC,KACxC,OAAO,CAAC,oCAAoC,GAAG,SAAS,CAAC,CAAA;IAC9D,gBAAgB,CAAC,EAAE,CACjB,KAAK,EAAE,kCAAkC,KACtC,OAAO,CAAC,oCAAoC,GAAG,SAAS,CAAC,CAAA;CAC/D;AAED,MAAM,WAAW,qCAAqC;IACpD,KAAK,CAAC,EAAE;QACN,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,OAAO,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE,oCAAoC,KAAK,MAAM,CAAC,CAAA;KAC7E,CAAA;IACD,GAAG,CAAC,EAAE;QACJ,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;KAClB,CAAA;CACF;AAED,qBAAa,2BAA4B,SAAQ,KAAK;IAGlD,QAAQ,CAAC,IAAI,EACT,uBAAuB,GACvB,qBAAqB,GACrB,mBAAmB,GACnB,mBAAmB,GACnB,kBAAkB;gBANtB,OAAO,EAAE,MAAM,EACN,IAAI,EACT,uBAAuB,GACvB,qBAAqB,GACrB,mBAAmB,GACnB,mBAAmB,GACnB,kBAAkB;CAKzB;AA6MD,wBAAgB,gDAAgD,CAC9D,SAAS,EAAE,aAAa,CAAC,oBAAoB,CAAC,EAC9C,OAAO,GAAE,qCAA0C,GAClD,6BAA6B,CA8C/B;AAED,wBAAgB,mCAAmC,CACjD,OAAO,CAAC,EAAE,oCAAoC;4BAItC,kBAAkB,SACf,oCAAoC,WAClC,6BAA6B;;;;;;;;;;;0BA6ClC,kBAAkB,SACf,kCAAkC,WAChC,6BAA6B;;;;;;;;;;;8BA6ClC,kBAAkB,SACf,sCAAsC;;;;;;;;;;;4BAajB,kBAAkB,SAAS,oCAAoC;;;;;;;;;;;EAYhG"}
@@ -0,0 +1,268 @@
1
+ import { createNotificationService } from "@voyantjs/notifications";
2
+ import { and, desc, eq } from "drizzle-orm";
3
+ import { storefrontVerificationChallenges } from "./schema.js";
4
+ export class StorefrontVerificationError extends Error {
5
+ code;
6
+ constructor(message, code) {
7
+ super(message);
8
+ this.code = code;
9
+ this.name = "StorefrontVerificationError";
10
+ }
11
+ }
12
+ function normalizeEmail(value) {
13
+ return value.trim().toLowerCase();
14
+ }
15
+ function normalizePhone(value) {
16
+ return value.trim();
17
+ }
18
+ function generateVerificationCode(length) {
19
+ const chars = "0123456789";
20
+ const bytes = new Uint8Array(length);
21
+ crypto.getRandomValues(bytes);
22
+ return Array.from(bytes, (byte) => chars[byte % chars.length]).join("");
23
+ }
24
+ async function hashVerificationCode(code) {
25
+ const bytes = new TextEncoder().encode(code);
26
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
27
+ return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
28
+ }
29
+ function toChallengeRecord(row) {
30
+ return {
31
+ id: row.id,
32
+ channel: row.channel,
33
+ destination: row.destination,
34
+ purpose: row.purpose,
35
+ status: row.status,
36
+ expiresAt: row.expiresAt,
37
+ verifiedAt: row.verifiedAt ?? null,
38
+ createdAt: row.createdAt,
39
+ updatedAt: row.updatedAt,
40
+ };
41
+ }
42
+ function requireChallengeRow(row, operation) {
43
+ if (!row) {
44
+ throw new Error(`Storefront verification ${operation} did not return a challenge row`);
45
+ }
46
+ return row;
47
+ }
48
+ async function getLatestChallenge(db, channel, destination, purpose) {
49
+ const [row] = await db
50
+ .select()
51
+ .from(storefrontVerificationChallenges)
52
+ .where(and(eq(storefrontVerificationChallenges.channel, channel), eq(storefrontVerificationChallenges.destination, destination), eq(storefrontVerificationChallenges.purpose, purpose)))
53
+ .orderBy(desc(storefrontVerificationChallenges.updatedAt), desc(storefrontVerificationChallenges.createdAt))
54
+ .limit(1);
55
+ return row ?? null;
56
+ }
57
+ async function startChallenge(db, channel, destination, purpose, metadata, options) {
58
+ const now = options?.now?.() ?? new Date();
59
+ const codeLength = Math.max(4, Math.min(8, options?.codeLength ?? 6));
60
+ const maxAttempts = Math.max(1, options?.maxAttempts ?? 5);
61
+ const expiresInSeconds = Math.max(60, options?.expiresInSeconds ?? 600);
62
+ const expiresAt = new Date(now.getTime() + expiresInSeconds * 1000);
63
+ const code = generateVerificationCode(codeLength);
64
+ const codeHash = await hashVerificationCode(code);
65
+ const existing = await getLatestChallenge(db, channel, destination, purpose);
66
+ if (existing && existing.status === "pending" && existing.expiresAt > now) {
67
+ const [updated] = await db
68
+ .update(storefrontVerificationChallenges)
69
+ .set({
70
+ codeHash,
71
+ attemptCount: 0,
72
+ maxAttempts,
73
+ expiresAt,
74
+ lastSentAt: now,
75
+ failedAt: null,
76
+ verifiedAt: null,
77
+ metadata: metadata ?? null,
78
+ updatedAt: now,
79
+ })
80
+ .where(eq(storefrontVerificationChallenges.id, existing.id))
81
+ .returning();
82
+ return { challenge: requireChallengeRow(updated, "update"), code };
83
+ }
84
+ if (existing && existing.status === "pending" && existing.expiresAt <= now) {
85
+ await db
86
+ .update(storefrontVerificationChallenges)
87
+ .set({
88
+ status: "expired",
89
+ failedAt: now,
90
+ updatedAt: now,
91
+ })
92
+ .where(eq(storefrontVerificationChallenges.id, existing.id));
93
+ }
94
+ const [created] = await db
95
+ .insert(storefrontVerificationChallenges)
96
+ .values({
97
+ channel,
98
+ destination,
99
+ purpose,
100
+ codeHash,
101
+ status: "pending",
102
+ attemptCount: 0,
103
+ maxAttempts,
104
+ expiresAt,
105
+ lastSentAt: now,
106
+ metadata: metadata ?? null,
107
+ createdAt: now,
108
+ updatedAt: now,
109
+ })
110
+ .returning();
111
+ return { challenge: requireChallengeRow(created, "insert"), code };
112
+ }
113
+ async function confirmChallenge(db, channel, destination, purpose, code, options) {
114
+ const now = options?.now?.() ?? new Date();
115
+ const row = await getLatestChallenge(db, channel, destination, purpose);
116
+ if (!row || row.status !== "pending") {
117
+ throw new StorefrontVerificationError("Verification challenge not found", "challenge_not_found");
118
+ }
119
+ if (row.expiresAt <= now) {
120
+ await db
121
+ .update(storefrontVerificationChallenges)
122
+ .set({
123
+ status: "expired",
124
+ failedAt: now,
125
+ updatedAt: now,
126
+ })
127
+ .where(eq(storefrontVerificationChallenges.id, row.id));
128
+ throw new StorefrontVerificationError("Verification challenge expired", "challenge_expired");
129
+ }
130
+ if (row.codeHash !== (await hashVerificationCode(code))) {
131
+ const nextAttemptCount = row.attemptCount + 1;
132
+ const terminal = nextAttemptCount >= row.maxAttempts;
133
+ await db
134
+ .update(storefrontVerificationChallenges)
135
+ .set({
136
+ attemptCount: nextAttemptCount,
137
+ status: terminal ? "failed" : row.status,
138
+ failedAt: terminal ? now : row.failedAt,
139
+ updatedAt: now,
140
+ })
141
+ .where(eq(storefrontVerificationChallenges.id, row.id));
142
+ throw new StorefrontVerificationError(terminal ? "Verification challenge failed" : "Invalid verification code", terminal ? "challenge_failed" : "challenge_invalid");
143
+ }
144
+ const [verified] = await db
145
+ .update(storefrontVerificationChallenges)
146
+ .set({
147
+ status: "verified",
148
+ verifiedAt: now,
149
+ updatedAt: now,
150
+ })
151
+ .where(eq(storefrontVerificationChallenges.id, row.id))
152
+ .returning();
153
+ return requireChallengeRow(verified, "confirm");
154
+ }
155
+ export function createStorefrontVerificationSendersFromProviders(providers, options = {}) {
156
+ const dispatcher = createNotificationService([...providers]);
157
+ return {
158
+ async sendEmailChallenge(input) {
159
+ const subject = typeof options.email?.subject === "function"
160
+ ? options.email.subject(input)
161
+ : options.email?.subject;
162
+ const result = await dispatcher.send({
163
+ to: input.email,
164
+ channel: "email",
165
+ provider: options.email?.provider,
166
+ template: options.email?.template ?? "storefront-verification-email",
167
+ subject,
168
+ data: {
169
+ code: input.code,
170
+ purpose: input.purpose,
171
+ locale: input.locale ?? null,
172
+ expiresAt: input.expiresAt.toISOString(),
173
+ metadata: input.metadata ?? null,
174
+ },
175
+ });
176
+ return { id: result.id, provider: result.provider };
177
+ },
178
+ async sendSmsChallenge(input) {
179
+ const result = await dispatcher.send({
180
+ to: input.phone,
181
+ channel: "sms",
182
+ provider: options.sms?.provider,
183
+ template: options.sms?.template ?? "storefront-verification-sms",
184
+ data: {
185
+ code: input.code,
186
+ purpose: input.purpose,
187
+ locale: input.locale ?? null,
188
+ expiresAt: input.expiresAt.toISOString(),
189
+ metadata: input.metadata ?? null,
190
+ },
191
+ text: `${input.code} is your verification code.`,
192
+ });
193
+ return { id: result.id, provider: result.provider };
194
+ },
195
+ };
196
+ }
197
+ export function createStorefrontVerificationService(options) {
198
+ return {
199
+ async startEmailChallenge(db, input, senders) {
200
+ const email = normalizeEmail(input.email);
201
+ const { challenge, code } = await startChallenge(db, "email", email, input.purpose, input.metadata, options);
202
+ if (!senders.sendEmailChallenge) {
203
+ throw new StorefrontVerificationError("Email verification sender not configured", "sender_not_configured");
204
+ }
205
+ try {
206
+ await senders.sendEmailChallenge({
207
+ email,
208
+ code,
209
+ purpose: input.purpose,
210
+ locale: input.locale ?? null,
211
+ expiresAt: challenge.expiresAt,
212
+ metadata: input.metadata,
213
+ });
214
+ }
215
+ catch (error) {
216
+ const now = options?.now?.() ?? new Date();
217
+ await db
218
+ .update(storefrontVerificationChallenges)
219
+ .set({
220
+ status: "failed",
221
+ failedAt: now,
222
+ updatedAt: now,
223
+ })
224
+ .where(eq(storefrontVerificationChallenges.id, challenge.id));
225
+ throw error;
226
+ }
227
+ return toChallengeRecord(challenge);
228
+ },
229
+ async startSmsChallenge(db, input, senders) {
230
+ const phone = normalizePhone(input.phone);
231
+ const { challenge, code } = await startChallenge(db, "sms", phone, input.purpose, input.metadata, options);
232
+ if (!senders.sendSmsChallenge) {
233
+ throw new StorefrontVerificationError("SMS verification sender not configured", "sender_not_configured");
234
+ }
235
+ try {
236
+ await senders.sendSmsChallenge({
237
+ phone,
238
+ code,
239
+ purpose: input.purpose,
240
+ locale: input.locale ?? null,
241
+ expiresAt: challenge.expiresAt,
242
+ metadata: input.metadata,
243
+ });
244
+ }
245
+ catch (error) {
246
+ const now = options?.now?.() ?? new Date();
247
+ await db
248
+ .update(storefrontVerificationChallenges)
249
+ .set({
250
+ status: "failed",
251
+ failedAt: now,
252
+ updatedAt: now,
253
+ })
254
+ .where(eq(storefrontVerificationChallenges.id, challenge.id));
255
+ throw error;
256
+ }
257
+ return toChallengeRecord(challenge);
258
+ },
259
+ async confirmEmailChallenge(db, input) {
260
+ const verified = await confirmChallenge(db, "email", normalizeEmail(input.email), input.purpose, input.code, options);
261
+ return toChallengeRecord(verified);
262
+ },
263
+ async confirmSmsChallenge(db, input) {
264
+ const verified = await confirmChallenge(db, "sms", normalizePhone(input.phone), input.purpose, input.code, options);
265
+ return toChallengeRecord(verified);
266
+ },
267
+ };
268
+ }
@@ -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
+ pending: "pending";
8
+ verified: "verified";
9
+ failed: "failed";
10
+ expired: "expired";
11
+ cancelled: "cancelled";
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
+ pending: "pending";
45
+ verified: "verified";
46
+ failed: "failed";
47
+ expired: "expired";
48
+ cancelled: "cancelled";
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
+ pending: "pending";
65
+ verified: "verified";
66
+ failed: "failed";
67
+ expired: "expired";
68
+ cancelled: "cancelled";
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/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
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voyantjs/storefront-verification",
3
- "version": "0.24.1",
3
+ "version": "0.24.3",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -34,10 +34,10 @@
34
34
  "drizzle-orm": "^0.45.2",
35
35
  "hono": "^4.12.10",
36
36
  "zod": "^4.3.6",
37
- "@voyantjs/core": "0.24.1",
38
- "@voyantjs/db": "0.24.1",
39
- "@voyantjs/hono": "0.24.1",
40
- "@voyantjs/notifications": "0.24.1"
37
+ "@voyantjs/core": "0.24.3",
38
+ "@voyantjs/db": "0.24.3",
39
+ "@voyantjs/hono": "0.24.3",
40
+ "@voyantjs/notifications": "0.24.3"
41
41
  },
42
42
  "devDependencies": {
43
43
  "typescript": "^6.0.2",
@@ -66,7 +66,7 @@
66
66
  "lint": "biome check src/",
67
67
  "test": "vitest run",
68
68
  "build": "pnpm run clean && tsc -p tsconfig.json",
69
- "clean": "rm -rf dist"
69
+ "clean": "rm -rf dist tsconfig.tsbuildinfo"
70
70
  },
71
71
  "main": "./dist/index.js",
72
72
  "types": "./dist/index.d.ts"