@molecule/api-resource-user 1.0.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.
Files changed (164) hide show
  1. package/LICENSE +115 -0
  2. package/dist/authorization.d.ts +107 -0
  3. package/dist/authorization.d.ts.map +1 -0
  4. package/dist/authorization.js +375 -0
  5. package/dist/authorization.js.map +1 -0
  6. package/dist/authorizers/auth.d.ts +8 -0
  7. package/dist/authorizers/auth.d.ts.map +1 -0
  8. package/dist/authorizers/auth.js +19 -0
  9. package/dist/authorizers/auth.js.map +1 -0
  10. package/dist/authorizers/authSelf.d.ts +14 -0
  11. package/dist/authorizers/authSelf.d.ts.map +1 -0
  12. package/dist/authorizers/authSelf.js +31 -0
  13. package/dist/authorizers/authSelf.js.map +1 -0
  14. package/dist/authorizers/index.d.ts +5 -0
  15. package/dist/authorizers/index.d.ts.map +1 -0
  16. package/dist/authorizers/index.js +5 -0
  17. package/dist/authorizers/index.js.map +1 -0
  18. package/dist/authorizers/rateLimit.d.ts +84 -0
  19. package/dist/authorizers/rateLimit.d.ts.map +1 -0
  20. package/dist/authorizers/rateLimit.js +144 -0
  21. package/dist/authorizers/rateLimit.js.map +1 -0
  22. package/dist/authorizers/requireWebhookAuthenticity.d.ts +23 -0
  23. package/dist/authorizers/requireWebhookAuthenticity.d.ts.map +1 -0
  24. package/dist/authorizers/requireWebhookAuthenticity.js +76 -0
  25. package/dist/authorizers/requireWebhookAuthenticity.js.map +1 -0
  26. package/dist/browser-guard.d.ts +2 -0
  27. package/dist/browser-guard.d.ts.map +1 -0
  28. package/dist/browser-guard.js +19 -0
  29. package/dist/browser-guard.js.map +1 -0
  30. package/dist/handlers/create.d.ts +52 -0
  31. package/dist/handlers/create.d.ts.map +1 -0
  32. package/dist/handlers/create.js +208 -0
  33. package/dist/handlers/create.js.map +1 -0
  34. package/dist/handlers/del.d.ts +30 -0
  35. package/dist/handlers/del.d.ts.map +1 -0
  36. package/dist/handlers/del.js +55 -0
  37. package/dist/handlers/del.js.map +1 -0
  38. package/dist/handlers/forgotPassword.d.ts +34 -0
  39. package/dist/handlers/forgotPassword.d.ts.map +1 -0
  40. package/dist/handlers/forgotPassword.js +119 -0
  41. package/dist/handlers/forgotPassword.js.map +1 -0
  42. package/dist/handlers/index.d.ts +16 -0
  43. package/dist/handlers/index.d.ts.map +1 -0
  44. package/dist/handlers/index.js +16 -0
  45. package/dist/handlers/index.js.map +1 -0
  46. package/dist/handlers/logIn.d.ts +91 -0
  47. package/dist/handlers/logIn.d.ts.map +1 -0
  48. package/dist/handlers/logIn.js +223 -0
  49. package/dist/handlers/logIn.js.map +1 -0
  50. package/dist/handlers/logInOAuth.d.ts +66 -0
  51. package/dist/handlers/logInOAuth.d.ts.map +1 -0
  52. package/dist/handlers/logInOAuth.js +462 -0
  53. package/dist/handlers/logInOAuth.js.map +1 -0
  54. package/dist/handlers/logout.d.ts +22 -0
  55. package/dist/handlers/logout.d.ts.map +1 -0
  56. package/dist/handlers/logout.js +57 -0
  57. package/dist/handlers/logout.js.map +1 -0
  58. package/dist/handlers/oauthAuthorize.d.ts +44 -0
  59. package/dist/handlers/oauthAuthorize.d.ts.map +1 -0
  60. package/dist/handlers/oauthAuthorize.js +141 -0
  61. package/dist/handlers/oauthAuthorize.js.map +1 -0
  62. package/dist/handlers/payments/handlePaymentNotification.d.ts +28 -0
  63. package/dist/handlers/payments/handlePaymentNotification.d.ts.map +1 -0
  64. package/dist/handlers/payments/handlePaymentNotification.js +217 -0
  65. package/dist/handlers/payments/handlePaymentNotification.js.map +1 -0
  66. package/dist/handlers/payments/registry.d.ts +13 -0
  67. package/dist/handlers/payments/registry.d.ts.map +1 -0
  68. package/dist/handlers/payments/registry.js +13 -0
  69. package/dist/handlers/payments/registry.js.map +1 -0
  70. package/dist/handlers/payments/verifyPayment.d.ts +37 -0
  71. package/dist/handlers/payments/verifyPayment.d.ts.map +1 -0
  72. package/dist/handlers/payments/verifyPayment.js +383 -0
  73. package/dist/handlers/payments/verifyPayment.js.map +1 -0
  74. package/dist/handlers/read.d.ts +17 -0
  75. package/dist/handlers/read.d.ts.map +1 -0
  76. package/dist/handlers/read.js +60 -0
  77. package/dist/handlers/read.js.map +1 -0
  78. package/dist/handlers/readSelf.d.ts +23 -0
  79. package/dist/handlers/readSelf.d.ts.map +1 -0
  80. package/dist/handlers/readSelf.js +77 -0
  81. package/dist/handlers/readSelf.js.map +1 -0
  82. package/dist/handlers/resetPassword.d.ts +40 -0
  83. package/dist/handlers/resetPassword.d.ts.map +1 -0
  84. package/dist/handlers/resetPassword.js +147 -0
  85. package/dist/handlers/resetPassword.js.map +1 -0
  86. package/dist/handlers/update.d.ts +28 -0
  87. package/dist/handlers/update.d.ts.map +1 -0
  88. package/dist/handlers/update.js +250 -0
  89. package/dist/handlers/update.js.map +1 -0
  90. package/dist/handlers/updatePassword.d.ts +35 -0
  91. package/dist/handlers/updatePassword.d.ts.map +1 -0
  92. package/dist/handlers/updatePassword.js +113 -0
  93. package/dist/handlers/updatePassword.js.map +1 -0
  94. package/dist/handlers/updatePlan.d.ts +41 -0
  95. package/dist/handlers/updatePlan.d.ts.map +1 -0
  96. package/dist/handlers/updatePlan.js +242 -0
  97. package/dist/handlers/updatePlan.js.map +1 -0
  98. package/dist/handlers/verifyTwoFactor.d.ts +46 -0
  99. package/dist/handlers/verifyTwoFactor.d.ts.map +1 -0
  100. package/dist/handlers/verifyTwoFactor.js +162 -0
  101. package/dist/handlers/verifyTwoFactor.js.map +1 -0
  102. package/dist/i18n.d.ts +10 -0
  103. package/dist/i18n.d.ts.map +1 -0
  104. package/dist/i18n.js +25 -0
  105. package/dist/i18n.js.map +1 -0
  106. package/dist/index.d.ts +88 -0
  107. package/dist/index.d.ts.map +1 -0
  108. package/dist/index.js +88 -0
  109. package/dist/index.js.map +1 -0
  110. package/dist/requestHandlerMap.d.ts +44 -0
  111. package/dist/requestHandlerMap.d.ts.map +1 -0
  112. package/dist/requestHandlerMap.js +68 -0
  113. package/dist/requestHandlerMap.js.map +1 -0
  114. package/dist/resource.d.ts +22 -0
  115. package/dist/resource.d.ts.map +1 -0
  116. package/dist/resource.js +23 -0
  117. package/dist/resource.js.map +1 -0
  118. package/dist/routes.d.ts +44 -0
  119. package/dist/routes.d.ts.map +1 -0
  120. package/dist/routes.js +121 -0
  121. package/dist/routes.js.map +1 -0
  122. package/dist/schema.d.ts +193 -0
  123. package/dist/schema.d.ts.map +1 -0
  124. package/dist/schema.js +229 -0
  125. package/dist/schema.js.map +1 -0
  126. package/dist/secrets.d.ts +16 -0
  127. package/dist/secrets.d.ts.map +1 -0
  128. package/dist/secrets.js +28 -0
  129. package/dist/secrets.js.map +1 -0
  130. package/dist/types.d.ts +14 -0
  131. package/dist/types.d.ts.map +1 -0
  132. package/dist/types.js +9 -0
  133. package/dist/types.js.map +1 -0
  134. package/dist/utilities/getPlan.d.ts +19 -0
  135. package/dist/utilities/getPlan.d.ts.map +1 -0
  136. package/dist/utilities/getPlan.js +26 -0
  137. package/dist/utilities/getPlan.js.map +1 -0
  138. package/dist/utilities/hashResetToken.d.ts +16 -0
  139. package/dist/utilities/hashResetToken.d.ts.map +1 -0
  140. package/dist/utilities/hashResetToken.js +19 -0
  141. package/dist/utilities/hashResetToken.js.map +1 -0
  142. package/dist/utilities/index.d.ts +5 -0
  143. package/dist/utilities/index.d.ts.map +1 -0
  144. package/dist/utilities/index.js +5 -0
  145. package/dist/utilities/index.js.map +1 -0
  146. package/dist/utilities/invalidateEntitlements.d.ts +24 -0
  147. package/dist/utilities/invalidateEntitlements.d.ts.map +1 -0
  148. package/dist/utilities/invalidateEntitlements.js +41 -0
  149. package/dist/utilities/invalidateEntitlements.js.map +1 -0
  150. package/dist/utilities/normalizeEmail.d.ts +22 -0
  151. package/dist/utilities/normalizeEmail.d.ts.map +1 -0
  152. package/dist/utilities/normalizeEmail.js +28 -0
  153. package/dist/utilities/normalizeEmail.js.map +1 -0
  154. package/dist/utilities/notify.d.ts +21 -0
  155. package/dist/utilities/notify.d.ts.map +1 -0
  156. package/dist/utilities/notify.js +39 -0
  157. package/dist/utilities/notify.js.map +1 -0
  158. package/dist/utilities/stripSensitiveUserColumns.d.ts +25 -0
  159. package/dist/utilities/stripSensitiveUserColumns.d.ts.map +1 -0
  160. package/dist/utilities/stripSensitiveUserColumns.js +43 -0
  161. package/dist/utilities/stripSensitiveUserColumns.js.map +1 -0
  162. package/package.json +94 -0
  163. package/setup/users.sql +22 -0
  164. package/setup/usersSecrets.sql +9 -0
