@molecule/api-resource-user 1.0.0 → 1.1.0

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 ADDED
@@ -0,0 +1,978 @@
1
+ <!--
2
+ AUTO-GENERATED — DO NOT EDIT THIS FILE.
3
+ Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
4
+ Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
5
+ To change this document, edit the module-level JSDoc in src/index.ts.
6
+ Generated: 2026-08-08T07:15:54.389Z
7
+ -->
8
+
9
+ # @molecule/api-resource-user
10
+
11
+ > **Auto-generated, AI-first package reference** for the [molecule.dev](https://molecule.dev) ecosystem.
12
+ > It is written to be read by coding agents as much as by people, and is generated from this
13
+ > package's source — edit `src/index.ts` JSDoc, not this file.
14
+
15
+ The `user` resource types, schema, and definition.
16
+
17
+ ## Quick Start
18
+
19
+ ```ts
20
+ // Extend safely: a display field in Props, a secret in SecretProps.
21
+ // propsSchema: { …, timezone: z.string().optional() } // safe → client
22
+ // secretPropsSchema: { …, passwordResetToken: z.string().optional() } // server-only, secrets table
23
+
24
+ // A custom handler returns SAFE props — never the secrets row.
25
+ router.get('/me/timezone', async (req, res) => {
26
+ const userId = getUserId(res)
27
+ if (!userId) return res.status(401).json({ error: 'Authentication required.' })
28
+ const user = await findById('users', userId) // the users table holds Props only
29
+ res.json({ timezone: user?.timezone }) // never spread a secrets-table row here
30
+ })
31
+ ```
32
+
33
+ ## Type
34
+
35
+ `resource`
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ npm install @molecule/api-resource-user @molecule/api-bond @molecule/api-config @molecule/api-database @molecule/api-entitlements @molecule/api-i18n @molecule/api-jwt @molecule/api-locales-user @molecule/api-locales-user-payments @molecule/api-password @molecule/api-payments @molecule/api-push-notifications @molecule/api-rate-limit @molecule/api-resource @molecule/api-resource-device @molecule/api-secrets @molecule/api-two-factor zod
41
+ ```
42
+
43
+ ## API
44
+
45
+ ### Interfaces
46
+
47
+ #### `UserRequestHandlerMap`
48
+
49
+ Shape of the user request-handler map produced by `createRequestHandlerMap`.
50
+ Names match the route definitions in `routes.ts`. Exported so helpers that
51
+ accept the map (e.g. `mountDefaultUserAuthRoutes`, `mountDefaultUserCrudRoutes`)
52
+ can type their parameter precisely instead of widening to
53
+ `Record<string, MoleculeRequestHandler>`.
54
+
55
+ ```typescript
56
+ interface UserRequestHandlerMap {
57
+ auth: MoleculeRequestHandler
58
+ authSelf: MoleculeRequestHandler
59
+ rateLimitAuth: MoleculeRequestHandler
60
+ rateLimitTwoFactor: MoleculeRequestHandler
61
+ create: MoleculeRequestHandler
62
+ logIn: MoleculeRequestHandler
63
+ oauthAuthorize: MoleculeRequestHandler
64
+ logInOAuth: MoleculeRequestHandler
65
+ logout: MoleculeRequestHandler
66
+ read: MoleculeRequestHandler
67
+ readSelf: MoleculeRequestHandler
68
+ update: MoleculeRequestHandler
69
+ del: MoleculeRequestHandler
70
+ updatePassword: MoleculeRequestHandler
71
+ forgotPassword: MoleculeRequestHandler
72
+ resetPassword: MoleculeRequestHandler
73
+ verifyTwoFactor: MoleculeRequestHandler
74
+ updatePlan: MoleculeRequestHandler
75
+ verifyPayment: MoleculeRequestHandler
76
+ handlePaymentNotification: MoleculeRequestHandler
77
+ requireWebhookAuthenticity: MoleculeRequestHandler
78
+ }
79
+ ```
80
+
81
+ ### Types
82
+
83
+ #### `CreateOAuthProps`
84
+
85
+ Create O Auth Props type.
86
+
87
+ ```typescript
88
+ type CreateOAuthProps = z.infer<typeof createOAuthPropsSchema>
89
+ ```
90
+
91
+ #### `CreateProps`
92
+
93
+ Create Props type.
94
+
95
+ ```typescript
96
+ type CreateProps = z.infer<typeof createPropsSchema>
97
+ ```
98
+
99
+ #### `CreateSecretProps`
100
+
101
+ Create Secret Props type.
102
+
103
+ ```typescript
104
+ type CreateSecretProps = z.infer<typeof createSecretPropsSchema>
105
+ ```
106
+
107
+ #### `Props`
108
+
109
+ User props type inferred from schema.
110
+
111
+ ```typescript
112
+ type Props = z.infer<typeof propsSchema>
113
+ ```
114
+
115
+ #### `SecretProps`
116
+
117
+ Secret Props type.
118
+
119
+ ```typescript
120
+ type SecretProps = z.infer<typeof secretPropsSchema>
121
+ ```
122
+
123
+ #### `Session`
124
+
125
+ User session data (userId, email, role, permissions, metadata) inferred from sessionSchema.
126
+
127
+ ```typescript
128
+ type Session = z.infer<typeof sessionSchema>
129
+ ```
130
+
131
+ #### `UpdatePasswordSecretProps`
132
+
133
+ Update Password Secret Props type.
134
+
135
+ ```typescript
136
+ type UpdatePasswordSecretProps = z.infer<typeof updatePasswordSecretPropsSchema>
137
+ ```
138
+
139
+ #### `UpdatePlanProps`
140
+
141
+ Update Plan Props type.
142
+
143
+ ```typescript
144
+ type UpdatePlanProps = z.infer<typeof updatePlanPropsSchema>
145
+ ```
146
+
147
+ #### `UpdateProps`
148
+
149
+ Update Props type.
150
+
151
+ ```typescript
152
+ type UpdateProps = z.infer<typeof updatePropsSchema>
153
+ ```
154
+
155
+ #### `VerifyTwoFactorProps`
156
+
157
+ Verify Two Factor Props type.
158
+
159
+ ```typescript
160
+ type VerifyTwoFactorProps = z.infer<typeof verifyTwoFactorPropsSchema>
161
+ ```
162
+
163
+ #### `VerifyTwoFactorSecretProps`
164
+
165
+ Verify Two Factor Secret Props type.
166
+
167
+ ```typescript
168
+ type VerifyTwoFactorSecretProps = z.infer<typeof verifyTwoFactorSecretPropsSchema>
169
+ ```
170
+
171
+ ### Functions
172
+
173
+ #### `createRequestHandlerMap(createRequestHandler)`
174
+
175
+ Creates the full request handler map for the User resource.
176
+ Optional features (OAuth, payments) are conditionally included
177
+ based on bonded providers.
178
+
179
+ Handler names match the route definitions in routes.ts.
180
+
181
+ ```typescript
182
+ function createRequestHandlerMap(
183
+ createRequestHandler: (
184
+ handler: Handler,
185
+ ) => (req: MoleculeRequest, res: MoleculeResponse, next: MoleculeNextFunction) => Promise<void>,
186
+ ): UserRequestHandlerMap
187
+ ```
188
+
189
+ - `createRequestHandler` — Factory from `@molecule/api-resource` that wraps handler configs into Express middleware.
190
+
191
+ **Returns:** A `UserRequestHandlerMap` of handler names to Express middleware.
192
+
193
+ #### `createResource(options)`
194
+
195
+ Creates a user resource definition with optional OAuth servers and plan keys.
196
+
197
+ ```typescript
198
+ function createResource(options?: {
199
+ oauthServers?: OAuthServers
200
+ planKeys?: PlanKeys
201
+ }): types.Resource<unknown>
202
+ ```
203
+
204
+ - `options` — Optional configuration.
205
+ - `options.oauthServers` — Tuple of allowed OAuth server names (e.g. `['google', 'github']`). Constrains the `oauthServer` schema field.
206
+ - `options.planKeys` — Tuple of allowed plan key strings (e.g. `['free', 'pro']`). Constrains the `planKey` schema field.
207
+
208
+ **Returns:** A `Resource` with name `'User'`, table `'users'`, and a Zod schema reflecting the options.
209
+
210
+ #### `createSchema(options)`
211
+
212
+ Creates a full schema for user props.
213
+
214
+ OAuth servers and plan keys can be constrained by passing them as options.
215
+
216
+ ```typescript
217
+ function createSchema(options?: { oauthServers?: OAuthServers; planKeys?: PlanKeys }): z.ZodObject<
218
+ {
219
+ id: z.ZodString
220
+ createdAt: z.ZodString
221
+ updatedAt: z.ZodString
222
+ username: z.ZodOptional<z.ZodString>
223
+ name: z.ZodOptional<z.ZodString>
224
+ email: z.ZodOptional<z.ZodNullable<z.ZodString>>
225
+ emailVerified: z.ZodOptional<z.ZodBoolean>
226
+ avatar: z.ZodOptional<z.ZodNullable<z.ZodString>>
227
+ bio: z.ZodOptional<z.ZodNullable<z.ZodString>>
228
+ twoFactorEnabled: z.ZodOptional<z.ZodBoolean>
229
+ oauthServer:
230
+ | z.ZodOptional<z.ZodString>
231
+ | z.ZodOptional<
232
+ z.ZodEnum<{
233
+ [k in keyof { [k in NonNullable<OAuthServers>[number]]: k }]: {
234
+ [k in NonNullable<OAuthServers>[number]]: k
235
+ }[k]
236
+ }>
237
+ >
238
+ oauthId: z.ZodOptional<z.ZodString>
239
+ oauthData: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>
240
+ planKey:
241
+ | z.ZodOptional<z.ZodString>
242
+ | z.ZodOptional<
243
+ z.ZodEnum<{
244
+ [k in keyof { [k in NonNullable<PlanKeys>[number]]: k }]: {
245
+ [k in NonNullable<PlanKeys>[number]]: k
246
+ }[k]
247
+ }>
248
+ >
249
+ planExpiresAt: z.ZodOptional<z.ZodString>
250
+ planAutoRenews: z.ZodOptional<z.ZodBoolean>
251
+ },
252
+ z.core.$strip
253
+ >
254
+ ```
255
+
256
+ - `options` — Optional configuration.
257
+ - `options.oauthServers` — Tuple of allowed OAuth server names. Constrains `oauthServer` to a Zod enum.
258
+ - `options.planKeys` — Tuple of allowed plan key strings. Constrains `planKey` to a Zod enum.
259
+
260
+ **Returns:** A Zod object schema extending `basePropsSchema` with user-specific fields (username, email, OAuth, plan).
261
+
262
+ ### Constants
263
+
264
+ #### `createOAuthPropsSchema`
265
+
266
+ Schema for creating a user via OAuth.
267
+
268
+ ```typescript
269
+ const createOAuthPropsSchema: z.ZodObject<
270
+ {
271
+ username: z.ZodOptional<z.ZodString>
272
+ name: z.ZodOptional<z.ZodString>
273
+ email: z.ZodOptional<z.ZodNullable<z.ZodString>>
274
+ emailVerified: z.ZodOptional<z.ZodBoolean>
275
+ avatar: z.ZodOptional<z.ZodNullable<z.ZodString>>
276
+ bio: z.ZodOptional<z.ZodNullable<z.ZodString>>
277
+ oauthServer: z.ZodOptional<z.ZodString> | z.ZodOptional<z.ZodEnum<{}>>
278
+ oauthId: z.ZodOptional<z.ZodString>
279
+ oauthData: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>
280
+ },
281
+ z.core.$strip
282
+ >
283
+ ```
284
+
285
+ #### `createPropsSchema`
286
+
287
+ Schema for creating a user via password.
288
+
289
+ ```typescript
290
+ const createPropsSchema: z.ZodObject<
291
+ {
292
+ username: z.ZodOptional<z.ZodString>
293
+ name: z.ZodOptional<z.ZodString>
294
+ email: z.ZodOptional<z.ZodNullable<z.ZodString>>
295
+ },
296
+ z.core.$strip
297
+ >
298
+ ```
299
+
300
+ #### `createSecretPropsSchema`
301
+
302
+ Schema for creating secret props (password hash only).
303
+
304
+ ```typescript
305
+ const createSecretPropsSchema: z.ZodObject<
306
+ { passwordHash: z.ZodOptional<z.ZodString> },
307
+ z.core.$strip
308
+ >
309
+ ```
310
+
311
+ #### `i18nRegistered`
312
+
313
+ The i18n registered.
314
+
315
+ ```typescript
316
+ const i18nRegistered: true
317
+ ```
318
+
319
+ #### `MAX_AVATAR_LENGTH`
320
+
321
+ Maximum length (in characters) of a user's `avatar`. Sized to permit a small
322
+ inline data-URI (~256KB) without requiring an external upload/storage bond.
323
+ Larger avatars must be hosted elsewhere and referenced by URL.
324
+
325
+ ```typescript
326
+ const MAX_AVATAR_LENGTH: number
327
+ ```
328
+
329
+ #### `MAX_BIO_LENGTH`
330
+
331
+ Maximum length (in characters) of a user's `bio`.
332
+
333
+ ```typescript
334
+ const MAX_BIO_LENGTH: 1000
335
+ ```
336
+
337
+ #### `propsSchema`
338
+
339
+ Default schema for user props.
340
+
341
+ ```typescript
342
+ const propsSchema: z.ZodObject<
343
+ {
344
+ id: z.ZodString
345
+ createdAt: z.ZodString
346
+ updatedAt: z.ZodString
347
+ username: z.ZodOptional<z.ZodString>
348
+ name: z.ZodOptional<z.ZodString>
349
+ email: z.ZodOptional<z.ZodNullable<z.ZodString>>
350
+ emailVerified: z.ZodOptional<z.ZodBoolean>
351
+ avatar: z.ZodOptional<z.ZodNullable<z.ZodString>>
352
+ bio: z.ZodOptional<z.ZodNullable<z.ZodString>>
353
+ twoFactorEnabled: z.ZodOptional<z.ZodBoolean>
354
+ oauthServer: z.ZodOptional<z.ZodString> | z.ZodOptional<z.ZodEnum<{}>>
355
+ oauthId: z.ZodOptional<z.ZodString>
356
+ oauthData: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>
357
+ planKey: z.ZodOptional<z.ZodString> | z.ZodOptional<z.ZodEnum<{}>>
358
+ planExpiresAt: z.ZodOptional<z.ZodString>
359
+ planAutoRenews: z.ZodOptional<z.ZodBoolean>
360
+ },
361
+ z.core.$strip
362
+ >
363
+ ```
364
+
365
+ #### `resource`
366
+
367
+ Default user resource definition.
368
+
369
+ ```typescript
370
+ const resource: types.Resource<unknown>
371
+ ```
372
+
373
+ #### `resourceUserSecretDefinitions`
374
+
375
+ Secret definitions required by the user resource.
376
+
377
+ ```typescript
378
+ const resourceUserSecretDefinitions: SecretDefinition[]
379
+ ```
380
+
381
+ #### `routes`
382
+
383
+ Route definitions for the User resource.
384
+ Routes marked optional require additional packages to be installed.
385
+
386
+ Declarative route definitions used by the injection engine.
387
+
388
+ ```typescript
389
+ const routes: (
390
+ | { method: 'post'; path: string; middlewares: string[]; handler: string; optional?: undefined }
391
+ | { method: 'get'; path: string; middlewares: string[]; handler: string; optional: string }
392
+ | { method: 'post'; path: string; middlewares: string[]; handler: string; optional: string }
393
+ | { method: 'get'; path: string; middlewares: string[]; handler: string; optional?: undefined }
394
+ | { method: 'patch'; path: string; middlewares: string[]; handler: string; optional?: undefined }
395
+ | { method: 'delete'; path: string; middlewares: string[]; handler: string; optional?: undefined }
396
+ )[]
397
+ ```
398
+
399
+ #### `secretPropsSchema`
400
+
401
+ Secret properties stored in a separate table.
402
+
403
+ ```typescript
404
+ const secretPropsSchema: z.ZodObject<
405
+ {
406
+ id: z.ZodString
407
+ passwordHash: z.ZodOptional<z.ZodString>
408
+ passwordResetToken: z.ZodOptional<z.ZodString>
409
+ passwordResetTokenAt: z.ZodOptional<z.ZodString>
410
+ pendingTwoFactorSecret: z.ZodOptional<z.ZodString>
411
+ twoFactorSecret: z.ZodOptional<z.ZodString>
412
+ lastTwoFactorTimeStep: z.ZodOptional<z.ZodNumber>
413
+ },
414
+ z.core.$strip
415
+ >
416
+ ```
417
+
418
+ #### `sessionSchema`
419
+
420
+ Zod schema for JWT session payloads (userId, deviceId, optional OAuth fields).
421
+
422
+ ```typescript
423
+ const sessionSchema: z.ZodObject<
424
+ {
425
+ id: z.ZodOptional<z.ZodString>
426
+ userId: z.ZodString
427
+ deviceId: z.ZodString
428
+ oauthServer: z.ZodOptional<z.ZodString>
429
+ oauthId: z.ZodOptional<z.ZodString>
430
+ },
431
+ z.core.$strip
432
+ >
433
+ ```
434
+
435
+ #### `updatePasswordSecretPropsSchema`
436
+
437
+ Schema for updating password secret props (partial password hash).
438
+
439
+ ```typescript
440
+ const updatePasswordSecretPropsSchema: z.ZodObject<
441
+ { passwordHash: z.ZodOptional<z.ZodOptional<z.ZodString>> },
442
+ z.core.$strip
443
+ >
444
+ ```
445
+
446
+ #### `updatePlanPropsSchema`
447
+
448
+ Schema for updating a user's plan (partial planKey, planExpiresAt, planAutoRenews).
449
+
450
+ ```typescript
451
+ const updatePlanPropsSchema: z.ZodObject<
452
+ {
453
+ planKey: z.ZodOptional<z.ZodOptional<z.ZodString> | z.ZodOptional<z.ZodEnum<{}>>>
454
+ planExpiresAt: z.ZodOptional<z.ZodOptional<z.ZodString>>
455
+ planAutoRenews: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>
456
+ },
457
+ z.core.$strip
458
+ >
459
+ ```
460
+
461
+ #### `updatePropsSchema`
462
+
463
+ Schema for updating a user (partial username, name, email, avatar, bio).
464
+
465
+ ```typescript
466
+ const updatePropsSchema: z.ZodObject<
467
+ {
468
+ username: z.ZodOptional<z.ZodOptional<z.ZodString>>
469
+ name: z.ZodOptional<z.ZodOptional<z.ZodString>>
470
+ email: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodString>>>
471
+ avatar: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodString>>>
472
+ bio: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodString>>>
473
+ },
474
+ z.core.$strip
475
+ >
476
+ ```
477
+
478
+ #### `verifyTwoFactorPropsSchema`
479
+
480
+ Schema for verifying two-factor authentication.
481
+
482
+ ```typescript
483
+ const verifyTwoFactorPropsSchema: z.ZodObject<
484
+ { twoFactorEnabled: z.ZodOptional<z.ZodOptional<z.ZodBoolean>> },
485
+ z.core.$strip
486
+ >
487
+ ```
488
+
489
+ #### `verifyTwoFactorSecretPropsSchema`
490
+
491
+ Schema for two-factor secret props.
492
+
493
+ ```typescript
494
+ const verifyTwoFactorSecretPropsSchema: z.ZodObject<
495
+ {
496
+ pendingTwoFactorSecret: z.ZodOptional<z.ZodOptional<z.ZodString>>
497
+ twoFactorSecret: z.ZodOptional<z.ZodOptional<z.ZodString>>
498
+ },
499
+ z.core.$strip
500
+ >
501
+ ```
502
+
503
+ ### Namespaces
504
+
505
+ #### `authorization`
506
+
507
+ Members:
508
+
509
+ - `authorization.getAuthCookieName` — function: Resolve the actual cookie name for an auth cookie.
510
+ - `authorization.getAuthCookieOptions` — function: Base cookie attributes shared by EVERY auth cookie this resource sets and
511
+ - `authorization.invalidateDeviceExistsCache` — function: Evict a single device's positive entry from the device-exists cache so the
512
+ - `authorization.invalidateAllDeviceExistsCache` — function: Evict ALL positive entries from the device-exists cache.
513
+ - `authorization.set` — function: Set authorization headers and cookie for a session.
514
+ - `authorization.verifyMiddleware` — function: Middleware that verifies the JWT token from the `Authorization` header and sets `res.locals.session`.
515
+
516
+ #### `authorizers`
517
+
518
+ Members:
519
+
520
+ - `authorizers.auth` — function: Middleware that checks if the request has an authenticated session (`res.locals.session.userId`).
521
+ - `authorizers.authSelf` — function: Middleware that checks if the authenticated user's ID matches the `:id` route parameter.
522
+ - `authorizers.RateLimitAuthOptions` — interface: Configuration for {@link rateLimit}.
523
+ - `authorizers.rateLimit` — function: Creates an authorizer middleware that brute-force-protects an auth endpoint.
524
+ - `authorizers.loginAccountKey` — function: Account-identifier extractor for the login endpoint: the submitted username
525
+ - `authorizers.emailAccountKey` — function: Account-identifier extractor for the forgot-password endpoint: the submitted
526
+ - `authorizers.paramIdAccountKey` — function: Account-identifier extractor for the verify-two-factor endpoint: the target
527
+ - `authorizers.requireWebhookAuthenticity` — function: Middleware guarding the public `POST /users/payment-notification/:provider`
528
+
529
+ #### `handlers`
530
+
531
+ Members:
532
+
533
+ - `handlers.handlePaymentNotification` — function: Generic payment notification handler that works with any bonded PaymentProvider. Reads the
534
+ - `handlers.verifyPayment` — function: Generic payment verification handler that works with any bonded PaymentProvider. Reads the
535
+ - `handlers.CreateRequest` — interface: Request body for user creation, including password and optional device name.
536
+ - `handlers.create` — function: Creates a user with username and password. Validates username uniqueness and email format,
537
+ - `handlers.del` — function: Deletes a user and their associated data. Removes secrets from the secrets table,
538
+ - `handlers.ForgotPasswordRequest` — interface: Request body for password reset initiation.
539
+ - `handlers.forgotPassword` — function: Generates a UUID password reset token, stores it in the secrets table, and sends a reset
540
+ - `handlers.LogInRequest` — interface: Request body for user login, supporting password, reset token, and 2FA flows.
541
+ - `handlers.logIn` — function: Logs in a user by username or email. Supports password authentication, password reset token
542
+ - `handlers.LogInOAuthRequest` — interface: Request body for OAuth login, including the OAuth server name, authorization code, and PKCE verifier.
543
+ - `handlers.logInOAuth` — function: Logs in or creates a user via OAuth. Verifies the authorization code with the bonded OAuth
544
+ - `handlers.logout` — function: Logs the current user out. Revokes the session device server-side (so the JWT
545
+ - `handlers.oauthAuthorize` — function: OAuth initiation — `GET /users/oauth/:provider`.
546
+ - `handlers.read` — function: Reads a user by ID from the database. Attaches plan info (with expiration/renewal status) via the
547
+ - `handlers.readSelf` — function: Reads the AUTHENTICATED user from the session — no `:id` param. Backs
548
+ - `handlers.ResetPasswordRequest` — interface: Request body for confirming a password reset using a token.
549
+ - `handlers.resetPassword` — function: Confirms a password reset by validating the one-time token previously generated by
550
+ - `handlers.update` — function: Updates a user's profile fields (username, name, email). Validates username format
551
+ - `handlers.UpdatePasswordRequest` — interface: Request body for password update, with current password verification and new password.
552
+ - `handlers.updatePassword` — function: Updates a user's password. If the user already has a password hash, the current password
553
+ - `handlers.UpdatePlanRequest` — interface: Request body for plan update, containing the target plan key.
554
+ - `handlers.updatePlan` — function: Updates a user's subscription plan. Uses the bonded PlanService to look up plan metadata and
555
+ - `handlers.VerifyTwoFactorRequest` — interface: Request body for two-factor authentication operations (setup, enable, or disable).
556
+ - `handlers.verifyTwoFactor` — function: Handles two-factor authentication lifecycle via `@molecule/api-two-factor`:
557
+
558
+ #### `types`
559
+
560
+ Members:
561
+
562
+ - `types.CreateOAuthProps` — type: Create O Auth Props type.
563
+ - `types.CreateProps` — type: Create Props type.
564
+ - `types.CreateSecretProps` — type: Create Secret Props type.
565
+ - `types.Props` — type: User props type inferred from schema.
566
+ - `types.SecretProps` — type: Secret Props type.
567
+ - `types.Session` — type: User session data (userId, email, role, permissions, metadata) inferred from sessionSchema.
568
+ - `types.UpdatePasswordSecretProps` — type: Update Password Secret Props type.
569
+ - `types.UpdatePlanProps` — type: Update Plan Props type.
570
+ - `types.UpdateProps` — type: Update Props type.
571
+ - `types.VerifyTwoFactorProps` — type: Verify Two Factor Props type.
572
+ - `types.VerifyTwoFactorSecretProps` — type: Verify Two Factor Secret Props type.
573
+ - `types.Resource` — type: An object describing the `user` resource.
574
+
575
+ #### `utilities`
576
+
577
+ Members:
578
+
579
+ - `utilities.fetchAvatarDataUri` — function: Downloads an OAuth provider's profile image and re-hosts it as an inline
580
+ - `utilities.getPlan` — function: Get a user's current plan info.
581
+ - `utilities.invalidateEntitlementsCache` — function: Invalidates the entitlements plan-key cache for a user after their plan
582
+ - `utilities.invalidateEntitlementsCacheSafe` — function: Fire-and-forget variant of {@link invalidateEntitlementsCache} for call
583
+ - `utilities.normalizeEmail` — function: Normalizes an email address for storage and lookup so case/whitespace
584
+ - `utilities.notify` — function: Sends push notifications to all of a user's devices except the current one. Retrieves devices
585
+
586
+ #### `z`
587
+
588
+ Members:
589
+
590
+ - `z.core` — namespace
591
+ - `z.infer` — type
592
+ - `z.output` — type
593
+ - `z.input` — type
594
+ - `z.JSONType` — type
595
+ - `z.globalRegistry` — const
596
+ - `z.GlobalMeta` — interface
597
+ - `z.registry` — function
598
+ - `z.config` — function
599
+ - `z.$output` — const
600
+ - `z.$input` — const
601
+ - `z.$brand` — const
602
+ - `z.clone` — function
603
+ - `z.regexes` — namespace
604
+ - `z.treeifyError` — function
605
+ - `z.prettifyError` — function
606
+ - `z.formatError` — function
607
+ - `z.flattenError` — function
608
+ - `z.TimePrecision` — const
609
+ - `z.util` — namespace
610
+ - `z.NEVER` — const: A special constant with type `never`
611
+ - `z.toJSONSchema` — function
612
+ - `z.fromJSONSchema` — function: Converts a JSON Schema to a Zod schema. This function should be considered semi-experimental. It's behavior is liable to change.
613
+ - `z.locales` — namespace
614
+ - `z.ZodISODateTime` — interface
615
+ - `z.ZodISODate` — interface
616
+ - `z.ZodISOTime` — interface
617
+ - `z.ZodISODuration` — interface
618
+ - `z.iso` — namespace
619
+ - `z.ZodCoercedString` — interface
620
+ - `z.ZodCoercedNumber` — interface
621
+ - `z.ZodCoercedBigInt` — interface
622
+ - `z.ZodCoercedBoolean` — interface
623
+ - `z.ZodCoercedDate` — interface
624
+ - `z.coerce` — namespace
625
+ - `z.string` — function
626
+ - `z.email` — function
627
+ - `z.guid` — function
628
+ - `z.uuid` — function
629
+ - `z.uuidv4` — function
630
+ - `z.uuidv6` — function
631
+ - `z.uuidv7` — function
632
+ - `z.url` — function
633
+ - `z.httpUrl` — function
634
+ - `z.emoji` — function
635
+ - `z.nanoid` — function
636
+ - `z.cuid` — function: Validates a CUID v1 string.
637
+ - `z.cuid2` — function
638
+ - `z.ulid` — function
639
+ - `z.xid` — function
640
+ - `z.ksuid` — function
641
+ - `z.ipv4` — function
642
+ - `z.mac` — function
643
+ - `z.ipv6` — function
644
+ - `z.cidrv4` — function
645
+ - `z.cidrv6` — function
646
+ - `z.base64` — function
647
+ - `z.base64url` — function
648
+ - `z.e164` — function
649
+ - `z.jwt` — function
650
+ - `z.stringFormat` — function
651
+ - `z.hostname` — function
652
+ - `z.hex` — function
653
+ - `z.hash` — function
654
+ - `z.number` — function
655
+ - `z.int` — function
656
+ - `z.float32` — function
657
+ - `z.float64` — function
658
+ - `z.int32` — function
659
+ - `z.uint32` — function
660
+ - `z.boolean` — function
661
+ - `z.bigint` — function
662
+ - `z.int64` — function
663
+ - `z.uint64` — function
664
+ - `z.symbol` — function
665
+ - `z.any` — function
666
+ - `z.unknown` — function
667
+ - `z.never` — function
668
+ - `z.date` — function
669
+ - `z.array` — function
670
+ - `z.keyof` — function
671
+ - `z.object` — function
672
+ - `z.strictObject` — function
673
+ - `z.looseObject` — function
674
+ - `z.union` — function
675
+ - `z.xor` — function: Creates an exclusive union (XOR) where exactly one option must match.
676
+ - `z.discriminatedUnion` — function
677
+ - `z.intersection` — function
678
+ - `z.tuple` — function
679
+ - `z.record` — function
680
+ - `z.partialRecord` — function
681
+ - `z.looseRecord` — function
682
+ - `z.map` — function
683
+ - `z.set` — function
684
+ - `z.nativeEnum` — function
685
+ - `z.literal` — function
686
+ - `z.file` — function
687
+ - `z.transform` — function
688
+ - `z.optional` — function
689
+ - `z.exactOptional` — function
690
+ - `z.nullable` — function
691
+ - `z.nullish` — function
692
+ - `z._default` — function
693
+ - `z.prefault` — function
694
+ - `z.nonoptional` — function
695
+ - `z.success` — function
696
+ - `z.nan` — function
697
+ - `z.pipe` — function
698
+ - `z.codec` — function
699
+ - `z.invertCodec` — function
700
+ - `z.readonly` — function
701
+ - `z.templateLiteral` — function
702
+ - `z.lazy` — function
703
+ - `z.promise` — function
704
+ - `z._function` — function
705
+ - `z.check` — function
706
+ - `z.custom` — function
707
+ - `z.refine` — function
708
+ - `z.superRefine` — function
709
+ - `z.json` — function
710
+ - `z.preprocess` — function
711
+ - `z.ZodStandardSchemaWithJSON` — type
712
+ - `z.ZodType` — interface
713
+ - `z._ZodType` — interface
714
+ - `z._ZodString` — interface
715
+ - `z.ZodString` — interface
716
+ - `z.ZodStringFormat` — interface
717
+ - `z.ZodEmail` — interface
718
+ - `z.ZodGUID` — interface
719
+ - `z.ZodUUID` — interface
720
+ - `z.ZodURL` — interface
721
+ - `z.ZodEmoji` — interface
722
+ - `z.ZodNanoID` — interface
723
+ - `z.ZodCUID` — interface
724
+ - `z.ZodCUID2` — interface
725
+ - `z.ZodULID` — interface
726
+ - `z.ZodXID` — interface
727
+ - `z.ZodKSUID` — interface
728
+ - `z.ZodIPv4` — interface
729
+ - `z.ZodMAC` — interface
730
+ - `z.ZodIPv6` — interface
731
+ - `z.ZodCIDRv4` — interface
732
+ - `z.ZodCIDRv6` — interface
733
+ - `z.ZodBase64` — interface
734
+ - `z.ZodBase64URL` — interface
735
+ - `z.ZodE164` — interface
736
+ - `z.ZodJWT` — interface
737
+ - `z.ZodCustomStringFormat` — interface
738
+ - `z._ZodNumber` — interface
739
+ - `z.ZodNumber` — interface
740
+ - `z.ZodNumberFormat` — interface
741
+ - `z.ZodInt` — interface
742
+ - `z.ZodFloat32` — interface
743
+ - `z.ZodFloat64` — interface
744
+ - `z.ZodInt32` — interface
745
+ - `z.ZodUInt32` — interface
746
+ - `z._ZodBoolean` — interface
747
+ - `z.ZodBoolean` — interface
748
+ - `z._ZodBigInt` — interface
749
+ - `z.ZodBigInt` — interface
750
+ - `z.ZodBigIntFormat` — interface
751
+ - `z.ZodSymbol` — interface
752
+ - `z.ZodUndefined` — interface
753
+ - `z.undefined` — function
754
+ - `z.ZodNull` — interface
755
+ - `z.null` — function
756
+ - `z.ZodAny` — interface
757
+ - `z.ZodUnknown` — interface
758
+ - `z.ZodNever` — interface
759
+ - `z.ZodVoid` — interface
760
+ - `z.void` — function
761
+ - `z._ZodDate` — interface
762
+ - `z.ZodDate` — interface
763
+ - `z.ZodArray` — interface
764
+ - `z.SafeExtendShape` — type
765
+ - `z.ZodObject` — interface
766
+ - `z.ZodUnion` — interface
767
+ - `z.ZodXor` — interface
768
+ - `z.ZodDiscriminatedUnion` — interface
769
+ - `z.ZodIntersection` — interface
770
+ - `z.ZodTuple` — interface
771
+ - `z.ZodRecord` — interface
772
+ - `z.ZodMap` — interface
773
+ - `z.ZodSet` — interface
774
+ - `z.ZodEnum` — interface
775
+ - `z.enum` — function
776
+ - `z.ZodLiteral` — interface
777
+ - `z.ZodFile` — interface
778
+ - `z.ZodTransform` — interface
779
+ - `z.ZodOptional` — interface
780
+ - `z.ZodExactOptional` — interface
781
+ - `z.ZodNullable` — interface
782
+ - `z.ZodDefault` — interface
783
+ - `z.ZodPrefault` — interface
784
+ - `z.ZodNonOptional` — interface
785
+ - `z.ZodSuccess` — interface
786
+ - `z.ZodCatch` — interface
787
+ - `z.catch` — function
788
+ - `z.ZodNaN` — interface
789
+ - `z.ZodPipe` — interface
790
+ - `z.ZodCodec` — interface
791
+ - `z.ZodPreprocess` — interface
792
+ - `z.ZodReadonly` — interface
793
+ - `z.ZodTemplateLiteral` — interface
794
+ - `z.ZodLazy` — interface
795
+ - `z.ZodPromise` — interface
796
+ - `z.ZodFunction` — interface
797
+ - `z.function` — function
798
+ - `z.ZodCustom` — interface
799
+ - `z.describe` — const
800
+ - `z.meta` — const
801
+ - `z.instanceof` — function
802
+ - `z.stringbool` — const
803
+ - `z.ZodJSONSchemaInternals` — interface
804
+ - `z.ZodJSONSchema` — interface
805
+ - `z.lt` — function
806
+ - `z.lte` — function
807
+ - `z.gt` — function
808
+ - `z.gte` — function
809
+ - `z.positive` — function
810
+ - `z.negative` — function
811
+ - `z.nonpositive` — function
812
+ - `z.nonnegative` — function
813
+ - `z.multipleOf` — function
814
+ - `z.maxSize` — function
815
+ - `z.minSize` — function
816
+ - `z.size` — function
817
+ - `z.maxLength` — function
818
+ - `z.minLength` — function
819
+ - `z.length` — function
820
+ - `z.regex` — function
821
+ - `z.lowercase` — function
822
+ - `z.uppercase` — function
823
+ - `z.includes` — function
824
+ - `z.startsWith` — function
825
+ - `z.endsWith` — function
826
+ - `z.property` — function
827
+ - `z.mime` — function
828
+ - `z.overwrite` — function
829
+ - `z.normalize` — function
830
+ - `z.trim` — function
831
+ - `z.toLowerCase` — function
832
+ - `z.toUpperCase` — function
833
+ - `z.slugify` — function
834
+ - `z.RefinementCtx` — interface
835
+ - `z.ZodIssue` — type
836
+ - `z.ZodError` — interface: An Error-like class used to store Zod validation issues.
837
+ - `z.ZodRealError` — const
838
+ - `z.ZodFlattenedError` — type
839
+ - `z.ZodFormattedError` — type
840
+ - `z.ZodErrorMap` — interface
841
+ - `z.IssueData` — type
842
+ - `z.ZodSafeParseResult` — type
843
+ - `z.ZodSafeParseSuccess` — type
844
+ - `z.ZodSafeParseError` — type
845
+ - `z.parse` — const
846
+ - `z.parseAsync` — const
847
+ - `z.safeParse` — const
848
+ - `z.safeParseAsync` — const
849
+ - `z.encode` — const
850
+ - `z.decode` — const
851
+ - `z.encodeAsync` — const
852
+ - `z.decodeAsync` — const
853
+ - `z.safeEncode` — const
854
+ - `z.safeDecode` — const
855
+ - `z.safeEncodeAsync` — const
856
+ - `z.safeDecodeAsync` — const
857
+ - `z.setErrorMap` — function
858
+ - `z.getErrorMap` — function
859
+ - `z.TypeOf` — type
860
+ - `z.Infer` — type
861
+ - `z.ZodFirstPartySchemaTypes` — type
862
+ - `z.ZodIssueCode` — const
863
+ - `z.inferFlattenedErrors` — type
864
+ - `z.inferFormattedError` — type
865
+ - `z.BRAND` — type: Use `z.$brand` instead
866
+ - `z.ZodTypeAny` — interface
867
+ - `z.ZodSchema` — interface
868
+ - `z.Schema` — interface
869
+ - `z.ZodRawShape` — type: Included for Zod 3 compatibility
870
+ - `z.ZodFirstPartyTypeKind` — enum
871
+
872
+ ## Injection Notes
873
+
874
+ ### Requirements
875
+
876
+ Peer dependencies:
877
+
878
+ - `@molecule/api-bond` ^1.0.1
879
+ - `@molecule/api-config` ^1.0.1
880
+ - `@molecule/api-database` ^1.0.1
881
+ - `@molecule/api-entitlements` ^1.0.1
882
+ - `@molecule/api-i18n` ^1.0.1
883
+ - `@molecule/api-jwt` ^1.0.1
884
+ - `@molecule/api-locales-user` ^1.0.1
885
+ - `@molecule/api-locales-user-payments` ^1.0.1
886
+ - `@molecule/api-password` ^1.0.1
887
+ - `@molecule/api-payments` ^1.0.1
888
+ - `@molecule/api-push-notifications` ^1.0.1
889
+ - `@molecule/api-rate-limit` ^1.0.1
890
+ - `@molecule/api-resource` ^1.0.1
891
+ - `@molecule/api-resource-device` ^1.0.1
892
+ - `@molecule/api-secrets` ^1.0.1
893
+ - `@molecule/api-two-factor` ^1.0.1
894
+
895
+ ### Environment Variables
896
+
897
+ - `JWT_PRIVATE_KEY` _(required)_ — JWT signing key (RSA private)
898
+ - **Auto-generated at scaffold — no manual setup.**
899
+ - `JWT_PUBLIC_KEY` _(required)_ — JWT verification key (RSA public)
900
+ - **Auto-generated at scaffold — no manual setup.**
901
+
902
+ ### Runtime Dependencies
903
+
904
+ - `@molecule/api-bond`
905
+ - `@molecule/api-config`
906
+ - `@molecule/api-database`
907
+ - `@molecule/api-entitlements`
908
+ - `@molecule/api-i18n`
909
+ - `@molecule/api-jwt`
910
+ - `@molecule/api-locales-user`
911
+ - `@molecule/api-locales-user-payments`
912
+ - `@molecule/api-password`
913
+ - `@molecule/api-payments`
914
+ - `@molecule/api-push-notifications`
915
+ - `@molecule/api-rate-limit`
916
+ - `@molecule/api-resource`
917
+ - `@molecule/api-resource-device`
918
+ - `@molecule/api-secrets`
919
+ - `@molecule/api-two-factor`
920
+ - `zod`
921
+
922
+ The user record is split across TWO schemas — pick the right one or you leak credentials:
923
+
924
+ - **{@link Props} (`propsSchema`)** — SAFE, client-facing fields (username, name, email,
925
+ `emailVerified`, `twoFactorEnabled`, plan). This is what handlers return and what lives
926
+ in the `users` table.
927
+ - **{@link SecretProps} (`secretPropsSchema`)** — SERVER-ONLY secrets: `passwordHash`, the
928
+ TOTP `twoFactorSecret` (and its pending-setup value). Stored in a SEPARATE secrets table
929
+ and NEVER serialized to the client. Note the pair `twoFactorEnabled` (safe boolean, in
930
+ `Props`) vs `twoFactorSecret` (secret, in `SecretProps`).
931
+
932
+ When you extend the user, put a secret (token, hash, key, provider refresh token) in
933
+ `SecretProps`; put a display field in `Props`. **Never add a secret to `Props`, never
934
+ return a secrets-table value in a response or log, and never `res.json(userRow)` a raw DB
935
+ row** — return `Props`.
936
+
937
+ Auth is ALREADY wired globally (the router's `verifyMiddleware` → `res.locals.session`),
938
+ so a handler reads the current user with `getUserId(res)` and does NOT add per-route auth
939
+ middleware (see the `auth` skill). Scope every custom user query by the authenticated id.
940
+
941
+ On the CLIENT, the bearer token is held IN MEMORY only — a `localStorage` copy is
942
+ XSS-exfiltratable and is forbidden. The session is restored after a reload via the
943
+ httpOnly cookie + `GET /users/me`; don't persist the token yourself.
944
+
945
+ **Client-facing endpoints** (mounted under the app's `/api` prefix → `/api/users/...`).
946
+ The auth CLIENT (`useAuth()` → `login` / `register` / `logout` / `refresh`) already wraps
947
+ login / signup / logout — do NOT hand-roll those against the raw routes. The rest have NO
948
+ client method; call them with raw `http.*`. Use these EXACT paths — a weak model guesses
949
+ `/api/auth/*` or `/api/user` (singular), and neither exists:
950
+
951
+ - `POST /api/users/forgot-password` — request a reset email (body `{ email }`)
952
+ - `POST /api/users/reset-password` — confirm with the emailed token (body `{ token, password }`)
953
+ - `PATCH /api/users/:id` — update profile fields (name, username, email, bio); NOT `PUT /api/user`
954
+ - `PATCH /api/users/:id/password` — change password · `DELETE /api/users/:id` — delete account
955
+ - `PATCH /api/users/:id/plan` — update the subscription plan
956
+ - `GET /api/users/me` — the current user (session restore) · `GET /api/users/:id` — read one
957
+ The full, authoritative route list is the `routes` export (see `routes.ts`).
958
+
959
+ ## E2E Tests
960
+
961
+ Integration checklist — drive the real UI (live preview, no mocks), adapt
962
+ each item to this app's actual screens/flows, and check every box off one
963
+ by one. A box you can't check is an integration bug to fix — not a skip:
964
+
965
+ - [ ] A new user can sign up with email + password and lands authenticated (the
966
+ UI reflects the signed-in user, e.g. their name/menu appears).
967
+ - [ ] Any flow that emails a link/code (signup verification, password reset)
968
+ round-trips: the sandbox CAPTURES the message instead of sending — read it
969
+ with the `read_activity` tool (filter type 'email') and follow the link/code
970
+ in its payload; never mock the flow or modify production code to expose it.
971
+ - [ ] Logging out and logging back in with the same credentials reaches the same
972
+ account and its data.
973
+ - [ ] The session survives a full page reload (restored via the httpOnly cookie +
974
+ `GET /users/me` — never from a token persisted in localStorage).
975
+ - [ ] A wrong password shows a visible error and does NOT authenticate.
976
+ - [ ] Authenticated-only screens are unreachable when logged out (redirect to
977
+ login or an explicit denial — never a blank page).
978
+ - [ ] A profile/account edit (e.g. display name) persists across a reload.