@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
package/LICENSE ADDED
@@ -0,0 +1,115 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work.
38
+
39
+ "Derivative Works" shall mean any work, whether in Source or Object
40
+ form, that is based on (or derived from) the Work and for which the
41
+ editorial revisions, annotations, elaborations, or other modifications
42
+ represent, as a whole, an original work of authorship.
43
+
44
+ "Contribution" shall mean any work of authorship, including the
45
+ original version of the Work and any modifications or additions
46
+ to that Work, that is intentionally submitted to the Licensor for
47
+ inclusion in the Work by the copyright owner or by an individual or
48
+ Legal Entity authorized to submit on behalf of the copyright owner.
49
+
50
+ "Contributor" shall mean Licensor and any individual or Legal Entity
51
+ on behalf of whom a Contribution has been received by the Licensor and
52
+ subsequently incorporated within the Work.
53
+
54
+ 2. Grant of Copyright License. Subject to the terms and conditions of
55
+ this License, each Contributor hereby grants to You a perpetual,
56
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
57
+ copyright license to reproduce, prepare Derivative Works of,
58
+ publicly display, publicly perform, sublicense, and distribute the
59
+ Work and such Derivative Works in Source or Object form.
60
+
61
+ 3. Grant of Patent License. Subject to the terms and conditions of
62
+ this License, each Contributor hereby grants to You a perpetual,
63
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
64
+ patent license to make, have made, use, offer to sell, sell, import,
65
+ and otherwise transfer the Work.
66
+
67
+ 4. Redistribution. You may reproduce and distribute copies of the
68
+ Work or Derivative Works thereof in any medium, with or without
69
+ modifications, and in Source or Object form, provided that You
70
+ meet the following conditions:
71
+
72
+ (a) You must give any other recipients of the Work or
73
+ Derivative Works a copy of this License; and
74
+
75
+ (b) You must cause any modified files to carry prominent notices
76
+ stating that You changed the files; and
77
+
78
+ (c) You must retain, in the Source form of any Derivative Works
79
+ that You distribute, all copyright, patent, trademark, and
80
+ attribution notices from the Source form of the Work,
81
+ excluding those notices that do not pertain to any part of
82
+ the Derivative Works; and
83
+
84
+ (d) If the Work includes a "NOTICE" text file as part of its
85
+ distribution, then any Derivative Works that You distribute must
86
+ include a readable copy of the attribution notices contained
87
+ within such NOTICE file.
88
+
89
+ 5. Submission of Contributions.
90
+
91
+ 6. Trademarks. This License does not grant permission to use the trade
92
+ names, trademarks, service marks, or product names of the Licensor.
93
+
94
+ 7. Disclaimer of Warranty. Unless required by applicable law or
95
+ agreed to in writing, Licensor provides the Work on an "AS IS" BASIS,
96
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND.
97
+
98
+ 8. Limitation of Liability. In no event and under no legal theory shall
99
+ any Contributor be liable to You for damages.
100
+
101
+ 9. Accepting Warranty or Additional Liability.
102
+
103
+ Copyright 2026 Molecule Dev, Inc.
104
+
105
+ Licensed under the Apache License, Version 2.0 (the "License");
106
+ you may not use this file except in compliance with the License.
107
+ You may obtain a copy of the License at
108
+
109
+ http://www.apache.org/licenses/LICENSE-2.0
110
+
111
+ Unless required by applicable law or agreed to in writing, software
112
+ distributed under the License is distributed on an "AS IS" BASIS,
113
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
114
+ See the License for the specific language governing permissions and
115
+ limitations under the License.
@@ -0,0 +1,107 @@
1
+ /**
2
+ * JWT session authorization for user resources.
3
+ *
4
+ * Provides middleware to verify, set, and refresh JWT tokens.
5
+ *
6
+ * @module
7
+ */
8
+ import './secrets.js';
9
+ import type { MoleculeRequest, MoleculeRequestHandler, MoleculeResponse } from '@molecule/api-resource';
10
+ import type { Session } from './schema.js';
11
+ /**
12
+ * Resolve the actual cookie name for an auth cookie.
13
+ *
14
+ * In production the auth-bearing cookies get the `__Host-` prefix. Browsers
15
+ * reject any `__Host-`-prefixed cookie that carries a `Domain` attribute (and
16
+ * require `Secure` + `Path=/`), so a sibling subdomain (e.g. a tenant preview
17
+ * at `<token>.preview.molecule.dev`) can no longer create a colliding
18
+ * `.molecule.dev`-scoped duplicate that shadows the platform's real session
19
+ * cookie — closing the cross-tenant cookie-tossing / session-fixation hole.
20
+ *
21
+ * In dev/test the cookies are not `Secure` (plain HTTP origins), so a
22
+ * `__Host-` cookie would be rejected by the browser; use the plain name there.
23
+ *
24
+ * Exported so consumers (e.g. molecule-dev's guest-session / OAuth-state code)
25
+ * set and read the exact same name the platform expects.
26
+ *
27
+ * @param base - The base cookie name (e.g. `'token'`, `'sessionId'`).
28
+ * @returns The environment-appropriate cookie name.
29
+ */
30
+ export declare const getAuthCookieName: (base: string) => string;
31
+ /**
32
+ * Base cookie attributes shared by EVERY auth cookie this resource sets and
33
+ * clears — the single source of truth so a SET and its matching CLEAR always
34
+ * agree (a mismatched attribute leaves an undeletable cookie) and every auth
35
+ * cookie gets identical behaviour.
36
+ *
37
+ * - **Cross-site preview** (see {@link isCrossSitePreview}) → `{ secure: true,
38
+ * sameSite: 'none', partitioned: true, path: '/' }`. The molecule.dev live
39
+ * preview renders the app in a CROSS-SITE iframe; browsers evaluate SameSite
40
+ * against the TOP-LEVEL site, so a `Lax`/`Strict` cookie is neither stored nor
41
+ * sent on ANY preview request — the httpOnly session cookie vanishes on every
42
+ * iframe reload and the user is logged out of their own app. `SameSite=None`
43
+ * makes it cross-site-sendable (which REQUIRES `Secure` — permitted over http
44
+ * on the potentially-trustworthy `localhost` / `127.0.0.1` origins), and
45
+ * `Partitioned` (CHIPS) keeps it working when third-party cookies are blocked
46
+ * (Brave/Chrome), which `None` alone does not. Applies whether the platform is
47
+ * dev or prod, because the preview app is cross-site either way.
48
+ * - **Production, top-level** → `{ secure: true, sameSite: 'lax', path: '/' }`.
49
+ * The correct, CSRF-hardened default; pairs with the `__Host-` prefix (see
50
+ * {@link getAuthCookieName}). NEVER weaken it.
51
+ * - **Development, top-level** → `{ secure: false, sameSite: 'lax', path: '/' }`.
52
+ * The original dev posture (plain http, not `Secure`) — used by the molecule.dev
53
+ * IDE and by a developer running their generated app locally. Both are top-level
54
+ * documents, so `Lax` is correct and `Secure`/`None`/`Partitioned` would be both
55
+ * unnecessary and (as above) actively harmful.
56
+ *
57
+ * `httpOnly` and `maxAge` are intentionally omitted: the SET call adds those per
58
+ * cookie, and the CLEAR call spreads these attributes verbatim so the deletion
59
+ * targets the exact same (possibly Partitioned) cookie jar.
60
+ *
61
+ * @returns The environment-appropriate base cookie options.
62
+ */
63
+ export declare const getAuthCookieOptions: () => {
64
+ secure: boolean;
65
+ sameSite: "lax" | "none";
66
+ partitioned?: boolean;
67
+ path: string;
68
+ };
69
+ /**
70
+ * Evict a single device's positive entry from the device-exists cache so the
71
+ * next authorization check for that device re-consults the database.
72
+ *
73
+ * Call this from any in-process revocation path that deletes a specific device
74
+ * row (e.g. a logout handler that knows the `deviceId`) so the revocation is
75
+ * effective immediately on this instance rather than after the cache TTL.
76
+ *
77
+ * @param deviceId - The device id whose cached "exists" result should be dropped.
78
+ */
79
+ export declare const invalidateDeviceExistsCache: (deviceId: string) => void;
80
+ /**
81
+ * Evict ALL positive entries from the device-exists cache.
82
+ *
83
+ * Call this from in-process bulk revocation paths that delete every device for
84
+ * a user (password change/reset, account deletion, OAuth account-claim) where
85
+ * the affected device ids are not enumerated — the cache is keyed by deviceId,
86
+ * not userId, so a full clear is the correct way to guarantee none of the
87
+ * just-deleted devices keep authenticating on this instance. The normal-path
88
+ * per-request DB-cost reduction is preserved: the cache simply repopulates on
89
+ * subsequent requests; only these rare revocation events pay a brief re-check.
90
+ */
91
+ export declare const invalidateAllDeviceExistsCache: () => void;
92
+ /**
93
+ * Set authorization headers and cookie for a session.
94
+ *
95
+ * @param _req - The request object (unused; reserved for handler signature parity).
96
+ * @param res - The response object.
97
+ * @param session - The session.
98
+ * @returns The signed JWT token string, or null if signing failed.
99
+ */
100
+ export declare const set: (_req: MoleculeRequest, res: MoleculeResponse, session: Session) => string | null;
101
+ /**
102
+ * Middleware that verifies the JWT token from the `Authorization` header and sets `res.locals.session`.
103
+ * Falls back to cookie-based session for browser clients. Auto-refreshes tokens older than `JWT_REFRESH_TIME`.
104
+ * @returns An Express-compatible middleware function.
105
+ */
106
+ export declare const verifyMiddleware: () => MoleculeRequestHandler;
107
+ //# sourceMappingURL=authorization.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authorization.d.ts","sourceRoot":"","sources":["../src/authorization.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,cAAc,CAAA;AAMrB,OAAO,KAAK,EACV,eAAe,EACf,sBAAsB,EACtB,gBAAgB,EACjB,MAAM,wBAAwB,CAAA;AAE/B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAwB1C;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,iBAAiB,GAAI,MAAM,MAAM,KAAG,MACP,CAAA;AA4B1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,eAAO,MAAM,oBAAoB,QAAO;IACtC,MAAM,EAAE,OAAO,CAAA;IACf,QAAQ,EAAE,KAAK,GAAG,MAAM,CAAA;IACxB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,IAAI,EAAE,MAAM,CAAA;CAQb,CAAA;AAiDD;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,GAAI,UAAU,MAAM,KAAG,IAE9D,CAAA;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,8BAA8B,QAAO,IAEjD,CAAA;AAsDD;;;;;;;GAOG;AACH,eAAO,MAAM,GAAG,GACd,MAAM,eAAe,EACrB,KAAK,gBAAgB,EACrB,SAAS,OAAO,KACf,MAAM,GAAG,IA6DX,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,QAAO,sBAsEnC,CAAA"}
@@ -0,0 +1,375 @@
1
+ /**
2
+ * JWT session authorization for user resources.
3
+ *
4
+ * Provides middleware to verify, set, and refresh JWT tokens.
5
+ *
6
+ * @module
7
+ */
8
+ // Side-effect import: registers this resource's secret definitions so the
9
+ // runtime registry is populated even when authorization.js is imported directly
10
+ // (not through the package barrel).
11
+ import './secrets.js';
12
+ import { v4 as uuid } from 'uuid';
13
+ import { get, getLogger } from '@molecule/api-bond';
14
+ import { get as getConfig } from '@molecule/api-config';
15
+ import { decode, sign, verify as jwtVerify } from '@molecule/api-jwt';
16
+ const logger = getLogger();
17
+ const getJwtRefreshTime = () => {
18
+ try {
19
+ return Number(getConfig('JWT_REFRESH_TIME', '30d')) || 1000 * 60 * 30; // 30 minutes
20
+ }
21
+ catch (_error) {
22
+ // Config read is optional — fall back to the hard-coded default safely.
23
+ return 1000 * 60 * 30; // 30 minutes default
24
+ }
25
+ };
26
+ const isProduction = () => {
27
+ try {
28
+ return getConfig('NODE_ENV') === 'production';
29
+ }
30
+ catch (_error) {
31
+ // Config read is optional — treat an unreadable env as non-production so
32
+ // we never accidentally emit `__Host-` cookies on an insecure (non-HTTPS)
33
+ // origin where the browser would silently reject them.
34
+ return false;
35
+ }
36
+ };
37
+ /**
38
+ * Resolve the actual cookie name for an auth cookie.
39
+ *
40
+ * In production the auth-bearing cookies get the `__Host-` prefix. Browsers
41
+ * reject any `__Host-`-prefixed cookie that carries a `Domain` attribute (and
42
+ * require `Secure` + `Path=/`), so a sibling subdomain (e.g. a tenant preview
43
+ * at `<token>.preview.molecule.dev`) can no longer create a colliding
44
+ * `.molecule.dev`-scoped duplicate that shadows the platform's real session
45
+ * cookie — closing the cross-tenant cookie-tossing / session-fixation hole.
46
+ *
47
+ * In dev/test the cookies are not `Secure` (plain HTTP origins), so a
48
+ * `__Host-` cookie would be rejected by the browser; use the plain name there.
49
+ *
50
+ * Exported so consumers (e.g. molecule-dev's guest-session / OAuth-state code)
51
+ * set and read the exact same name the platform expects.
52
+ *
53
+ * @param base - The base cookie name (e.g. `'token'`, `'sessionId'`).
54
+ * @returns The environment-appropriate cookie name.
55
+ */
56
+ export const getAuthCookieName = (base) => isProduction() ? `__Host-${base}` : base;
57
+ /**
58
+ * Whether this API serves an app embedded in a CROSS-SITE iframe — i.e. the
59
+ * molecule.dev live preview, where the app renders at `127.0.0.1:<port>` inside
60
+ * the `localhost:3000` (prod: `*.preview.molecule.dev` inside `molecule.dev`)
61
+ * IDE. The platform sets `MOL_CROSS_SITE_COOKIES=1` in the sandbox container's
62
+ * env for exactly this case; a normally-deployed or locally-run app (which is a
63
+ * TOP-LEVEL document) never sets it.
64
+ *
65
+ * This is deliberately NOT derived from `NODE_ENV`: the molecule.dev IDE itself
66
+ * and a developer running their generated app locally are BOTH non-production
67
+ * AND top-level, so keying cross-site cookies off `NODE_ENV` wrongly forced
68
+ * `SameSite=None; Partitioned` onto those top-level sessions — which then
69
+ * coexisted with the browser's pre-existing `Lax` cookie of the same name in a
70
+ * SEPARATE jar, sending a duplicate `token` header and breaking auth.
71
+ *
72
+ * @returns `true` only when the app is served cross-site in the preview iframe.
73
+ */
74
+ const isCrossSitePreview = () => {
75
+ try {
76
+ return getConfig('MOL_CROSS_SITE_COOKIES') === '1';
77
+ }
78
+ catch (_error) {
79
+ // Config read is optional — absence means "not the preview" (top-level).
80
+ return false;
81
+ }
82
+ };
83
+ /**
84
+ * Base cookie attributes shared by EVERY auth cookie this resource sets and
85
+ * clears — the single source of truth so a SET and its matching CLEAR always
86
+ * agree (a mismatched attribute leaves an undeletable cookie) and every auth
87
+ * cookie gets identical behaviour.
88
+ *
89
+ * - **Cross-site preview** (see {@link isCrossSitePreview}) → `{ secure: true,
90
+ * sameSite: 'none', partitioned: true, path: '/' }`. The molecule.dev live
91
+ * preview renders the app in a CROSS-SITE iframe; browsers evaluate SameSite
92
+ * against the TOP-LEVEL site, so a `Lax`/`Strict` cookie is neither stored nor
93
+ * sent on ANY preview request — the httpOnly session cookie vanishes on every
94
+ * iframe reload and the user is logged out of their own app. `SameSite=None`
95
+ * makes it cross-site-sendable (which REQUIRES `Secure` — permitted over http
96
+ * on the potentially-trustworthy `localhost` / `127.0.0.1` origins), and
97
+ * `Partitioned` (CHIPS) keeps it working when third-party cookies are blocked
98
+ * (Brave/Chrome), which `None` alone does not. Applies whether the platform is
99
+ * dev or prod, because the preview app is cross-site either way.
100
+ * - **Production, top-level** → `{ secure: true, sameSite: 'lax', path: '/' }`.
101
+ * The correct, CSRF-hardened default; pairs with the `__Host-` prefix (see
102
+ * {@link getAuthCookieName}). NEVER weaken it.
103
+ * - **Development, top-level** → `{ secure: false, sameSite: 'lax', path: '/' }`.
104
+ * The original dev posture (plain http, not `Secure`) — used by the molecule.dev
105
+ * IDE and by a developer running their generated app locally. Both are top-level
106
+ * documents, so `Lax` is correct and `Secure`/`None`/`Partitioned` would be both
107
+ * unnecessary and (as above) actively harmful.
108
+ *
109
+ * `httpOnly` and `maxAge` are intentionally omitted: the SET call adds those per
110
+ * cookie, and the CLEAR call spreads these attributes verbatim so the deletion
111
+ * targets the exact same (possibly Partitioned) cookie jar.
112
+ *
113
+ * @returns The environment-appropriate base cookie options.
114
+ */
115
+ export const getAuthCookieOptions = () => {
116
+ if (isCrossSitePreview()) {
117
+ return { secure: true, sameSite: 'none', partitioned: true, path: '/' };
118
+ }
119
+ return isProduction()
120
+ ? { secure: true, sameSite: 'lax', path: '/' }
121
+ : { secure: false, sameSite: 'lax', path: '/' };
122
+ };
123
+ /**
124
+ * Read an auth cookie by its base name. In production the cookie is `__Host-`-
125
+ * prefixed (untossable: browsers reject `__Host-` cookies carrying a Domain), so
126
+ * we read ONLY that name and do NOT fall back to the plain name. [C4-1] Accepting
127
+ * the plain name in production re-opens cookie-tossing session fixation: a tenant
128
+ * on a sibling `*.molecule.dev` preview can write a `.molecule.dev`-scoped plain
129
+ * cookie that a plain-name fallback would honor. The `__Host-`-prefixed name is the
130
+ * only legitimate production cookie; sessions still carrying a plain cookie simply
131
+ * re-authenticate on the deploy that flips the prefix on. In dev there is no prefix,
132
+ * so the (plain) name returned by getAuthCookieName is the real cookie.
133
+ *
134
+ * @param cookies - The request cookies map.
135
+ * @param base - The base cookie name.
136
+ * @returns The cookie value, or `undefined` if absent.
137
+ */
138
+ const readAuthCookie = (cookies, base) => {
139
+ const primary = cookies?.[getAuthCookieName(base)];
140
+ if (isProduction())
141
+ return primary;
142
+ return primary ?? cookies?.[base];
143
+ };
144
+ /**
145
+ * Short-TTL positive cache for "device still exists" lookups, to bound the
146
+ * per-request DB cost of server-side session-revocation enforcement.
147
+ *
148
+ * Caching model and its honest revocation guarantee:
149
+ * - Only POSITIVE results (device exists) are cached; a definitive "device not
150
+ * found" result is NEVER cached, and a found row drops any stale entry. So a
151
+ * revoked device is rejected as soon as the cache misses or expires.
152
+ * - Revocations performed IN THIS PROCESS (logout, remote device removal,
153
+ * password change/reset, account deletion — the user-resource handlers that
154
+ * wipe device rows) are made effective IMMEDIATELY by actively evicting the
155
+ * cache via {@link invalidateDeviceExistsCache} / {@link invalidateAllDeviceExistsCache}
156
+ * at the revocation call site. The very next request re-checks the DB and is
157
+ * rejected — there is no residual window for in-process revocation.
158
+ * - Revocations performed by ANOTHER process (a different API instance in a
159
+ * multi-instance deployment, or a direct DB delete) cannot evict THIS
160
+ * process's in-memory cache, so a token whose positive entry was warmed here
161
+ * within the last `deviceExistsCacheTtlMs` may keep authenticating on THIS
162
+ * instance for up to that window (≤10s) before the entry expires and the DB
163
+ * is consulted again. This is the deliberate eventual-consistency tradeoff of
164
+ * a per-process cache; lower the TTL or back the cache with a shared
165
+ * pub/sub store to shrink it further.
166
+ */
167
+ const deviceExistsCacheTtlMs = 10_000;
168
+ const deviceExistsCache = new Map();
169
+ /**
170
+ * Evict a single device's positive entry from the device-exists cache so the
171
+ * next authorization check for that device re-consults the database.
172
+ *
173
+ * Call this from any in-process revocation path that deletes a specific device
174
+ * row (e.g. a logout handler that knows the `deviceId`) so the revocation is
175
+ * effective immediately on this instance rather than after the cache TTL.
176
+ *
177
+ * @param deviceId - The device id whose cached "exists" result should be dropped.
178
+ */
179
+ export const invalidateDeviceExistsCache = (deviceId) => {
180
+ deviceExistsCache.delete(deviceId);
181
+ };
182
+ /**
183
+ * Evict ALL positive entries from the device-exists cache.
184
+ *
185
+ * Call this from in-process bulk revocation paths that delete every device for
186
+ * a user (password change/reset, account deletion, OAuth account-claim) where
187
+ * the affected device ids are not enumerated — the cache is keyed by deviceId,
188
+ * not userId, so a full clear is the correct way to guarantee none of the
189
+ * just-deleted devices keep authenticating on this instance. The normal-path
190
+ * per-request DB-cost reduction is preserved: the cache simply repopulates on
191
+ * subsequent requests; only these rare revocation events pay a brief re-check.
192
+ */
193
+ export const invalidateAllDeviceExistsCache = () => {
194
+ deviceExistsCache.clear();
195
+ };
196
+ /**
197
+ * Determine whether a session's device is still active (i.e. has not been
198
+ * revoked by logout, remote device removal, or a password reset). This is the
199
+ * server-side revocation check that makes those controls actually terminate a
200
+ * session instead of leaving the JWT valid until natural expiry.
201
+ *
202
+ * Behaviour:
203
+ * - If no device bond exposing `exists` is wired (e.g. an app that doesn't
204
+ * track devices), revocation cannot be enforced, so the session is allowed
205
+ * (backward-compatible — no functionality regression).
206
+ * - If `exists` returns `false`, the device was revoked → reject.
207
+ * - If the lookup throws (transient infra failure), the session is allowed
208
+ * (best-effort, fail-open on errors only) and the error is logged, so a DB
209
+ * blip does not log out the entire platform. A genuine revocation returns a
210
+ * definitive `false`, which is always honoured.
211
+ *
212
+ * @param deviceId - The device id from the verified session.
213
+ * @returns `true` if the session may proceed, `false` if it was revoked.
214
+ */
215
+ const isDeviceActive = async (deviceId) => {
216
+ const deviceService = get('device');
217
+ // No device-tracking bond, or a bond that predates the `exists` capability —
218
+ // cannot enforce revocation here; do not break auth.
219
+ if (typeof deviceService?.exists !== 'function') {
220
+ return true;
221
+ }
222
+ const cachedAt = deviceExistsCache.get(deviceId);
223
+ if (cachedAt !== undefined && Date.now() - cachedAt < deviceExistsCacheTtlMs) {
224
+ return true;
225
+ }
226
+ try {
227
+ const exists = await deviceService.exists(deviceId);
228
+ if (exists) {
229
+ deviceExistsCache.set(deviceId, Date.now());
230
+ return true;
231
+ }
232
+ // Definitive revocation — drop any stale positive cache entry and reject.
233
+ deviceExistsCache.delete(deviceId);
234
+ return false;
235
+ }
236
+ catch (error) {
237
+ // Best-effort: a transient lookup failure must not log out everyone.
238
+ logger.warn('authorization: device revocation check failed; allowing session', {
239
+ deviceId,
240
+ error,
241
+ });
242
+ return true;
243
+ }
244
+ };
245
+ /**
246
+ * Set authorization headers and cookie for a session.
247
+ *
248
+ * @param _req - The request object (unused; reserved for handler signature parity).
249
+ * @param res - The response object.
250
+ * @param session - The session.
251
+ * @returns The signed JWT token string, or null if signing failed.
252
+ */
253
+ export const set = (_req, res, session) => {
254
+ try {
255
+ // Create a session ID for web browser clients.
256
+ if (!session.id) {
257
+ session.id = uuid();
258
+ }
259
+ // Strip JWT-specific claims from the session before re-signing —
260
+ // sign() sets expiresIn which conflicts with an existing exp property.
261
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
262
+ const { exp, iat, nbf, ...sessionPayload } = session;
263
+ const token = sign(sessionPayload);
264
+ if (token) {
265
+ res.setHeader('Authorization', `Bearer ${token}`);
266
+ // Set as cookies for web browser clients.
267
+ // In production the names are `__Host-`-prefixed (see getAuthCookieName),
268
+ // which REQUIRES Secure + Path=/ + NO Domain — all satisfied by
269
+ // getAuthCookieOptions() — and makes the cookies unshadowable by a sibling
270
+ // subdomain. In the sandbox live-preview iframe those same options switch to
271
+ // SameSite=None + Secure + Partitioned so the cross-site iframe actually
272
+ // keeps them (otherwise the session vanishes on every reload).
273
+ const cookieOptions = getAuthCookieOptions();
274
+ const maxAge = 1000 * 60 * 60 * 24 * 7; // 7 days (match JWT expiry)
275
+ res.cookie(getAuthCookieName('token'), token, {
276
+ ...cookieOptions,
277
+ httpOnly: true,
278
+ maxAge,
279
+ });
280
+ res.cookie(getAuthCookieName('sessionId'), session.id, {
281
+ ...cookieOptions,
282
+ httpOnly: true,
283
+ maxAge,
284
+ });
285
+ // [M1-1] Non-httpOnly session-PRESENCE hint (carries no token — just "1").
286
+ // The bearer token is in-memory + the httpOnly `token` cookie (both
287
+ // JS-unreadable), so on a fresh load the app can't tell "logged in" from
288
+ // "anonymous" without probing the server. This readable hint lets the auth
289
+ // client's initialize() probe the cookie-restore endpoint ONLY when a
290
+ // session plausibly exists — so anonymous/public page loads make ZERO API
291
+ // calls (no /users/me, no 401). A tossed/forged hint only costs one
292
+ // harmless 401 probe; it grants nothing (the real credential stays
293
+ // httpOnly). Cleared by logout alongside the credential cookies.
294
+ res.cookie('mol_auth', '1', {
295
+ ...cookieOptions,
296
+ httpOnly: false,
297
+ maxAge,
298
+ });
299
+ return token;
300
+ }
301
+ }
302
+ catch (error) {
303
+ logger.error(error);
304
+ }
305
+ return null;
306
+ };
307
+ /**
308
+ * Middleware that verifies the JWT token from the `Authorization` header and sets `res.locals.session`.
309
+ * Falls back to cookie-based session for browser clients. Auto-refreshes tokens older than `JWT_REFRESH_TIME`.
310
+ * @returns An Express-compatible middleware function.
311
+ */
312
+ export const verifyMiddleware = () => async (req, res, next) => {
313
+ try {
314
+ // Get token from Authorization header, falling back to the `token`
315
+ // cookie that `set()` writes alongside the header. The JSDoc above
316
+ // promises this fallback for browser clients; without it the cookie
317
+ // we hand out is decorative — every page-driven request after login
318
+ // gets a 401, even though the cookie is sitting right there. We still
319
+ // verify the signature via `jwtVerify` so a tampered cookie value is
320
+ // never trusted (no decode-fallback bypass).
321
+ const rawAuth = req.headers.authorization;
322
+ const authHeader = Array.isArray(rawAuth) ? rawAuth[0] : rawAuth;
323
+ const headerToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null;
324
+ const cookies = req.cookies;
325
+ const cookieToken = readAuthCookie(cookies, 'token');
326
+ const token = headerToken ?? (typeof cookieToken === 'string' && cookieToken ? cookieToken : null);
327
+ if (!token) {
328
+ return next();
329
+ }
330
+ // Try to verify the token.
331
+ let session = null;
332
+ try {
333
+ session = jwtVerify(token);
334
+ }
335
+ catch (_error) {
336
+ // Token is invalid or expired — do NOT accept unverified tokens.
337
+ // Previous code used decode() (no signature check) as a fallback,
338
+ // which allowed expired/tampered tokens to be accepted if a cookie matched.
339
+ // This was an authentication bypass. Require re-authentication instead.
340
+ }
341
+ if (session?.userId && session?.deviceId) {
342
+ // Server-side session revocation. A logged-out / remotely-revoked /
343
+ // password-reset device row is deleted; that MUST invalidate every copy
344
+ // of the JWT, not just the browser that called logout. Without this a
345
+ // valid signature alone keeps the session alive until natural expiry
346
+ // (default 7 days), so logout is purely cosmetic. Check BEFORE exposing
347
+ // the session so a revoked token never reaches a handler.
348
+ if (!(await isDeviceActive(session.deviceId))) {
349
+ return next();
350
+ }
351
+ res.locals.session = session;
352
+ // Auto-refresh the token if it's old enough.
353
+ try {
354
+ const decoded = decode(token);
355
+ if (decoded?.iat) {
356
+ const tokenAge = Date.now() - decoded.iat * 1000;
357
+ if (tokenAge > getJwtRefreshTime()) {
358
+ // Update device last seen timestamp via bond.
359
+ await get('device')?.updateLastSeen(session.deviceId);
360
+ // Issue a fresh token.
361
+ set(req, res, session);
362
+ }
363
+ }
364
+ }
365
+ catch (_error) {
366
+ // Non-critical — token refresh is best-effort; continue with existing session.
367
+ }
368
+ }
369
+ }
370
+ catch (error) {
371
+ logger.error('authorization.verify error:', error);
372
+ }
373
+ return next();
374
+ };
375
+ //# sourceMappingURL=authorization.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authorization.js","sourceRoot":"","sources":["../src/authorization.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import type { MoleculeRequestHandler } from '@molecule/api-resource';
2
+ /**
3
+ * Middleware that checks if the request has an authenticated session (`res.locals.session.userId`).
4
+ * Calls `next()` on success or `next('Unauthorized')` on failure.
5
+ * @returns An Express-compatible middleware function.
6
+ */
7
+ export declare const auth: () => MoleculeRequestHandler;
8
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/authorizers/auth.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAA;AAEpE;;;;GAIG;AACH,eAAO,MAAM,IAAI,QAAO,sBAUvB,CAAA"}
@@ -0,0 +1,19 @@
1
+ import { t } from '@molecule/api-i18n';
2
+ /**
3
+ * Middleware that checks if the request has an authenticated session (`res.locals.session.userId`).
4
+ * Calls `next()` on success or `next('Unauthorized')` on failure.
5
+ * @returns An Express-compatible middleware function.
6
+ */
7
+ export const auth = () => (req, res, next) => {
8
+ try {
9
+ const { session } = res.locals;
10
+ if (session?.userId) {
11
+ return next();
12
+ }
13
+ }
14
+ catch (_error) {
15
+ // Safe to ignore: if session access throws, we fall through to Unauthorized below.
16
+ }
17
+ return next(t('resource.error.unauthorized', undefined, { defaultValue: 'Unauthorized' }));
18
+ };
19
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/authorizers/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,oBAAoB,CAAA;AAGtC;;;;GAIG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,GAA2B,EAAE,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IACnE,IAAI,CAAC;QACH,MAAM,EAAE,OAAO,EAAE,GAAG,GAAG,CAAC,MAAM,CAAA;QAC9B,IAAI,OAAO,EAAE,MAAM,EAAE,CAAC;YACpB,OAAO,IAAI,EAAE,CAAA;QACf,CAAC;IACH,CAAC;IAAC,OAAO,MAAM,EAAE,CAAC;QAChB,mFAAmF;IACrF,CAAC;IACD,OAAO,IAAI,CAAC,CAAC,CAAC,6BAA6B,EAAE,SAAS,EAAE,EAAE,YAAY,EAAE,cAAc,EAAE,CAAC,CAAC,CAAA;AAC5F,CAAC,CAAA"}