@molecule/api-resource-user 1.0.0 → 1.0.1

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