customer-registration 0.0.124 → 0.0.126

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,53 +1,77 @@
1
- # Medusa Plugin: Customer Registration & OTP Verification
1
+ # customer-registration
2
+ > Medusa v2 plugin that extends customer auth with OTP verification, phone login (`phonepass`), verified contact updates, referral linking, and account deletion workflows.
2
3
 
3
- A comprehensive Medusa v2 plugin that provides OTP-based verification for email and phone functionality, with support for flexible registration identifiers, phone-only authentication, contact change flows, and account deletion.
4
+ ## Plugin Overview
4
5
 
5
- ## Features
6
+ This plugin customizes Medusa customer authentication and profile flows with:
6
7
 
7
- - **Unified OTP API**: Single endpoints for sending and verifying OTPs
8
- - **Token-based Verification**: Secure JWT token system for OTP verification
9
- - **Multiple Verification Types**: Email verification and phone verification
10
- - **Workflow-based Processing**: Automatic handling of verification flags
11
- - **Password Reset Support**: Notification subscriber for Medusa's built-in password reset flow
12
- - **Flexible Configuration**: Per-purpose channel configuration (email/SMS)
13
- - **Throttling & Rate Limiting**: Built-in protection against OTP spam
14
- - **Database Migrations**: Automatic schema updates for verification columns
15
- - **Account Deletion Request Flow**: Two-step OTP flow to request and confirm account deletion with optional cancel flow
16
- - **Unified registration & login**: **`POST /auth/customer/emailpass/register`** creates an **emailpass** or **phonepass** identity (from JSON body + `registration.identifier`); **`POST /auth/customer/emailpass`** logs in with `{ email, password }` or `{ phone, password }` per `login.identifier`, or **`{ email_or_phone, password }`** (values containing `@` are treated as email; otherwise as phone)
17
- - **Per-channel OTP at login**: When `registration.identifier` is **`"both"`** and `require_verification` is on, **email login** only requires **email** verified; **phone login** only requires **phone** verified — you can use either method after completing that channel’s OTP
18
- - **Authenticated Contact Change**: Dedicated OTP-verified routes for updating phone or email — new value is embedded in the signed JWT, no metadata staging required
8
+ - OTP verification for email/phone with configurable channels (email/SMS)
9
+ - unified register/login routes for email and phone credentials
10
+ - secure contact change flow (`/store/customers/me/contact`) with OTP verification
11
+ - account deletion request/cancel flows with OTP and scheduled deletion job
12
+ - referral linkage at signup plus a customer-facing `my-referrals` endpoint
13
+ - password reset support for both email and phone channels
19
14
 
20
- ## Quick Start
15
+ ### Problem it solves
16
+
17
+ It adds production-oriented identity controls that are not available in default customer auth flows:
18
+
19
+ - enforce verified contact channels before login
20
+ - allow phone-based auth (`phonepass`) in Medusa v2
21
+ - avoid direct email/phone mutation without OTP proof
22
+ - formalize account deletion lifecycle with cancellation and delayed execution
23
+
24
+ ### Medusa version
25
+
26
+ Built for **Medusa v2** (`@medusajs/framework`, `@medusajs/medusa`, `@medusajs/cli` pinned to `2.11.2`).
27
+
28
+ ## Installation & Setup
29
+
30
+ ### Install package
21
31
 
22
- 1. **Install the plugin**:
23
32
  ```bash
24
33
  npm install customer-registration
25
34
  ```
26
35
 
27
- 2. **Add to medusa-config.ts**:
28
- ```typescript
29
- import { defineConfig } from "@medusajs/framework/utils"
36
+ or
37
+
38
+ ```bash
39
+ yarn add customer-registration
40
+ ```
41
+
42
+ ### Register plugin in `medusa-config.ts`
43
+
44
+ ```ts
45
+ import { defineConfig, Modules } from "@medusajs/framework/utils"
30
46
 