@@ -0,0 +1,383 @@
1
+ import { get, getAnalytics, getLogger, require as bondRequire } from '@molecule/api-bond';
2
+ import { t } from '@molecule/api-i18n';
3
+ import { isConfigNotConfiguredError, } from '@molecule/api-payments';
4
+ import { update as resourceUpdate } from '@molecule/api-resource';
5
+ import { updatePlanPropsSchema } from '../../schema.js';
6
+ import { invalidateEntitlementsCacheSafe } from '../../utilities/invalidateEntitlements.js';
7
+ const analytics = getAnalytics();
8
+ const logger = getLogger();
9
+ /**
10
+ * Generic payment verification handler that works with any bonded PaymentProvider. Reads the
11
+ * provider name from the `:provider` route parameter and dispatches based on `verifyFlow`:
12
+ * - `'subscription'` (Stripe-style): Calls `verifySubscription(subscriptionId)`.
13
+ * - Default (Apple/Google-style): Calls `verifyReceipt()` or `verifyPurchase()` with a receipt
14
+ * string and the plan's platform product ID.
15
+ *
16
+ * On successful verification, updates the user's plan (planKey, planExpiresAt, planAutoRenews)
17
+ * and stores the payment record via the bonded PaymentRecordService.
18
+ * @param resource - The user resource configuration (name, tableName, schema).
19
+ * @param resource.name - The resource name.
20
+ * @param resource.tableName - The database table name for users.
21
+ * @param resource.schema - The validation schema for user properties.
22
+ * @returns A request handler for payment verification.
23
+ */
24
+ export const verifyPayment = ({ name, tableName, schema: _schema }) => {
25
+ return async (req) => {
26
+ const providerName = req.params.provider;
27
+ if (!providerName) {
28
+ return {
29
+ statusCode: 400,
30
+ body: {
31
+ error: t('user.payment.providerRequired'),
32
+ errorKey: 'user.payment.providerRequired',
33
+ },
34
+ };
35
+ }
36
+ try {
37
+ const provider = bondRequire('payments', providerName);
38
+ const plans = bondRequire('plans');
39
+ let verified = null;
40
+ let receipt;
41
+ // Dispatch based on verifyFlow (with method-existence fallback).
42
+ const isSubscriptionFlow = provider.verifyFlow === 'subscription' ||
43
+ (!provider.verifyFlow && !!provider.verifySubscription);
44
+ if (isSubscriptionFlow) {
45
+ // Stripe-style: subscriptionId in body or query. Providers that redirect
46
+ // back to this endpoint append their OWN parameter names — PayPal adds
47
+ // `subscription_id` (billing subscriptions) or `token` (v2 orders) to the
48
+ // return_url query — so accept those snake_case echoes as fallbacks. The
49
+ // value is still verified with the provider below before anything grants.
50
+ const subscriptionId = (req.body?.subscriptionId ??
51
+ req.query?.subscriptionId ??
52
+ req.query?.subscription_id ??
53
+ req.query?.token);
54
+ if (!subscriptionId) {
55
+ return {
56
+ statusCode: 400,
57
+ body: {
58
+ error: t('user.payment.subscriptionIdRequired', { provider: providerName }),
59
+ errorKey: 'user.payment.subscriptionIdRequired',
60
+ },
61
+ };
62
+ }
63
+ if (!provider.verifySubscription) {
64
+ return {
65
+ statusCode: 500,
66
+ body: {
67
+ error: t('user.payment.verificationNotConfigured', { provider: providerName }),
68
+ errorKey: 'user.payment.verificationNotConfigured',
69
+ },
70
+ };
71
+ }
72
+ verified = await provider.verifySubscription(subscriptionId);
73
+ }
74
+ else {
75
+ // Apple/Google-style: receipt + planKey in body.
76
+ const body = req.body || {};
77
+ receipt = body.receipt;
78
+ const planKey = body.planKey;
79
+ if (!receipt || !planKey) {
80
+ return {
81
+ statusCode: 400,
82
+ body: {
83
+ error: t('user.payment.receiptAndPlanRequired', { provider: providerName }),
84
+ errorKey: 'user.payment.receiptAndPlanRequired',
85
+ },
86
+ };
87
+ }
88
+ const plan = plans.findPlan(planKey);
89
+ if (!plan || plan.platformKey !== providerName) {
90
+ return {
91
+ statusCode: 400,
92
+ body: {
93
+ error: t('user.payment.invalidPlan', { provider: providerName }),
94
+ errorKey: 'user.payment.invalidPlan',
95
+ },
96
+ };
97
+ }
98
+ const verifyFn = provider.verifyReceipt ?? provider.verifyPurchase;
99
+ if (!verifyFn) {
100
+ return {
101
+ statusCode: 500,
102
+ body: {
103
+ error: t('user.payment.verificationNotConfigured', { provider: providerName }),
104
+ errorKey: 'user.payment.verificationNotConfigured',
105
+ },
106
+ };
107
+ }
108
+ verified = await verifyFn.call(provider, receipt, plan.platformProductId);
109
+ }
110
+ if (!verified) {
111
+ const id = req.params.id;
112
+ analytics
113
+ .track({
114
+ name: 'payment.verification_failed',
115
+ userId: id,
116
+ properties: { provider: providerName },
117
+ })
118
+ .catch(() => { });
119
+ return {
120
+ statusCode: 400,
121
+ body: {
122
+ error: t('user.payment.verificationFailed', { provider: providerName }),
123
+ errorKey: 'user.payment.verificationFailed',
124
+ },
125
+ };
126
+ }
127
+ // Subscription-flow ownership binding. The resolved subscription must
128
+ // belong to THIS user, so a caller cannot upgrade themselves by submitting
129
+ // a subscription id they merely learned (leaked log / shared success_url /
130
+ // support channel):
131
+ // • If the user already has a stored customer id for this provider, the
132
+ // verified subscription's customer MUST match it.
133
+ // • Otherwise (no prior record) a grant is only allowed when the
134
+ // verification came through the per-user, unguessable checkout-session
135
+ // redirect (`viaCheckoutSession`) — never from a bare, caller-supplied
136
+ // subscription id.
137
+ if (isSubscriptionFlow) {
138
+ const ownerId = req.params.id;
139
+ const data = (verified.data ?? {});
140
+ const verifiedCustomerId = data.customerId;
141
+ const records = get('paymentRecords');
142
+ const existing = records ? await records.findByUserId(ownerId, providerName) : null;
143
+ const storedCustomerId = existing?.data?.customerId;
144
+ const ownsByStoredCustomer = !!storedCustomerId && !!verifiedCustomerId && storedCustomerId === verifiedCustomerId;
145
+ const ownsByFreshCheckout = !storedCustomerId && data.viaCheckoutSession === true;
146
+ if (!ownsByStoredCustomer && !ownsByFreshCheckout) {
147
+ analytics
148
+ .track({
149
+ name: 'payment.verification_ownership_rejected',
150
+ userId: ownerId,
151
+ properties: { provider: providerName },
152
+ })
153
+ .catch(() => { });
154
+ return {
155
+ statusCode: 403,
156
+ body: {
157
+ error: t('user.payment.subscriptionOwnershipMismatch', { provider: providerName }, { defaultValue: 'This subscription does not belong to your account.' }),
158
+ errorKey: 'user.payment.subscriptionOwnershipMismatch',
159
+ },
160
+ };
161
+ }
162
+ // First-claim-wins replay defense (M3-1), mirroring the receipt branch.
163
+ // The ownership check above proves the caller MAY claim the subscription;
164
+ // this proves the subscription has not ALREADY been claimed by another
165
+ // account. A `cs_` checkout-session id is reusable by whoever obtained it
166
+ // (e.g. the attacker, from their own legitimate checkout redirect), so
167
+ // without this a single paid session could be replayed to grant premium
168
+ // to unlimited fresh accounts — each would pass `ownsByFreshCheckout`.
169
+ // The atomic, race-safe backstop is the insert-before-grant below; this
170
+ // is the fast, fail-closed pre-check.
171
+ //
172
+ // A subscription with no transaction id cannot be bound to a single
173
+ // account and therefore cannot be protected from replay — fail closed
174
+ // rather than grant an unbounded entitlement.
175
+ if (!verified.transactionId) {
176
+ analytics
177
+ .track({
178
+ name: 'payment.verification_failed',
179
+ userId: ownerId,
180
+ properties: { provider: providerName, reason: 'missing_transaction_id' },
181
+ })
182
+ .catch(() => { });
183
+ return {
184
+ statusCode: 400,
185
+ body: {
186
+ error: t('user.payment.verificationFailed', { provider: providerName }),
187
+ errorKey: 'user.payment.verificationFailed',
188
+ },
189
+ };
190
+ }
191
+ const existingTransaction = records
192
+ ? await records.findByTransaction(providerName, verified.transactionId)
193
+ : null;
194
+ if (existingTransaction && existingTransaction.userId !== ownerId) {
195
+ analytics
196
+ .track({
197
+ name: 'payment.verification_ownership_rejected',
198
+ userId: ownerId,
199
+ properties: { provider: providerName },
200
+ })
201
+ .catch(() => { });
202
+ return {
203
+ statusCode: 403,
204
+ body: {
205
+ error: t('user.payment.transactionAlreadyClaimed', { provider: providerName }, { defaultValue: 'This purchase is already associated with another account.' }),
206
+ errorKey: 'user.payment.transactionAlreadyClaimed',
207
+ },
208
+ };
209
+ }
210
+ }
211
+ else {
212
+ // Receipt-flow (Apple/Google) ownership/replay binding (R2-1), mirroring
213
+ // the subscription block above. A single purchased receipt must bind to
214
+ // exactly ONE account (first-claim-wins) so it cannot be replayed to
215
+ // upgrade many accounts. The atomic, race-safe backstop is the
216
+ // insert-before-grant below; this is the fast, fail-closed pre-check.
217
+ const ownerId = req.params.id;
218
+ // A receipt with no transaction id cannot be bound to a single account
219
+ // and therefore cannot be protected from replay — fail closed rather
220
+ // than grant an unbounded entitlement. (Google `latestOrderId` can be
221
+ // missing.)
222
+ if (!verified.transactionId) {
223
+ analytics
224
+ .track({
225
+ name: 'payment.verification_failed',
226
+ userId: ownerId,
227
+ properties: { provider: providerName, reason: 'missing_transaction_id' },
228
+ })
229
+ .catch(() => { });
230
+ return {
231
+ statusCode: 400,
232
+ body: {
233
+ error: t('user.payment.verificationFailed', { provider: providerName }),
234
+ errorKey: 'user.payment.verificationFailed',
235
+ },
236
+ };
237
+ }
238
+ const records = get('paymentRecords');
239
+ const existing = records
240
+ ? await records.findByTransaction(providerName, verified.transactionId)
241
+ : null;
242
+ if (existing && existing.userId !== ownerId) {
243
+ analytics
244
+ .track({
245
+ name: 'payment.verification_ownership_rejected',
246
+ userId: ownerId,
247
+ properties: { provider: providerName },
248
+ })
249
+ .catch(() => { });
250
+ return {
251
+ statusCode: 403,
252
+ body: {
253
+ error: t('user.payment.transactionAlreadyClaimed', { provider: providerName }, { defaultValue: 'This purchase is already associated with another account.' }),
254
+ errorKey: 'user.payment.transactionAlreadyClaimed',
255
+ },
256
+ };
257
+ }
258
+ }
259
+ // Find matching plan from the verified platform identifiers. Providers
260
+ // report the parent PRODUCT id on subscriptions, but apps register
261
+ // their catalogue with the PRICE ids checkout was started with — try
262
+ // both before rejecting.
263
+ const plan = plans.findPlanByProductId(verified.productId) ??
264
+ (verified.priceId ? plans.findPlanByProductId(verified.priceId) : null);
265
+ if (!plan) {
266
+ return {
267
+ statusCode: 400,
268
+ body: {
269
+ error: t('user.payment.unknownPlan', { provider: providerName }),
270
+ errorKey: 'user.payment.unknownPlan',
271
+ },
272
+ };
273
+ }
274
+ const id = req.params.id;
275
+ const records = get('paymentRecords');
276
+ // Bind the transaction to THIS user BEFORE granting the plan —
277
+ // fail-closed, first-claim-wins — for BOTH the receipt (Apple/Google, R2-1)
278
+ // and subscription (Stripe, M3-1) flows. The UNIQUE(platformKey,
279
+ // transactionId) index makes a second account's insert fail; `store()`
280
+ // surfaces that conflict (rather than swallowing it) so a replayed receipt
281
+ // OR a replayed checkout-session id is rejected here instead of being
282
+ // granted. Never grant the plan before the transaction is bound, and never
283
+ // swallow the store conflict in a path that has already granted.
284
+ if (records && verified.transactionId) {
285
+ try {
286
+ await records.store({
287
+ userId: id,
288
+ platformKey: providerName,
289
+ transactionId: verified.transactionId,
290
+ productId: verified.productId,
291
+ data: verified.data || {},
292
+ ...(receipt ? { receipt } : {}),
293
+ });
294
+ }
295
+ catch (error) {
296
+ // Conflict: the transaction is already bound. Determine the owner —
297
+ // if it belongs to another account, reject WITHOUT granting; if it is
298
+ // this user re-verifying their own receipt, fall through to an
299
+ // idempotent re-grant.
300
+ const owner = await records.findByTransaction(providerName, verified.transactionId);
301
+ if (!owner || owner.userId !== id) {
302
+ analytics
303
+ .track({
304
+ name: 'payment.verification_ownership_rejected',
305
+ userId: id,
306
+ properties: { provider: providerName },
307
+ })
308
+ .catch(() => { });
309
+ return {
310
+ statusCode: 403,
311
+ body: {
312
+ error: t('user.payment.transactionAlreadyClaimed', { provider: providerName }, { defaultValue: 'This purchase is already associated with another account.' }),
313
+ errorKey: 'user.payment.transactionAlreadyClaimed',
314
+ },
315
+ };
316
+ }
317
+ // Same user re-verifying their own receipt → idempotent re-grant.
318
+ logger.warn('Receipt already recorded for this user; re-granting idempotently', { error });
319
+ }
320
+ }
321
+ // Update user's plan. Use the partial plan-props schema, NOT the full
322
+ // user `schema` — the resource's full schema makes `id`/`createdAt`/
323
+ // `updatedAt` required and rejects the partial
324
+ // `{ planKey, planExpiresAt, planAutoRenews }` payload applied here
325
+ // (same fix as handlePaymentNotification).
326
+ const updateUser = resourceUpdate({
327
+ name,
328
+ tableName,
329
+ schema: updatePlanPropsSchema,
330
+ });
331
+ const result = await updateUser({
332
+ id,
333
+ props: {
334
+ planKey: plan.planKey,
335
+ planExpiresAt: verified.expiresAt,
336
+ planAutoRenews: verified.autoRenews,
337
+ },
338
+ });
339
+ // The entitlements hot-path caches the plan key (default 5-min TTL) —
340
+ // drop it so the user who just paid sees their new tier immediately.
341
+ invalidateEntitlementsCacheSafe(id);
342
+ analytics
343
+ .track({
344
+ name: 'payment.verified',
345
+ userId: id,
346
+ properties: {
347
+ provider: providerName,
348
+ productId: verified.productId,
349
+ planKey: plan.planKey,
350
+ },
351
+ })
352
+ .catch(() => { });
353
+ // Both flows now bind their payment record BEFORE granting (above), so
354
+ // there is no post-grant store here — a swallowed conflict after a grant
355
+ // is exactly the replay hole this closes (M3-1 / R2-1).
356
+ return result;
357
+ }
358
+ catch (error) {
359
+ // A bonded payment provider rethrows a tagged config-not-configured error
360
+ // (e.g. STRIPE_SECRET_KEY/APPLE_SHARED_SECRET/GOOGLE_API_SERVICE_KEY_OBJECT
361
+ // unset) instead of swallowing it into the same `null` a genuine failed
362
+ // verification returns — pass its REAL statusCode/errorKey through so the
363
+ // actionable "which key, where to get it" message reaches the caller
364
+ // instead of a generic 500 that reads identically to "bad receipt."
365
+ if (isConfigNotConfiguredError(error)) {
366
+ logger.warn(error.message, { errorKey: error.errorKey, provider: providerName });
367
+ return {
368
+ statusCode: error.statusCode,
369
+ body: { error: error.message, errorKey: error.errorKey },
370
+ };
371
+ }
372
+ logger.error(`${providerName} verifyPayment error:`, error);
373
+ return {
374
+ statusCode: 500,
375
+ body: {
376
+ error: t('user.payment.verificationFailed', { provider: providerName }),
377
+ errorKey: 'user.payment.verificationFailed',
378
+ },
379
+ };
380
+ }
381
+ };
382
+ };
383
+ //# sourceMappingURL=verifyPayment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verifyPayment.js","sourceRoot":"","sources":["../../../src/handlers/payments/verifyPayment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,YAAY,EAAE,SAAS,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACzF,OAAO,EAAE,CAAC,EAAE,MAAM,oBAAoB,CAAA;AACtC,OAAO,EACL,0BAA0B,GAI3B,MAAM,wBAAwB,CAAA;AAE/B,OAAO,EAAE,MAAM,IAAI,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAEjE,OAAO,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAA;AAEvD,OAAO,EAAE,+BAA+B,EAAE,MAAM,2CAA2C,CAAA;AAE3F,MAAM,SAAS,GAAG,YAAY,EAAE,CAAA;AAChC,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;AAE1B;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAkB,EAAE,EAAE;IACpF,OAAO,KAAK,EAAE,GAAoB,EAAE,EAAE;QACpC,MAAM,YAAY,GAAG,GAAG,CAAC,MAAM,CAAC,QAAkB,CAAA;QAElD,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,OAAO;gBACL,UAAU,EAAE,GAAG;gBACf,IAAI,EAAE;oBACJ,KAAK,EAAE,CAAC,CAAC,+BAA+B,CAAC;oBACzC,QAAQ,EAAE,+BAA+B;iBAC1C;aACF,CAAA;QACH,CAAC;QAED,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,WAAW,CAAkB,UAAU,EAAE,YAAY,CAAC,CAAA;YACvE,MAAM,KAAK,GAAG,WAAW,CAAc,OAAO,CAAC,CAAA;YAE/C,IAAI,QAAQ,GAOD,IAAI,CAAA;YACf,IAAI,OAA2B,CAAA;YAE/B,iEAAiE;YACjE,MAAM,kBAAkB,GACtB,QAAQ,CAAC,UAAU,KAAK,cAAc;gBACtC,CAAC,CAAC,QAAQ,CAAC,UAAU,IAAI,CAAC,CAAC,QAAQ,CAAC,kBAAkB,CAAC,CAAA;YACzD,IAAI,kBAAkB,EAAE,CAAC;gBACvB,yEAAyE;gBACzE,uEAAuE;gBACvE,0EAA0E;gBAC1E,yEAAyE;gBACzE,0EAA0E;gBAC1E,MAAM,cAAc,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,cAAc;oBAC9C,GAAG,CAAC,KAAK,EAAE,cAAc;oBACzB,GAAG,CAAC,KAAK,EAAE,eAAe;oBAC1B,GAAG,CAAC,KAAK,EAAE,KAAK,CAAuB,CAAA;gBAEzC,IAAI,CAAC,cAAc,EAAE,CAAC;oBACpB,OAAO;wBACL,UAAU,EAAE,GAAG;wBACf,IAAI,EAAE;4BACJ,KAAK,EAAE,CAAC,CAAC,qCAAqC,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC;4BAC3E,QAAQ,EAAE,qCAAqC;yBAChD;qBACF,CAAA;gBACH,CAAC;gBAED,IAAI,CAAC,QAAQ,CAAC,kBAAkB,EAAE,CAAC;oBACjC,OAAO;wBACL,UAAU,EAAE,GAAG;wBACf,IAAI,EAAE;4BACJ,KAAK,EAAE,CAAC,CAAC,wCAAwC,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC;4BAC9E,QAAQ,EAAE,wCAAwC;yBACnD;qBACF,CAAA;gBACH,CAAC;gBAED,QAAQ,GAAG,MAAM,QAAQ,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAA;YAC9D,CAAC;iBAAM,CAAC;gBACN,iDAAiD;gBACjD,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,IAAI,EAAE,CAAA;gBAC3B,OAAO,GAAG,IAAI,CAAC,OAA6B,CAAA;gBAC5C,MAAM,OAAO,GAAG,IAAI,CAAC,OAA6B,CAAA;gBAElD,IAAI,CAAC,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC;oBACzB,OAAO;wBACL,UAAU,EAAE,GAAG;wBACf,IAAI,EAAE;4BACJ,KAAK,EAAE,CAAC,CAAC,qCAAqC,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC;4BAC3E,QAAQ,EAAE,qCAAqC;yBAChD;qBACF,CAAA;gBACH,CAAC;gBAED,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;gBACpC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,WAAW,KAAK,YAAY,EAAE,CAAC;oBAC/C,OAAO;wBACL,UAAU,EAAE,GAAG;wBACf,IAAI,EAAE;4BACJ,KAAK,EAAE,CAAC,CAAC,0BAA0B,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC;4BAChE,QAAQ,EAAE,0BAA0B;yBACrC;qBACF,CAAA;gBACH,CAAC;gBAED,MAAM,QAAQ,GAAG,QAAQ,CAAC,aAAa,IAAI,QAAQ,CAAC,cAAc,CAAA;gBAClE,IAAI,CAAC,QAAQ,EAAE,CAAC;oBACd,OAAO;wBACL,UAAU,EAAE,GAAG;wBACf,IAAI,EAAE;4BACJ,KAAK,EAAE,CAAC,CAAC,wCAAwC,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC;4BAC9E,QAAQ,EAAE,wCAAwC;yBACnD;qBACF,CAAA;gBACH,CAAC;gBAED,QAAQ,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC,iBAAiB,CAAC,CAAA;YAC3E,CAAC;YAED,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,CAAC,EAAY,CAAA;gBAClC,SAAS;qBACN,KAAK,CAAC;oBACL,IAAI,EAAE,6BAA6B;oBACnC,MAAM,EAAE,EAAE;oBACV,UAAU,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAE;iBACvC,CAAC;qBACD,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;gBAClB,OAAO;oBACL,UAAU,EAAE,GAAG;oBACf,IAAI,EAAE;wBACJ,KAAK,EAAE,CAAC,CAAC,iCAAiC,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC;wBACvE,QAAQ,EAAE,iCAAiC;qBAC5C;iBACF,CAAA;YACH,CAAC;YAED,sEAAsE;YACtE,2EAA2E;YAC3E,2EAA2E;YAC3E,oBAAoB;YACpB,0EAA0E;YAC1E,sDAAsD;YACtD,mEAAmE;YACnE,2EAA2E;YAC3E,2EAA2E;YAC3E,uBAAuB;YACvB,IAAI,kBAAkB,EAAE,CAAC;gBACvB,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,EAAY,CAAA;gBACvC,MAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,IAAI,IAAI,EAAE,CAA0D,CAAA;gBAC3F,MAAM,kBAAkB,GAAG,IAAI,CAAC,UAAU,CAAA;gBAC1C,MAAM,OAAO,GAAG,GAAG,CAAuB,gBAAgB,CAAC,CAAA;gBAC3D,MAAM,QAAQ,GAAG,OAAO,CAAC,CAAC,CAAC,MAAM,OAAO,CAAC,YAAY,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;gBACnF,MAAM,gBAAgB,GAAI,QAAQ,EAAE,IAA4C,EAAE,UAAU,CAAA;gBAE5F,MAAM,oBAAoB,GACxB,CAAC,CAAC,gBAAgB,IAAI,CAAC,CAAC,kBAAkB,IAAI,gBAAgB,KAAK,kBAAkB,CAAA;gBACvF,MAAM,mBAAmB,GAAG,CAAC,gBAAgB,IAAI,IAAI,CAAC,kBAAkB,KAAK,IAAI,CAAA;gBAEjF,IAAI,CAAC,oBAAoB,IAAI,CAAC,mBAAmB,EAAE,CAAC;oBAClD,SAAS;yBACN,KAAK,CAAC;wBACL,IAAI,EAAE,yCAAyC;wBAC/C,MAAM,EAAE,OAAO;wBACf,UAAU,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAE;qBACvC,CAAC;yBACD,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;oBAClB,OAAO;wBACL,UAAU,EAAE,GAAG;wBACf,IAAI,EAAE;4BACJ,KAAK,EAAE,CAAC,CACN,4CAA4C,EAC5C,EAAE,QAAQ,EAAE,YAAY,EAAE,EAC1B,EAAE,YAAY,EAAE,oDAAoD,EAAE,CACvE;4BACD,QAAQ,EAAE,4CAA4C;yBACvD;qBACF,CAAA;gBACH,CAAC;gBAED,wEAAwE;gBACxE,0EAA0E;gBAC1E,uEAAuE;gBACvE,0EAA0E;gBAC1E,uEAAuE;gBACvE,wEAAwE;gBACxE,uEAAuE;gBACvE,wEAAwE;gBACxE,sCAAsC;gBACtC,EAAE;gBACF,oEAAoE;gBACpE,sEAAsE;gBACtE,8CAA8C;gBAC9C,IAAI,CAAC,QAAQ,CAAC,aAAa,EAAE,CAAC;oBAC5B,SAAS;yBACN,KAAK,CAAC;wBACL,IAAI,EAAE,6BAA6B;wBACnC,MAAM,EAAE,OAAO;wBACf,UAAU,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,EAAE,wBAAwB,EAAE;qBACzE,CAAC;yBACD,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;oBAClB,OAAO;wBACL,UAAU,EAAE,GAAG;wBACf,IAAI,EAAE;4BACJ,KAAK,EAAE,CAAC,CAAC,iCAAiC,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC;4BACvE,QAAQ,EAAE,iCAAiC;yBAC5C;qBACF,CAAA;gBACH,CAAC;gBAED,MAAM,mBAAmB,GAAG,OAAO;oBACjC,CAAC,CAAC,MAAM,OAAO,CAAC,iBAAiB,CAAC,YAAY,EAAE,QAAQ,CAAC,aAAa,CAAC;oBACvE,CAAC,CAAC,IAAI,CAAA;gBACR,IAAI,mBAAmB,IAAI,mBAAmB,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC;oBAClE,SAAS;yBACN,KAAK,CAAC;wBACL,IAAI,EAAE,yCAAyC;wBAC/C,MAAM,EAAE,OAAO;wBACf,UAAU,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAE;qBACvC,CAAC;yBACD,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;oBAClB,OAAO;wBACL,UAAU,EAAE,GAAG;wBACf,IAAI,EAAE;4BACJ,KAAK,EAAE,CAAC,CACN,wCAAwC,EACxC,EAAE,QAAQ,EAAE,YAAY,EAAE,EAC1B,EAAE,YAAY,EAAE,2DAA2D,EAAE,CAC9E;4BACD,QAAQ,EAAE,wCAAwC;yBACnD;qBACF,CAAA;gBACH,CAAC;YACH,CAAC;iBAAM,CAAC;gBACN,yEAAyE;gBACzE,wEAAwE;gBACxE,qEAAqE;gBACrE,+DAA+D;gBAC/D,sEAAsE;gBACtE,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,EAAY,CAAA;gBAEvC,uEAAuE;gBACvE,qEAAqE;gBACrE,sEAAsE;gBACtE,YAAY;gBACZ,IAAI,CAAC,QAAQ,CAAC,aAAa,EAAE,CAAC;oBAC5B,SAAS;yBACN,KAAK,CAAC;wBACL,IAAI,EAAE,6BAA6B;wBACnC,MAAM,EAAE,OAAO;wBACf,UAAU,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,EAAE,wBAAwB,EAAE;qBACzE,CAAC;yBACD,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;oBAClB,OAAO;wBACL,UAAU,EAAE,GAAG;wBACf,IAAI,EAAE;4BACJ,KAAK,EAAE,CAAC,CAAC,iCAAiC,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC;4BACvE,QAAQ,EAAE,iCAAiC;yBAC5C;qBACF,CAAA;gBACH,CAAC;gBAED,MAAM,OAAO,GAAG,GAAG,CAAuB,gBAAgB,CAAC,CAAA;gBAC3D,MAAM,QAAQ,GAAG,OAAO;oBACtB,CAAC,CAAC,MAAM,OAAO,CAAC,iBAAiB,CAAC,YAAY,EAAE,QAAQ,CAAC,aAAa,CAAC;oBACvE,CAAC,CAAC,IAAI,CAAA;gBACR,IAAI,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC;oBAC5C,SAAS;yBACN,KAAK,CAAC;wBACL,IAAI,EAAE,yCAAyC;wBAC/C,MAAM,EAAE,OAAO;wBACf,UAAU,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAE;qBACvC,CAAC;yBACD,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;oBAClB,OAAO;wBACL,UAAU,EAAE,GAAG;wBACf,IAAI,EAAE;4BACJ,KAAK,EAAE,CAAC,CACN,wCAAwC,EACxC,EAAE,QAAQ,EAAE,YAAY,EAAE,EAC1B,EAAE,YAAY,EAAE,2DAA2D,EAAE,CAC9E;4BACD,QAAQ,EAAE,wCAAwC;yBACnD;qBACF,CAAA;gBACH,CAAC;YACH,CAAC;YAED,uEAAuE;YACvE,mEAAmE;YACnE,qEAAqE;YACrE,yBAAyB;YACzB,MAAM,IAAI,GACR,KAAK,CAAC,mBAAmB,CAAC,QAAQ,CAAC,SAAS,CAAC;gBAC7C,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,mBAAmB,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;YACzE,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,OAAO;oBACL,UAAU,EAAE,GAAG;oBACf,IAAI,EAAE;wBACJ,KAAK,EAAE,CAAC,CAAC,0BAA0B,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC;wBAChE,QAAQ,EAAE,0BAA0B;qBACrC;iBACF,CAAA;YACH,CAAC;YAED,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,CAAC,EAAY,CAAA;YAClC,MAAM,OAAO,GAAG,GAAG,CAAuB,gBAAgB,CAAC,CAAA;YAE3D,+DAA+D;YAC/D,4EAA4E;YAC5E,iEAAiE;YACjE,uEAAuE;YACvE,2EAA2E;YAC3E,sEAAsE;YACtE,2EAA2E;YAC3E,iEAAiE;YACjE,IAAI,OAAO,IAAI,QAAQ,CAAC,aAAa,EAAE,CAAC;gBACtC,IAAI,CAAC;oBACH,MAAM,OAAO,CAAC,KAAK,CAAC;wBAClB,MAAM,EAAE,EAAE;wBACV,WAAW,EAAE,YAAY;wBACzB,aAAa,EAAE,QAAQ,CAAC,aAAa;wBACrC,SAAS,EAAE,QAAQ,CAAC,SAAS;wBAC7B,IAAI,EAAE,QAAQ,CAAC,IAAI,IAAI,EAAE;wBACzB,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;qBAChC,CAAC,CAAA;gBACJ,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,oEAAoE;oBACpE,sEAAsE;oBACtE,+DAA+D;oBAC/D,uBAAuB;oBACvB,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,iBAAiB,CAAC,YAAY,EAAE,QAAQ,CAAC,aAAa,CAAC,CAAA;oBACnF,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,KAAK,EAAE,EAAE,CAAC;wBAClC,SAAS;6BACN,KAAK,CAAC;4BACL,IAAI,EAAE,yCAAyC;4BAC/C,MAAM,EAAE,EAAE;4BACV,UAAU,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAE;yBACvC,CAAC;6BACD,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;wBAClB,OAAO;4BACL,UAAU,EAAE,GAAG;4BACf,IAAI,EAAE;gCACJ,KAAK,EAAE,CAAC,CACN,wCAAwC,EACxC,EAAE,QAAQ,EAAE,YAAY,EAAE,EAC1B,EAAE,YAAY,EAAE,2DAA2D,EAAE,CAC9E;gCACD,QAAQ,EAAE,wCAAwC;6BACnD;yBACF,CAAA;oBACH,CAAC;oBACD,kEAAkE;oBAClE,MAAM,CAAC,IAAI,CAAC,kEAAkE,EAAE,EAAE,KAAK,EAAE,CAAC,CAAA;gBAC5F,CAAC;YACH,CAAC;YAED,sEAAsE;YACtE,qEAAqE;YACrE,+CAA+C;YAC/C,oEAAoE;YACpE,2CAA2C;YAC3C,MAAM,UAAU,GAAG,cAAc,CAAwB;gBACvD,IAAI;gBACJ,SAAS;gBACT,MAAM,EAAE,qBAAqB;aAC4C,CAAC,CAAA;YAE5E,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC;gBAC9B,EAAE;gBACF,KAAK,EAAE;oBACL,OAAO,EAAE,IAAI,CAAC,OAAO;oBACrB,aAAa,EAAE,QAAQ,CAAC,SAAS;oBACjC,cAAc,EAAE,QAAQ,CAAC,UAAU;iBACpC;aACF,CAAC,CAAA;YAEF,sEAAsE;YACtE,qEAAqE;YACrE,+BAA+B,CAAC,EAAE,CAAC,CAAA;YAEnC,SAAS;iBACN,KAAK,CAAC;gBACL,IAAI,EAAE,kBAAkB;gBACxB,MAAM,EAAE,EAAE;gBACV,UAAU,EAAE;oBACV,QAAQ,EAAE,YAAY;oBACtB,SAAS,EAAE,QAAQ,CAAC,SAAS;oBAC7B,OAAO,EAAE,IAAI,CAAC,OAAO;iBACtB;aACF,CAAC;iBACD,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;YAElB,uEAAuE;YACvE,yEAAyE;YACzE,wDAAwD;YAExD,OAAO,MAAM,CAAA;QACf,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,0EAA0E;YAC1E,4EAA4E;YAC5E,wEAAwE;YACxE,0EAA0E;YAC1E,qEAAqE;YACrE,oEAAoE;YACpE,IAAI,0BAA0B,CAAC,KAAK,CAAC,EAAE,CAAC;gBACtC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC,CAAA;gBAChF,OAAO;oBACL,UAAU,EAAE,KAAK,CAAC,UAAU;oBAC5B,IAAI,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,OAAO,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE;iBACzD,CAAA;YACH,CAAC;YACD,MAAM,CAAC,KAAK,CAAC,GAAG,YAAY,uBAAuB,EAAE,KAAK,CAAC,CAAA;YAC3D,OAAO;gBACL,UAAU,EAAE,GAAG;gBACf,IAAI,EAAE;oBACJ,KAAK,EAAE,CAAC,CAAC,iCAAiC,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC;oBACvE,QAAQ,EAAE,iCAAiC;iBAC5C;aACF,CAAA;QACH,CAAC;IACH,CAAC,CAAA;AACH,CAAC,CAAA"}
@@ -0,0 +1,17 @@
1
+ import type { MoleculeRequest, MoleculeResponse } from '@molecule/api-resource';
2
+ import type * as types from '../types.js';
3
+ /**
4
+ * Reads a user by ID from the database. Attaches plan info (with expiration/renewal status) via the
5
+ * bonded PlanService if available. When reading the authenticated user's own record, also includes
6
+ * two-factor authentication status from the secrets table.
7
+ * @param resource - The user resource configuration (name, tableName, schema).
8
+ * @param resource.name - The resource name.
9
+ * @param resource.tableName - The database table name for users.
10
+ * @param resource.schema - The validation schema for user properties.
11
+ * @returns A request handler that responds with `{ statusCode: 200, body: { props, plan?, twoFactorEnabled? } }`.
12
+ */
13
+ export declare const read: ({ name: _name, tableName, schema: _schema }: types.Resource) => (req: MoleculeRequest, res: MoleculeResponse) => Promise<{
14
+ statusCode: number;
15
+ body: Record<string, unknown>;
16
+ }>;
17
+ //# sourceMappingURL=read.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../src/handlers/read.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAE/E,OAAO,KAAK,KAAK,KAAK,MAAM,aAAa,CAAA;AAMzC;;;;;;;;;GASG;AACH,eAAO,MAAM,IAAI,GAAI,6CAA6C,KAAK,CAAC,QAAQ,MAChE,KAAK,eAAe,EAAE,KAAK,gBAAgB;;;EA8C1D,CAAA"}
@@ -0,0 +1,60 @@
1
+ import { getLogger } from '@molecule/api-bond';
2
+ import { findById } from '@molecule/api-database';
3
+ import { t } from '@molecule/api-i18n';
4
+ import { getPlan } from '../utilities/getPlan.js';
5
+ import { stripSensitiveUserColumns } from '../utilities/stripSensitiveUserColumns.js';
6
+ const logger = getLogger();
7
+ /**
8
+ * Reads a user by ID from the database. Attaches plan info (with expiration/renewal status) via the
9
+ * bonded PlanService if available. When reading the authenticated user's own record, also includes
10
+ * two-factor authentication status from the secrets table.
11
+ * @param resource - The user resource configuration (name, tableName, schema).
12
+ * @param resource.name - The resource name.
13
+ * @param resource.tableName - The database table name for users.
14
+ * @param resource.schema - The validation schema for user properties.
15
+ * @returns A request handler that responds with `{ statusCode: 200, body: { props, plan?, twoFactorEnabled? } }`.
16
+ */
17
+ export const read = ({ name: _name, tableName, schema: _schema }) => {
18
+ return async (req, res) => {
19
+ try {
20
+ const { session } = res.locals;
21
+ const id = req.params.id;
22
+ const user = await findById(tableName, id);
23
+ if (!user) {
24
+ return {
25
+ statusCode: 404,
26
+ body: { error: t('user.error.notFound'), errorKey: 'user.error.notFound' },
27
+ };
28
+ }
29
+ // Attach plan info if available.
30
+ const plan = await getPlan(user);
31
+ const body = { props: stripSensitiveUserColumns(user) };
32
+ if (plan) {
33
+ body.plan = plan;
34
+ }
35
+ // If reading self, also include two-factor setup info.
36
+ if (session?.userId === id) {
37
+ try {
38
+ const secrets = await findById(`${tableName}Secrets`, id);
39
+ if (secrets) {
40
+ body.twoFactorEnabled = user.twoFactorEnabled || false;
41
+ body.hasPendingTwoFactor = Boolean(secrets.pendingTwoFactorSecret);
42
+ }
43
+ }
44
+ catch (_error) {
45
+ // Non-critical: the secrets table may not exist yet (pre-migration) or the row may be absent.
46
+ // Omitting twoFactorEnabled/hasPendingTwoFactor from the response is safe — callers treat absence as false.
47
+ }
48
+ }
49
+ return { statusCode: 200, body };
50
+ }
51
+ catch (error) {
52
+ logger.error(error);
53
+ return {
54
+ statusCode: 500,
55
+ body: { error: t('user.error.failedToReadUser'), errorKey: 'user.error.failedToReadUser' },
56
+ };
57
+ }
58
+ };
59
+ };
60
+ //# sourceMappingURL=read.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read.js","sourceRoot":"","sources":["../../src/handlers/read.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AACjD,OAAO,EAAE,CAAC,EAAE,MAAM,oBAAoB,CAAA;AAItC,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAA;AACjD,OAAO,EAAE,yBAAyB,EAAE,MAAM,2CAA2C,CAAA;AAErF,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;AAE1B;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAkB,EAAE,EAAE;IAClF,OAAO,KAAK,EAAE,GAAoB,EAAE,GAAqB,EAAE,EAAE;QAC3D,IAAI,CAAC;YACH,MAAM,EAAE,OAAO,EAAE,GAAG,GAAG,CAAC,MAAM,CAAA;YAC9B,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,CAAC,EAAY,CAAA;YAElC,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAc,SAAS,EAAE,EAAE,CAAC,CAAA;YAEvD,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,OAAO;oBACL,UAAU,EAAE,GAAG;oBACf,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,qBAAqB,CAAC,EAAE,QAAQ,EAAE,qBAAqB,EAAE;iBAC3E,CAAA;YACH,CAAC;YAED,iCAAiC;YACjC,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;YAChC,MAAM,IAAI,GAA4B,EAAE,KAAK,EAAE,yBAAyB,CAAC,IAAI,CAAC,EAAE,CAAA;YAEhF,IAAI,IAAI,EAAE,CAAC;gBACT,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;YAClB,CAAC;YAED,uDAAuD;YACvD,IAAI,OAAO,EAAE,MAAM,KAAK,EAAE,EAAE,CAAC;gBAC3B,IAAI,CAAC;oBACH,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAoB,GAAG,SAAS,SAAS,EAAE,EAAE,CAAC,CAAA;oBAE5E,IAAI,OAAO,EAAE,CAAC;wBACZ,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC,gBAAgB,IAAI,KAAK,CAAA;wBACtD,IAAI,CAAC,mBAAmB,GAAG,OAAO,CAAC,OAAO,CAAC,sBAAsB,CAAC,CAAA;oBACpE,CAAC;gBACH,CAAC;gBAAC,OAAO,MAAM,EAAE,CAAC;oBAChB,8FAA8F;oBAC9F,4GAA4G;gBAC9G,CAAC;YACH,CAAC;YAED,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,CAAA;QAClC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;YACnB,OAAO;gBACL,UAAU,EAAE,GAAG;gBACf,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,6BAA6B,CAAC,EAAE,QAAQ,EAAE,6BAA6B,EAAE;aAC3F,CAAA;QACH,CAAC;IACH,CAAC,CAAA;AACH,CAAC,CAAA"}
@@ -0,0 +1,23 @@
1
+ import type { MoleculeRequest, MoleculeResponse } from '@molecule/api-resource';
2
+ import type * as types from '../types.js';
3
+ /**
4
+ * Reads the AUTHENTICATED user from the session — no `:id` param. Backs
5
+ * `GET /users/me`, which the app's auth client calls on init to RESTORE the
6
+ * session after a full page load.
7
+ *
8
+ * [M1-1] The bearer token is held in memory (a localStorage copy is
9
+ * XSS-exfiltratable and is forbidden), so it is lost on reload; the httpOnly
10
+ * session cookie is the persistent credential. The `auth` middleware reads that
11
+ * cookie into `res.locals.session`, and this handler returns the current user
12
+ * so the client can re-establish `authenticated` state WITHOUT the token ever
13
+ * being exposed to JavaScript. Always self, so it mirrors `read`'s self branch
14
+ * (plan + two-factor status).
15
+ * @param resource - The user resource configuration (name, tableName, schema).
16
+ * @param resource.tableName - The database table name for users.
17
+ * @returns A request handler responding `{ statusCode: 200, body: { props, plan?, twoFactorEnabled? } }`, or 401 when unauthenticated.
18
+ */
19
+ export declare const readSelf: ({ tableName }: types.Resource) => (_req: MoleculeRequest, res: MoleculeResponse) => Promise<{
20
+ statusCode: number;
21
+ body: Record<string, unknown>;
22
+ }>;
23
+ //# sourceMappingURL=readSelf.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"readSelf.d.ts","sourceRoot":"","sources":["../../src/handlers/readSelf.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAE/E,OAAO,KAAK,KAAK,KAAK,MAAM,aAAa,CAAA;AAMzC;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,QAAQ,GAAI,eAAe,KAAK,CAAC,QAAQ,MACtC,MAAM,eAAe,EAAE,KAAK,gBAAgB;;;EAsD3D,CAAA"}
@@ -0,0 +1,77 @@
1
+ import { getLogger } from '@molecule/api-bond';
2
+ import { findById } from '@molecule/api-database';
3
+ import { t } from '@molecule/api-i18n';
4
+ import { getPlan } from '../utilities/getPlan.js';
5
+ import { stripSensitiveUserColumns } from '../utilities/stripSensitiveUserColumns.js';
6
+ const logger = getLogger();
7
+ /**
8
+ * Reads the AUTHENTICATED user from the session — no `:id` param. Backs
9
+ * `GET /users/me`, which the app's auth client calls on init to RESTORE the
10
+ * session after a full page load.
11
+ *
12
+ * [M1-1] The bearer token is held in memory (a localStorage copy is
13
+ * XSS-exfiltratable and is forbidden), so it is lost on reload; the httpOnly
14
+ * session cookie is the persistent credential. The `auth` middleware reads that
15
+ * cookie into `res.locals.session`, and this handler returns the current user
16
+ * so the client can re-establish `authenticated` state WITHOUT the token ever
17
+ * being exposed to JavaScript. Always self, so it mirrors `read`'s self branch
18
+ * (plan + two-factor status).
19
+ * @param resource - The user resource configuration (name, tableName, schema).
20
+ * @param resource.tableName - The database table name for users.
21
+ * @returns A request handler responding `{ statusCode: 200, body: { props, plan?, twoFactorEnabled? } }`, or 401 when unauthenticated.
22
+ */
23
+ export const readSelf = ({ tableName }) => {
24
+ return async (_req, res) => {
25
+ try {
26
+ const userId = res.locals.session?.userId;
27
+ if (!userId) {
28
+ return {
29
+ statusCode: 401,
30
+ body: {
31
+ error: t('resource.error.unauthorized', undefined, { defaultValue: 'Unauthorized' }),
32
+ errorKey: 'resource.error.unauthorized',
33
+ },
34
+ };
35
+ }
36
+ const user = await findById(tableName, userId);
37
+ if (!user) {
38
+ // Valid session whose user row no longer exists (deleted account holding
39
+ // a still-live cookie) — treat as unauthenticated so the client clears
40
+ // its state, not 404.
41
+ return {
42
+ statusCode: 401,
43
+ body: {
44
+ error: t('resource.error.unauthorized', undefined, { defaultValue: 'Unauthorized' }),
45
+ errorKey: 'resource.error.unauthorized',
46
+ },
47
+ };
48
+ }
49
+ const plan = await getPlan(user);
50
+ const body = { props: stripSensitiveUserColumns(user) };
51
+ if (plan) {
52
+ body.plan = plan;
53
+ }
54
+ // Always self — include two-factor setup info like `read`'s self branch.
55
+ try {
56
+ const secrets = await findById(`${tableName}Secrets`, userId);
57
+ if (secrets) {
58
+ body.twoFactorEnabled = user.twoFactorEnabled || false;
59
+ body.hasPendingTwoFactor = Boolean(secrets.pendingTwoFactorSecret);
60
+ }
61
+ }
62
+ catch (_error) {
63
+ // Non-critical: the secrets table may not exist yet (pre-migration) or the
64
+ // row may be absent. Callers treat absence as false.
65
+ }
66
+ return { statusCode: 200, body };
67
+ }
68
+ catch (error) {
69
+ logger.error(error);
70
+ return {
71
+ statusCode: 500,
72
+ body: { error: t('user.error.failedToReadUser'), errorKey: 'user.error.failedToReadUser' },
73
+ };
74
+ }
75
+ };
76
+ };
77
+ //# sourceMappingURL=readSelf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"readSelf.js","sourceRoot":"","sources":["../../src/handlers/readSelf.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AACjD,OAAO,EAAE,CAAC,EAAE,MAAM,oBAAoB,CAAA;AAItC,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAA;AACjD,OAAO,EAAE,yBAAyB,EAAE,MAAM,2CAA2C,CAAA;AAErF,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;AAE1B;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,EAAE,SAAS,EAAkB,EAAE,EAAE;IACxD,OAAO,KAAK,EAAE,IAAqB,EAAE,GAAqB,EAAE,EAAE;QAC5D,IAAI,CAAC;YACH,MAAM,MAAM,GAAI,GAAG,CAAC,MAAM,CAAC,OAA2C,EAAE,MAAM,CAAA;YAC9E,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,OAAO;oBACL,UAAU,EAAE,GAAG;oBACf,IAAI,EAAE;wBACJ,KAAK,EAAE,CAAC,CAAC,6BAA6B,EAAE,SAAS,EAAE,EAAE,YAAY,EAAE,cAAc,EAAE,CAAC;wBACpF,QAAQ,EAAE,6BAA6B;qBACxC;iBACF,CAAA;YACH,CAAC;YAED,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAc,SAAS,EAAE,MAAM,CAAC,CAAA;YAC3D,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,yEAAyE;gBACzE,uEAAuE;gBACvE,sBAAsB;gBACtB,OAAO;oBACL,UAAU,EAAE,GAAG;oBACf,IAAI,EAAE;wBACJ,KAAK,EAAE,CAAC,CAAC,6BAA6B,EAAE,SAAS,EAAE,EAAE,YAAY,EAAE,cAAc,EAAE,CAAC;wBACpF,QAAQ,EAAE,6BAA6B;qBACxC;iBACF,CAAA;YACH,CAAC;YAED,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;YAChC,MAAM,IAAI,GAA4B,EAAE,KAAK,EAAE,yBAAyB,CAAC,IAAI,CAAC,EAAE,CAAA;YAChF,IAAI,IAAI,EAAE,CAAC;gBACT,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;YAClB,CAAC;YAED,yEAAyE;YACzE,IAAI,CAAC;gBACH,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAoB,GAAG,SAAS,SAAS,EAAE,MAAM,CAAC,CAAA;gBAChF,IAAI,OAAO,EAAE,CAAC;oBACZ,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC,gBAAgB,IAAI,KAAK,CAAA;oBACtD,IAAI,CAAC,mBAAmB,GAAG,OAAO,CAAC,OAAO,CAAC,sBAAsB,CAAC,CAAA;gBACpE,CAAC;YACH,CAAC;YAAC,OAAO,MAAM,EAAE,CAAC;gBAChB,2EAA2E;gBAC3E,qDAAqD;YACvD,CAAC;YAED,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,CAAA;QAClC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;YACnB,OAAO;gBACL,UAAU,EAAE,GAAG;gBACf,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,6BAA6B,CAAC,EAAE,QAAQ,EAAE,6BAA6B,EAAE;aAC3F,CAAA;QACH,CAAC;IACH,CAAC,CAAA;AACH,CAAC,CAAA"}
@@ -0,0 +1,40 @@
1
+ import type { MoleculeRequest } from '@molecule/api-resource';
2
+ import type * as types from '../types.js';
3
+ /** Request body for confirming a password reset using a token. */
4
+ export interface ResetPasswordRequest extends MoleculeRequest {
5
+ body: {
6
+ token?: string;
7
+ password?: string;
8
+ };
9
+ }
10
+ /**
11
+ * Confirms a password reset by validating the one-time token previously generated by
12
+ * `forgotPassword`, then hashing and storing the new password and clearing the token.
13
+ *
14
+ * Token TTL is one hour. The handler responds with 400 for missing fields, 400 for
15
+ * passwords that fail length validation (matching `updatePassword`), and 401 for
16
+ * tokens that are unknown, expired, or already consumed.
17
+ *
18
+ * On success, all existing device sessions are invalidated to force a fresh login,
19
+ * mirroring the behavior of `updatePassword`.
20
+ *
21
+ * @param resource - The user resource configuration (name, tableName, schema).
22
+ * @param resource.tableName - The database table name for users.
23
+ * @returns A request handler that responds with `{ statusCode: 200, body: { success: true } }` on success.
24
+ */
25
+ export declare const resetPassword: ({ name: _name, tableName, schema: _schema }: types.Resource) => (req: ResetPasswordRequest) => Promise<{
26
+ statusCode: number;
27
+ body: {
28
+ error: string;
29
+ errorKey: string;
30
+ success?: undefined;
31
+ };
32
+ } | {
33
+ statusCode: number;
34
+ body: {
35
+ success: boolean;
36
+ error?: undefined;
37
+ errorKey?: undefined;
38
+ };
39
+ }>;
40
+ //# sourceMappingURL=resetPassword.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resetPassword.d.ts","sourceRoot":"","sources":["../../src/handlers/resetPassword.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAG7D,OAAO,KAAK,KAAK,KAAK,MAAM,aAAa,CAAA;AAQzC,kEAAkE;AAClE,MAAM,WAAW,oBAAqB,SAAQ,eAAe;IAC3D,IAAI,EAAE;QACJ,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,QAAQ,CAAC,EAAE,MAAM,CAAA;KAClB,CAAA;CACF;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,aAAa,GAAI,6CAA6C,KAAK,CAAC,QAAQ,MACzE,KAAK,oBAAoB;;;;;;;;;;;;;;EAoIxC,CAAA"}