@poly-x/next 0.1.0 → 0.1.1
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/proxy.cjs +1 -1
- package/dist/proxy.mjs +1 -1
- package/dist/{server-session-QJLA_Tz-.cjs → server-session-Df9HXl_5.cjs} +85 -19
- package/dist/{server-session-CjP44zFC.mjs → server-session-pY-nzh5x.mjs} +69 -21
- package/dist/server.cjs +33 -8
- package/dist/server.d.cts +34 -3
- package/dist/server.d.mts +34 -3
- package/dist/server.mjs +31 -9
- package/package.json +3 -3
package/dist/proxy.cjs
CHANGED
package/dist/proxy.mjs
CHANGED
|
@@ -1,23 +1,45 @@
|
|
|
1
1
|
let _poly_x_core = require("@poly-x/core");
|
|
2
|
-
//#region src/cookie-
|
|
2
|
+
//#region src/cookie-codec.ts
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* AES-GCM sealing of the session cookie (F007 / ADR-0004). The key is derived
|
|
5
|
+
* (SHA-256) from the consumer's `POLYX_SESSION_SECRET`; AES-GCM gives
|
|
6
|
+
* confidentiality + tamper-detection together, so any corruption, truncation,
|
|
7
|
+
* or wrong-secret input decrypts to `null` rather than a partial session.
|
|
8
|
+
* Web Crypto is universal (Node ≥20 + edge runtimes), so this runs anywhere a
|
|
9
|
+
* Next route handler runs.
|
|
9
10
|
*/
|
|
10
|
-
function buildSessionCookieOptions(secure) {
|
|
11
|
-
return {
|
|
12
|
-
httpOnly: true,
|
|
13
|
-
secure,
|
|
14
|
-
sameSite: "lax",
|
|
15
|
-
path: "/"
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
//#endregion
|
|
19
|
-
//#region src/cookie-codec.ts
|
|
20
11
|
const IV_BYTES = 12;
|
|
12
|
+
/**
|
|
13
|
+
* Browsers cap a single cookie at roughly 4096 bytes for the whole `Set-Cookie` pair —
|
|
14
|
+
* name, value and attributes together — and enforce it by DISCARDING the cookie without
|
|
15
|
+
* an error. For a session cookie that failure is invisible and badly misleading: the
|
|
16
|
+
* platform authenticates, the handler answers 200, and the user is bounced back to
|
|
17
|
+
* sign-in on the next request because the cookie was never stored.
|
|
18
|
+
*
|
|
19
|
+
* poly-x-sdk/ADR-0006 measured a live Odin session at 2628 bytes and recorded the missing
|
|
20
|
+
* guard as an open follow-up; a per-tenant flag that inflates the JWT is enough to cross
|
|
21
|
+
* the line. We fail loudly here instead.
|
|
22
|
+
*/
|
|
23
|
+
const SESSION_COOKIE_MAX_BYTES = 4096;
|
|
24
|
+
/** Attributes `buildSessionCookieOptions` always sets: `Path=/; HttpOnly; Secure; SameSite=Lax`. */
|
|
25
|
+
const COOKIE_ATTRIBUTE_BYTES = 45;
|
|
26
|
+
/** Bytes the whole `Set-Cookie` pair will occupy for this value under this cookie name. */
|
|
27
|
+
function sealedCookieByteLength(value, cookieName = "polyx_session") {
|
|
28
|
+
return new TextEncoder().encode(cookieName).length + 1 + value.length + COOKIE_ATTRIBUTE_BYTES;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Thrown when a sealed session would exceed what a browser will store. Never includes the
|
|
32
|
+
* session itself — the claims that caused the overflow are exactly the data we must not log.
|
|
33
|
+
*/
|
|
34
|
+
var SessionCookieTooLargeError = class extends _poly_x_core.PolyXError {
|
|
35
|
+
bytes;
|
|
36
|
+
limit;
|
|
37
|
+
constructor(bytes, limit, cookieName) {
|
|
38
|
+
super(`PolyX session cookie is ${bytes} bytes, over the ~${limit}-byte browser limit for "${cookieName}". The browser would discard it silently and the user would appear signed out. Reduce what the platform puts in the session — the usual cause is a tenant configured to embed full permissions in the JWT (includeFullPermissionInJwt).`, { code: "SESSION_COOKIE_TOO_LARGE" });
|
|
39
|
+
this.bytes = bytes;
|
|
40
|
+
this.limit = limit;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
21
43
|
function base64UrlEncode(bytes) {
|
|
22
44
|
let binary = "";
|
|
23
45
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
@@ -35,7 +57,7 @@ async function deriveKey(secret) {
|
|
|
35
57
|
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(secret));
|
|
36
58
|
return crypto.subtle.importKey("raw", hash, "AES-GCM", false, ["encrypt", "decrypt"]);
|
|
37
59
|
}
|
|
38
|
-
async function sealSession(session, secret) {
|
|
60
|
+
async function sealSession(session, secret, options = {}) {
|
|
39
61
|
const key = await deriveKey(secret);
|
|
40
62
|
const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
|
|
41
63
|
const plaintext = new TextEncoder().encode(JSON.stringify(session));
|
|
@@ -46,7 +68,11 @@ async function sealSession(session, secret) {
|
|
|
46
68
|
const packed = new Uint8Array(iv.length + ciphertext.length);
|
|
47
69
|
packed.set(iv);
|
|
48
70
|
packed.set(ciphertext, iv.length);
|
|
49
|
-
|
|
71
|
+
const sealed = base64UrlEncode(packed);
|
|
72
|
+
const cookieName = options.cookieName ?? "polyx_session";
|
|
73
|
+
const bytes = sealedCookieByteLength(sealed, cookieName);
|
|
74
|
+
if (bytes > 4096) throw new SessionCookieTooLargeError(bytes, SESSION_COOKIE_MAX_BYTES, cookieName);
|
|
75
|
+
return sealed;
|
|
50
76
|
}
|
|
51
77
|
async function openSession(token, secret) {
|
|
52
78
|
try {
|
|
@@ -65,6 +91,23 @@ async function openSession(token, secret) {
|
|
|
65
91
|
}
|
|
66
92
|
}
|
|
67
93
|
//#endregion
|
|
94
|
+
//#region src/cookie-adapter.ts
|
|
95
|
+
/**
|
|
96
|
+
* Attributes for the session cookie (ADR-0004 secure custody). `secure` is decided at
|
|
97
|
+
* write time from the actual transport: a `Secure` cookie is silently dropped by the
|
|
98
|
+
* browser over plain http, so http/localhost dev must set `secure:false` while https
|
|
99
|
+
* always sets `secure:true`. This is not a downgrade — `Secure` carries no meaning on
|
|
100
|
+
* http; setting it there just breaks the session. httpOnly/sameSite/path are unchanged.
|
|
101
|
+
*/
|
|
102
|
+
function buildSessionCookieOptions(secure) {
|
|
103
|
+
return {
|
|
104
|
+
httpOnly: true,
|
|
105
|
+
secure,
|
|
106
|
+
sameSite: "lax",
|
|
107
|
+
path: "/"
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
//#endregion
|
|
68
111
|
//#region src/server-session.ts
|
|
69
112
|
/**
|
|
70
113
|
* The framework-agnostic BFF session controller (F007): tokens sealed in an
|
|
@@ -113,6 +156,11 @@ var ServerSession = class {
|
|
|
113
156
|
this.clear();
|
|
114
157
|
return null;
|
|
115
158
|
}
|
|
159
|
+
if (error instanceof SessionCookieTooLargeError) {
|
|
160
|
+
console.error(`[polyx] ${error.message}`);
|
|
161
|
+
this.clear();
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
116
164
|
throw error;
|
|
117
165
|
}
|
|
118
166
|
}
|
|
@@ -138,7 +186,7 @@ var ServerSession = class {
|
|
|
138
186
|
this.cookies.delete(this.cookieName);
|
|
139
187
|
}
|
|
140
188
|
async write(session) {
|
|
141
|
-
const sealed = await sealSession(session, this.secret);
|
|
189
|
+
const sealed = await sealSession(session, this.secret, { cookieName: this.cookieName });
|
|
142
190
|
this.cookies.set(this.cookieName, sealed, buildSessionCookieOptions(this.secure));
|
|
143
191
|
}
|
|
144
192
|
};
|
|
@@ -162,12 +210,24 @@ Object.defineProperty(exports, "DEFAULT_SESSION_COOKIE", {
|
|
|
162
210
|
return DEFAULT_SESSION_COOKIE;
|
|
163
211
|
}
|
|
164
212
|
});
|
|
213
|
+
Object.defineProperty(exports, "SESSION_COOKIE_MAX_BYTES", {
|
|
214
|
+
enumerable: true,
|
|
215
|
+
get: function() {
|
|
216
|
+
return SESSION_COOKIE_MAX_BYTES;
|
|
217
|
+
}
|
|
218
|
+
});
|
|
165
219
|
Object.defineProperty(exports, "ServerSession", {
|
|
166
220
|
enumerable: true,
|
|
167
221
|
get: function() {
|
|
168
222
|
return ServerSession;
|
|
169
223
|
}
|
|
170
224
|
});
|
|
225
|
+
Object.defineProperty(exports, "SessionCookieTooLargeError", {
|
|
226
|
+
enumerable: true,
|
|
227
|
+
get: function() {
|
|
228
|
+
return SessionCookieTooLargeError;
|
|
229
|
+
}
|
|
230
|
+
});
|
|
171
231
|
Object.defineProperty(exports, "openSession", {
|
|
172
232
|
enumerable: true,
|
|
173
233
|
get: function() {
|
|
@@ -180,6 +240,12 @@ Object.defineProperty(exports, "sealSession", {
|
|
|
180
240
|
return sealSession;
|
|
181
241
|
}
|
|
182
242
|
});
|
|
243
|
+
Object.defineProperty(exports, "sealedCookieByteLength", {
|
|
244
|
+
enumerable: true,
|
|
245
|
+
get: function() {
|
|
246
|
+
return sealedCookieByteLength;
|
|
247
|
+
}
|
|
248
|
+
});
|
|
183
249
|
Object.defineProperty(exports, "toAuthContext", {
|
|
184
250
|
enumerable: true,
|
|
185
251
|
get: function() {
|
|
@@ -1,23 +1,45 @@
|
|
|
1
|
-
import { SessionExpiredError, SessionRevokedError } from "@poly-x/core";
|
|
2
|
-
//#region src/cookie-
|
|
1
|
+
import { PolyXError, SessionExpiredError, SessionRevokedError } from "@poly-x/core";
|
|
2
|
+
//#region src/cookie-codec.ts
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* AES-GCM sealing of the session cookie (F007 / ADR-0004). The key is derived
|
|
5
|
+
* (SHA-256) from the consumer's `POLYX_SESSION_SECRET`; AES-GCM gives
|
|
6
|
+
* confidentiality + tamper-detection together, so any corruption, truncation,
|
|
7
|
+
* or wrong-secret input decrypts to `null` rather than a partial session.
|
|
8
|
+
* Web Crypto is universal (Node ≥20 + edge runtimes), so this runs anywhere a
|
|
9
|
+
* Next route handler runs.
|
|
9
10
|
*/
|
|
10
|
-
function buildSessionCookieOptions(secure) {
|
|
11
|
-
return {
|
|
12
|
-
httpOnly: true,
|
|
13
|
-
secure,
|
|
14
|
-
sameSite: "lax",
|
|
15
|
-
path: "/"
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
//#endregion
|
|
19
|
-
//#region src/cookie-codec.ts
|
|
20
11
|
const IV_BYTES = 12;
|
|
12
|
+
/**
|
|
13
|
+
* Browsers cap a single cookie at roughly 4096 bytes for the whole `Set-Cookie` pair —
|
|
14
|
+
* name, value and attributes together — and enforce it by DISCARDING the cookie without
|
|
15
|
+
* an error. For a session cookie that failure is invisible and badly misleading: the
|
|
16
|
+
* platform authenticates, the handler answers 200, and the user is bounced back to
|
|
17
|
+
* sign-in on the next request because the cookie was never stored.
|
|
18
|
+
*
|
|
19
|
+
* poly-x-sdk/ADR-0006 measured a live Odin session at 2628 bytes and recorded the missing
|
|
20
|
+
* guard as an open follow-up; a per-tenant flag that inflates the JWT is enough to cross
|
|
21
|
+
* the line. We fail loudly here instead.
|
|
22
|
+
*/
|
|
23
|
+
const SESSION_COOKIE_MAX_BYTES = 4096;
|
|
24
|
+
/** Attributes `buildSessionCookieOptions` always sets: `Path=/; HttpOnly; Secure; SameSite=Lax`. */
|
|
25
|
+
const COOKIE_ATTRIBUTE_BYTES = 45;
|
|
26
|
+
/** Bytes the whole `Set-Cookie` pair will occupy for this value under this cookie name. */
|
|
27
|
+
function sealedCookieByteLength(value, cookieName = "polyx_session") {
|
|
28
|
+
return new TextEncoder().encode(cookieName).length + 1 + value.length + COOKIE_ATTRIBUTE_BYTES;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Thrown when a sealed session would exceed what a browser will store. Never includes the
|
|
32
|
+
* session itself — the claims that caused the overflow are exactly the data we must not log.
|
|
33
|
+
*/
|
|
34
|
+
var SessionCookieTooLargeError = class extends PolyXError {
|
|
35
|
+
bytes;
|
|
36
|
+
limit;
|
|
37
|
+
constructor(bytes, limit, cookieName) {
|
|
38
|
+
super(`PolyX session cookie is ${bytes} bytes, over the ~${limit}-byte browser limit for "${cookieName}". The browser would discard it silently and the user would appear signed out. Reduce what the platform puts in the session — the usual cause is a tenant configured to embed full permissions in the JWT (includeFullPermissionInJwt).`, { code: "SESSION_COOKIE_TOO_LARGE" });
|
|
39
|
+
this.bytes = bytes;
|
|
40
|
+
this.limit = limit;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
21
43
|
function base64UrlEncode(bytes) {
|
|
22
44
|
let binary = "";
|
|
23
45
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
@@ -35,7 +57,7 @@ async function deriveKey(secret) {
|
|
|
35
57
|
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(secret));
|
|
36
58
|
return crypto.subtle.importKey("raw", hash, "AES-GCM", false, ["encrypt", "decrypt"]);
|
|
37
59
|
}
|
|
38
|
-
async function sealSession(session, secret) {
|
|
60
|
+
async function sealSession(session, secret, options = {}) {
|
|
39
61
|
const key = await deriveKey(secret);
|
|
40
62
|
const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
|
|
41
63
|
const plaintext = new TextEncoder().encode(JSON.stringify(session));
|
|
@@ -46,7 +68,11 @@ async function sealSession(session, secret) {
|
|
|
46
68
|
const packed = new Uint8Array(iv.length + ciphertext.length);
|
|
47
69
|
packed.set(iv);
|
|
48
70
|
packed.set(ciphertext, iv.length);
|
|
49
|
-
|
|
71
|
+
const sealed = base64UrlEncode(packed);
|
|
72
|
+
const cookieName = options.cookieName ?? "polyx_session";
|
|
73
|
+
const bytes = sealedCookieByteLength(sealed, cookieName);
|
|
74
|
+
if (bytes > 4096) throw new SessionCookieTooLargeError(bytes, SESSION_COOKIE_MAX_BYTES, cookieName);
|
|
75
|
+
return sealed;
|
|
50
76
|
}
|
|
51
77
|
async function openSession(token, secret) {
|
|
52
78
|
try {
|
|
@@ -65,6 +91,23 @@ async function openSession(token, secret) {
|
|
|
65
91
|
}
|
|
66
92
|
}
|
|
67
93
|
//#endregion
|
|
94
|
+
//#region src/cookie-adapter.ts
|
|
95
|
+
/**
|
|
96
|
+
* Attributes for the session cookie (ADR-0004 secure custody). `secure` is decided at
|
|
97
|
+
* write time from the actual transport: a `Secure` cookie is silently dropped by the
|
|
98
|
+
* browser over plain http, so http/localhost dev must set `secure:false` while https
|
|
99
|
+
* always sets `secure:true`. This is not a downgrade — `Secure` carries no meaning on
|
|
100
|
+
* http; setting it there just breaks the session. httpOnly/sameSite/path are unchanged.
|
|
101
|
+
*/
|
|
102
|
+
function buildSessionCookieOptions(secure) {
|
|
103
|
+
return {
|
|
104
|
+
httpOnly: true,
|
|
105
|
+
secure,
|
|
106
|
+
sameSite: "lax",
|
|
107
|
+
path: "/"
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
//#endregion
|
|
68
111
|
//#region src/server-session.ts
|
|
69
112
|
/**
|
|
70
113
|
* The framework-agnostic BFF session controller (F007): tokens sealed in an
|
|
@@ -113,6 +156,11 @@ var ServerSession = class {
|
|
|
113
156
|
this.clear();
|
|
114
157
|
return null;
|
|
115
158
|
}
|
|
159
|
+
if (error instanceof SessionCookieTooLargeError) {
|
|
160
|
+
console.error(`[polyx] ${error.message}`);
|
|
161
|
+
this.clear();
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
116
164
|
throw error;
|
|
117
165
|
}
|
|
118
166
|
}
|
|
@@ -138,7 +186,7 @@ var ServerSession = class {
|
|
|
138
186
|
this.cookies.delete(this.cookieName);
|
|
139
187
|
}
|
|
140
188
|
async write(session) {
|
|
141
|
-
const sealed = await sealSession(session, this.secret);
|
|
189
|
+
const sealed = await sealSession(session, this.secret, { cookieName: this.cookieName });
|
|
142
190
|
this.cookies.set(this.cookieName, sealed, buildSessionCookieOptions(this.secure));
|
|
143
191
|
}
|
|
144
192
|
};
|
|
@@ -156,4 +204,4 @@ function toAuthContext(session) {
|
|
|
156
204
|
};
|
|
157
205
|
}
|
|
158
206
|
//#endregion
|
|
159
|
-
export {
|
|
207
|
+
export { SessionCookieTooLargeError as a, sealedCookieByteLength as c, SESSION_COOKIE_MAX_BYTES as i, ServerSession as n, openSession as o, toAuthContext as r, sealSession as s, DEFAULT_SESSION_COOKIE as t };
|
package/dist/server.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_server_session = require("./server-session-
|
|
2
|
+
const require_server_session = require("./server-session-Df9HXl_5.cjs");
|
|
3
3
|
let _poly_x_core = require("@poly-x/core");
|
|
4
4
|
let next_headers = require("next/headers");
|
|
5
5
|
//#region src/config.ts
|
|
@@ -223,16 +223,30 @@ function createAuthHandlers(handlersConfig = {}) {
|
|
|
223
223
|
tenantId
|
|
224
224
|
});
|
|
225
225
|
switch (outcome.type) {
|
|
226
|
-
case "authorized":
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
226
|
+
case "authorized": {
|
|
227
|
+
const store = await (0, next_headers.cookies)();
|
|
228
|
+
try {
|
|
229
|
+
await newSession(adapt(store), config, secure).establishFromCode(outcome.code, verifier, redirectUri, {
|
|
230
|
+
...collectForwardedDeviceHeaders(request),
|
|
231
|
+
...geoHeaders,
|
|
232
|
+
...deviceContextHeaders
|
|
233
|
+
});
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (error instanceof require_server_session.SessionCookieTooLargeError) {
|
|
236
|
+
console.error(`[polyx] ${error.message}`);
|
|
237
|
+
return Response.json({
|
|
238
|
+
status: "session_too_large",
|
|
239
|
+
bytes: error.bytes,
|
|
240
|
+
limit: error.limit
|
|
241
|
+
}, { status: 500 });
|
|
242
|
+
}
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
232
245
|
return Response.json({
|
|
233
246
|
status: "success",
|
|
234
247
|
redirectTo: afterSignInPath
|
|
235
248
|
});
|
|
249
|
+
}
|
|
236
250
|
case "select_tenant": return Response.json({
|
|
237
251
|
status: "select_tenant",
|
|
238
252
|
tenants: outcome.tenants,
|
|
@@ -328,7 +342,15 @@ function createAuthHandlers(handlersConfig = {}) {
|
|
|
328
342
|
if (!code || !state || !raw) return Response.redirect(home, 302);
|
|
329
343
|
const tx = JSON.parse(raw);
|
|
330
344
|
if (tx.state !== state) return Response.redirect(home, 302);
|
|
331
|
-
|
|
345
|
+
try {
|
|
346
|
+
await newSession(adapt(store), config, secure).establishFromCode(code, tx.verifier, tx.redirectUri, collectForwardedDeviceHeaders(request));
|
|
347
|
+
} catch (error) {
|
|
348
|
+
if (error instanceof require_server_session.SessionCookieTooLargeError) {
|
|
349
|
+
console.error(`[polyx] ${error.message}`);
|
|
350
|
+
return Response.redirect(home, 302);
|
|
351
|
+
}
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
332
354
|
return Response.redirect(home, 302);
|
|
333
355
|
},
|
|
334
356
|
async refresh(request) {
|
|
@@ -375,9 +397,12 @@ function createAuthHandlers(handlersConfig = {}) {
|
|
|
375
397
|
}
|
|
376
398
|
//#endregion
|
|
377
399
|
exports.DEFAULT_SESSION_COOKIE = require_server_session.DEFAULT_SESSION_COOKIE;
|
|
400
|
+
exports.SESSION_COOKIE_MAX_BYTES = require_server_session.SESSION_COOKIE_MAX_BYTES;
|
|
378
401
|
exports.ServerSession = require_server_session.ServerSession;
|
|
402
|
+
exports.SessionCookieTooLargeError = require_server_session.SessionCookieTooLargeError;
|
|
379
403
|
exports.auth = auth;
|
|
380
404
|
exports.createAuthHandlers = createAuthHandlers;
|
|
381
405
|
exports.openSession = require_server_session.openSession;
|
|
382
406
|
exports.resolveNextConfig = resolveNextConfig;
|
|
383
407
|
exports.sealSession = require_server_session.sealSession;
|
|
408
|
+
exports.sealedCookieByteLength = require_server_session.sealedCookieByteLength;
|
package/dist/server.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AuthClient, StoredSession } from "@poly-x/core";
|
|
1
|
+
import { AuthClient, PolyXError, StoredSession } from "@poly-x/core";
|
|
2
2
|
|
|
3
3
|
//#region src/config.d.ts
|
|
4
4
|
interface NextServerConfig {
|
|
@@ -85,7 +85,34 @@ interface AuthContext {
|
|
|
85
85
|
}
|
|
86
86
|
//#endregion
|
|
87
87
|
//#region src/cookie-codec.d.ts
|
|
88
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Browsers cap a single cookie at roughly 4096 bytes for the whole `Set-Cookie` pair —
|
|
90
|
+
* name, value and attributes together — and enforce it by DISCARDING the cookie without
|
|
91
|
+
* an error. For a session cookie that failure is invisible and badly misleading: the
|
|
92
|
+
* platform authenticates, the handler answers 200, and the user is bounced back to
|
|
93
|
+
* sign-in on the next request because the cookie was never stored.
|
|
94
|
+
*
|
|
95
|
+
* poly-x-sdk/ADR-0006 measured a live Odin session at 2628 bytes and recorded the missing
|
|
96
|
+
* guard as an open follow-up; a per-tenant flag that inflates the JWT is enough to cross
|
|
97
|
+
* the line. We fail loudly here instead.
|
|
98
|
+
*/
|
|
99
|
+
declare const SESSION_COOKIE_MAX_BYTES = 4096;
|
|
100
|
+
/** Bytes the whole `Set-Cookie` pair will occupy for this value under this cookie name. */
|
|
101
|
+
declare function sealedCookieByteLength(value: string, cookieName?: string): number;
|
|
102
|
+
/**
|
|
103
|
+
* Thrown when a sealed session would exceed what a browser will store. Never includes the
|
|
104
|
+
* session itself — the claims that caused the overflow are exactly the data we must not log.
|
|
105
|
+
*/
|
|
106
|
+
declare class SessionCookieTooLargeError extends PolyXError {
|
|
107
|
+
readonly bytes: number;
|
|
108
|
+
readonly limit: number;
|
|
109
|
+
constructor(bytes: number, limit: number, cookieName: string);
|
|
110
|
+
}
|
|
111
|
+
interface SealSessionOptions {
|
|
112
|
+
/** The cookie name the value will be stored under; counted against the browser budget. */
|
|
113
|
+
cookieName?: string;
|
|
114
|
+
}
|
|
115
|
+
declare function sealSession(session: StoredSession, secret: string, options?: SealSessionOptions): Promise<string>;
|
|
89
116
|
declare function openSession(token: string, secret: string): Promise<StoredSession | null>;
|
|
90
117
|
//#endregion
|
|
91
118
|
//#region src/server.d.ts
|
|
@@ -175,9 +202,13 @@ type AuthenticateResult = {
|
|
|
175
202
|
status: "no_access";
|
|
176
203
|
} | {
|
|
177
204
|
status: "invalid_credentials";
|
|
205
|
+
} /** Credentials were correct, but the sealed session exceeds what a browser will store. */ | {
|
|
206
|
+
status: "session_too_large";
|
|
207
|
+
bytes: number;
|
|
208
|
+
limit: number;
|
|
178
209
|
} | {
|
|
179
210
|
status: "invalid_request";
|
|
180
211
|
};
|
|
181
212
|
declare function createAuthHandlers(handlersConfig?: AuthHandlersConfig): AuthHandlers;
|
|
182
213
|
//#endregion
|
|
183
|
-
export { type AuthContext, AuthHandlers, AuthHandlersConfig, AuthenticateResult, type CookieAdapter, type CookieSetOptions, DEFAULT_SESSION_COOKIE, ForgotPasswordResult, type NextServerConfig, ResetPasswordResult, ServerSession, auth, createAuthHandlers, openSession, resolveNextConfig, sealSession };
|
|
214
|
+
export { type AuthContext, AuthHandlers, AuthHandlersConfig, AuthenticateResult, type CookieAdapter, type CookieSetOptions, DEFAULT_SESSION_COOKIE, ForgotPasswordResult, type NextServerConfig, ResetPasswordResult, SESSION_COOKIE_MAX_BYTES, ServerSession, SessionCookieTooLargeError, auth, createAuthHandlers, openSession, resolveNextConfig, sealSession, sealedCookieByteLength };
|
package/dist/server.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AuthClient, StoredSession } from "@poly-x/core";
|
|
1
|
+
import { AuthClient, PolyXError, StoredSession } from "@poly-x/core";
|
|
2
2
|
|
|
3
3
|
//#region src/config.d.ts
|
|
4
4
|
interface NextServerConfig {
|
|
@@ -85,7 +85,34 @@ interface AuthContext {
|
|
|
85
85
|
}
|
|
86
86
|
//#endregion
|
|
87
87
|
//#region src/cookie-codec.d.ts
|
|
88
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Browsers cap a single cookie at roughly 4096 bytes for the whole `Set-Cookie` pair —
|
|
90
|
+
* name, value and attributes together — and enforce it by DISCARDING the cookie without
|
|
91
|
+
* an error. For a session cookie that failure is invisible and badly misleading: the
|
|
92
|
+
* platform authenticates, the handler answers 200, and the user is bounced back to
|
|
93
|
+
* sign-in on the next request because the cookie was never stored.
|
|
94
|
+
*
|
|
95
|
+
* poly-x-sdk/ADR-0006 measured a live Odin session at 2628 bytes and recorded the missing
|
|
96
|
+
* guard as an open follow-up; a per-tenant flag that inflates the JWT is enough to cross
|
|
97
|
+
* the line. We fail loudly here instead.
|
|
98
|
+
*/
|
|
99
|
+
declare const SESSION_COOKIE_MAX_BYTES = 4096;
|
|
100
|
+
/** Bytes the whole `Set-Cookie` pair will occupy for this value under this cookie name. */
|
|
101
|
+
declare function sealedCookieByteLength(value: string, cookieName?: string): number;
|
|
102
|
+
/**
|
|
103
|
+
* Thrown when a sealed session would exceed what a browser will store. Never includes the
|
|
104
|
+
* session itself — the claims that caused the overflow are exactly the data we must not log.
|
|
105
|
+
*/
|
|
106
|
+
declare class SessionCookieTooLargeError extends PolyXError {
|
|
107
|
+
readonly bytes: number;
|
|
108
|
+
readonly limit: number;
|
|
109
|
+
constructor(bytes: number, limit: number, cookieName: string);
|
|
110
|
+
}
|
|
111
|
+
interface SealSessionOptions {
|
|
112
|
+
/** The cookie name the value will be stored under; counted against the browser budget. */
|
|
113
|
+
cookieName?: string;
|
|
114
|
+
}
|
|
115
|
+
declare function sealSession(session: StoredSession, secret: string, options?: SealSessionOptions): Promise<string>;
|
|
89
116
|
declare function openSession(token: string, secret: string): Promise<StoredSession | null>;
|
|
90
117
|
//#endregion
|
|
91
118
|
//#region src/server.d.ts
|
|
@@ -175,9 +202,13 @@ type AuthenticateResult = {
|
|
|
175
202
|
status: "no_access";
|
|
176
203
|
} | {
|
|
177
204
|
status: "invalid_credentials";
|
|
205
|
+
} /** Credentials were correct, but the sealed session exceeds what a browser will store. */ | {
|
|
206
|
+
status: "session_too_large";
|
|
207
|
+
bytes: number;
|
|
208
|
+
limit: number;
|
|
178
209
|
} | {
|
|
179
210
|
status: "invalid_request";
|
|
180
211
|
};
|
|
181
212
|
declare function createAuthHandlers(handlersConfig?: AuthHandlersConfig): AuthHandlers;
|
|
182
213
|
//#endregion
|
|
183
|
-
export { type AuthContext, AuthHandlers, AuthHandlersConfig, AuthenticateResult, type CookieAdapter, type CookieSetOptions, DEFAULT_SESSION_COOKIE, ForgotPasswordResult, type NextServerConfig, ResetPasswordResult, ServerSession, auth, createAuthHandlers, openSession, resolveNextConfig, sealSession };
|
|
214
|
+
export { type AuthContext, AuthHandlers, AuthHandlersConfig, AuthenticateResult, type CookieAdapter, type CookieSetOptions, DEFAULT_SESSION_COOKIE, ForgotPasswordResult, type NextServerConfig, ResetPasswordResult, SESSION_COOKIE_MAX_BYTES, ServerSession, SessionCookieTooLargeError, auth, createAuthHandlers, openSession, resolveNextConfig, sealSession, sealedCookieByteLength };
|
package/dist/server.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as SessionCookieTooLargeError, c as sealedCookieByteLength, i as SESSION_COOKIE_MAX_BYTES, n as ServerSession, o as openSession, r as toAuthContext, s as sealSession, t as DEFAULT_SESSION_COOKIE } from "./server-session-pY-nzh5x.mjs";
|
|
2
2
|
import { AuthClient, FetchTransport, generatePkce, randomState, resolveConfig } from "@poly-x/core";
|
|
3
3
|
import { cookies } from "next/headers";
|
|
4
4
|
//#region src/config.ts
|
|
@@ -222,16 +222,30 @@ function createAuthHandlers(handlersConfig = {}) {
|
|
|
222
222
|
tenantId
|
|
223
223
|
});
|
|
224
224
|
switch (outcome.type) {
|
|
225
|
-
case "authorized":
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
225
|
+
case "authorized": {
|
|
226
|
+
const store = await cookies();
|
|
227
|
+
try {
|
|
228
|
+
await newSession(adapt(store), config, secure).establishFromCode(outcome.code, verifier, redirectUri, {
|
|
229
|
+
...collectForwardedDeviceHeaders(request),
|
|
230
|
+
...geoHeaders,
|
|
231
|
+
...deviceContextHeaders
|
|
232
|
+
});
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if (error instanceof SessionCookieTooLargeError) {
|
|
235
|
+
console.error(`[polyx] ${error.message}`);
|
|
236
|
+
return Response.json({
|
|
237
|
+
status: "session_too_large",
|
|
238
|
+
bytes: error.bytes,
|
|
239
|
+
limit: error.limit
|
|
240
|
+
}, { status: 500 });
|
|
241
|
+
}
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
231
244
|
return Response.json({
|
|
232
245
|
status: "success",
|
|
233
246
|
redirectTo: afterSignInPath
|
|
234
247
|
});
|
|
248
|
+
}
|
|
235
249
|
case "select_tenant": return Response.json({
|
|
236
250
|
status: "select_tenant",
|
|
237
251
|
tenants: outcome.tenants,
|
|
@@ -327,7 +341,15 @@ function createAuthHandlers(handlersConfig = {}) {
|
|
|
327
341
|
if (!code || !state || !raw) return Response.redirect(home, 302);
|
|
328
342
|
const tx = JSON.parse(raw);
|
|
329
343
|
if (tx.state !== state) return Response.redirect(home, 302);
|
|
330
|
-
|
|
344
|
+
try {
|
|
345
|
+
await newSession(adapt(store), config, secure).establishFromCode(code, tx.verifier, tx.redirectUri, collectForwardedDeviceHeaders(request));
|
|
346
|
+
} catch (error) {
|
|
347
|
+
if (error instanceof SessionCookieTooLargeError) {
|
|
348
|
+
console.error(`[polyx] ${error.message}`);
|
|
349
|
+
return Response.redirect(home, 302);
|
|
350
|
+
}
|
|
351
|
+
throw error;
|
|
352
|
+
}
|
|
331
353
|
return Response.redirect(home, 302);
|
|
332
354
|
},
|
|
333
355
|
async refresh(request) {
|
|
@@ -373,4 +395,4 @@ function createAuthHandlers(handlersConfig = {}) {
|
|
|
373
395
|
};
|
|
374
396
|
}
|
|
375
397
|
//#endregion
|
|
376
|
-
export { DEFAULT_SESSION_COOKIE, ServerSession, auth, createAuthHandlers, openSession, resolveNextConfig, sealSession };
|
|
398
|
+
export { DEFAULT_SESSION_COOKIE, SESSION_COOKIE_MAX_BYTES, ServerSession, SessionCookieTooLargeError, auth, createAuthHandlers, openSession, resolveNextConfig, sealSession, sealedCookieByteLength };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poly-x/next",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "PolyX SDK for Next.js App Router - components plus server helpers",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
"module": "./dist/index.mjs",
|
|
48
48
|
"types": "./dist/index.d.cts",
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@poly-x/core": "0.1.
|
|
51
|
-
"@poly-x/react": "0.1.
|
|
50
|
+
"@poly-x/core": "0.1.1",
|
|
51
|
+
"@poly-x/react": "0.1.1"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"next": ">=14",
|