31
47
  export default defineConfig({
48
+ modules: [
49
+ {
50
+ resolve: "@medusajs/medusa/auth",
51
+ options: {
52
+ providers: [
53
+ {
54
+ resolve: "customer-registration/providers/phonepass",
55
+ id: "phonepass",
56
+ },
57
+ ],
58
+ },
59
+ },
60
+ ],
32
61
  plugins: [
33
62
  {
34
63
  resolve: "customer-registration",
35
64
  options: {
36
65
  storefrontUrl: "http://localhost:8000",
37
-
38
- registration: {
39
- identifier: "phone", // "email" | "phone" | "both"
40
- require_verification: true,
41
- },
42
- login: {
43
- identifier: "phone", // defaults to registration.identifier
44
- },
45
-
66
+ registration: { identifier: "both", require_verification: true },
67
+ login: { identifier: "both" },
68
+ email_verification: { channel: "email", subject: "Verify your email" },
69
+ phone_verification: { channel: "sms" },
46
70
  password_reset: {
47
- // With `login.identifier: "phone"`, set `sms_body` (and `storefrontUrl`). With `"email"`, set `template`. With `"both"`, set both.
48
- sms_body: "Reset your password: {{reset_url}}",
71
+ template: "src/templates/emails/reset-password.html",
72
+ subject: "Reset Your Password",
73
+ sms_body: "Reset password: {{reset_url}}",
49
74
  },
50
-
51
75
  account_deletion_request: {
52
76
  template: "src/templates/emails/account-deletion-request.html",
53
77
  subject: "Confirm your account deletion request",
@@ -57,539 +81,657 @@ export default defineConfig({
57
81
  template: "src/templates/emails/account-deletion-cancel.html",
58
82
  subject: "Confirm cancellation of account deletion",
59
83
  },
60
-
61
- email_verification: {
62
- channel: "email",
63
- subject: "Verify your email",
64
- },
65
- phone_verification: {
66
- channel: "sms",
67
- },
68
84
  },
69
85
  },
70
86
  ],
71
87
  })
72
88
  ```
73
89
 
74
- 3. **Run migrations**:
90
+ ### Run migrations
91
+
75
92
  ```bash
76
93
  npx medusa db:migrate
77
94
  ```
78
95
 
79
- 4. **Use the API** — see [API Endpoints](#api-endpoints) below.
80
-
81
- ---
96
+ ## Configuration
82
97
 
83
- ## Installation
98
+ ### Plugin options (`options`)
99
+
100
+ | Option | Type | Required | Default | Description |
101
+ |---|---|---:|---|---|
102
+ | `storefrontUrl` | `string` | No | `undefined` | Base storefront URL used for password reset link generation. Required when `password_reset` is configured. |
103
+ | `registration.identifier` | `"email" \| "phone" \| "both"` | No | `"email"` | Which identifier is used at registration. |
104
+ | `registration.require_verification` | `boolean` | No | `true` | Whether login requires verification flags. |
105
+ | `login.identifier` | `"email" \| "phone" \| "both"` | No | `registration.identifier` | Which login method(s) are accepted at `/auth/customer/emailpass`. |
106
+ | `password_reset.template` | `string` | Conditional | `undefined` | Email HTML template path for reset email. Required if login channel includes email. |
107
+ | `password_reset.subject` | `string` | No | `"Reset Your Password"` | Password reset email subject. |
108
+ | `password_reset.sms_body` | `string` | Conditional | `undefined` | SMS template body for reset flow; supports `{{token}}`, `{{reset_url}}`, `{{phone}}`. Required if login channel includes phone. |
109
+ | `account_deletion_request.template` | `string` | Yes when section used | none | Template for account deletion request OTP notification. |
110
+ | `account_deletion_request.subject` | `string` | No | `"Confirm your account deletion request"` | Subject for request OTP notification. |
111
+ | `account_deletion_request.scheduled_days` | `number` | No | `7` | Days from confirmation until deletion is due. |
112
+ | `account_deletion_cancel.template` | `string` | Yes when section used | none | Template for cancellation OTP notification. |
113
+ | `account_deletion_cancel.subject` | `string` | No | `"Confirm cancellation of account deletion"` | Subject for cancellation OTP notification. |
114
+
115
+ ### OTP module options (supported by OTP service)
116
+
117
+ > ⚠️ Note: these options are implemented in `OtpVerificationService`/`OtpConfig`, but are not fully declared in `CustomerRegistrationPluginOptions` type. Confirm your expected typing strategy before publishing.
118
+
119
+ | Option | Type | Required | Default | Description |
120
+ |---|---|---:|---|---|
121
+ | `otpLength` | `number` | No | `6` | OTP length. |
122
+ | `otpCharset` | `"numeric" \| "alphanumeric"` | No | `"numeric"` | OTP charset. |
123
+ | `otpExpiryMinutes` | `number` | No | `15` | OTP expiry in minutes. |
124
+ | `maxAttempts` | `number` | No | `5` | Max verify attempts before lockout. |
125
+ | `email_verification.channel` | `string` | Yes for email OTP | none | Notification channel (usually `email`). |
126
+ | `email_verification.template` | `string \| null` | No | `null` | Template path. |
127
+ | `email_verification.subject` | `string` | No | provider default | Subject for email. |
128
+ | `email_verification.resendThrottleSeconds` | `number` | No | `90` | Resend throttle. |
129
+ | `email_verification.autoSendOnRegistration` | `boolean` | No | `undefined` | Auto-send toggle (if used by caller flow). |
130
+ | `phone_verification.channel` | `string` | Yes for phone OTP | none | Notification channel (usually `sms`). |
131
+ | `phone_verification.template` | `string \| null` | No | `null` | SMS template path. |
132
+ | `phone_verification.resendThrottleSeconds` | `number` | No | `90` | Resend throttle. |
133
+ | `account_deletion_request.channel` | `string` | Yes for strict validation | fallback `email` | Channel for request OTP notifications. |
134
+ | `account_deletion_request.template` | `string \| null` | Yes | none | Template path. |
135
+ | `account_deletion_request.subject` | `string` | No | provider default | Subject. |
136
+ | `account_deletion_request.resendThrottleSeconds` | `number` | No | `90` | Resend throttle. |
137
+ | `account_deletion_cancel.channel` | `string` | Yes for strict validation | fallback `email` | Channel for cancellation OTP notifications. |
138
+ | `account_deletion_cancel.template` | `string \| null` | Yes | none | Template path. |
139
+ | `account_deletion_cancel.subject` | `string` | No | provider default | Subject. |
140
+ | `account_deletion_cancel.resendThrottleSeconds` | `number` | No | `90` | Resend throttle. |
141
+
142
+ ### Complete config example
143
+
144
+ ```ts
145
+ {
146
+ resolve: "customer-registration",
147
+ options: {
148
+ storefrontUrl: "https://store.example.com",
149
+ registration: {
150
+ identifier: "both",
151
+ require_verification: true,
152
+ },
153
+ login: {
154
+ identifier: "both",
155
+ },
156
+ otpLength: 6,
157
+ otpCharset: "numeric",
158
+ otpExpiryMinutes: 15,
159
+ maxAttempts: 5,
160
+ email_verification: {
161
+ channel: "email",
162
+ template: "src/templates/emails/otp-verification.html",
163
+ subject: "Verify your email",
164
+ resendThrottleSeconds: 90,
165
+ },
166
+ phone_verification: {
167
+ channel: "sms",
168
+ template: "src/templates/sms/otp.txt",
169
+ resendThrottleSeconds: 90,
170
+ },
171
+ password_reset: {
172
+ template: "src/templates/emails/reset-password.html",
173
+ subject: "Reset Your Password",
174
+ sms_body: "Reset password: {{reset_url}}",
175
+ },
176
+ account_deletion_request: {
177
+ channel: "email",
178
+ template: "src/templates/emails/account-deletion-request.html",
179
+ subject: "Confirm your account deletion request",
180
+ scheduled_days: 7,
181
+ },
182
+ account_deletion_cancel: {
183
+ channel: "email",
184
+ template: "src/templates/emails/account-deletion-cancel.html",
185
+ subject: "Confirm cancellation",
186
+ },
187
+ },
188
+ }
189
+ ```
84
190
 
85
- ### Local Development
191
+ ## Environment Variables
86
192
 
87
- 1. Publish the plugin to local registry:
88
- ```bash
89
- cd plugins/customer-registration
90
- npx medusa plugin:publish
91
- ```
193
+ Only one direct environment variable usage exists in plugin source:
92
194
 
93
- 2. Install in your Medusa application:
94
- ```bash
95
- cd ../../test-medusa
96
- npx medusa plugin:add customer-registration
97
- ```
195
+ | Variable | Required | Example | Purpose |
196
+ |---|---:|---|---|
197
+ | `NODE_ENV` | No | `production` | Used for development-only OTP config debug logging in OTP service. |
98
198
 
99
- 3. Register the plugin in `medusa-config.ts` (see Quick Start above).
199
+ > ⚠️ Note: JWT secret/expiry are read from Medusa config module (`projectConfig.http.jwtSecret` / `jwtExpiresIn`), not directly from `process.env` in this plugin.
100
200
 
101
- 4. Start development mode (in plugin directory):
102
- ```bash
103
- cd plugins/customer-registration
104
- npx medusa plugin:develop
105
- ```
201
+ ## REST APIs / Routes
106
202
 
107
- 5. Start your Medusa application:
108
- ```bash
109
- cd ../../test-medusa
110
- yarn dev
111
- ```
203
+ ### Auth routes
112
204
 
113
- ---
205
+ #### `POST /auth/customer/emailpass/register`
206
+ - Auth: Public
207
+ - Description: Registers customer identity through `emailpass` or `phonepass` based on `registration.identifier`.
114
208
 
115
- ## Configuration
209
+ Body:
116
210
 
117
- ### `registration.identifier`
211
+ | Field | Type | Required | Notes |
212
+ |---|---|---:|---|
213
+ | `password` | `string` | Yes | Required always. |
214
+ | `email` | `string` | Conditional | Required when mode resolves to email. |
215
+ | `phone` | `string` | Conditional | Required when mode resolves to phone. |
118
216
 
119
- Controls which field is required at sign-up:
217
+ Response:
120
218
 
121
- | Value | Behaviour |
122
- |---|---|
123
- | `"email"` (default) | Email required; standard Medusa registration |
124
- | `"phone"` | Phone required; no email needed; uses `phonepass` provider |
125
- | `"both"` | Both email and phone required |
219
+ ```json
220
+ { "token": "..." }
221
+ ```
126
222
 
127
- ### `login.identifier`
223
+ ```bash
224
+ curl -X POST http://localhost:9000/auth/customer/emailpass/register \
225
+ -H "Content-Type: application/json" \
226
+ -d '{"email":"alice@example.com","password":"Secret123!"}'
227
+ ```
128
228
 
129
- Controls which auth providers are accepted at login. Defaults to `registration.identifier`.
229
+ #### `POST /auth/customer/emailpass`
230
+ - Auth: Public
231
+ - Description: Unified login endpoint for email/phone credentials.
130
232
 
131
- | Value | Accepted login methods |
132
- |---|---|
133
- | `"email"` | `POST /auth/customer/emailpass` with `{ "email", "password" }` only |
134
- | `"phone"` | Same endpoint with `{ "phone", "password" }` only |
135
- | `"both"` | Same endpoint; **either** email **or** phone (not both) plus `password` in the JSON body |
233
+ Body/query params:
136
234
 
137
- You may send **`email_or_phone`** instead of `email` or `phone` (same endpoint): if the value contains `@`, it is used as email; otherwise as phone. Do not send `email_or_phone` together with `email` or `phone`.
235
+ | Field | Type | Required | Notes |
236
+ |---|---|---:|---|
237
+ | `password` | `string` | Yes | Required. |
238
+ | `email` | `string` | XOR | Use with password. |
239
+ | `phone` | `string` | XOR | Use with password. |
240
+ | `email_or_phone` | `string` | Optional alternative | Must not be combined with `email`/`phone`; value with `@` becomes email. |
138
241
 
139
- Login always uses **`POST /auth/customer/emailpass`** (single URL). There is **no** `/auth/customer/phonepass` route; phone registration uses the same **`POST /auth/customer/emailpass/register`** as email registration.
242
+ Response:
140
243
 
141
- Credentials are normally sent in the **JSON body**; the same keys (`email`, `phone`, `email_or_phone`, `password`) can be supplied on the **query string** for missing fields (see `buildUnifiedLoginAuthData`) — prefer **body** for passwords.
244
+ ```json
245
+ { "token": "..." }
246
+ ```
142
247
 
143
- ### `require_verification` and login
248
+ #### `POST /auth/customer/emailpass/reset-password`
249
+ - Auth: Public
250
+ - Description: Requests password reset for email or phone channel depending on config.
144
251
 
145
- When **`registration.require_verification`** is **`true`** (default), login checks OTP flags **for the credential you use** (see [`enforce-registration-verification.ts`](src/api/auth/customer/shared/enforce-registration-verification.ts)):
252
+ Body:
146
253
 
147
- - **`registration.identifier: "email"`** signing in with **email** requires **`email_verified`**.
148
- - **`registration.identifier: "phone"`** — signing in with **phone** requires **`phone_verified`**.
149
- - **`registration.identifier: "both"`** signing in with **email** requires **`email_verified`** only (phone may still be unverified). Signing in with **phone** requires **`phone_verified`** only (email may still be unverified).
254
+ | Field | Type | Required | Notes |
255
+ |---|---|---:|---|
256
+ | `identifier` | `string` | Conditional | Email alias field. |
257
+ | `email` | `string` | Conditional | Email lookup. |
258
+ | `phone` | `string` | Conditional | Phone lookup. |
259
+ | `email_or_phone` | `string` | Conditional | Auto-resolves by `@`. |
150
260
 
151
- So with **`both`**, a customer can **log in with email** after email OTP even if **phone** OTP is still pending, and the reverse for phone login.
261
+ Response:
152
262
 
153
- When **`require_verification`** is **`false`**, these checks are skipped.
263
+ ```json
264
+ {}
265
+ ```
154
266
 
155
- ### Full options reference
267
+ #### `POST /auth/customer/emailpass/update`
268
+ - Auth: Public with reset token
269
+ - Description: Completes password reset via `Authorization: Bearer <token>`.
156
270
 
157
- ```typescript
158
- {
159
- resolve: "customer-registration",
160
- options: {
161
- storefrontUrl?: string, // Base URL for password reset links
271
+ Body:
162
272
 
163
- registration?: {
164
- identifier?: "email" | "phone" | "both", // default: "email"
165
- require_verification?: boolean, // default: true
166
- },
167
- login?: {
168
- identifier?: "email" | "phone" | "both", // default: registration.identifier
169
- },
273
+ | Field | Type | Required |
274
+ |---|---|---:|
275
+ | `password` | `string` | Yes |
276
+ | `identifier` or `email` or `phone` or `email_or_phone` | `string` | Yes (exactly one logical lookup) |
170
277
 
171
- // OTP settings (apply to all types)
172
- otpLength?: number, // default: 6
173
- otpCharset?: "numeric" | "alphanumeric", // default: "numeric"
174
- otpExpiryMinutes?: number, // default: 15
175
- maxAttempts?: number, // default: 5
176
-
177
- email_verification?: {
178
- channel: string, // e.g. "email"
179
- template?: string,
180
- subject?: string,
181
- resendThrottleSeconds?: number,
182
- autoSendOnRegistration?: boolean,
183
- },
184
- phone_verification?: {
185
- channel: string, // e.g. "sms"
186
- template?: string,
187
- resendThrottleSeconds?: number,
188
- },
278
+ Response:
189
279
 
190
- password_reset?: {
191
- template?: string, // HTML email path; required if login.identifier is "email" or "both"
192
- subject?: string,
193
- sms_body?: string, // SMS text; supports {{token}}, {{reset_url}}, {{phone}} — required if login is "phone" or "both"
194
- },
195
- account_deletion_request?: {
196
- template: string,
197
- subject?: string,
198
- scheduled_days?: number, // default: 7
199
- },
200
- account_deletion_cancel?: {
201
- template: string,
202
- subject?: string,
203
- },
204
- },
205
- }
280
+ ```json
281
+ { "message": "Password reset successfully", "success": true }
206
282
  ```
207
283
 
208
- ---
284
+ ### Store routes
209
285
 
210
- ## API Endpoints
286
+ #### `POST /store/customers`
287
+ - Auth: Pre-customer auth token (registration flow)
288
+ - Description: Overrides default customer creation; supports `phone` and referral linking.
211
289
 
212
- ### Registration & Initial Verification
290
+ Body:
213
291
 
214
- | Endpoint | Method | Auth | Description |
215
- |---|---|---|---|
216
- | `/store/customers` | POST | Bearer (pre-customer) | Create customer account |
217
- | `/store/customers/otp/send` | POST | | Send OTP for initial email/phone verification |
218
- | `/store/customers/otp/verify` | POST | | Verify OTP code; returns login token on success |
292
+ | Field | Type | Required | Notes |
293
+ |---|---|---:|---|
294
+ | `email` | `string` | Conditional | Depends on `registration.identifier`. |
295
+ | `phone` | `string` | Conditional | Depends on `registration.identifier`. |
296
+ | `first_name` | `string` | No | |
297
+ | `last_name` | `string` | No | |
298
+ | `company_name` | `string` | No | |
299
+ | `metadata` | `object` | No | `metadata.referral_code` is consumed then removed. |
300
+ | `referral_code` | `string` | No | Referrer customer id. |
219
301
 
220
- #### Send OTP
302
+ Response:
221
303
 
222
- ```bash
223
- POST /store/customers/otp/send
224
- {
225
- "customer_id": "cus_...",
226
- "type": "phone_verification" # or "email_verification"
227
- }
228
- # Response: { "token": "...", "expires_at": "..." }
304
+ ```json
305
+ { "customer": { "id": "cus_...", "...": "..." } }
229
306
  ```
230
307
 
231
- #### Verify OTP
308
+ #### `GET /store/customers/me`
309
+ - Auth: Customer JWT
310
+ - Description: Returns customer with appended `email_verified` and `phone_verified`.
232
311
 
233
- ```bash
234
- POST /store/customers/otp/verify
235
- {
236
- "token": "<token from send>",
237
- "code": "482917"
238
- }
239
- # Response: { "verified": true, "customer": {...}, "token": "<login jwt>", "needs_login": false }
240
- ```
312
+ #### `PATCH /store/customers/me`
313
+ - Auth: Customer JWT
314
+ - Description: Updates non-contact profile fields only.
315
+ - Rejects: `email`, `phone` with `NOT_ALLOWED`.
241
316
 
242
- ---
317
+ #### `POST /store/customers/me/contact`
318
+ - Auth: Customer JWT
319
+ - Description: Starts contact change OTP flow.
243
320
 
244
- ### Login & registration (customer auth)
321
+ Body:
245
322
 
246
- Implementation lives under [`src/api/auth/customer/`](src/api/auth/customer/): **`emailpass/`** (login, **register**, password reset), **`shared/`** (credential parsing, verification, JWT helpers).
323
+ | Field | Type | Required | Notes |
324
+ |---|---|---:|---|
325
+ | `email` | `string` | XOR | Exactly one of `email`/`phone`. |
326
+ | `phone` | `string` | XOR | Exactly one of `email`/`phone`. |
247
327
 
248
- | Endpoint | Method | Description |
249
- |---|---|---|
250
- | `/auth/customer/emailpass/register` | POST | **Register.** JSON: `password` plus identifier per `registration.identifier` — **`email`** only for `"email"` and `"both"` (for `"both"`, phone is added on `POST /store/customers`); **`phone`** only for `"phone"`. Dispatches to `register("emailpass")` or `register("phonepass")`. Response: `{ "token" }` (pre-customer JWT). See [`emailpass/register/route.ts`](src/api/auth/customer/emailpass/register/route.ts), [`parse-register-body.ts`](src/api/auth/customer/shared/parse-register-body.ts), [`complete-registration-token.ts`](src/api/auth/customer/shared/complete-registration-token.ts). |
251
- | `/auth/customer/emailpass` | POST | **Login.** JSON: `password` plus **exactly one** of `email` or `phone` (XOR), **or** `email_or_phone` alone (expanded to email vs phone by `@`). Optional query params can fill missing keys (body wins). [`emailpass/route.ts`](src/api/auth/customer/emailpass/route.ts) → [`handleCustomerLogin`](src/api/auth/customer/shared/customer-login-post.ts). |
328
+ Response:
252
329
 
253
- Password reset supports email and/or SMS by `login.identifier`; see [Password Reset](#password-reset).
330
+ ```json
331
+ { "token": "...", "expires_at": "..." }
332
+ ```
254
333
 
255
- ---
334
+ #### `POST /store/customers/me/contact/verify`
335
+ - Auth: Customer JWT
336
+ - Description: Verifies OTP and atomically updates contact + verification flags.
256
337
 
257
- ### Contact Change (Phone or Email Update)
338
+ Body:
258
339
 
259
- > These are the dedicated authenticated routes for changing a customer's phone or email.
260
- > `PATCH /store/customers/me` **rejects** `phone` and `email` fields — all contact changes must go through these endpoints.
340
+ | Field | Type | Required |
341
+ |---|---|---:|
342
+ | `token` | `string` | Yes |
343
+ | `code` | `string` | Yes |
261
344
 
262
- | Endpoint | Method | Auth | Description |
263
- |---|---|---|---|
264
- | `/store/customers/me/contact` | POST | Customer JWT | Request contact change — validates new value, sends OTP, returns token |
265
- | `/store/customers/me/contact/verify` | POST | Customer JWT | Verify OTP and apply the change |
345
+ Response:
266
346
 
267
- #### How it works
347
+ ```json
348
+ { "customer": { "id": "cus_..." }, "token": "..." }
349
+ ```
268
350
 
269
- The new phone or email is embedded directly in the signed OTP JWT. No metadata is written to the customer record during the pending state. On successful verification:
351
+ #### `POST /store/customers/otp/send`
352
+ - Auth: Public
353
+ - Description: Sends OTP for verification/account deletion purpose.
270
354
 
271
- 1. `customer.phone` / `customer.email` is updated
272
- 2. `phone_verified` / `email_verified` is set to `true`
273
- 3. `provider_identity.entity_id` is updated for `phonepass` / `emailpass` **only when the changed field is the active `login.identifier`** — so login continues to work with the new value
355
+ Body:
274
356
 
275
- #### Step 1 Request the change
357
+ | Field | Type | Required |
358
+ |---|---|---:|
359
+ | `customer_id` | `string` | Yes |
360
+ | `type` | `"email_verification" \| "phone_verification"` | Yes |
276
361
 
277
- ```bash
278
- curl -X POST http://localhost:9000/store/customers/me/contact \
279
- -H "Content-Type: application/json" \
280
- -H "Authorization: Bearer $CUSTOMER_JWT" \
281
- -d '{ "phone": "+15559998888" }'
362
+ Response:
282
363
 
283
- # Response
284
- { "token": "<otp_token>", "expires_at": "2026-03-12T10:15:00.000Z" }
364
+ ```json
365
+ { "token": "...", "expires_at": "..." }
285
366
  ```
286
367
 
287
- Only one field at a time is accepted. Sending both `phone` and `email` returns a `400`.
368
+ #### `POST /store/customers/otp/verify`
369
+ - Auth: Public
370
+ - Description: Verifies OTP and triggers verification workflow.
288
371
 
289
- #### Step 2 — Verify the OTP
372
+ Body:
290
373
 
291
- ```bash
292
- curl -X POST http://localhost:9000/store/customers/me/contact/verify \
293
- -H "Content-Type: application/json" \
294
- -H "Authorization: Bearer $CUSTOMER_JWT" \
295
- -d '{
296
- "token": "<otp_token from step 1>",
297
- "code": "482917"
298
- }'
374
+ | Field | Type | Required |
375
+ |---|---|---:|
376
+ | `token` | `string` | Yes |
377
+ | `code` | `string` | Yes |
299
378
 
300
- # Response
379
+ Response:
380
+
381
+ ```json
301
382
  {
302
- "customer": { "id": "cus_...", "phone": "+15559998888", ... },
303
- "token": "<fresh_login_jwt>"
383
+ "verified": true,
384
+ "customer": {},
385
+ "email_verified": true,
386
+ "token": null,
387
+ "needs_login": true
304
388
  }
305
389
  ```
306
390
 
307
- The response includes a **fresh login JWT** so the caller is immediately re-authenticated after the change.
308
-
309
- #### Security notes
391
+ #### `POST /store/customers/change-password`
392
+ - Auth: Customer JWT
393
+ - Description: Changes password using old/new/confirm values.
310
394
 
311
- - The OTP token is signed with the server's `jwtSecret` — the new value cannot be tampered with
312
- - The verify route checks that `token.customer_id === auth_context.actor_id`, preventing one customer from applying another's OTP
313
- - `provider_identity.entity_id` sync has a compensation function: if a later workflow step fails the `entity_id` is rolled back to the original value
314
- - If the current phone/email is `null` (first-time contact assignment), the flow works identically — the field is set from null to the new value and marked verified
395
+ Body:
315
396
 
316
- #### What `PATCH /store/customers/me` does now
397
+ | Field | Type | Required |
398
+ |---|---|---:|
399
+ | `old_password` | `string` | Yes |
400
+ | `new_password` | `string` | Yes |
401
+ | `confirm_password` | `string` | Yes |
317
402
 
318
- Non-contact fields (e.g. `first_name`, `last_name`) continue to work as before. Passing `phone` or `email` in the body returns:
403
+ Response:
319
404
 
320
405
  ```json
321
- {
322
- "type": "not_allowed",
323
- "message": "Phone changes require OTP verification. Use POST /store/customers/me/contact to request a change."
324
- }
406
+ { "message": "Password changed successfully", "customer_id": "cus_..." }
325
407
  ```
326
408
 
327
- ---
409
+ #### `POST /store/customers/account-deletion/request`
410
+ - Auth: Customer JWT
411
+ - Description: Sends OTP for deletion confirmation.
328
412
 
329
- ### Profile Update
413
+ Body:
330
414
 
331
- | Endpoint | Method | Auth | Description |
332
- |---|---|---|---|
333
- | `/store/customers/me` | PATCH | Customer JWT | Update non-contact profile fields (first_name, last_name, etc.) |
415
+ | Field | Type | Required |
416
+ |---|---|---:|
417
+ | `reason` | `string \| null` | No |
334
418
 
335
- ---
419
+ Response:
336
420
 
337
- ### Password Reset
421
+ ```json
422
+ { "token": "...", "expires_at": "..." }
423
+ ```
338
424
 
339
- Allowed JSON fields follow **`login.identifier`** (same idea as login): **`email`**, **`phone`**, **`email_or_phone`**, or **`identifier`** (email alias). Exactly one lookup field; do not combine with `email_or_phone` and `email`/`phone` at once.
425
+ #### `POST /store/customers/account-deletion/confirm`
426
+ - Auth: Public
427
+ - Description: Confirms deletion request via OTP; creates confirmed request row.
340
428
 
341
- | `login.identifier` | Request reset with |
342
- |---|---|
343
- | `email` | `email`, `identifier`, or `email_or_phone` containing `@` |
344
- | `phone` | `phone` or `email_or_phone` without `@` |
345
- | `both` | Any of the above |
429
+ Body:
346
430
 
347
- - **Email** path: sends HTML email using `password_reset.template`.
348
- - **Phone** path: sends **SMS** using `password_reset.sms_body` (placeholders: `{{token}}`, `{{reset_url}}`, `{{phone}}`).
349
- - **Config**: `storefrontUrl` is required when `password_reset` is set. If login is `email`, `template` is required; if `phone`, `sms_body` is required; if `both`, **both** `template` and `sms_body` are required (validated at plugin load).
431
+ | Field | Type | Required |
432
+ |---|---|---:|
433
+ | `token` | `string` | Yes |
434
+ | `code` | `string` | Yes |
350
435
 
351
- | Endpoint | Method | Description |
352
- |---|---|---|
353
- | `/auth/customer/emailpass/reset-password` | POST | Request reset (email and/or SMS per channel) |
354
- | `/auth/customer/emailpass/update` | POST | Set new password; `Authorization: Bearer <token>` + same lookup field shape as request + `password` |
436
+ Response:
355
437
 
356
- ```bash
357
- # Request reset (email example)
358
- POST /auth/customer/emailpass/reset-password
359
- { "identifier": "customer@example.com" }
360
-
361
- # Request reset (phone example)
362
- POST /auth/customer/emailpass/reset-password
363
- { "phone": "+15551234567" }
364
-
365
- # Complete reset (use Bearer token from link; body must include matching email or phone)
366
- POST /auth/customer/emailpass/update
367
- Authorization: Bearer <reset_token>
368
- { "email": "customer@example.com", "password": "NewPassword123!" }
438
+ ```json
439
+ {
440
+ "request": {
441
+ "id": "adr_...",
442
+ "customer_id": "cus_...",
443
+ "reason": null,
444
+ "deletion_scheduled_at": "...",
445
+ "status": "confirmed"
446
+ }
447
+ }
369
448
  ```
370
449
 
371
- ---
450
+ #### `POST /store/customers/account-deletion/cancel-request`
451
+ - Auth: Public (IP rate-limited)
452
+ - Description: Sends OTP for canceling an active deletion request.
372
453
 
373
- ### Account Deletion
454
+ Body:
374
455
 
375
- | Endpoint | Method | Auth | Description |
376
- |---|---|---|---|
377
- | `/store/customers/account-deletion/request` | POST | Customer JWT | Request deletion — sends OTP, returns token |
378
- | `/store/customers/account-deletion/confirm` | POST | | Confirm deletion with OTP code |
379
- | `/store/customers/account-deletion/cancel-request` | POST | — | Request cancellation (lookup by email/phone) |
380
- | `/store/customers/account-deletion/cancel-confirm` | POST | — | Confirm cancellation with OTP code |
381
- | `/admin/account-deletion-requests` | GET | Admin JWT | List deletion requests |
456
+ | Field | Type | Required |
457
+ |---|---|---:|
458
+ | `email` | `string` | Conditional |
459
+ | `phone` | `string` | Conditional |
382
460
 
383
- ```bash
384
- # 1. Request deletion
385
- curl -X POST http://localhost:9000/store/customers/account-deletion/request \
386
- -H "Authorization: Bearer $CUSTOMER_JWT" \
387
- -d '{"reason": "No longer need account"}'
388
- # Response: { "token": "...", "expires_at": "..." }
389
-
390
- # 2. Confirm deletion
391
- curl -X POST http://localhost:9000/store/customers/account-deletion/confirm \
392
- -d '{"token": "...", "code": "123456"}'
393
-
394
- # 3. Request cancel (no auth; email lookup)
395
- curl -X POST http://localhost:9000/store/customers/account-deletion/cancel-request \
396
- -d '{"email": "customer@example.com"}'
397
-
398
- # 4. Confirm cancel
399
- curl -X POST http://localhost:9000/store/customers/account-deletion/cancel-confirm \
400
- -d '{"token": "...", "code": "123456"}'
401
- ```
461
+ Response:
402
462
 
403
- ---
463
+ ```json
464
+ { "token": "...", "expires_at": "..." }
465
+ ```
404
466
 
405
- ## Phone-only Registration & Login (`phonepass`)
467
+ #### `POST /store/customers/account-deletion/cancel-confirm`
468
+ - Auth: Public
469
+ - Description: Confirms cancellation OTP and marks request canceled.
406
470
 
407
- When `registration.identifier = "phone"`, customers register and log in with phone + password only. No email address is required.
471
+ Body:
408
472
 
409
- ### 1. Register the provider in `medusa-config.ts`
473
+ | Field | Type | Required |
474
+ |---|---|---:|
475
+ | `token` | `string` | Yes |
476
+ | `code` | `string` | Yes |
410
477
 
411
- ```typescript
412
- import { Modules } from "@medusajs/framework/utils"
478
+ Response:
413
479
 
414
- export default defineConfig({
415
- modules: [
416
- {
417
- resolve: "@medusajs/medusa/auth",
418
- options: {
419
- providers: [
420
- {
421
- resolve: "customer-registration/providers/phonepass",
422
- id: "phonepass",
423
- },
424
- ],
425
- },
426
- },
427
- ],
428
- plugins: [
429
- {
430
- resolve: "customer-registration",
431
- options: {
432
- registration: {
433
- identifier: "phone",
434
- require_verification: true,
435
- },
436
- login: {
437
- identifier: "phone",
438
- },
439
- phone_verification: {
440
- channel: "sms",
441
- },
442
- },
443
- },
444
- ],
445
- })
480
+ ```json
481
+ {
482
+ "cancelled": true,
483
+ "request": {
484
+ "id": "adr_...",
485
+ "customer_id": "cus_...",
486
+ "status": "cancelled",
487
+ "cancelled_at": "..."
488
+ }
489
+ }
446
490
  ```
447
491
 
448
- ### 2. Registration flow
449
-
450
- ```bash
451
- # Step 1 — create phonepass auth identity (unified register URL)
452
- POST /auth/customer/emailpass/register
453
- { "phone": "+15551234567", "password": "SecretPass1!" }
454
- # Response: { "token": "<pre-customer jwt>" }
455
-
456
- # Step 2 — create customer record
457
- POST /store/customers
458
- Authorization: Bearer <token>
459
- { "phone": "+15551234567", "first_name": "Jane" }
460
-
461
- # Step 3 — send phone OTP
462
- POST /store/customers/otp/send
463
- { "customer_id": "cus_...", "type": "phone_verification" }
464
- # Response: { "token": "<otp_token>", "expires_at": "..." }
465
-
466
- # Step 4 — verify phone OTP (returns login token)
467
- POST /store/customers/otp/verify
468
- { "token": "<otp_token>", "code": "482917" }
469
- # Response: { "verified": true, "phone_verified": true, "token": "<login jwt>" }
470
- ```
492
+ #### `GET /store/my-referrals`
493
+ - Auth: Customer JWT
494
+ - Description: Returns customers referred by the authenticated customer.
471
495
 
472
- ### 3. Login flow
496
+ Response:
473
497
 
474
- ```bash
475
- POST /auth/customer/emailpass
476
- { "phone": "+15551234567", "password": "SecretPass1!" }
477
- # Response: { "token": "<jwt>" }
498
+ ```json
499
+ {
500
+ "children": [
501
+ {
502
+ "referral_link_id": "refl_...",
503
+ "referred_at": "...",
504
+ "parent_customers_by_level": { "1": "cus_referrer" },
505
+ "customer": {
506
+ "id": "cus_child",
507
+ "email": "child@example.com",
508
+ "first_name": null,
509
+ "last_name": null,
510
+ "phone": null,
511
+ "created_at": "..."
512
+ }
513
+ }
514
+ ]
515
+ }
478
516
  ```
479
517
 
480
- ### 3b. Password reset (phone)
518
+ ### Admin routes
481
519
 
482
- Configure `password_reset.sms_body`, `storefrontUrl`, and the notification module for SMS. Then:
520
+ #### `GET /admin/account-deletion-requests`
521
+ - Auth: Admin JWT
522
+ - Description: Lists account deletion requests with filtering/pagination.
483
523
 
484
- ```bash
485
- POST /auth/customer/emailpass/reset-password
486
- { "phone": "+15551234567" }
524
+ Query params:
487
525
 
488
- POST /auth/customer/emailpass/update
489
- Authorization: Bearer <token from SMS link>
490
- { "phone": "+15551234567", "password": "NewSecretPass1!" }
491
- ```
526
+ | Param | Type | Required | Default |
527
+ |---|---|---:|---|
528
+ | `status` | `pending \| confirmed \| cancelled \| completed` | No | none |
529
+ | `limit` | `number` | No | `20` |
530
+ | `offset` | `number` | No | `0` |
531
+ | `order` | `created_at \| updated_at \| customer_id` | No | `created_at` |
532
+ | `order_direction` | `ASC \| DESC` | No | `DESC` |
492
533
 
493
- ### 4. Update phone (after registration)
534
+ Response:
494
535
 
495
- ```bash
496
- # Request change
497
- POST /store/customers/me/contact
498
- Authorization: Bearer <login jwt>
499
- { "phone": "+15559998888" }
500
- # Response: { "token": "<otp_token>", "expires_at": "..." }
501
-
502
- # Verify and apply
503
- POST /store/customers/me/contact/verify
504
- Authorization: Bearer <login jwt>
505
- { "token": "<otp_token>", "code": "739201" }
506
- # Response: { "customer": { "phone": "+15559998888", ... }, "token": "<fresh jwt>" }
536
+ ```json
537
+ {
538
+ "requests": [],
539
+ "count": 0,
540
+ "offset": 0,
541
+ "limit": 20
542
+ }
507
543
  ```
508
544
 
509
- ---
545
+ ## Services
510
546
 
511
- ## Modules
547
+ ### `OtpVerificationService`
548
+ Manages OTP generation, validation, verification state updates, and channel config lookup.
512
549
 
513
- The plugin includes three modules:
550
+ Key methods:
551
+ - `generateOtpWithCode(input, jwtSecret)`
552
+ - `generateOtpForContactChange(input, jwtSecret, newValue, contactType)`
553
+ - `verifyOtp(input, jwtSecret)`
554
+ - `decodeContactChangeToken(token, jwtSecret)`
555
+ - `getCustomerVerificationByCustomerId(context, customerId)`
556
+ - `updateEmailVerified(context, customerId)`
557
+ - `updatePhoneVerified(context, customerId)`
514
558
 
515
- 1. **`otp-verification`** — OTP generation, verification, and management
516
- 2. **`customer-registration`** Customer registration logic and route overrides
517
- 3. **`account-deletion-request`** — Account deletion request lifecycle (pending → confirmed → completed/cancelled)
559
+ ### `AccountDeletionRequestService`
560
+ Manages deletion lifecycle records and admin/job access patterns.
518
561
 
519
- ## Workflows
562
+ Key methods:
563
+ - `createConfirmed(customer_id, deletion_scheduled_at)`
564
+ - `cancelRequest(customer_id)`
565
+ - `getActiveByCustomerId(customer_id)`
566
+ - `hasPendingRequest(customerId)`
567
+ - `hasActiveRequest(customerId)`
568
+ - `listForAdmin(selector, listConfig)`
569
+ - `listDueForDeletion(limit)`
570
+ - `markCompleted(id)`
520
571
 
521
- | Workflow | Description |
522
- |---|---|
523
- | `verify-email` | Sets `email_verified = true` on the customer record |
524
- | `verify-phone` | Sets `phone_verified = true` on the customer record |
525
- | `update-contact` | Updates `customer.phone` / `customer.email`, sets verified flag, syncs `provider_identity.entity_id` when applicable |
526
- | `send-otp` | Resolves channel config, generates OTP, sends notification |
527
- | `send-contact-change-otp` | Generates OTP with `new_value` embedded in JWT, sends to the new contact address |
528
- | `change-password` | Updates customer password via the auth module |
572
+ ### `ReferralLinkModuleService`
573
+ CRUD service for referral links (inherits generated methods from `MedusaService`).
529
574
 
530
- ## Database Migrations
575
+ ### `PasswordManagementService`
576
+ Utility service for password hash lookup/verification/update against provider identities.
531
577
 
532
- | Migration | Description |
533
- |---|---|
534
- | `Migration20250120000000AddCustomerVerificationColumns` | Adds `email_verified` and `phone_verified` columns to the customer table |
535
- | `Migration20250118001000CreateOtpVerificationTable` | Creates `otp_verification` table |
536
- | `Migration20250221000000CreateAccountDeletionRequestTable` | Creates `account_deletion_request` table |
537
- | `Migration20250221000000AddAccountDeletionOtpPurposes` | Extends `otp_verification.purpose` enum for account deletion flows |
578
+ ### `CustomerRegistrationService`
579
+ Placeholder module service for registration-related extension points.
538
580
 
539
- ```bash
540
- npx medusa db:migrate
541
- ```
542
-
543
- ## Password Reset (Helper Functions)
544
-
545
- ```typescript
546
- import {
547
- requestPasswordReset,
548
- completePasswordReset,
549
- } from "customer-registration/helpers"
550
-
551
- await requestPasswordReset(
552
- { email: "customer@example.com" },
553
- { baseUrl: "https://store.example.com", publishableApiKey: "pk_..." }
554
- )
555
-
556
- await requestPasswordReset(
557
- { phone: "+15551234567" },
558
- { baseUrl: "https://store.example.com", publishableApiKey: "pk_..." }
559
- )
560
-
561
- await completePasswordReset(
562
- {
563
- email: "customer@example.com",
564
- password: "NewPassword123!",
565
- token: "reset_token_from_email",
566
- },
567
- { baseUrl: "https://store.example.com", publishableApiKey: "pk_..." }
568
- )
569
- ```
581
+ ## Workflows & Steps
570
582
 
571
- ## Requirements
583
+ ### Workflows
572
584
 
573
- - Medusa v2.11.2 or higher
574
- - Node.js >= 20
575
- - Notification module configured with at least one provider (email/SMS)
576
- - Database migrations applied (`npx medusa db:migrate`)
577
- - **`phonepass`** auth provider registered in `medusa-config` (required when customers can **register** or **log in** with phone i.e. `registration.identifier` and/or `login.identifier` is `"phone"` or `"both"`)
578
-
579
- ## Documentation
580
-
581
- This **README** is the main overview: configuration, auth routes, OTP behaviour, and API tables.
582
-
583
- ## Development
584
-
585
- ```bash
586
- # Build
587
- npm run build
588
-
589
- # Watch mode
590
- npx medusa plugin:develop
591
- ```
592
-
593
- ## License
594
-
595
- MIT
585
+ | Workflow | Input | Output | Purpose |
586
+ |---|---|---|---|
587
+ | `send-otp` | `{ customer_id, type }` | `{ token, expires_at }` | Generic OTP send pipeline (customer lookup, config, token, notification). |
588
+ | `send-contact-change-otp` | `{ customer_id, new_value, contact_type, otp_type }` | `{ token, expires_at }` | Sends OTP to new email/phone; token embeds pending value. |
589
+ | `update-contact` | `{ customer_id, new_value, contact_type, login_identifier }` | `{ customer_id, customer }` | Updates contact, sets verified flags, syncs provider identity with compensation. |
590
+ | `verify-email` | `{ customer_id }` | `{ customer_id, email_verified, customer }` | Marks email verified and returns updated customer. |
591
+ | `verify-phone` | `{ customer_id, login_identifier? }` | `{ customer_id, phone_verified, customer }` | Marks phone verified and syncs `phonepass` entity id when needed. |
592
+ | `change-password` | `{ customer_id, old_password, new_password, confirm_password }` | `{ customer_id, success }` | Validates and updates password through auth provider. |
593
+
594
+ ### Steps
595
+
596
+ - `retrieve-customer`
597
+ - `resolve-channel-config`
598
+ - `determine-contact-method`
599
+ - `generate-otp`
600
+ - `generate-contact-change-otp`
601
+ - `prepare-template-data`
602
+ - `load-template`
603
+ - `send-notification`
604
+ - `find-customer-by-email`
605
+ - `update-password`
606
+ - `sync-phonepass-entity-id`
607
+
608
+ ## Subscribers / Event Hooks
609
+
610
+ ### `auth.password_reset` subscriber
611
+ - File: `src/subscribers/password-reset.ts`
612
+ - Event: `auth.password_reset`
613
+ - Behavior: renders configured password reset template and sends email notification using notification module.
614
+
615
+ ## Admin UI / Extensions
616
+
617
+ ### Account Deletion Requests page
618
+ - File: `src/admin/routes/account-deletion-requests/page.tsx`
619
+ - Placement: Admin route labeled **Account Deletion Requests** (trash icon)
620
+ - Renders:
621
+ - filterable/paginated table
622
+ - status badges (`pending`, `confirmed`, `cancelled`, `completed`)
623
+ - refresh + load more interactions
624
+ - Data source: `GET /admin/account-deletion-requests`
625
+
626
+ ## Models & Entities
627
+
628
+ ### `otp_verification`
629
+ | Field | Type | Nullable |
630
+ |---|---|---:|
631
+ | `id` | `text` | No |
632
+ | `customer_id` | `text` | No |
633
+ | `purpose` | enum | No |
634
+ | `hashed_code` | `text` | No |
635
+ | `expires_at` | `datetime` | No |
636
+ | `attempts` | `number` | No |
637
+ | `verified_at` | `datetime` | Yes |
638
+
639
+ ### `account_deletion_request`
640
+ | Field | Type | Nullable |
641
+ |---|---|---:|
642
+ | `id` | `text` | No |
643
+ | `customer_id` | `text` | No |
644
+ | `reason` | `text` | Yes |
645
+ | `deletion_scheduled_at` | `datetime` | Yes |
646
+ | `status` | enum | No |
647
+ | `cancelled_at` | `datetime` | Yes |
648
+
649
+ ### `referral_link`
650
+ | Field | Type | Nullable |
651
+ |---|---|---:|
652
+ | `id` | `text` | No |
653
+ | `customer_id` | `text` | No |
654
+ | `referrer_id` | `text` | No |
655
+ | `parent_customers_by_level` | `json` | No |
656
+
657
+ ### Core Medusa relationships
658
+ - `customer_id` fields link to Medusa `customer` table by convention.
659
+ - auth linkage is handled through `auth_identity.app_metadata.customer_id` and `provider_identity`.
660
+
661
+ ## Jobs
662
+
663
+ ### `process-account-deletions`
664
+ - Schedule: hourly (`0 * * * *`)
665
+ - Flow:
666
+ 1. load due confirmed requests
667
+ 2. delete auth identities for customer
668
+ 3. delete customer
669
+ 4. mark request as `completed`
670
+
671
+ ## Use Cases & Examples
672
+
673
+ 1. **Phone-first storefront signup**
674
+ Use `POST /auth/customer/emailpass/register` with phone+password, then `POST /store/customers`, then OTP send/verify routes.
675
+
676
+ 2. **Secure email/phone change in customer profile**
677
+ Use `POST /store/customers/me/contact` + `POST /store/customers/me/contact/verify` instead of direct patch.
678
+
679
+ 3. **Regulatory account deletion process**
680
+ Use request/confirm/cancel flows under `/store/customers/account-deletion/*` and rely on scheduled deletion job.
681
+
682
+ 4. **Referral tree tracking for customers**
683
+ Pass `referral_code` on signup and fetch referrals via `GET /store/my-referrals`.
684
+
685
+ 5. **Channel-aware password reset**
686
+ Use `/auth/customer/emailpass/reset-password` and `/auth/customer/emailpass/update` with email, phone, or `email_or_phone`.
687
+
688
+ ## Troubleshooting
689
+
690
+ ### `storefrontUrl is required when password_reset is configured`
691
+ - Cause: `password_reset` enabled without `storefrontUrl`.
692
+ - Fix: set `options.storefrontUrl`.
693
+
694
+ ### OTP channel configuration not found
695
+ - Cause: missing `email_verification.channel`, `phone_verification.channel`, or account deletion channel/template.
696
+ - Fix: add channel config in plugin options.
697
+
698
+ ### `Phone login is not enabled` / `Email login is not enabled`
699
+ - Cause: credential channel does not match `login.identifier`.
700
+ - Fix: align request payload with config, or change `login.identifier`.
701
+
702
+ ### Password reset succeeds with empty `{}` but user gets no message
703
+ - Cause: security behavior intentionally hides account existence and may swallow notification delivery errors.
704
+ - Fix: verify notification module config, template path, and channel provider setup.
705
+
706
+ ### Contact change verify returns unauthorized token mismatch
707
+ - Cause: OTP token used by different authenticated customer.
708
+ - Fix: ensure same customer JWT that initiated the contact-change request is used during verify.
709
+
710
+ ### Account deletion routes blocked / login blocked
711
+ - Cause: customer has active (`pending`/`confirmed`) deletion request; guard middleware blocks most store routes.
712
+ - Fix: complete cancel flow (`cancel-request` + `cancel-confirm`) or allow job to complete deletion.
713
+
714
+ ### `JWT secret is not configured`
715
+ - Cause: Medusa HTTP JWT config missing.
716
+ - Fix: configure `projectConfig.http.jwtSecret` in Medusa app config.
717
+
718
+ ## Helper Utilities
719
+
720
+ The package exports helper functions from `customer-registration/helpers`:
721
+
722
+ - `requestPasswordReset(input, options)`
723
+ - `completePasswordReset(input, options)`
724
+ - `createRequestPasswordReset(options)`
725
+ - `createCompletePasswordReset(options)`
726
+
727
+ These support Medusa SDK client mode or direct fetch mode with optional `x-publishable-api-key`.
728
+
729
+ ## Migrations Included
730
+
731
+ - `Migration20250118001000CreateOtpVerificationTable`
732
+ - `Migration20250120000000RemoveForgotPasswordFromOtpPurpose`
733
+ - `Migration20250221000000AddAccountDeletionOtpPurposes`
734
+ - `Migration20250120000000AddCustomerVerificationColumns`
735
+ - `Migration20250221000000CreateAccountDeletionRequestTable`
736
+ - `Migration20250221100000AddCompletedStatusToAccountDeletionRequest`
737
+ - `Migration20260502100000CreateReferralLinkTable`