@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.
- package/LICENSE +115 -0
- package/dist/authorization.d.ts +107 -0
- package/dist/authorization.d.ts.map +1 -0
- package/dist/authorization.js +375 -0
- package/dist/authorization.js.map +1 -0
- package/dist/authorizers/auth.d.ts +8 -0
- package/dist/authorizers/auth.d.ts.map +1 -0
- package/dist/authorizers/auth.js +19 -0
- package/dist/authorizers/auth.js.map +1 -0
- package/dist/authorizers/authSelf.d.ts +14 -0
- package/dist/authorizers/authSelf.d.ts.map +1 -0
- package/dist/authorizers/authSelf.js +31 -0
- package/dist/authorizers/authSelf.js.map +1 -0
- package/dist/authorizers/index.d.ts +5 -0
- package/dist/authorizers/index.d.ts.map +1 -0
- package/dist/authorizers/index.js +5 -0
- package/dist/authorizers/index.js.map +1 -0
- package/dist/authorizers/rateLimit.d.ts +84 -0
- package/dist/authorizers/rateLimit.d.ts.map +1 -0
- package/dist/authorizers/rateLimit.js +144 -0
- package/dist/authorizers/rateLimit.js.map +1 -0
- package/dist/authorizers/requireWebhookAuthenticity.d.ts +23 -0
- package/dist/authorizers/requireWebhookAuthenticity.d.ts.map +1 -0
- package/dist/authorizers/requireWebhookAuthenticity.js +76 -0
- package/dist/authorizers/requireWebhookAuthenticity.js.map +1 -0
- package/dist/browser-guard.d.ts +2 -0
- package/dist/browser-guard.d.ts.map +1 -0
- package/dist/browser-guard.js +19 -0
- package/dist/browser-guard.js.map +1 -0
- package/dist/handlers/create.d.ts +52 -0
- package/dist/handlers/create.d.ts.map +1 -0
- package/dist/handlers/create.js +208 -0
- package/dist/handlers/create.js.map +1 -0
- package/dist/handlers/del.d.ts +30 -0
- package/dist/handlers/del.d.ts.map +1 -0
- package/dist/handlers/del.js +55 -0
- package/dist/handlers/del.js.map +1 -0
- package/dist/handlers/forgotPassword.d.ts +34 -0
- package/dist/handlers/forgotPassword.d.ts.map +1 -0
- package/dist/handlers/forgotPassword.js +119 -0
- package/dist/handlers/forgotPassword.js.map +1 -0
- package/dist/handlers/index.d.ts +16 -0
- package/dist/handlers/index.d.ts.map +1 -0
- package/dist/handlers/index.js +16 -0
- package/dist/handlers/index.js.map +1 -0
- package/dist/handlers/logIn.d.ts +91 -0
- package/dist/handlers/logIn.d.ts.map +1 -0
- package/dist/handlers/logIn.js +223 -0
- package/dist/handlers/logIn.js.map +1 -0
- package/dist/handlers/logInOAuth.d.ts +66 -0
- package/dist/handlers/logInOAuth.d.ts.map +1 -0
- package/dist/handlers/logInOAuth.js +462 -0
- package/dist/handlers/logInOAuth.js.map +1 -0
- package/dist/handlers/logout.d.ts +22 -0
- package/dist/handlers/logout.d.ts.map +1 -0
- package/dist/handlers/logout.js +57 -0
- package/dist/handlers/logout.js.map +1 -0
- package/dist/handlers/oauthAuthorize.d.ts +44 -0
- package/dist/handlers/oauthAuthorize.d.ts.map +1 -0
- package/dist/handlers/oauthAuthorize.js +141 -0
- package/dist/handlers/oauthAuthorize.js.map +1 -0
- package/dist/handlers/payments/handlePaymentNotification.d.ts +28 -0
- package/dist/handlers/payments/handlePaymentNotification.d.ts.map +1 -0
- package/dist/handlers/payments/handlePaymentNotification.js +217 -0
- package/dist/handlers/payments/handlePaymentNotification.js.map +1 -0
- package/dist/handlers/payments/registry.d.ts +13 -0
- package/dist/handlers/payments/registry.d.ts.map +1 -0
- package/dist/handlers/payments/registry.js +13 -0
- package/dist/handlers/payments/registry.js.map +1 -0
- package/dist/handlers/payments/verifyPayment.d.ts +37 -0
- package/dist/handlers/payments/verifyPayment.d.ts.map +1 -0
- package/dist/handlers/payments/verifyPayment.js +383 -0
- package/dist/handlers/payments/verifyPayment.js.map +1 -0
- package/dist/handlers/read.d.ts +17 -0
- package/dist/handlers/read.d.ts.map +1 -0
- package/dist/handlers/read.js +60 -0
- package/dist/handlers/read.js.map +1 -0
- package/dist/handlers/readSelf.d.ts +23 -0
- package/dist/handlers/readSelf.d.ts.map +1 -0
- package/dist/handlers/readSelf.js +77 -0
- package/dist/handlers/readSelf.js.map +1 -0
- package/dist/handlers/resetPassword.d.ts +40 -0
- package/dist/handlers/resetPassword.d.ts.map +1 -0
- package/dist/handlers/resetPassword.js +147 -0
- package/dist/handlers/resetPassword.js.map +1 -0
- package/dist/handlers/update.d.ts +28 -0
- package/dist/handlers/update.d.ts.map +1 -0
- package/dist/handlers/update.js +250 -0
- package/dist/handlers/update.js.map +1 -0
- package/dist/handlers/updatePassword.d.ts +35 -0
- package/dist/handlers/updatePassword.d.ts.map +1 -0
- package/dist/handlers/updatePassword.js +113 -0
- package/dist/handlers/updatePassword.js.map +1 -0
- package/dist/handlers/updatePlan.d.ts +41 -0
- package/dist/handlers/updatePlan.d.ts.map +1 -0
- package/dist/handlers/updatePlan.js +242 -0
- package/dist/handlers/updatePlan.js.map +1 -0
- package/dist/handlers/verifyTwoFactor.d.ts +46 -0
- package/dist/handlers/verifyTwoFactor.d.ts.map +1 -0
- package/dist/handlers/verifyTwoFactor.js +162 -0
- package/dist/handlers/verifyTwoFactor.js.map +1 -0
- package/dist/i18n.d.ts +10 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +25 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.d.ts +88 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +88 -0
- package/dist/index.js.map +1 -0
- package/dist/requestHandlerMap.d.ts +44 -0
- package/dist/requestHandlerMap.d.ts.map +1 -0
- package/dist/requestHandlerMap.js +68 -0
- package/dist/requestHandlerMap.js.map +1 -0
- package/dist/resource.d.ts +22 -0
- package/dist/resource.d.ts.map +1 -0
- package/dist/resource.js +23 -0
- package/dist/resource.js.map +1 -0
- package/dist/routes.d.ts +44 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +121 -0
- package/dist/routes.js.map +1 -0
- package/dist/schema.d.ts +193 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +229 -0
- package/dist/schema.js.map +1 -0
- package/dist/secrets.d.ts +16 -0
- package/dist/secrets.d.ts.map +1 -0
- package/dist/secrets.js +28 -0
- package/dist/secrets.js.map +1 -0
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/utilities/getPlan.d.ts +19 -0
- package/dist/utilities/getPlan.d.ts.map +1 -0
- package/dist/utilities/getPlan.js +26 -0
- package/dist/utilities/getPlan.js.map +1 -0
- package/dist/utilities/hashResetToken.d.ts +16 -0
- package/dist/utilities/hashResetToken.d.ts.map +1 -0
- package/dist/utilities/hashResetToken.js +19 -0
- package/dist/utilities/hashResetToken.js.map +1 -0
- package/dist/utilities/index.d.ts +5 -0
- package/dist/utilities/index.d.ts.map +1 -0
- package/dist/utilities/index.js +5 -0
- package/dist/utilities/index.js.map +1 -0
- package/dist/utilities/invalidateEntitlements.d.ts +24 -0
- package/dist/utilities/invalidateEntitlements.d.ts.map +1 -0
- package/dist/utilities/invalidateEntitlements.js +41 -0
- package/dist/utilities/invalidateEntitlements.js.map +1 -0
- package/dist/utilities/normalizeEmail.d.ts +22 -0
- package/dist/utilities/normalizeEmail.d.ts.map +1 -0
- package/dist/utilities/normalizeEmail.js +28 -0
- package/dist/utilities/normalizeEmail.js.map +1 -0
- package/dist/utilities/notify.d.ts +21 -0
- package/dist/utilities/notify.d.ts.map +1 -0
- package/dist/utilities/notify.js +39 -0
- package/dist/utilities/notify.js.map +1 -0
- package/dist/utilities/stripSensitiveUserColumns.d.ts +25 -0
- package/dist/utilities/stripSensitiveUserColumns.d.ts.map +1 -0
- package/dist/utilities/stripSensitiveUserColumns.js +43 -0
- package/dist/utilities/stripSensitiveUserColumns.js.map +1 -0
- package/package.json +94 -0
- package/setup/users.sql +22 -0
- 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":"AAAA;;;;;;GAMG;AAEH,0EAA0E;AAC1E,gFAAgF;AAChF,oCAAoC;AACpC,OAAO,cAAc,CAAA;AACrB,OAAO,EAAE,EAAE,IAAI,IAAI,EAAE,MAAM,MAAM,CAAA;AAEjC,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACnD,OAAO,EAAE,GAAG,IAAI,SAAS,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,IAAI,SAAS,EAAE,MAAM,mBAAmB,CAAA;AASrE,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;AAE1B,MAAM,iBAAiB,GAAG,GAAW,EAAE;IACrC,IAAI,CAAC;QACH,OAAO,MAAM,CAAC,SAAS,CAAC,kBAAkB,EAAE,KAAK,CAAC,CAAC,IAAI,IAAI,GAAG,EAAE,GAAG,EAAE,CAAA,CAAC,aAAa;IACrF,CAAC;IAAC,OAAO,MAAM,EAAE,CAAC;QAChB,wEAAwE;QACxE,OAAO,IAAI,GAAG,EAAE,GAAG,EAAE,CAAA,CAAC,qBAAqB;IAC7C,CAAC;AACH,CAAC,CAAA;AAED,MAAM,YAAY,GAAG,GAAY,EAAE;IACjC,IAAI,CAAC;QACH,OAAO,SAAS,CAAC,UAAU,CAAC,KAAK,YAAY,CAAA;IAC/C,CAAC;IAAC,OAAO,MAAM,EAAE,CAAC;QAChB,yEAAyE;QACzE,0EAA0E;QAC1E,uDAAuD;QACvD,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC,CAAA;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,IAAY,EAAU,EAAE,CACxD,YAAY,EAAE,CAAC,CAAC,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;AAE1C;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,kBAAkB,GAAG,GAAY,EAAE;IACvC,IAAI,CAAC;QACH,OAAO,SAAS,CAAC,wBAAwB,CAAC,KAAK,GAAG,CAAA;IACpD,CAAC;IAAC,OAAO,MAAM,EAAE,CAAC;QAChB,yEAAyE;QACzE,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,GAKlC,EAAE;IACF,IAAI,kBAAkB,EAAE,EAAE,CAAC;QACzB,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,CAAA;IACzE,CAAC;IACD,OAAO,YAAY,EAAE;QACnB,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE;QAC9C,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,CAAA;AACnD,CAAC,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,cAAc,GAAG,CAAC,OAA4C,EAAE,IAAY,EAAW,EAAE;IAC7F,MAAM,OAAO,GAAG,OAAO,EAAE,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAA;IAClD,IAAI,YAAY,EAAE;QAAE,OAAO,OAAO,CAAA;IAClC,OAAO,OAAO,IAAI,OAAO,EAAE,CAAC,IAAI,CAAC,CAAA;AACnC,CAAC,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,sBAAsB,GAAG,MAAM,CAAA;AACrC,MAAM,iBAAiB,GAAG,IAAI,GAAG,EAAkB,CAAA;AAEnD;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,QAAgB,EAAQ,EAAE;IACpE,iBAAiB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;AACpC,CAAC,CAAA;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,8BAA8B,GAAG,GAAS,EAAE;IACvD,iBAAiB,CAAC,KAAK,EAAE,CAAA;AAC3B,CAAC,CAAA;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,cAAc,GAAG,KAAK,EAAE,QAAgB,EAAoB,EAAE;IAClE,MAAM,aAAa,GAAG,GAAG,CAAkD,QAAQ,CAAC,CAAA;IAEpF,6EAA6E;IAC7E,qDAAqD;IACrD,IAAI,OAAO,aAAa,EAAE,MAAM,KAAK,UAAU,EAAE,CAAC;QAChD,OAAO,IAAI,CAAA;IACb,CAAC;IAED,MAAM,QAAQ,GAAG,iBAAiB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IAChD,IAAI,QAAQ,KAAK,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,GAAG,sBAAsB,EAAE,CAAC;QAC7E,OAAO,IAAI,CAAA;IACb,CAAC;IAED,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;QACnD,IAAI,MAAM,EAAE,CAAC;YACX,iBAAiB,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAA;YAC3C,OAAO,IAAI,CAAA;QACb,CAAC;QACD,0EAA0E;QAC1E,iBAAiB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;QAClC,OAAO,KAAK,CAAA;IACd,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,qEAAqE;QACrE,MAAM,CAAC,IAAI,CAAC,iEAAiE,EAAE;YAC7E,QAAQ;YACR,KAAK;SACN,CAAC,CAAA;QACF,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,GAAG,GAAG,CACjB,IAAqB,EACrB,GAAqB,EACrB,OAAgB,EACD,EAAE;IACjB,IAAI,CAAC;QACH,+CAA+C;QAC/C,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC;YAChB,OAAO,CAAC,EAAE,GAAG,IAAI,EAAE,CAAA;QACrB,CAAC;QAED,iEAAiE;QACjE,uEAAuE;QACvE,6DAA6D;QAC7D,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,cAAc,EAAE,GAAG,OAI5C,CAAA;QACD,MAAM,KAAK,GAAG,IAAI,CAAC,cAAc,CAAC,CAAA;QAElC,IAAI,KAAK,EAAE,CAAC;YACV,GAAG,CAAC,SAAS,CAAC,eAAe,EAAE,UAAU,KAAK,EAAE,CAAC,CAAA;YAEjD,0CAA0C;YAC1C,0EAA0E;YAC1E,gEAAgE;YAChE,2EAA2E;YAC3E,6EAA6E;YAC7E,yEAAyE;YACzE,+DAA+D;YAC/D,MAAM,aAAa,GAAG,oBAAoB,EAAE,CAAA;YAC5C,MAAM,MAAM,GAAG,IAAI,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAA,CAAC,4BAA4B;YACnE,GAAG,CAAC,MAAM,CAAC,iBAAiB,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE;gBAC5C,GAAG,aAAa;gBAChB,QAAQ,EAAE,IAAI;gBACd,MAAM;aACP,CAAC,CAAA;YACF,GAAG,CAAC,MAAM,CAAC,iBAAiB,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC,EAAE,EAAE;gBACrD,GAAG,aAAa;gBAChB,QAAQ,EAAE,IAAI;gBACd,MAAM;aACP,CAAC,CAAA;YACF,2EAA2E;YAC3E,oEAAoE;YACpE,yEAAyE;YACzE,2EAA2E;YAC3E,sEAAsE;YACtE,0EAA0E;YAC1E,oEAAoE;YACpE,mEAAmE;YACnE,iEAAiE;YACjE,GAAG,CAAC,MAAM,CAAC,UAAU,EAAE,GAAG,EAAE;gBAC1B,GAAG,aAAa;gBAChB,QAAQ,EAAE,KAAK;gBACf,MAAM;aACP,CAAC,CAAA;YAEF,OAAO,KAAK,CAAA;QACd,CAAC;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;IACrB,CAAC;IAED,OAAO,IAAI,CAAA;AACb,CAAC,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAA2B,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;IACrF,IAAI,CAAC;QACH,mEAAmE;QACnE,mEAAmE;QACnE,oEAAoE;QACpE,oEAAoE;QACpE,sEAAsE;QACtE,qEAAqE;QACrE,6CAA6C;QAC7C,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,CAAA;QACzC,MAAM,UAAU,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAA;QAChE,MAAM,WAAW,GAAG,UAAU,EAAE,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;QAClF,MAAM,OAAO,GAAI,GAAwD,CAAC,OAAO,CAAA;QACjF,MAAM,WAAW,GAAG,cAAc,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;QACpD,MAAM,KAAK,GACT,WAAW,IAAI,CAAC,OAAO,WAAW,KAAK,QAAQ,IAAI,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;QAEtF,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,IAAI,EAAE,CAAA;QACf,CAAC;QAED,2BAA2B;QAC3B,IAAI,OAAO,GAAmB,IAAI,CAAA;QAElC,IAAI,CAAC;YACH,OAAO,GAAG,SAAS,CAAC,KAAK,CAAY,CAAA;QACvC,CAAC;QAAC,OAAO,MAAM,EAAE,CAAC;YAChB,iEAAiE;YACjE,kEAAkE;YAClE,4EAA4E;YAC5E,wEAAwE;QAC1E,CAAC;QAED,IAAI,OAAO,EAAE,MAAM,IAAI,OAAO,EAAE,QAAQ,EAAE,CAAC;YACzC,oEAAoE;YACpE,wEAAwE;YACxE,sEAAsE;YACtE,qEAAqE;YACrE,wEAAwE;YACxE,0DAA0D;YAC1D,IAAI,CAAC,CAAC,MAAM,cAAc,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC;gBAC9C,OAAO,IAAI,EAAE,CAAA;YACf,CAAC;YAED,GAAG,CAAC,MAAM,CAAC,OAAO,GAAG,OAAO,CAAA;YAE5B,6CAA6C;YAC7C,IAAI,CAAC;gBACH,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAA4B,CAAA;gBACxD,IAAI,OAAO,EAAE,GAAG,EAAE,CAAC;oBACjB,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,GAAG,GAAG,IAAI,CAAA;oBAChD,IAAI,QAAQ,GAAG,iBAAiB,EAAE,EAAE,CAAC;wBACnC,8CAA8C;wBAC9C,MAAM,GAAG,CACP,QAAQ,CACT,EAAE,cAAc,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;wBAEnC,uBAAuB;wBACvB,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,CAAC,CAAA;oBACxB,CAAC;gBACH,CAAC;YACH,CAAC;YAAC,OAAO,MAAM,EAAE,CAAC;gBAChB,+EAA+E;YACjF,CAAC;QACH,CAAC;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,CAAC,KAAK,CAAC,6BAA6B,EAAE,KAAK,CAAC,CAAA;IACpD,CAAC;IAED,OAAO,IAAI,EAAE,CAAA;AACf,CAAC,CAAA"}
|
|
@@ -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"}
|