@payez/next-mvp 4.12.0 → 4.13.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/CHANGELOG.md +51 -0
- package/dist/api-handlers/auth/credential-login.js +28 -4
- package/dist/api-handlers/mfa/totp-confirm.js +8 -4
- package/dist/api-handlers/mfa/totp-enroll.js +8 -4
- package/dist/api-handlers/session/viability.js +6 -2
- package/dist/auth/better-auth.d.ts +6 -0
- package/dist/auth/better-auth.js +29 -2
- package/dist/auth/session-reset-link.d.ts +32 -0
- package/dist/auth/session-reset-link.js +45 -1
- package/dist/components/auth/CredentialSignInForm.d.ts +7 -1
- package/dist/components/auth/CredentialSignInForm.js +13 -4
- package/dist/components/auth/EnrollAuthenticatorOffer.d.ts +21 -0
- package/dist/components/auth/EnrollAuthenticatorOffer.js +23 -0
- package/dist/components/mfa/QrCode.d.ts +6 -0
- package/dist/components/mfa/QrCode.js +14 -0
- package/dist/components/mfa/TotpEnrollPanel.d.ts +6 -0
- package/dist/components/mfa/TotpEnrollPanel.js +117 -0
- package/dist/edge/middleware.d.ts +1 -1
- package/dist/edge/middleware.js +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -1
- package/dist/lib/credential-sign-in-client.d.ts +26 -1
- package/dist/lib/credential-sign-in-client.js +32 -6
- package/dist/lib/ensure-fresh-access-token.js +15 -10
- package/dist/lib/idp-client-config.js +4 -1
- package/dist/lib/login-error-message.d.ts +3 -0
- package/dist/lib/login-error-message.js +12 -1
- package/dist/lib/mfa-json-guard.d.ts +12 -0
- package/dist/lib/mfa-json-guard.js +19 -0
- package/dist/lib/nextauth-secret.js +3 -1
- package/dist/lib/payez-client-secret.d.ts +14 -0
- package/dist/lib/payez-client-secret.js +41 -0
- package/dist/lib/qrcode.d.ts +22 -0
- package/dist/lib/qrcode.js +362 -0
- package/dist/lib/sign-in-failure.d.ts +35 -0
- package/dist/lib/sign-in-failure.js +85 -0
- package/dist/lib/twofa-rejection.d.ts +2 -0
- package/dist/lib/twofa-rejection.js +5 -2
- package/dist/middleware/create-middleware.d.ts +24 -0
- package/dist/middleware/create-middleware.js +56 -9
- package/dist/middleware/prefetch-guard-status.d.ts +21 -0
- package/dist/middleware/prefetch-guard-status.js +42 -0
- package/dist/pages/enroll/page.d.ts +7 -0
- package/dist/pages/enroll/page.js +15 -0
- package/dist/pages/login/page.js +1 -1
- package/dist/routes/auth/session-reset.d.ts +11 -1
- package/dist/routes/auth/session-reset.js +38 -13
- package/dist/routes/auth/viability.js +11 -2
- package/package.json +11 -1
- package/src/api-handlers/auth/credential-login.ts +29 -4
- package/src/api-handlers/mfa/totp-confirm.ts +7 -4
- package/src/api-handlers/mfa/totp-enroll.ts +7 -4
- package/src/api-handlers/session/viability.ts +6 -2
- package/src/auth/better-auth.ts +27 -2
- package/src/auth/session-reset-link.ts +47 -0
- package/src/components/auth/CredentialSignInForm.tsx +21 -3
- package/src/components/auth/EnrollAuthenticatorOffer.tsx +48 -0
- package/src/components/mfa/QrCode.tsx +24 -0
- package/src/components/mfa/TotpEnrollPanel.tsx +203 -0
- package/src/edge/middleware.ts +1 -1
- package/src/index.ts +1 -1
- package/src/lib/credential-sign-in-client.ts +48 -8
- package/src/lib/ensure-fresh-access-token.ts +15 -10
- package/src/lib/idp-client-config.ts +5 -1
- package/src/lib/login-error-message.ts +13 -1
- package/src/lib/mfa-json-guard.ts +19 -0
- package/src/lib/nextauth-secret.ts +3 -1
- package/src/lib/payez-client-secret.ts +39 -0
- package/src/lib/qrcode.ts +348 -0
- package/src/lib/sign-in-failure.ts +88 -0
- package/src/lib/twofa-rejection.ts +7 -2
- package/src/middleware/create-middleware.ts +69 -9
- package/src/middleware/prefetch-guard-status.ts +56 -0
- package/src/pages/enroll/page.tsx +22 -0
- package/src/pages/login/page.tsx +2 -1
- package/src/routes/auth/session-reset.ts +46 -11
- package/src/routes/auth/viability.ts +10 -2
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* qrcode.ts — minimal QR Code generator (byte mode, ECC level M), a
|
|
4
|
+
* TypeScript port of Nayuki's qrcodegen (MIT) trimmed to what TOTP
|
|
5
|
+
* otpauth:// URIs need. Renders to an SVG path string (no library, no
|
|
6
|
+
* canvas) — D3 MFA page generates the QR client-side per contract
|
|
7
|
+
* (fe-be-contract §D3.1: "no BE QR image needed").
|
|
8
|
+
*
|
|
9
|
+
* Cross-checked bit-exact against python `qrcode` (see D3 day-end report).
|
|
10
|
+
* PAY-2631: copied verbatim from VibeID-Customer-Portal src/lib/qrcode.ts so the generated app draws the QR itself:
|
|
11
|
+
* the TOTP secret is never sent to a third-party QR service.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.debugFinalBytes = debugFinalBytes;
|
|
15
|
+
exports.encodeQr = encodeQr;
|
|
16
|
+
exports.qrToSvgPath = qrToSvgPath;
|
|
17
|
+
// Galois field tables for Reed–Solomon
|
|
18
|
+
const EXP = new Uint8Array(512);
|
|
19
|
+
const LOG = new Uint8Array(256);
|
|
20
|
+
(() => {
|
|
21
|
+
let x = 1;
|
|
22
|
+
for (let i = 0; i < 255; i++) {
|
|
23
|
+
EXP[i] = x;
|
|
24
|
+
LOG[x] = i;
|
|
25
|
+
x <<= 1;
|
|
26
|
+
if (x & 0x100)
|
|
27
|
+
x ^= 0x11d;
|
|
28
|
+
}
|
|
29
|
+
for (let i = 255; i < 512; i++)
|
|
30
|
+
EXP[i] = EXP[i - 255];
|
|
31
|
+
})();
|
|
32
|
+
function gfMul(a, b) {
|
|
33
|
+
return a === 0 || b === 0 ? 0 : EXP[LOG[a] + LOG[b]];
|
|
34
|
+
}
|
|
35
|
+
// ECC codewords per block + block count, level M, versions 1–40
|
|
36
|
+
// [eccPerBlock, numBlocks] indexed by version
|
|
37
|
+
const M_TABLE = [
|
|
38
|
+
[0, 0],
|
|
39
|
+
[10, 1], [16, 1], [26, 1], [18, 2], [24, 2], [16, 4], [18, 4], [22, 4], [22, 5], [26, 5], // 1-10
|
|
40
|
+
[26, 6], [24, 6], [28, 8], [28, 7], [24, 10], [26, 10], [26, 11], [28, 12], [28, 11], [30, 12], // 11-20
|
|
41
|
+
];
|
|
42
|
+
function rawDataModules(version) {
|
|
43
|
+
// Nayuki qrcodegen getNumRawDataModules — exact.
|
|
44
|
+
let result = (16 * version + 128) * version + 64;
|
|
45
|
+
if (version >= 2) {
|
|
46
|
+
const numAlign = Math.floor(version / 7) + 2;
|
|
47
|
+
result -= (25 * numAlign - 10) * numAlign - 55;
|
|
48
|
+
if (version >= 7)
|
|
49
|
+
result -= 36;
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
function dataCodewords(version, eccPerBlock, numBlocks) {
|
|
54
|
+
return Math.floor(rawDataModules(version) / 8) - eccPerBlock * numBlocks;
|
|
55
|
+
}
|
|
56
|
+
function appendBits(buf, val, len) {
|
|
57
|
+
for (let i = len - 1; i >= 0; i--)
|
|
58
|
+
buf.bits.push((val >>> i) & 1);
|
|
59
|
+
}
|
|
60
|
+
function buildSegments(text, buf) {
|
|
61
|
+
// byte mode: 0100, count(8 for v1-9 / 16 for v10+), data
|
|
62
|
+
const bytes = new TextEncoder().encode(text);
|
|
63
|
+
appendBits(buf, 0x4, 4);
|
|
64
|
+
return bytes;
|
|
65
|
+
}
|
|
66
|
+
function reedSolomon(data, degree) {
|
|
67
|
+
// generator polynomial
|
|
68
|
+
const gen = new Uint8Array(degree);
|
|
69
|
+
gen[degree - 1] = 1;
|
|
70
|
+
let root = 1;
|
|
71
|
+
for (let i = 0; i < degree; i++) {
|
|
72
|
+
for (let j = 0; j < degree; j++) {
|
|
73
|
+
gen[j] = gfMul(gen[j], root);
|
|
74
|
+
if (j + 1 < degree)
|
|
75
|
+
gen[j] ^= gen[j + 1];
|
|
76
|
+
}
|
|
77
|
+
root = gfMul(root, 0x02);
|
|
78
|
+
}
|
|
79
|
+
const res = new Uint8Array(degree);
|
|
80
|
+
for (const b of data) {
|
|
81
|
+
const factor = b ^ res[0];
|
|
82
|
+
res.copyWithin(0, 1);
|
|
83
|
+
res[degree - 1] = 0;
|
|
84
|
+
for (let i = 0; i < degree; i++)
|
|
85
|
+
res[i] ^= gfMul(gen[i], factor);
|
|
86
|
+
}
|
|
87
|
+
return res;
|
|
88
|
+
}
|
|
89
|
+
const MASKS = [
|
|
90
|
+
(r, c) => (r + c) % 2 === 0,
|
|
91
|
+
(r) => r % 2 === 0,
|
|
92
|
+
(r, c) => c % 3 === 0,
|
|
93
|
+
(r, c) => (r + c) % 3 === 0,
|
|
94
|
+
(r, c) => (Math.floor(r / 2) + Math.floor(c / 3)) % 2 === 0,
|
|
95
|
+
(r, c) => ((r * c) % 2) + ((r * c) % 3) === 0,
|
|
96
|
+
(r, c) => (((r * c) % 2) + ((r * c) % 3)) % 2 === 0,
|
|
97
|
+
(r, c) => (((r + c) % 2) + ((r * c) % 3)) % 2 === 0,
|
|
98
|
+
];
|
|
99
|
+
const FORMAT_ECC_M = 0; // format bits use (ecc,mask); M=00
|
|
100
|
+
const FORMAT_LOOKUP = (() => {
|
|
101
|
+
// Precomputed BCH(15,5) format strings for ecc=M(00), mask 0-7
|
|
102
|
+
const table = [];
|
|
103
|
+
for (let mask = 0; mask < 8; mask++) {
|
|
104
|
+
const data = (0 << 3) | mask;
|
|
105
|
+
let rem = data;
|
|
106
|
+
for (let i = 0; i < 10; i++)
|
|
107
|
+
rem = (rem << 1) ^ ((rem >>> 9) * 0x537);
|
|
108
|
+
table.push(((data << 10) | rem) ^ 0x5412);
|
|
109
|
+
}
|
|
110
|
+
return table;
|
|
111
|
+
})();
|
|
112
|
+
/** Full pre-matrix pipeline: version pick → bitstream → ECC → interleave.
|
|
113
|
+
* Exported for the python cross-check harness (debugFinalBytes). */
|
|
114
|
+
function buildFinalData(text) {
|
|
115
|
+
const bytes = new TextEncoder().encode(text);
|
|
116
|
+
// pick smallest version (1-10 supported) that fits: capacity M byte mode
|
|
117
|
+
let version = 0;
|
|
118
|
+
let dataCw = 0;
|
|
119
|
+
let eccPerBlock = 0;
|
|
120
|
+
let numBlocks = 0;
|
|
121
|
+
for (let v = 1; v <= 10; v++) {
|
|
122
|
+
[eccPerBlock, numBlocks] = M_TABLE[v];
|
|
123
|
+
dataCw = dataCodewords(v, eccPerBlock, numBlocks);
|
|
124
|
+
const countBits = v <= 9 ? 8 : 16;
|
|
125
|
+
const needed = Math.ceil((4 + countBits + bytes.length * 8) / 8);
|
|
126
|
+
if (needed <= dataCw) {
|
|
127
|
+
version = v;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (version === 0)
|
|
132
|
+
throw new Error('text too long for QR v10-M');
|
|
133
|
+
// bit stream: mode, count, data, terminator, pad
|
|
134
|
+
const buf = { bits: [] };
|
|
135
|
+
buildSegments(text, buf);
|
|
136
|
+
const countBits = version <= 9 ? 8 : 16;
|
|
137
|
+
appendBits(buf, bytes.length, countBits);
|
|
138
|
+
for (const b of bytes)
|
|
139
|
+
appendBits(buf, b, 8);
|
|
140
|
+
const capacity = dataCw * 8;
|
|
141
|
+
appendBits(buf, 0, Math.min(4, capacity - buf.bits.length));
|
|
142
|
+
while (buf.bits.length % 8 !== 0)
|
|
143
|
+
buf.bits.push(0);
|
|
144
|
+
const data = new Uint8Array(dataCw);
|
|
145
|
+
for (let i = 0; i < buf.bits.length / 8; i++) {
|
|
146
|
+
let b = 0;
|
|
147
|
+
for (let j = 0; j < 8; j++)
|
|
148
|
+
b = (b << 1) | buf.bits[i * 8 + j];
|
|
149
|
+
data[i] = b;
|
|
150
|
+
}
|
|
151
|
+
for (let i = buf.bits.length / 8, pad = 0xec; i < dataCw; i++, pad ^= 0xec ^ 0x11)
|
|
152
|
+
data[i] = pad;
|
|
153
|
+
// ECC + interleave
|
|
154
|
+
const blockLen = Math.floor(dataCw / numBlocks);
|
|
155
|
+
const longBlocks = dataCw % numBlocks;
|
|
156
|
+
const blocks = [];
|
|
157
|
+
const eccs = [];
|
|
158
|
+
let off = 0;
|
|
159
|
+
for (let i = 0; i < numBlocks; i++) {
|
|
160
|
+
const len = blockLen + (i >= numBlocks - longBlocks ? 1 : 0);
|
|
161
|
+
const blk = data.slice(off, off + len);
|
|
162
|
+
off += len;
|
|
163
|
+
blocks.push(blk);
|
|
164
|
+
eccs.push(reedSolomon(blk, eccPerBlock));
|
|
165
|
+
}
|
|
166
|
+
const finalData = [];
|
|
167
|
+
const maxLen = blockLen + 1;
|
|
168
|
+
for (let i = 0; i < maxLen; i++) {
|
|
169
|
+
for (let b = 0; b < numBlocks; b++) {
|
|
170
|
+
if (i < blocks[b].length)
|
|
171
|
+
finalData.push(blocks[b][i]);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
for (let i = 0; i < eccPerBlock; i++) {
|
|
175
|
+
for (let b = 0; b < numBlocks; b++)
|
|
176
|
+
finalData.push(eccs[b][i]);
|
|
177
|
+
}
|
|
178
|
+
return { version, finalData };
|
|
179
|
+
}
|
|
180
|
+
function debugFinalBytes(text) {
|
|
181
|
+
const { version, finalData } = buildFinalData(text);
|
|
182
|
+
return { version, bytes: finalData };
|
|
183
|
+
}
|
|
184
|
+
function encodeQr(text, maskOverride) {
|
|
185
|
+
const { version, finalData } = buildFinalData(text);
|
|
186
|
+
const size = 17 + version * 4;
|
|
187
|
+
const mod = Array.from({ length: size }, () => new Array(size).fill(false));
|
|
188
|
+
const isFunc = Array.from({ length: size }, () => new Array(size).fill(false));
|
|
189
|
+
const setF = (r, c, v) => {
|
|
190
|
+
mod[r][c] = v;
|
|
191
|
+
isFunc[r][c] = true;
|
|
192
|
+
};
|
|
193
|
+
const finder = (r, c) => {
|
|
194
|
+
for (let dr = -1; dr <= 7; dr++)
|
|
195
|
+
for (let dc = -1; dc <= 7; dc++) {
|
|
196
|
+
const rr = r + dr;
|
|
197
|
+
const cc = c + dc;
|
|
198
|
+
if (rr < 0 || cc < 0 || rr >= size || cc >= size)
|
|
199
|
+
continue;
|
|
200
|
+
const inOuter = dr >= 0 && dr <= 6 && dc >= 0 && dc <= 6;
|
|
201
|
+
const v = inOuter && (dr === 0 || dr === 6 || dc === 0 || dc === 6 || (dr >= 2 && dr <= 4 && dc >= 2 && dc <= 4));
|
|
202
|
+
setF(rr, cc, v);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
finder(0, 0);
|
|
206
|
+
finder(0, size - 7);
|
|
207
|
+
finder(size - 7, 0);
|
|
208
|
+
// alignment pattern centers per spec table
|
|
209
|
+
const aligns = version === 1 ? [] :
|
|
210
|
+
version === 2 ? [6, 18] :
|
|
211
|
+
version === 3 ? [6, 22] :
|
|
212
|
+
version === 4 ? [6, 26] :
|
|
213
|
+
version === 5 ? [6, 30] :
|
|
214
|
+
version === 6 ? [6, 34] :
|
|
215
|
+
version === 7 ? [6, 22, 38] :
|
|
216
|
+
version === 8 ? [6, 24, 42] :
|
|
217
|
+
version === 9 ? [6, 26, 46] :
|
|
218
|
+
[6, 28, 50]; // v10
|
|
219
|
+
for (const r of aligns)
|
|
220
|
+
for (const c of aligns) {
|
|
221
|
+
if ((r === 6 && c === 6) || (r === 6 && c === size - 7) || (r === size - 7 && c === 6))
|
|
222
|
+
continue;
|
|
223
|
+
for (let dr = -2; dr <= 2; dr++)
|
|
224
|
+
for (let dc = -2; dc <= 2; dc++)
|
|
225
|
+
setF(r + dr, c + dc, Math.max(Math.abs(dr), Math.abs(dc)) !== 1);
|
|
226
|
+
}
|
|
227
|
+
// timing
|
|
228
|
+
for (let i = 8; i < size - 8; i++) {
|
|
229
|
+
if (!isFunc[6][i])
|
|
230
|
+
setF(6, i, i % 2 === 0);
|
|
231
|
+
if (!isFunc[i][6])
|
|
232
|
+
setF(i, 6, i % 2 === 0);
|
|
233
|
+
}
|
|
234
|
+
// dark module + format cells reserved (values written per mask).
|
|
235
|
+
// Canonical layout (verified against python-qrcode cell-for-cell):
|
|
236
|
+
// copy 1: (8,0..5)=bits14..9, (8,7)=bit8, (8,8)=bit7, (7,8)=bit6,
|
|
237
|
+
// (0..5,8)=bits0..5
|
|
238
|
+
// copy 2: (n-1..n-7, 8)=bits14..8, (8, n-8..n-1)=bits7..0
|
|
239
|
+
// dark module (n-8, 8) = 1
|
|
240
|
+
setF(size - 8, 8, true);
|
|
241
|
+
const formatCells = [];
|
|
242
|
+
for (let i = 0; i <= 5; i++)
|
|
243
|
+
formatCells.push([8, i]);
|
|
244
|
+
formatCells.push([8, 7], [8, 8], [7, 8]);
|
|
245
|
+
for (let i = 0; i <= 5; i++)
|
|
246
|
+
formatCells.push([i, 8]);
|
|
247
|
+
for (let i = 0; i <= 6; i++)
|
|
248
|
+
formatCells.push([size - 1 - i, 8]);
|
|
249
|
+
for (let i = 0; i < 8; i++)
|
|
250
|
+
formatCells.push([8, size - 8 + i]);
|
|
251
|
+
for (const [r, c] of formatCells) {
|
|
252
|
+
if (!(r === size - 8 && c === 8))
|
|
253
|
+
setF(r, c, false);
|
|
254
|
+
}
|
|
255
|
+
// data placement (zigzag) — finalData is BYTES; place their BITS MSB-first
|
|
256
|
+
const placeData = (mask) => {
|
|
257
|
+
const m = mod.map((row) => row.slice());
|
|
258
|
+
let bit = 0;
|
|
259
|
+
let upward = true;
|
|
260
|
+
for (let c = size - 1; c >= 1; c -= 2) {
|
|
261
|
+
if (c === 6)
|
|
262
|
+
c = 5;
|
|
263
|
+
for (let i = 0; i < size; i++) {
|
|
264
|
+
const r = upward ? size - 1 - i : i;
|
|
265
|
+
for (const cc of [c, c - 1]) {
|
|
266
|
+
if (isFunc[r][cc])
|
|
267
|
+
continue;
|
|
268
|
+
const byte = finalData[bit >>> 3] ?? 0;
|
|
269
|
+
let v = bit < finalData.length * 8 ? ((byte >>> (7 - (bit & 7))) & 1) === 1 : false;
|
|
270
|
+
if (MASKS[mask](r, cc))
|
|
271
|
+
v = !v;
|
|
272
|
+
m[r][cc] = v;
|
|
273
|
+
bit++;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
upward = !upward;
|
|
277
|
+
}
|
|
278
|
+
// format bits — canonical placement, both copies
|
|
279
|
+
const fmt = FORMAT_LOOKUP[(FORMAT_ECC_M << 3) | mask];
|
|
280
|
+
const fbit = (i) => ((fmt >>> i) & 1) === 1;
|
|
281
|
+
for (let i = 0; i <= 5; i++)
|
|
282
|
+
m[8][i] = fbit(14 - i);
|
|
283
|
+
m[8][7] = fbit(8);
|
|
284
|
+
m[8][8] = fbit(7);
|
|
285
|
+
m[7][8] = fbit(6);
|
|
286
|
+
for (let i = 0; i <= 5; i++)
|
|
287
|
+
m[i][8] = fbit(i);
|
|
288
|
+
for (let i = 0; i <= 6; i++)
|
|
289
|
+
m[size - 1 - i][8] = fbit(14 - i);
|
|
290
|
+
for (let i = 0; i < 8; i++)
|
|
291
|
+
m[8][size - 8 + i] = fbit(7 - i);
|
|
292
|
+
return m;
|
|
293
|
+
};
|
|
294
|
+
// mask selection by penalty
|
|
295
|
+
const penalty = (g) => {
|
|
296
|
+
let score = 0;
|
|
297
|
+
const s = g.length;
|
|
298
|
+
// N1 runs
|
|
299
|
+
for (let i = 0; i < s; i++) {
|
|
300
|
+
for (const dir of [0, 1]) {
|
|
301
|
+
let run = 1;
|
|
302
|
+
for (let j = 1; j <= s; j++) {
|
|
303
|
+
const same = j < s && (dir === 0 ? g[i][j] === g[i][j - 1] : g[j][i] === g[j - 1][i]);
|
|
304
|
+
if (same)
|
|
305
|
+
run++;
|
|
306
|
+
else {
|
|
307
|
+
if (run >= 5)
|
|
308
|
+
score += 3 + (run - 5);
|
|
309
|
+
run = 1;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// N2 blocks
|
|
315
|
+
for (let r = 0; r < s - 1; r++)
|
|
316
|
+
for (let c = 0; c < s - 1; c++)
|
|
317
|
+
if (g[r][c] === g[r + 1][c] && g[r][c] === g[r][c + 1] && g[r][c] === g[r + 1][c + 1])
|
|
318
|
+
score += 3;
|
|
319
|
+
// N3 finder-like
|
|
320
|
+
const pat = [true, false, true, true, true, false, true, false, false, false, false];
|
|
321
|
+
const patR = [...pat].reverse();
|
|
322
|
+
for (let i = 0; i < s; i++) {
|
|
323
|
+
for (let j = 0; j + 11 <= s; j++) {
|
|
324
|
+
const row = g[i].slice(j, j + 11);
|
|
325
|
+
const rowS = row.every((v, k) => v === pat[k]) || row.every((v, k) => v === patR[k]);
|
|
326
|
+
if (rowS)
|
|
327
|
+
score += 40;
|
|
328
|
+
const col = Array.from({ length: 11 }, (_, k) => g[j + k][i]);
|
|
329
|
+
const colS = col.every((v, k) => v === pat[k]) || col.every((v, k) => v === patR[k]);
|
|
330
|
+
if (colS)
|
|
331
|
+
score += 40;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// N4 balance
|
|
335
|
+
const dark = g.flat().filter(Boolean).length;
|
|
336
|
+
const pct = (dark * 100) / (s * s);
|
|
337
|
+
score += Math.floor(Math.abs(pct - 50) / 5) * 10;
|
|
338
|
+
return score;
|
|
339
|
+
};
|
|
340
|
+
if (maskOverride !== undefined)
|
|
341
|
+
return { size, modules: placeData(maskOverride) };
|
|
342
|
+
let best = [];
|
|
343
|
+
let bestScore = Infinity;
|
|
344
|
+
for (let mask = 0; mask < 8; mask++) {
|
|
345
|
+
const g = placeData(mask);
|
|
346
|
+
const sc = penalty(g);
|
|
347
|
+
if (sc < bestScore) {
|
|
348
|
+
bestScore = sc;
|
|
349
|
+
best = g;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return { size, modules: best };
|
|
353
|
+
}
|
|
354
|
+
/** Render to an SVG path (dark modules as 1x1 squares). */
|
|
355
|
+
function qrToSvgPath(qr) {
|
|
356
|
+
const parts = [];
|
|
357
|
+
for (let r = 0; r < qr.size; r++)
|
|
358
|
+
for (let c = 0; c < qr.size; c++)
|
|
359
|
+
if (qr.modules[r][c])
|
|
360
|
+
parts.push(`M${c},${r}h1v1h-1z`);
|
|
361
|
+
return parts.join('');
|
|
362
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PAY-2568 / PAY-2279 (BAPert 85415, 85419): what a failed credential sign-in SAYS, and the stable machine code it carries.
|
|
3
|
+
*
|
|
4
|
+
* ONE closed table. The sentence the page shows comes from here, keyed on a code; it never comes from a server message string
|
|
5
|
+
* (the IdP's wire text, a stack, an upstream detail). The code is derived from the response STATUS plus the IdP's error CODE
|
|
6
|
+
* (never its message text):
|
|
7
|
+
*
|
|
8
|
+
* invalid_credentials wrong password OR unknown account OR any other refusal. Identical on purpose: the sentence and the body
|
|
9
|
+
* must not separate "no such account" from "wrong password" (user enumeration).
|
|
10
|
+
* not_a_member the IdP error code NotAMemberOfThisApp, whatever the HTTP status. Decided by the IdP AFTER the password is
|
|
11
|
+
* proven (PayEz-Core InternalTokenController.IssueCorporateToken: password validation 401s first, membership
|
|
12
|
+
* comes after), so this sentence is shown only to someone who proved the credentials. The STATUS is not part
|
|
13
|
+
* of the key: the internal mint door answers 403, but the PUBLIC door relays the same refusal as HTTP 400
|
|
14
|
+
* (measured on dev-93, PR 1719 walk, Scout 85449). Keying on 403 made rule 2 unreachable.
|
|
15
|
+
* too_many_attempts 429 (any code), or an AccountLocked / UserLockedOut refusal. One sentence for both: the user is not told
|
|
16
|
+
* which. A lockout exists only for a real account, which is an accepted, argued signal (BAPert 85419).
|
|
17
|
+
*/
|
|
18
|
+
export type SignInFailureCode = 'invalid_credentials' | 'not_a_member' | 'too_many_attempts' | 'invalid_second_factor' | 'second_factor_unavailable' | 'second_factor_rate_limited';
|
|
19
|
+
export declare const SIGN_IN_FAILURE_COPY: Record<SignInFailureCode, string>;
|
|
20
|
+
/**
|
|
21
|
+
* BAPert 85807: the lockout sentence states the window when the server sent one. N = retryAfter (seconds) rounded UP to whole minutes,
|
|
22
|
+
* clamped to an integer 1..60. Absent, unparseable or non-finite -> the plain table sentence. Only a NUMBER from the server is printed,
|
|
23
|
+
* never text. (1 is "1 minute", not "1 minutes".)
|
|
24
|
+
*/
|
|
25
|
+
export declare function tooManyAttemptsMessage(retryAfterSeconds: unknown, code?: 'too_many_attempts' | 'second_factor_rate_limited'): string;
|
|
26
|
+
/** True only for a code this table defines. A hostile or new string from the wire is not one. */
|
|
27
|
+
export declare function isSignInFailureCode(v: unknown): v is SignInFailureCode;
|
|
28
|
+
/** The sentence for a code, or null when the code is not in the table. */
|
|
29
|
+
export declare function signInFailureMessage(code: unknown): string | null;
|
|
30
|
+
/** BAPert 85432: an allowlisted policy refusal (SsoRequired / InvalidRequest) is the only server text a page may show: plain text only, at most this long. */
|
|
31
|
+
export declare const POLICY_DETAIL_MAX = 200;
|
|
32
|
+
/** The policy detail as shown: a string, trimmed, at most POLICY_DETAIL_MAX characters. Rendered as a React text node, never as HTML. */
|
|
33
|
+
export declare function capPolicyDetail(v: unknown): string | null;
|
|
34
|
+
/** Server side: the code for a refusal, from the HTTP status (429 only) and the IdP's error code. Called only for refusals. */
|
|
35
|
+
export declare function classifySignInFailure(status: number, idpCode: string | null | undefined): SignInFailureCode;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* PAY-2568 / PAY-2279 (BAPert 85415, 85419): what a failed credential sign-in SAYS, and the stable machine code it carries.
|
|
4
|
+
*
|
|
5
|
+
* ONE closed table. The sentence the page shows comes from here, keyed on a code; it never comes from a server message string
|
|
6
|
+
* (the IdP's wire text, a stack, an upstream detail). The code is derived from the response STATUS plus the IdP's error CODE
|
|
7
|
+
* (never its message text):
|
|
8
|
+
*
|
|
9
|
+
* invalid_credentials wrong password OR unknown account OR any other refusal. Identical on purpose: the sentence and the body
|
|
10
|
+
* must not separate "no such account" from "wrong password" (user enumeration).
|
|
11
|
+
* not_a_member the IdP error code NotAMemberOfThisApp, whatever the HTTP status. Decided by the IdP AFTER the password is
|
|
12
|
+
* proven (PayEz-Core InternalTokenController.IssueCorporateToken: password validation 401s first, membership
|
|
13
|
+
* comes after), so this sentence is shown only to someone who proved the credentials. The STATUS is not part
|
|
14
|
+
* of the key: the internal mint door answers 403, but the PUBLIC door relays the same refusal as HTTP 400
|
|
15
|
+
* (measured on dev-93, PR 1719 walk, Scout 85449). Keying on 403 made rule 2 unreachable.
|
|
16
|
+
* too_many_attempts 429 (any code), or an AccountLocked / UserLockedOut refusal. One sentence for both: the user is not told
|
|
17
|
+
* which. A lockout exists only for a real account, which is an accepted, argued signal (BAPert 85419).
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.POLICY_DETAIL_MAX = exports.SIGN_IN_FAILURE_COPY = void 0;
|
|
21
|
+
exports.tooManyAttemptsMessage = tooManyAttemptsMessage;
|
|
22
|
+
exports.isSignInFailureCode = isSignInFailureCode;
|
|
23
|
+
exports.signInFailureMessage = signInFailureMessage;
|
|
24
|
+
exports.capPolicyDetail = capPolicyDetail;
|
|
25
|
+
exports.classifySignInFailure = classifySignInFailure;
|
|
26
|
+
exports.SIGN_IN_FAILURE_COPY = {
|
|
27
|
+
invalid_credentials: 'Incorrect email or password.',
|
|
28
|
+
not_a_member: "Your account does not have access to this app. Ask the app's administrator to invite you.",
|
|
29
|
+
too_many_attempts: 'Too many sign-in attempts. Wait a few minutes and try again, or reset your password.',
|
|
30
|
+
// The code step (after the password was proven). BAPert 85795 item 2 wrote the first sentence; the second is the sentence this lane
|
|
31
|
+
// already showed for an IdP-side fault (twofa-rejection.ts), now chosen by code like the rest. Neither depends on IdP wording: the
|
|
32
|
+
// IdP answers a wrong TOTP code with the generic code INVALID_REQUEST + 'Invalid code', so the branch (status + lockout/fault codes),
|
|
33
|
+
// not the IdP code, decides.
|
|
34
|
+
invalid_second_factor: 'That code is not correct. Check the code and try again.',
|
|
35
|
+
// A plain IdP 429 on the code step (NOT a lockout, which stays too_many_attempts). BAPert 85813: 'your code is still valid' is dropped because
|
|
36
|
+
// nothing in this package proves a rate-limited attempt leaves the code unconsumed; add it back only on the IdP owner's confirmation.
|
|
37
|
+
second_factor_rate_limited: 'Too many attempts. Wait a few minutes and try again.',
|
|
38
|
+
second_factor_unavailable: 'We could not check your code just now - this is a problem on our side, not with your code. Try again shortly.',
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* BAPert 85807: the lockout sentence states the window when the server sent one. N = retryAfter (seconds) rounded UP to whole minutes,
|
|
42
|
+
* clamped to an integer 1..60. Absent, unparseable or non-finite -> the plain table sentence. Only a NUMBER from the server is printed,
|
|
43
|
+
* never text. (1 is "1 minute", not "1 minutes".)
|
|
44
|
+
*/
|
|
45
|
+
function tooManyAttemptsMessage(retryAfterSeconds, code = 'too_many_attempts') {
|
|
46
|
+
const n = typeof retryAfterSeconds === 'number' ? retryAfterSeconds : typeof retryAfterSeconds === 'string' && /^\s*\d+(\.\d+)?\s*$/.test(retryAfterSeconds) ? Number(retryAfterSeconds) : NaN;
|
|
47
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
48
|
+
return exports.SIGN_IN_FAILURE_COPY[code];
|
|
49
|
+
const minutes = Math.min(60, Math.max(1, Math.ceil(n / 60)));
|
|
50
|
+
const unit = minutes === 1 ? 'minute' : 'minutes';
|
|
51
|
+
return code === 'second_factor_rate_limited'
|
|
52
|
+
? `Too many attempts. Try again in ${minutes} ${unit}.`
|
|
53
|
+
: `Too many sign-in attempts. Try again in ${minutes} ${unit}, or reset your password.`;
|
|
54
|
+
}
|
|
55
|
+
/** True only for a code this table defines. A hostile or new string from the wire is not one. */
|
|
56
|
+
function isSignInFailureCode(v) {
|
|
57
|
+
return typeof v === 'string' && Object.prototype.hasOwnProperty.call(exports.SIGN_IN_FAILURE_COPY, v);
|
|
58
|
+
}
|
|
59
|
+
/** The sentence for a code, or null when the code is not in the table. */
|
|
60
|
+
function signInFailureMessage(code) {
|
|
61
|
+
return isSignInFailureCode(code) ? exports.SIGN_IN_FAILURE_COPY[code] : null;
|
|
62
|
+
}
|
|
63
|
+
/** BAPert 85432: an allowlisted policy refusal (SsoRequired / InvalidRequest) is the only server text a page may show: plain text only, at most this long. */
|
|
64
|
+
exports.POLICY_DETAIL_MAX = 200;
|
|
65
|
+
/** The policy detail as shown: a string, trimmed, at most POLICY_DETAIL_MAX characters. Rendered as a React text node, never as HTML. */
|
|
66
|
+
function capPolicyDetail(v) {
|
|
67
|
+
if (typeof v !== "string")
|
|
68
|
+
return null;
|
|
69
|
+
const t = v.trim();
|
|
70
|
+
return t ? t.slice(0, exports.POLICY_DETAIL_MAX) : null;
|
|
71
|
+
}
|
|
72
|
+
const norm = (c) => (typeof c === 'string' ? c.replace(/[^A-Za-z0-9]/g, '').toUpperCase() : '');
|
|
73
|
+
const NOT_A_MEMBER = 'NOTAMEMBEROFTHISAPP';
|
|
74
|
+
const LOCKED = new Set(['ACCOUNTLOCKED', 'USERLOCKEDOUT']);
|
|
75
|
+
/** Server side: the code for a refusal, from the HTTP status (429 only) and the IdP's error code. Called only for refusals. */
|
|
76
|
+
function classifySignInFailure(status, idpCode) {
|
|
77
|
+
if (status === 429)
|
|
78
|
+
return 'too_many_attempts';
|
|
79
|
+
const c = norm(idpCode);
|
|
80
|
+
if (LOCKED.has(c))
|
|
81
|
+
return 'too_many_attempts';
|
|
82
|
+
if (c === NOT_A_MEMBER)
|
|
83
|
+
return 'not_a_member';
|
|
84
|
+
return 'invalid_credentials';
|
|
85
|
+
}
|
|
@@ -10,6 +10,8 @@ export interface TwofaRejection {
|
|
|
10
10
|
status: 400 | 429 | 502;
|
|
11
11
|
error: string;
|
|
12
12
|
retryAfter?: number;
|
|
13
|
+
/** True only for the plain IdP 429 (a lane rate limit), not for a `locked` lockout: the page words them differently. */
|
|
14
|
+
rateLimited?: true;
|
|
13
15
|
/**
|
|
14
16
|
* PAY-2284: true when the authenticator (or text) lane failed on OUR side and the emailed-code lane has not faulted. The sign-in
|
|
15
17
|
* card then shows a real "Email me a code instead" action. Before this, the copy said "use another sign-in method" and the screen
|
|
@@ -44,8 +44,11 @@ function classifyTwofaRejection(lanes, retryAfterHeader) {
|
|
|
44
44
|
const parsed = retryAfterHeader ? parseInt(retryAfterHeader, 10) : NaN;
|
|
45
45
|
return {
|
|
46
46
|
status: 429,
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
// Not shown to users (the handler sends the table sentence for the code); no claim about the code's validity, which is unproven.
|
|
48
|
+
error: 'Too many attempts. Wait a few minutes and try again.',
|
|
49
|
+
// BAPert 85813: no invented number. The page prints retryAfter as minutes, so it is sent only when the IdP sent a usable Retry-After.
|
|
50
|
+
...(Number.isFinite(parsed) && parsed > 0 ? { retryAfter: parsed } : {}),
|
|
51
|
+
rateLimited: true,
|
|
49
52
|
};
|
|
50
53
|
}
|
|
51
54
|
// PAY-1589: an IdP-side fault that arrives as 400 + code (TOTP secret cannot be decrypted -> server_error;
|
|
@@ -117,6 +117,14 @@ export interface MvpMiddlewareOptions {
|
|
|
117
117
|
* an error is logged once. Default: off (the direct clear), because an unmounted route would 404 every sign-out.
|
|
118
118
|
*/
|
|
119
119
|
sessionResetRoute?: boolean | string;
|
|
120
|
+
/**
|
|
121
|
+
* PAY-2218 (BAPert 83102, Scout option 1): OFF by default. When the EDGE middleware cannot see BETTER_AUTH_SECRET / NEXTAUTH_SECRET (the IdP-supplied secret is
|
|
122
|
+
* published into process.env only in the Node process), the hand-off cannot be signed and the direct clear is kept. Turn this ON to sign it with a key derived from the
|
|
123
|
+
* session cookie instead (the env secret still wins where it IS visible). It must be turned on in BOTH places or the route refuses the link: here, and on the route
|
|
124
|
+
* with `createSessionResetHandler({ cookieKey: true })`. Accepted residual risk of ON: an attacker who controls a sibling subdomain can plant a cookie value they
|
|
125
|
+
* know and sign a link that clears THAT value (a forced logout of that value only). Every app that can see the env secret should leave this OFF.
|
|
126
|
+
*/
|
|
127
|
+
sessionResetCookieKey?: boolean;
|
|
120
128
|
/**
|
|
121
129
|
* PAY-1882 step 4 / PAY-2339 - how this app treats a producer that reports the session's
|
|
122
130
|
* revocation status as `'unknown'` (a Vibe read that FAILED: non-OK, timeout,
|
|
@@ -175,6 +183,22 @@ export interface MvpMiddlewareOptions {
|
|
|
175
183
|
* would swallow in-app navigations and stop the middleware refreshing/clearing for them.
|
|
176
184
|
*/
|
|
177
185
|
export declare function isPrefetchRequest(request: NextRequest): boolean;
|
|
186
|
+
/**
|
|
187
|
+
* PAY-2218 P2 (BAPert 86134, after NightHawk 86133 measured the sign-out on a real Next 15.5.9 server and a real Chromium).
|
|
188
|
+
* Next 15.x and 16.0-16.1 (and 16.2 without skipProxyUrlNormalize) strip the router-prefetch headers before middleware runs, so
|
|
189
|
+
* isPrefetchRequest() cannot see a router prefetch there. The browser's own fetch metadata is NOT stripped: a router prefetch is a
|
|
190
|
+
* fetch() (`Sec-Fetch-Mode: cors`, `Sec-Fetch-Dest: empty`), a real top-level navigation is `navigate` + `document`.
|
|
191
|
+
*
|
|
192
|
+
* THE TABLE (clearSessionCookies expires the cookies only when this returns false):
|
|
193
|
+
* Sec-Fetch-Mode present and not 'navigate' OR Sec-Fetch-Dest present and not 'document' -> true (do NOT clear)
|
|
194
|
+
* Sec-Fetch-Mode 'navigate' and/or Dest 'document', none different -> false (clear, as before)
|
|
195
|
+
* NO Sec-Fetch-* at all, and rsc / next-router-prefetch / next-router-state-tree / sec-purpose|purpose: prefetch present -> true (never clear)
|
|
196
|
+
* NO Sec-Fetch-* at all, none of those (curl, old clients, server-to-server) -> false (clear, as before)
|
|
197
|
+
* The redirect is still returned; the stale cookie is then cleared by the real navigation that follows.
|
|
198
|
+
* Fetch metadata is a forbidden header (page script cannot set it) and clearing is the safe direction, so a forged value can only delay a
|
|
199
|
+
* clear that the next navigation performs.
|
|
200
|
+
*/
|
|
201
|
+
export declare function isKnownNonNavigationRequest(request: NextRequest): boolean;
|
|
178
202
|
/** Login error code for a pre-2FA session that expired before the code was verified (PAY-884). */
|
|
179
203
|
export declare const TWO_FACTOR_TIMEOUT_ERROR = "TwoFactorTimeout";
|
|
180
204
|
export declare function createMvpMiddleware(options?: MvpMiddlewareOptions): (request: NextRequest) => Promise<NextResponse>;
|
|
@@ -20,9 +20,11 @@
|
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
21
|
exports.TWO_FACTOR_TIMEOUT_ERROR = void 0;
|
|
22
22
|
exports.isPrefetchRequest = isPrefetchRequest;
|
|
23
|
+
exports.isKnownNonNavigationRequest = isKnownNonNavigationRequest;
|
|
23
24
|
exports.createMvpMiddleware = createMvpMiddleware;
|
|
24
25
|
exports.decideRevocationOutcome = decideRevocationOutcome;
|
|
25
26
|
exports.decideForceInvalid = decideForceInvalid;
|
|
27
|
+
const prefetch_guard_status_1 = require("./prefetch-guard-status");
|
|
26
28
|
const server_1 = require("next/server");
|
|
27
29
|
const route_config_1 = require("../auth/route-config");
|
|
28
30
|
const safe_callback_1 = require("../auth/safe-callback");
|
|
@@ -105,6 +107,36 @@ function isPrefetchRequest(request) {
|
|
|
105
107
|
return true;
|
|
106
108
|
return false;
|
|
107
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* PAY-2218 P2 (BAPert 86134, after NightHawk 86133 measured the sign-out on a real Next 15.5.9 server and a real Chromium).
|
|
112
|
+
* Next 15.x and 16.0-16.1 (and 16.2 without skipProxyUrlNormalize) strip the router-prefetch headers before middleware runs, so
|
|
113
|
+
* isPrefetchRequest() cannot see a router prefetch there. The browser's own fetch metadata is NOT stripped: a router prefetch is a
|
|
114
|
+
* fetch() (`Sec-Fetch-Mode: cors`, `Sec-Fetch-Dest: empty`), a real top-level navigation is `navigate` + `document`.
|
|
115
|
+
*
|
|
116
|
+
* THE TABLE (clearSessionCookies expires the cookies only when this returns false):
|
|
117
|
+
* Sec-Fetch-Mode present and not 'navigate' OR Sec-Fetch-Dest present and not 'document' -> true (do NOT clear)
|
|
118
|
+
* Sec-Fetch-Mode 'navigate' and/or Dest 'document', none different -> false (clear, as before)
|
|
119
|
+
* NO Sec-Fetch-* at all, and rsc / next-router-prefetch / next-router-state-tree / sec-purpose|purpose: prefetch present -> true (never clear)
|
|
120
|
+
* NO Sec-Fetch-* at all, none of those (curl, old clients, server-to-server) -> false (clear, as before)
|
|
121
|
+
* The redirect is still returned; the stale cookie is then cleared by the real navigation that follows.
|
|
122
|
+
* Fetch metadata is a forbidden header (page script cannot set it) and clearing is the safe direction, so a forged value can only delay a
|
|
123
|
+
* clear that the next navigation performs.
|
|
124
|
+
*/
|
|
125
|
+
function isKnownNonNavigationRequest(request) {
|
|
126
|
+
const h = request.headers;
|
|
127
|
+
const mode = h.get('sec-fetch-mode');
|
|
128
|
+
const dest = h.get('sec-fetch-dest');
|
|
129
|
+
if (mode !== null || dest !== null) {
|
|
130
|
+
if (mode !== null && mode.trim().toLowerCase() !== 'navigate')
|
|
131
|
+
return true;
|
|
132
|
+
if (dest !== null && dest.trim().toLowerCase() !== 'document')
|
|
133
|
+
return true;
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
if (isPrefetchRequest(request))
|
|
137
|
+
return true;
|
|
138
|
+
return h.has('rsc') || h.has('next-router-prefetch') || h.has('next-router-state-tree');
|
|
139
|
+
}
|
|
108
140
|
/**
|
|
109
141
|
* Clear all session cookies on a response. The ONE site that writes the expiry Set-Cookie for the middleware,
|
|
110
142
|
* so the prefetch guard lives here: on a prefetch the redirect is still returned (routing unchanged) but
|
|
@@ -113,6 +145,8 @@ function isPrefetchRequest(request) {
|
|
|
113
145
|
function clearSessionCookies(response, request) {
|
|
114
146
|
if (isPrefetchRequest(request))
|
|
115
147
|
return;
|
|
148
|
+
if (isKnownNonNavigationRequest(request))
|
|
149
|
+
return;
|
|
116
150
|
(0, session_cookie_clear_1.expireSessionCookies)(response, Array.from(request.cookies, ([name]) => name));
|
|
117
151
|
}
|
|
118
152
|
/** Create redirect response, optionally clearing cookies */
|
|
@@ -141,7 +175,12 @@ async function clearingRedirect(request, location, reset) {
|
|
|
141
175
|
});
|
|
142
176
|
// Bound to THIS request's session cookie: the route clears only when its own request carries the same one.
|
|
143
177
|
const bound = (0, session_reset_link_1.boundSessionCookie)((n) => request.cookies.get(n)?.value, (0, app_slug_1.getSessionCookieName)(), (0, app_slug_1.getSecureSessionCookieName)());
|
|
144
|
-
const
|
|
178
|
+
const secret = reset.cookieKey ? await (0, session_reset_link_1.resetSigningSecret)(reset.envSecret, bound) : reset.envSecret;
|
|
179
|
+
if (!secret) {
|
|
180
|
+
reset.onCannotSign?.();
|
|
181
|
+
return redirectTo(request, location, true);
|
|
182
|
+
}
|
|
183
|
+
const response = server_1.NextResponse.redirect(new URL(await (0, session_reset_link_1.signedResetHref)(reset.route, params, secret, bound), request.url));
|
|
145
184
|
response.headers.set('cache-control', 'no-store');
|
|
146
185
|
return response;
|
|
147
186
|
}
|
|
@@ -414,6 +453,8 @@ function createMvpMiddleware(options = {}) {
|
|
|
414
453
|
const revocationFailMode = options.revocationFailMode || 'hold';
|
|
415
454
|
// PAY-2302 cut 1c: strictly `=== true`, so a truthy string in a config file cannot switch revocation off.
|
|
416
455
|
const noRevocationSource = options.noRevocationSource === true;
|
|
456
|
+
// PAY-2218 P1: log once if this Next version cannot show middleware a router prefetch. Log-only.
|
|
457
|
+
(0, prefetch_guard_status_1.warnPrefetchGuardDeadOnce)((m) => log.warn(m));
|
|
417
458
|
if (noRevocationSource) {
|
|
418
459
|
log.warn('[next-mvp] noRevocationSource is ON: sessions of this app CANNOT be revoked server-side (no vibe client / HMAC key). ' +
|
|
419
460
|
"revocationStatus 'not_configured' is treated as viable; 'unknown' and 'revoked' still sign the user out. " +
|
|
@@ -426,14 +467,20 @@ function createMvpMiddleware(options = {}) {
|
|
|
426
467
|
const resolveReset = () => {
|
|
427
468
|
if (!sessionResetRoute)
|
|
428
469
|
return undefined;
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
470
|
+
return {
|
|
471
|
+
route: sessionResetRoute,
|
|
472
|
+
envSecret: (0, session_reset_link_1.resetSecret)(),
|
|
473
|
+
cookieKey: options.sessionResetCookieKey === true,
|
|
474
|
+
onCannotSign: () => {
|
|
475
|
+
if (resetSecretMissingLogged)
|
|
476
|
+
return;
|
|
477
|
+
resetSecretMissingLogged = true;
|
|
478
|
+
// No values here: only the route name.
|
|
479
|
+
log.error(options.sessionResetCookieKey === true
|
|
480
|
+
? 'sessionResetRoute is set but there is neither a BETTER_AUTH_SECRET/NEXTAUTH_SECRET visible to the middleware nor a session cookie to derive a signing key from; clearing in middleware instead'
|
|
481
|
+
: 'sessionResetRoute is set but no BETTER_AUTH_SECRET/NEXTAUTH_SECRET is available to sign the hand-off; clearing in middleware instead (set sessionResetCookieKey, and createSessionResetHandler({ cookieKey: true }) on the route, if the edge cannot see the env secret)', { sessionResetRoute });
|
|
482
|
+
},
|
|
483
|
+
};
|
|
437
484
|
};
|
|
438
485
|
return async function middleware(request) {
|
|
439
486
|
const { pathname, searchParams } = request.nextUrl;
|