@payez/next-mvp 4.1.11 → 4.1.20
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/dist/api-handlers/account/profile.d.ts +38 -0
- package/dist/api-handlers/account/profile.js +73 -45
- package/dist/api-handlers/account/recovery/send-code.d.ts +33 -0
- package/dist/api-handlers/account/recovery/send-code.js +52 -18
- package/dist/api-handlers/account/recovery/verify-code.d.ts +33 -0
- package/dist/api-handlers/account/recovery/verify-code.js +52 -18
- package/dist/api-handlers/account/verify-email.js +15 -1
- package/dist/api-handlers/account/verify-sms.js +15 -1
- package/dist/api-handlers/auth/passkey-signin.d.ts +29 -0
- package/dist/api-handlers/auth/passkey-signin.js +97 -0
- package/dist/api-handlers/mfa/backup-code-verify.d.ts +73 -0
- package/dist/api-handlers/mfa/backup-code-verify.js +153 -0
- package/dist/api-handlers/mfa/backup-codes.d.ts +7 -0
- package/dist/api-handlers/mfa/backup-codes.js +40 -0
- package/dist/api-handlers/mfa/methods.d.ts +3 -0
- package/dist/api-handlers/mfa/methods.js +41 -0
- package/dist/api-handlers/mfa/totp-confirm.d.ts +8 -0
- package/dist/api-handlers/mfa/totp-confirm.js +41 -0
- package/dist/api-handlers/mfa/totp-enroll.d.ts +8 -0
- package/dist/api-handlers/mfa/totp-enroll.js +41 -0
- package/dist/api-handlers/mfa/totp-verify.d.ts +47 -0
- package/dist/api-handlers/mfa/totp-verify.js +134 -0
- package/dist/api-handlers/passkeys/list.d.ts +7 -0
- package/dist/api-handlers/passkeys/list.js +29 -0
- package/dist/api-handlers/passkeys/manage.d.ts +3 -0
- package/dist/api-handlers/passkeys/manage.js +54 -0
- package/dist/api-handlers/passkeys/register-options.d.ts +8 -0
- package/dist/api-handlers/passkeys/register-options.js +41 -0
- package/dist/api-handlers/passkeys/register-verify.d.ts +7 -0
- package/dist/api-handlers/passkeys/register-verify.js +40 -0
- package/dist/api-handlers/session/viability.js +19 -0
- package/dist/auth/better-auth.d.ts +68 -5
- package/dist/auth/better-auth.js +256 -42
- package/dist/auth/utils/idp-client.d.ts +13 -22
- package/dist/auth/utils/idp-client.js +12 -82
- package/dist/client/better-auth-client.d.ts +14 -2
- package/dist/lib/session-store.js +28 -21
- package/dist/lib/startup-init.js +19 -19
- package/dist/lib/token-lifecycle.js +40 -4
- package/dist/models/SessionModel.d.ts +17 -0
- package/dist/pages/login/page.d.ts +10 -1
- package/dist/pages/login/page.js +69 -80
- package/dist/pages/verify-code/page.d.ts +10 -1
- package/dist/pages/verify-code/page.js +72 -4
- package/dist/routes/auth/passkey-signin.d.ts +13 -0
- package/dist/routes/auth/passkey-signin.js +18 -0
- package/dist/routes/auth/viability.d.ts +2 -0
- package/dist/routes/auth/viability.js +88 -0
- package/dist/routes/mfa/backup-code-verify.d.ts +1 -0
- package/dist/routes/mfa/backup-code-verify.js +5 -0
- package/dist/routes/mfa/backup-codes.d.ts +10 -0
- package/dist/routes/mfa/backup-codes.js +14 -0
- package/dist/routes/mfa/methods.d.ts +21 -0
- package/dist/routes/mfa/methods.js +10 -0
- package/dist/routes/mfa/totp-confirm.d.ts +10 -0
- package/dist/routes/mfa/totp-confirm.js +14 -0
- package/dist/routes/mfa/totp-enroll.d.ts +10 -0
- package/dist/routes/mfa/totp-enroll.js +14 -0
- package/dist/routes/mfa/totp-verify.d.ts +14 -0
- package/dist/routes/mfa/totp-verify.js +18 -0
- package/dist/routes/passkeys/list.d.ts +10 -0
- package/dist/routes/passkeys/list.js +14 -0
- package/dist/routes/passkeys/manage.d.ts +18 -0
- package/dist/routes/passkeys/manage.js +13 -0
- package/dist/routes/passkeys/register-options.d.ts +10 -0
- package/dist/routes/passkeys/register-options.js +14 -0
- package/dist/routes/passkeys/register-verify.d.ts +10 -0
- package/dist/routes/passkeys/register-verify.js +14 -0
- package/dist/server/auth.d.ts +2 -2
- package/dist/server/decode-session.js +37 -3
- package/package.json +958 -893
- package/scripts/check-internal-url-usage.sh +73 -73
- package/src/api/index.ts +17 -17
- package/src/api-handlers/account/masked-info.ts +45 -45
- package/src/api-handlers/account/profile.ts +99 -80
- package/src/api-handlers/account/recovery/initiate.ts +23 -23
- package/src/api-handlers/account/recovery/send-code.ts +73 -25
- package/src/api-handlers/account/recovery/verify-code.ts +73 -25
- package/src/api-handlers/account/reset-password.ts +23 -23
- package/src/api-handlers/account/send-code.ts +76 -76
- package/src/api-handlers/account/validate-password.ts +118 -118
- package/src/api-handlers/account/verify-email.ts +15 -1
- package/src/api-handlers/account/verify-sms.ts +15 -1
- package/src/api-handlers/admin/stats.ts +249 -249
- package/src/api-handlers/anon/preferences.ts +123 -123
- package/src/api-handlers/auth/jwks.ts +20 -20
- package/src/api-handlers/auth/login.ts +240 -240
- package/src/api-handlers/auth/passkey-signin.ts +98 -0
- package/src/api-handlers/auth/validate.ts +43 -43
- package/src/api-handlers/mfa/backup-code-verify.ts +175 -0
- package/src/api-handlers/mfa/backup-codes.ts +41 -0
- package/src/api-handlers/mfa/methods.ts +46 -0
- package/src/api-handlers/mfa/totp-confirm.ts +42 -0
- package/src/api-handlers/mfa/totp-enroll.ts +42 -0
- package/src/api-handlers/mfa/totp-verify.ts +156 -0
- package/src/api-handlers/passkeys/list.ts +31 -0
- package/src/api-handlers/passkeys/manage.ts +54 -0
- package/src/api-handlers/passkeys/register-options.ts +42 -0
- package/src/api-handlers/passkeys/register-verify.ts +41 -0
- package/src/api-handlers/session/viability.ts +161 -142
- package/src/auth/better-auth.ts +247 -6
- package/src/auth/unauthenticated-routes.ts +2 -2
- package/src/auth/utils/idp-client.ts +13 -95
- package/src/client/better-auth-client.ts +70 -70
- package/src/client/fetchWithSession.ts +21 -21
- package/src/client/index.ts +13 -13
- package/src/client/useAnonSession.ts +131 -131
- package/src/components/admin/VibeAdminContext.tsx +87 -87
- package/src/config/logging-config.ts +82 -82
- package/src/config/unauthenticated-routes.ts +19 -19
- package/src/edge/internal-api-url.ts +65 -65
- package/src/edge/middleware.ts +42 -42
- package/src/hooks/useProfile.ts +75 -75
- package/src/lib/anon-session.ts +213 -213
- package/src/lib/auth-secret.ts +137 -137
- package/src/lib/demo-mode.ts +17 -17
- package/src/lib/idp-client-config.ts +559 -559
- package/src/lib/idp-fetch.ts +126 -126
- package/src/lib/internal-api.ts +171 -171
- package/src/lib/jwt-decode-client.ts +45 -45
- package/src/lib/rate-limit-service.ts +9 -9
- package/src/lib/redis.ts +26 -26
- package/src/lib/refresh-token-validator.ts +64 -64
- package/src/lib/secret-validation.ts +8 -8
- package/src/lib/session-store.ts +702 -695
- package/src/lib/session.ts +34 -34
- package/src/lib/startup-init.ts +251 -251
- package/src/lib/token-expiry.ts +40 -40
- package/src/lib/token-lifecycle.ts +533 -497
- package/src/lib/types/api-responses.ts +336 -336
- package/src/middleware/auth-decision.ts +43 -43
- package/src/models/DecodedAccessToken.ts +16 -16
- package/src/models/SessionModel.ts +282 -264
- package/src/pages/admin-roles/index.ts +9 -9
- package/src/pages/login/page.tsx +123 -228
- package/src/pages/security/EnhancedSecurityPage.tsx +9 -9
- package/src/pages/verify-code/page.tsx +71 -5
- package/src/routes/account/index.ts +31 -31
- package/src/routes/account/masked-info.ts +36 -36
- package/src/routes/account/send-code.ts +39 -39
- package/src/routes/account/verify-email.ts +40 -40
- package/src/routes/account/verify-sms.ts +40 -40
- package/src/routes/auth/index.ts +22 -22
- package/src/routes/auth/passkey-signin.ts +13 -0
- package/src/routes/auth/refresh.ts +48 -48
- package/src/routes/auth/session.ts +166 -166
- package/src/routes/auth/viability.ts +89 -0
- package/src/routes/index.ts +17 -17
- package/src/routes/mfa/backup-code-verify.ts +1 -0
- package/src/routes/mfa/backup-codes.ts +10 -0
- package/src/routes/mfa/methods.ts +22 -0
- package/src/routes/mfa/totp-confirm.ts +10 -0
- package/src/routes/mfa/totp-enroll.ts +10 -0
- package/src/routes/mfa/totp-verify.ts +14 -0
- package/src/routes/passkeys/list.ts +10 -0
- package/src/routes/passkeys/manage.ts +23 -0
- package/src/routes/passkeys/register-options.ts +10 -0
- package/src/routes/passkeys/register-verify.ts +10 -0
- package/src/routes/session/index.ts +7 -7
- package/src/routes/session/refresh-viability.ts +17 -17
- package/src/server/auth.ts +373 -373
- package/src/server/decode-session.ts +235 -202
- package/src/services/signalrActivityService.ts +258 -258
- package/src/theme/ThemeProvider.tsx +39 -39
- package/src/theme/default.ts +33 -33
- package/src/theme/index.ts +31 -31
- package/src/theme/types.ts +69 -69
- package/src/theme/useTheme.ts +57 -57
- package/src/theme/utils.ts +40 -40
- package/src/types/api.ts +12 -12
- package/src/types/auth.d.ts +23 -23
- package/src/types/auth.ts +22 -22
- package/src/types/logging.ts +11 -11
- package/src/types/recovery.ts +54 -54
- package/src/utils/api.ts +353 -353
- package/src/utils/circuitBreaker.ts +40 -40
- package/src/utils/error-message.ts +108 -108
- package/src/utils/layout/reservedSpace.ts +124 -124
package/dist/auth/better-auth.js
CHANGED
|
@@ -51,8 +51,11 @@ exports.configureBetterAuth = configureBetterAuth;
|
|
|
51
51
|
exports.getBetterAuthInstance = getBetterAuthInstance;
|
|
52
52
|
exports.getBetterAuthHandler = getBetterAuthHandler;
|
|
53
53
|
exports.exchangeOAuthForIdpTokens = exchangeOAuthForIdpTokens;
|
|
54
|
+
exports.storeIdpTokensInSession = storeIdpTokensInSession;
|
|
54
55
|
exports.createAuthGetHandler = createAuthGetHandler;
|
|
55
56
|
require("server-only");
|
|
57
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
58
|
+
const jose_1 = require("jose"); // better-auth's own JWT dependency
|
|
56
59
|
const better_auth_1 = require("better-auth");
|
|
57
60
|
const next_js_1 = require("better-auth/next-js");
|
|
58
61
|
const next_js_2 = require("better-auth/next-js");
|
|
@@ -64,6 +67,30 @@ const session_store_1 = require("../lib/session-store");
|
|
|
64
67
|
/**
|
|
65
68
|
* Build Better Auth social providers from IDP config.
|
|
66
69
|
*/
|
|
70
|
+
/**
|
|
71
|
+
* REQUEST-SCOPED carrier for the provider's raw id_token. Card 138635.
|
|
72
|
+
*
|
|
73
|
+
* WHY THIS EXISTS: /api/ExternalAuth/oauth-callback mints an IDP bearer for any
|
|
74
|
+
* ASSERTED email — we send {provider, email} and nothing verifiable. The fix is
|
|
75
|
+
* for the IDP to require a provider-signed id_token and verify it itself. This
|
|
76
|
+
* is the client half: capture the token Better Auth already receives, and
|
|
77
|
+
* forward it. Additive — the IDP ignores it until its gate flips.
|
|
78
|
+
*
|
|
79
|
+
* WHY AsyncLocalStorage AND NOT REDIS (BAPert's ruling: forward, do not persist):
|
|
80
|
+
* an id_token is credential material. Writing it to the session record would put
|
|
81
|
+
* it one allow-list edit away from leaking out of /api/internal/session-data,
|
|
82
|
+
* and it would outlive the request that needed it. ALS scopes it to the single
|
|
83
|
+
* request, and it is gone when the request ends — nothing to expire, nothing to
|
|
84
|
+
* scrub, nothing to leak.
|
|
85
|
+
*
|
|
86
|
+
* SAFE BY CONSTRUCTION: getUserInfo (which fills this) and the post-callback
|
|
87
|
+
* exchange (which reads it) both run inside createAuthGetHandler's own request.
|
|
88
|
+
* VERIFIED, not assumed: better-auth.ts:452 awaits ba.GET(request) — the call
|
|
89
|
+
* that invokes getUserInfo — and the exchange follows at :465 in the SAME
|
|
90
|
+
* function. Outside that scope the store is undefined and the body simply omits
|
|
91
|
+
* id_token, which is the correct additive behaviour.
|
|
92
|
+
*/
|
|
93
|
+
const idTokenScope = new node_async_hooks_1.AsyncLocalStorage();
|
|
67
94
|
function buildBetterAuthProviders(config) {
|
|
68
95
|
const providers = {};
|
|
69
96
|
for (const oauth of config.oauthProviders || []) {
|
|
@@ -93,6 +120,65 @@ function buildBetterAuthProviders(config) {
|
|
|
93
120
|
accessType: rawAccessType === 'online' ? 'online' : rawAccessType === 'offline' ? 'offline' : undefined,
|
|
94
121
|
hd: typeof rawHostedDomain === 'string' ? rawHostedDomain : undefined,
|
|
95
122
|
};
|
|
123
|
+
/**
|
|
124
|
+
* CAPTURE THE RAW id_token — card 138635, client half.
|
|
125
|
+
*
|
|
126
|
+
* getUserInfo is the ONLY provider hook that receives the token set.
|
|
127
|
+
* mapProfileToUser cannot do this: it is handed the DECODED claims, and a
|
|
128
|
+
* signed JWT cannot be reconstructed from them. We need the raw token
|
|
129
|
+
* because the IDP must verify the signature ITSELF — a client-decoded
|
|
130
|
+
* claim is just another assertable field, which is the defect restated.
|
|
131
|
+
*
|
|
132
|
+
* ⚠ THIS REPLACES THE DEFAULT google getUserInfo ENTIRELY. Upstream calls
|
|
133
|
+
* `if (options.getUserInfo) return options.getUserInfo(token)` FIRST and
|
|
134
|
+
* returns our result verbatim, so everything the default did is now ours to
|
|
135
|
+
* do — including the `hd` hosted-domain restriction.
|
|
136
|
+
*
|
|
137
|
+
* `hd` IS NOT HYPOTHETICAL: it is wired from additionalParams.hd four lines
|
|
138
|
+
* above, so a client can restrict sign-in to a Google Workspace domain.
|
|
139
|
+
* Silently dropping it would let ANY Google account into a client that had
|
|
140
|
+
* restricted itself — a security regression introduced by a security fix.
|
|
141
|
+
* (Measured on 93: zero clients set it today. That is a reason to be
|
|
142
|
+
* careful, not a reason to skip it — it is configurable and unused, which
|
|
143
|
+
* is exactly the state in which a regression goes unnoticed.)
|
|
144
|
+
*
|
|
145
|
+
* SO WE DO NOT OVERRIDE AT ALL WHEN `hd` IS SET. We take the hook over
|
|
146
|
+
* ONLY where upstream's check is PROVABLY VACUOUS — its own first line is
|
|
147
|
+
* `if (!configuredHostedDomain) return true`. With no hd configured the
|
|
148
|
+
* default would return true unconditionally, so omitting it is equivalent
|
|
149
|
+
* by the library's own definition rather than by our judgement.
|
|
150
|
+
*
|
|
151
|
+
* A client that DOES set hd keeps the stock provider, keeps its domain
|
|
152
|
+
* restriction, and simply does not contribute an id_token yet. Losing a
|
|
153
|
+
* capability is an acceptable cost; silently losing a security control is
|
|
154
|
+
* not. (better-auth does not re-export the predicate through its public
|
|
155
|
+
* types, and reimplementing a security check is how two copies drift — so
|
|
156
|
+
* the honest move is to not need it.)
|
|
157
|
+
*/
|
|
158
|
+
if (name === 'google' && !providers[name].hd) {
|
|
159
|
+
providers[name].getUserInfo = async (token) => {
|
|
160
|
+
if (!token?.idToken)
|
|
161
|
+
return null;
|
|
162
|
+
// The only new behaviour: hand the raw token to this request's scope.
|
|
163
|
+
const scope = idTokenScope.getStore();
|
|
164
|
+
if (scope)
|
|
165
|
+
scope.idToken = token.idToken;
|
|
166
|
+
// Mirrors the upstream default (google.mjs) verbatim, minus the hd
|
|
167
|
+
// branch that cannot fire here — so overriding the hook changes WHAT WE
|
|
168
|
+
// KEEP, never what we ACCEPT.
|
|
169
|
+
const claims = (0, jose_1.decodeJwt)(token.idToken);
|
|
170
|
+
return {
|
|
171
|
+
user: {
|
|
172
|
+
id: claims.sub,
|
|
173
|
+
name: claims.name,
|
|
174
|
+
email: claims.email,
|
|
175
|
+
image: claims.picture,
|
|
176
|
+
emailVerified: claims.email_verified,
|
|
177
|
+
},
|
|
178
|
+
data: claims,
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
}
|
|
96
182
|
}
|
|
97
183
|
return providers;
|
|
98
184
|
}
|
|
@@ -188,6 +274,9 @@ function createBetterAuthInstance(idpConfig, opts = {}) {
|
|
|
188
274
|
plugins: [
|
|
189
275
|
(0, next_js_1.nextCookies)(),
|
|
190
276
|
...(opts.magicLink ? [(0, magic_link_1.magicLink)(opts.magicLink)] : []),
|
|
277
|
+
// App-supplied plugins last: they may depend on the built-ins above, and
|
|
278
|
+
// ordering is BA's own concern once they are in the array.
|
|
279
|
+
...(opts.plugins ?? []),
|
|
191
280
|
],
|
|
192
281
|
});
|
|
193
282
|
}
|
|
@@ -306,6 +395,30 @@ async function exchangeOAuthForIdpTokens(sessionToken, provider = 'google') {
|
|
|
306
395
|
name,
|
|
307
396
|
image,
|
|
308
397
|
client_id: config.clientSlug || String(config.clientId),
|
|
398
|
+
/**
|
|
399
|
+
* Card 138635 — the RAW provider id_token, forwarded verbatim.
|
|
400
|
+
*
|
|
401
|
+
* Every other field in this body is an ASSERTION. The endpoint mints an
|
|
402
|
+
* IDP bearer from them today with no cryptographic check of any kind,
|
|
403
|
+
* which is why it is a complete authentication bypass and why it has
|
|
404
|
+
* been publicly reachable for 488 days.
|
|
405
|
+
*
|
|
406
|
+
* This field is the only one the IDP can VERIFY: signature against
|
|
407
|
+
* Google's JWKS, plus iss / exp / aud, and then read email_verified
|
|
408
|
+
* from the verified claims. Additive — the IDP ignores it until its
|
|
409
|
+
* gate flips, so this cannot break a login on its own.
|
|
410
|
+
*
|
|
411
|
+
* RAW, AND NOTHING DERIVED FROM IT (BAPert's ruling). We do NOT send a
|
|
412
|
+
* decoded email_verified. A client-computed verification flag is a body
|
|
413
|
+
* field with a better story: an attacker posts email_verified:true and
|
|
414
|
+
* the defect is rebuilt one layer up. The IDP verifies for itself or
|
|
415
|
+
* the fix is theatre.
|
|
416
|
+
*
|
|
417
|
+
* Absent when this runs outside the OAuth callback scope — the key is
|
|
418
|
+
* simply omitted rather than sent null, so "we never captured one" stays
|
|
419
|
+
* distinguishable from "the provider issued none".
|
|
420
|
+
*/
|
|
421
|
+
...(idTokenScope.getStore()?.idToken ? { id_token: idTokenScope.getStore().idToken } : {}),
|
|
309
422
|
}),
|
|
310
423
|
});
|
|
311
424
|
const oauthResText = await oauthRes.text();
|
|
@@ -355,30 +468,124 @@ async function exchangeOAuthForIdpTokens(sessionToken, provider = 'google') {
|
|
|
355
468
|
image: image,
|
|
356
469
|
roles: result.user?.roles || result.roles || [],
|
|
357
470
|
mfaVerified: false, // Always false post-OAuth — see IDP-2FA-BUG note above
|
|
471
|
+
// Cards 141688/141741 — RELAYED, NOT DERIVED.
|
|
472
|
+
//
|
|
473
|
+
// The IDP already decides this (ExternalAuthController:943 and :2024,
|
|
474
|
+
// WO 113023): "enroll_mfa" = no CONFIRMED method, "mfa_verify" = a
|
|
475
|
+
// confirmed method exists, null = fully authenticated. It was emitted
|
|
476
|
+
// here and dropped on this line — `idpTokenData` is built field-by-field,
|
|
477
|
+
// so anything not named is silently discarded. Both downstream consumers
|
|
478
|
+
// (the middleware gate and the verify-code page) then reasoned in its
|
|
479
|
+
// absence and sent zero-method users to a page they cannot act on.
|
|
480
|
+
//
|
|
481
|
+
// WIRE NAME IS SNAKE: `next_step`, per [JsonPropertyName("next_step")] on
|
|
482
|
+
// BOTH OAuthCallbackResponse and ExternalTokenResponse. Reading
|
|
483
|
+
// `result.nextStep` here would be `undefined` forever and fail silently —
|
|
484
|
+
// the same camel/snake break `requires_two_factor` already cost us once.
|
|
485
|
+
//
|
|
486
|
+
// Do NOT recompute this from a method query. The IDP owns the fact; it is
|
|
487
|
+
// already usable-aware (confirmed and not revoked, per DotNetPert), which
|
|
488
|
+
// is the distinction that decides correctness: a revoked-TOTP user has
|
|
489
|
+
// count>0 but usable=0, and deriving it locally would let through exactly
|
|
490
|
+
// the users we must challenge.
|
|
491
|
+
// No `?? null`: if the response has no `next_step` at all (older IDP
|
|
492
|
+
// build, or a path that does not set it), that is UNKNOWN, not "fully
|
|
493
|
+
// authenticated". Defaulting it to null here would manufacture a
|
|
494
|
+
// clean-bill-of-health the IDP never gave and fail open at the gate.
|
|
495
|
+
nextStep: result.next_step,
|
|
358
496
|
idpClientId: result.client_id ? String(result.client_id) : undefined,
|
|
359
497
|
merchantId: result.merchant_id ? String(result.merchant_id) : undefined,
|
|
360
498
|
};
|
|
361
|
-
//
|
|
499
|
+
// Storage is shared with the passkey sign-in path — see storeIdpTokensInSession.
|
|
500
|
+
const stored = await storeIdpTokensInSession(sessionToken, idpTokenData, provider);
|
|
501
|
+
if (stored)
|
|
502
|
+
console.log('[BETTER_AUTH] IDP tokens stored in session for', email);
|
|
503
|
+
return stored;
|
|
504
|
+
}
|
|
505
|
+
catch (err) {
|
|
506
|
+
console.error('[BETTER_AUTH] IDP token exchange failed:', err instanceof Error ? err.message : String(err));
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Write IDP tokens into an EXISTING Better Auth session. Storage only — this
|
|
512
|
+
* function never acquires tokens and never decides who the caller is.
|
|
513
|
+
*
|
|
514
|
+
* ── WHY THIS IS SPLIT OUT (BAPert ruling, 2026-08-04) ────────────────────────
|
|
515
|
+
*
|
|
516
|
+
* `exchangeOAuthForIdpTokens` above does TWO things: it ACQUIRES tokens by
|
|
517
|
+
* POSTing to `/api/ExternalAuth/oauth-callback`, and then it STORES them. Only
|
|
518
|
+
* the second half is reusable, and the first half is actively dangerous to
|
|
519
|
+
* reuse:
|
|
520
|
+
*
|
|
521
|
+
* oauth-callback mints an IDP bearer from an ASSERTED EMAIL with no
|
|
522
|
+
* cryptographic verification of any kind (card 138635).
|
|
523
|
+
*
|
|
524
|
+
* Passkey sign-in already holds something strictly better — a grant issued by
|
|
525
|
+
* `/api/ExternalAuth/passwordless/passkey/verify`, which checked a real
|
|
526
|
+
* signature against a registered credential. Routing that through
|
|
527
|
+
* oauth-callback to obtain a session would DOWNGRADE a cryptographically
|
|
528
|
+
* verified assertion into an unverified one, make passkey sign-in weaker than
|
|
529
|
+
* its own ceremony, and inherit 138635 wholesale.
|
|
530
|
+
*
|
|
531
|
+
* That is not hypothetical: "do what the OAuth path does" is the obvious
|
|
532
|
+
* implementation and it is the wrong one. Splitting acquisition from storage is
|
|
533
|
+
* what makes the wrong one hard to write by accident.
|
|
534
|
+
*
|
|
535
|
+
* ── WHAT CALLERS MUST HAVE DONE FIRST ───────────────────────────────────────
|
|
536
|
+
*
|
|
537
|
+
* A Better Auth session must ALREADY EXIST for `sessionToken`. This function
|
|
538
|
+
* enriches; it does not create. Better Auth owns session creation and cookie
|
|
539
|
+
* signing, and nothing in this package should replicate either — the cookie is
|
|
540
|
+
* `{token}.{base64(hmac)}` with standard padding, signed with the runtime
|
|
541
|
+
* `authSecret` from Redis, and hand-rolling that produces a session which
|
|
542
|
+
* decodes but does not authenticate.
|
|
543
|
+
*
|
|
544
|
+
* @param sessionToken the Better Auth session token (from its Set-Cookie)
|
|
545
|
+
* @param idpTokenData tokens + identity, already obtained by the CALLER'S OWN
|
|
546
|
+
* verified route. Built field-by-field by the caller so an
|
|
547
|
+
* unnamed field cannot ride along silently.
|
|
548
|
+
* @param authProvider provenance label recorded on the canonical row. Written
|
|
549
|
+
* to the historical `oauthProvider` key, which predates
|
|
550
|
+
* non-OAuth callers; the NAME is legacy, the VALUE should
|
|
551
|
+
* state what actually authenticated ('passkey', 'google').
|
|
552
|
+
*/
|
|
553
|
+
async function storeIdpTokensInSession(sessionToken, idpTokenData, authProvider) {
|
|
554
|
+
try {
|
|
555
|
+
const config = await (0, idp_client_config_1.getIDPClientConfig)();
|
|
556
|
+
const appSlug = config.clientSlug || (0, app_slug_1.getAppSlug)();
|
|
557
|
+
const baKey = `ba:${appSlug}:${sessionToken}`;
|
|
558
|
+
const baRaw = await (0, redis_1.getRedis)().get(baKey).catch(() => null);
|
|
559
|
+
if (!baRaw) {
|
|
560
|
+
// No session to enrich. Do NOT create one as a fallback: a session
|
|
561
|
+
// conjured here would carry no Better Auth cookie, so it would be
|
|
562
|
+
// unreachable by the browser and invisible to every other consumer —
|
|
563
|
+
// failing as "silently signed out" rather than as an error.
|
|
564
|
+
console.warn('[BETTER_AUTH] storeIdpTokensInSession: no BA session for token', sessionToken.substring(0, 10));
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
const baData = JSON.parse(baRaw);
|
|
362
568
|
baData.idpTokens = idpTokenData;
|
|
363
569
|
await (0, redis_1.getRedis)().setex(baKey, session_store_1.SESSION_TTL, JSON.stringify(baData));
|
|
364
|
-
//
|
|
365
|
-
//
|
|
570
|
+
// Canonical store — the key getSession() reads, used by the refresh handler
|
|
571
|
+
// and token lifecycle. A failure here leaves the BA session correct and the
|
|
572
|
+
// canonical row missing, so it is warned and not fatal: the user is signed
|
|
573
|
+
// in, and refresh degrades rather than the sign-in failing outright.
|
|
366
574
|
try {
|
|
367
575
|
const { getSessionPrefix } = await Promise.resolve().then(() => __importStar(require('../lib/app-slug')));
|
|
368
576
|
const canonicalKey = `${getSessionPrefix()}${sessionToken}`;
|
|
369
577
|
await (0, redis_1.getRedis)().setex(canonicalKey, session_store_1.SESSION_TTL, JSON.stringify({
|
|
370
578
|
...idpTokenData,
|
|
371
|
-
oauthProvider:
|
|
579
|
+
oauthProvider: authProvider,
|
|
372
580
|
}));
|
|
373
581
|
}
|
|
374
582
|
catch (canonicalErr) {
|
|
375
583
|
console.warn('[BETTER_AUTH] Failed to write canonical session:', canonicalErr instanceof Error ? canonicalErr.message : String(canonicalErr));
|
|
376
584
|
}
|
|
377
|
-
console.log('[BETTER_AUTH] IDP tokens stored in session for', email);
|
|
378
585
|
return true;
|
|
379
586
|
}
|
|
380
587
|
catch (err) {
|
|
381
|
-
console.error('[BETTER_AUTH]
|
|
588
|
+
console.error('[BETTER_AUTH] storeIdpTokensInSession failed:', err instanceof Error ? err.message : String(err));
|
|
382
589
|
return false;
|
|
383
590
|
}
|
|
384
591
|
}
|
|
@@ -401,45 +608,52 @@ async function exchangeOAuthForIdpTokens(sessionToken, provider = 'google') {
|
|
|
401
608
|
*/
|
|
402
609
|
function createAuthGetHandler(loginPath = '/account-auth/login') {
|
|
403
610
|
return async function GET(request) {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
console.warn('[BETTER_AUTH] OAuth state error, redirecting to login');
|
|
414
|
-
return Response.redirect(new URL(loginPath, request.url), 302);
|
|
611
|
+
// Card 138635: establish the request-scoped carrier for the provider's raw
|
|
612
|
+
// id_token. It must wrap BOTH ba.GET (which invokes getUserInfo, the only
|
|
613
|
+
// hook that sees the token) and the exchange below (which forwards it).
|
|
614
|
+
// Wrapping the whole handler is what makes "never persisted" achievable —
|
|
615
|
+
// the token exists only for the life of this request.
|
|
616
|
+
return idTokenScope.run({}, async () => {
|
|
617
|
+
const ba = await getBetterAuthHandler();
|
|
618
|
+
if (!ba) {
|
|
619
|
+
return new Response('Auth handler not configured', { status: 500 });
|
|
415
620
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
const setCookies = response.headers.getSetCookie?.() || [];
|
|
424
|
-
const cookieHeader = setCookies
|
|
425
|
-
.map((c) => c.split(';')[0])
|
|
426
|
-
.join('; ');
|
|
427
|
-
const headers = new Headers();
|
|
428
|
-
headers.set('cookie', cookieHeader);
|
|
429
|
-
const session = await auth.api.getSession({ headers });
|
|
430
|
-
if (session?.session?.token) {
|
|
431
|
-
console.log('[BETTER_AUTH] Got session token from callback:', session.session.token.substring(0, 10), '| email:', session.user?.email);
|
|
432
|
-
await exchangeOAuthForIdpTokens(session.session.token);
|
|
433
|
-
}
|
|
434
|
-
else {
|
|
435
|
-
console.warn('[BETTER_AUTH] Could not get session after OAuth callback');
|
|
436
|
-
}
|
|
621
|
+
const response = await ba.GET(request);
|
|
622
|
+
// Intercept auth errors (state mismatch, expired cookies) — redirect to login cleanly
|
|
623
|
+
if (response.status === 302) {
|
|
624
|
+
const location = response.headers.get('location') || '';
|
|
625
|
+
if (location.includes('/api/auth/error') || location.includes('please_restart')) {
|
|
626
|
+
console.warn('[BETTER_AUTH] OAuth state error, redirecting to login');
|
|
627
|
+
return Response.redirect(new URL(loginPath, request.url), 302);
|
|
437
628
|
}
|
|
438
629
|
}
|
|
439
|
-
|
|
440
|
-
|
|
630
|
+
// After successful OAuth callback: exchange Google identity for IDP tokens
|
|
631
|
+
const url = new URL(request.url);
|
|
632
|
+
if (url.pathname.includes('/callback/') && response.status === 302) {
|
|
633
|
+
try {
|
|
634
|
+
const auth = await getBetterAuthInstance();
|
|
635
|
+
if (auth?.api?.getSession) {
|
|
636
|
+
const setCookies = response.headers.getSetCookie?.() || [];
|
|
637
|
+
const cookieHeader = setCookies
|
|
638
|
+
.map((c) => c.split(';')[0])
|
|
639
|
+
.join('; ');
|
|
640
|
+
const headers = new Headers();
|
|
641
|
+
headers.set('cookie', cookieHeader);
|
|
642
|
+
const session = await auth.api.getSession({ headers });
|
|
643
|
+
if (session?.session?.token) {
|
|
644
|
+
console.log('[BETTER_AUTH] Got session token from callback:', session.session.token.substring(0, 10), '| email:', session.user?.email);
|
|
645
|
+
await exchangeOAuthForIdpTokens(session.session.token);
|
|
646
|
+
}
|
|
647
|
+
else {
|
|
648
|
+
console.warn('[BETTER_AUTH] Could not get session after OAuth callback');
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
catch (err) {
|
|
653
|
+
console.error('[BETTER_AUTH] IDP token exchange failed:', err.message);
|
|
654
|
+
}
|
|
441
655
|
}
|
|
442
|
-
|
|
443
|
-
|
|
656
|
+
return response;
|
|
657
|
+
});
|
|
444
658
|
};
|
|
445
659
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* @version 1.0.0
|
|
12
12
|
* @since auth-refactor-2026-01
|
|
13
13
|
*/
|
|
14
|
-
import type { IdpLoginResponse,
|
|
14
|
+
import type { IdpLoginResponse, IdpRefreshResponse, LoginCredentials } from '../types/auth-types';
|
|
15
15
|
/**
|
|
16
16
|
* Get IDP base URL. Throws if not configured.
|
|
17
17
|
*/
|
|
@@ -32,30 +32,21 @@ export declare function idpLogin(credentials: LoginCredentials, clientHeaders?:
|
|
|
32
32
|
userAgent?: string;
|
|
33
33
|
}): Promise<IdpLoginResponse>;
|
|
34
34
|
/**
|
|
35
|
-
*
|
|
35
|
+
* idpOAuthCallback REMOVED 2026-08-05 (NextPert, BAPert 20168 / QAPert 20166).
|
|
36
36
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
37
|
+
* It POSTed to /api/ExternalAuth/oauth-callback with provider, email, name,
|
|
38
|
+
* tokens and client_id — ALL ASSERTIONS, no id_token, nothing verifiable. After
|
|
39
|
+
* the 170003 fix (13436d56e) that call can only ever return 401.
|
|
39
40
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
provider: string;
|
|
45
|
-
providerAccountId: string;
|
|
46
|
-
email: string;
|
|
47
|
-
name?: string;
|
|
48
|
-
image?: string;
|
|
49
|
-
accessToken?: string;
|
|
50
|
-
refreshToken?: string;
|
|
51
|
-
expiresAt?: number;
|
|
52
|
-
}): Promise<IdpOAuthCallbackResponse>;
|
|
53
|
-
/**
|
|
54
|
-
* Refresh an expired access token using the refresh token.
|
|
41
|
+
* It was a SECOND caller of the same endpoint, and only the first
|
|
42
|
+
* (better-auth.ts, card 138635) was hardened — the same duplicated-integration
|
|
43
|
+
* trap the viability code documents ("adding the relay to just one would leave
|
|
44
|
+
* an app that mounts THIS one still blind").
|
|
55
45
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
46
|
+
* It had no call sites and was not reachable through the package exports map
|
|
47
|
+
* (no ./auth/utils key, no wildcard), so removing it breaks nothing. Deleted
|
|
48
|
+
* rather than fixed, because an unreachable function whose only outcome is a
|
|
49
|
+
* 401 that LOOKS like a security success is a trap for whoever finds it next.
|
|
59
50
|
*/
|
|
60
51
|
export declare function idpRefreshToken(refreshToken: string, mfaContext?: {
|
|
61
52
|
amr?: string[];
|
|
@@ -16,7 +16,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
16
16
|
exports.getIdpUrl = getIdpUrl;
|
|
17
17
|
exports.getClientId = getClientId;
|
|
18
18
|
exports.idpLogin = idpLogin;
|
|
19
|
-
exports.idpOAuthCallback = idpOAuthCallback;
|
|
20
19
|
exports.idpRefreshToken = idpRefreshToken;
|
|
21
20
|
exports.idpVerify2FA = idpVerify2FA;
|
|
22
21
|
exports.idpSend2FACode = idpSend2FACode;
|
|
@@ -133,90 +132,21 @@ function getLoginErrorMessage(status, responseData) {
|
|
|
133
132
|
// OAUTH CALLBACK
|
|
134
133
|
// ============================================================================
|
|
135
134
|
/**
|
|
136
|
-
*
|
|
135
|
+
* idpOAuthCallback REMOVED 2026-08-05 (NextPert, BAPert 20168 / QAPert 20166).
|
|
137
136
|
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
137
|
+
* It POSTed to /api/ExternalAuth/oauth-callback with provider, email, name,
|
|
138
|
+
* tokens and client_id — ALL ASSERTIONS, no id_token, nothing verifiable. After
|
|
139
|
+
* the 170003 fix (13436d56e) that call can only ever return 401.
|
|
140
140
|
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const idpUrl = getIdpUrl();
|
|
146
|
-
const clientId = getClientId();
|
|
147
|
-
try {
|
|
148
|
-
const response = await fetch(`${idpUrl}/api/ExternalAuth/oauth-callback`, {
|
|
149
|
-
method: 'POST',
|
|
150
|
-
headers: {
|
|
151
|
-
'Content-Type': 'application/json',
|
|
152
|
-
'X-Client-Id': clientId,
|
|
153
|
-
},
|
|
154
|
-
body: JSON.stringify({
|
|
155
|
-
provider: oauthData.provider,
|
|
156
|
-
provider_account_id: oauthData.providerAccountId,
|
|
157
|
-
email: oauthData.email,
|
|
158
|
-
name: oauthData.name || '',
|
|
159
|
-
image: oauthData.image || '',
|
|
160
|
-
access_token: oauthData.accessToken || '',
|
|
161
|
-
refresh_token: oauthData.refreshToken || '',
|
|
162
|
-
expires_at: oauthData.expiresAt || 0,
|
|
163
|
-
client_id: clientId,
|
|
164
|
-
}),
|
|
165
|
-
});
|
|
166
|
-
if (!response.ok) {
|
|
167
|
-
const errorText = await response.text().catch(() => 'Unknown error');
|
|
168
|
-
console.error('[IDP_CLIENT] OAuth callback failed:', response.status, errorText);
|
|
169
|
-
return {
|
|
170
|
-
success: false,
|
|
171
|
-
error: {
|
|
172
|
-
code: `HTTP_${response.status}`,
|
|
173
|
-
message: 'OAuth registration failed',
|
|
174
|
-
},
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
const data = await response.json();
|
|
178
|
-
const responseData = data.data || data;
|
|
179
|
-
// Normalize snake_case to camelCase
|
|
180
|
-
return {
|
|
181
|
-
success: responseData.success !== false,
|
|
182
|
-
data: responseData.success !== false
|
|
183
|
-
? {
|
|
184
|
-
accessToken: responseData.accessToken || responseData.access_token,
|
|
185
|
-
refreshToken: responseData.refreshToken || responseData.refresh_token,
|
|
186
|
-
isNewUser: responseData.isNewUser ?? responseData.is_new_user ?? false,
|
|
187
|
-
user: responseData.user
|
|
188
|
-
? {
|
|
189
|
-
userId: responseData.user.userId || responseData.user.user_id,
|
|
190
|
-
email: responseData.user.email || responseData.user.Email,
|
|
191
|
-
fullName: responseData.user.fullName || responseData.user.full_name || responseData.user.name,
|
|
192
|
-
roles: responseData.user.roles || [],
|
|
193
|
-
}
|
|
194
|
-
: undefined,
|
|
195
|
-
}
|
|
196
|
-
: undefined,
|
|
197
|
-
error: responseData.error,
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
catch (error) {
|
|
201
|
-
console.error('[IDP_CLIENT] OAuth callback request failed:', error);
|
|
202
|
-
return {
|
|
203
|
-
success: false,
|
|
204
|
-
error: {
|
|
205
|
-
code: 'NETWORK_ERROR',
|
|
206
|
-
message: 'Failed to connect to authentication service',
|
|
207
|
-
},
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
// ============================================================================
|
|
212
|
-
// TOKEN REFRESH
|
|
213
|
-
// ============================================================================
|
|
214
|
-
/**
|
|
215
|
-
* Refresh an expired access token using the refresh token.
|
|
141
|
+
* It was a SECOND caller of the same endpoint, and only the first
|
|
142
|
+
* (better-auth.ts, card 138635) was hardened — the same duplicated-integration
|
|
143
|
+
* trap the viability code documents ("adding the relay to just one would leave
|
|
144
|
+
* an app that mounts THIS one still blind").
|
|
216
145
|
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
146
|
+
* It had no call sites and was not reachable through the package exports map
|
|
147
|
+
* (no ./auth/utils key, no wildcard), so removing it breaks nothing. Deleted
|
|
148
|
+
* rather than fixed, because an unreachable function whose only outcome is a
|
|
149
|
+
* 401 that LOOKS like a security success is a trap for whoever finds it next.
|
|
220
150
|
*/
|
|
221
151
|
async function idpRefreshToken(refreshToken, mfaContext) {
|
|
222
152
|
const idpUrl = getIdpUrl();
|
|
@@ -814,7 +814,13 @@ export declare const authClient: {
|
|
|
814
814
|
isPending: boolean;
|
|
815
815
|
isRefetching: boolean;
|
|
816
816
|
error: import("@better-fetch/fetch").BetterFetchError | null;
|
|
817
|
-
refetch: (queryParams
|
|
817
|
+
refetch: (queryParams
|
|
818
|
+
/**
|
|
819
|
+
* NextAuth-compatible signOut wrapper.
|
|
820
|
+
*
|
|
821
|
+
* Maps NextAuth signOut({ redirect, callbackUrl }) to Better Auth.
|
|
822
|
+
*/
|
|
823
|
+
?: {
|
|
818
824
|
query?: import("better-auth/types").SessionQueryParams;
|
|
819
825
|
} | undefined) => Promise<void>;
|
|
820
826
|
};
|
|
@@ -988,7 +994,13 @@ export declare const useSession: () => {
|
|
|
988
994
|
isPending: boolean;
|
|
989
995
|
isRefetching: boolean;
|
|
990
996
|
error: import("@better-fetch/fetch").BetterFetchError | null;
|
|
991
|
-
refetch: (queryParams
|
|
997
|
+
refetch: (queryParams
|
|
998
|
+
/**
|
|
999
|
+
* NextAuth-compatible signOut wrapper.
|
|
1000
|
+
*
|
|
1001
|
+
* Maps NextAuth signOut({ redirect, callbackUrl }) to Better Auth.
|
|
1002
|
+
*/
|
|
1003
|
+
?: {
|
|
992
1004
|
query?: import("better-auth/types").SessionQueryParams;
|
|
993
1005
|
} | undefined) => Promise<void>;
|
|
994
1006
|
}, signIn: {
|
|
@@ -135,6 +135,13 @@ async function getBetterAuthSession(sessionToken, appSlug) {
|
|
|
135
135
|
idpRefreshToken: data.idpTokens?.idpRefreshToken,
|
|
136
136
|
idpAccessTokenExpires: data.idpTokens?.idpAccessTokenExpires,
|
|
137
137
|
mfaVerified: data.idpTokens?.mfaVerified ?? false,
|
|
138
|
+
// Cards 141688/141741 — the IDP's routing decision, carried not derived.
|
|
139
|
+
// `null` is a real value here (IDP said nothing is outstanding), so it
|
|
140
|
+
// must stay distinguishable from `undefined` (we never captured it —
|
|
141
|
+
// true of every session minted before this change). No `?? null`:
|
|
142
|
+
// defaulting would turn "unknown" into "fully authenticated" and
|
|
143
|
+
// fail open on the existing session population.
|
|
144
|
+
nextStep: data.idpTokens?.nextStep,
|
|
138
145
|
roles: data.idpTokens?.roles || [],
|
|
139
146
|
idpClientId: data.idpTokens?.idpClientId ?? data.idpTokens?.clientId ?? data.idpClientId,
|
|
140
147
|
merchantId: data.idpTokens?.merchantId ?? data.merchantId,
|
|
@@ -489,27 +496,27 @@ async function releaseRefreshLock(sessionToken, requestId, lockVersion) {
|
|
|
489
496
|
const lockKey = getRefreshLockKey(sessionToken);
|
|
490
497
|
try {
|
|
491
498
|
// Lua script for atomic lock validation and release
|
|
492
|
-
const luaScript = `
|
|
493
|
-
local lockKey = KEYS[1]
|
|
494
|
-
local expectedRequestId = ARGV[1]
|
|
495
|
-
local expectedVersion = ARGV[2]
|
|
496
|
-
|
|
497
|
-
local lockData = redis.call('GET', lockKey)
|
|
498
|
-
if not lockData then
|
|
499
|
-
return 0 -- Lock doesn't exist
|
|
500
|
-
end
|
|
501
|
-
|
|
502
|
-
local lockInfo = cjson.decode(lockData)
|
|
503
|
-
if lockInfo.acquiredBy == expectedRequestId then
|
|
504
|
-
if not expectedVersion or expectedVersion == '' or tostring(lockInfo.lockVersion) == expectedVersion then
|
|
505
|
-
redis.call('DEL', lockKey)
|
|
506
|
-
return 1 -- Successfully released
|
|
507
|
-
else
|
|
508
|
-
return -2 -- Version mismatch
|
|
509
|
-
end
|
|
510
|
-
else
|
|
511
|
-
return -1 -- Wrong owner
|
|
512
|
-
end
|
|
499
|
+
const luaScript = `
|
|
500
|
+
local lockKey = KEYS[1]
|
|
501
|
+
local expectedRequestId = ARGV[1]
|
|
502
|
+
local expectedVersion = ARGV[2]
|
|
503
|
+
|
|
504
|
+
local lockData = redis.call('GET', lockKey)
|
|
505
|
+
if not lockData then
|
|
506
|
+
return 0 -- Lock doesn't exist
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
local lockInfo = cjson.decode(lockData)
|
|
510
|
+
if lockInfo.acquiredBy == expectedRequestId then
|
|
511
|
+
if not expectedVersion or expectedVersion == '' or tostring(lockInfo.lockVersion) == expectedVersion then
|
|
512
|
+
redis.call('DEL', lockKey)
|
|
513
|
+
return 1 -- Successfully released
|
|
514
|
+
else
|
|
515
|
+
return -2 -- Version mismatch
|
|
516
|
+
end
|
|
517
|
+
else
|
|
518
|
+
return -1 -- Wrong owner
|
|
519
|
+
end
|
|
513
520
|
`;
|
|
514
521
|
const result = await redis_1.default.eval(luaScript, 1, lockKey, requestId, lockVersion ? lockVersion.toString() : '');
|
|
515
522
|
if (result === 1) {
|