@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 CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- require("./server-session-QJLA_Tz-.cjs");
2
+ require("./server-session-Df9HXl_5.cjs");
3
3
  let next_server = require("next/server");
4
4
  //#region src/proxy.ts
5
5
  /**
package/dist/proxy.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import "./server-session-CjP44zFC.mjs";
1
+ import "./server-session-pY-nzh5x.mjs";
2
2
  import { NextResponse } from "next/server";
3
3
  //#region src/proxy.ts
4
4
  /**
@@ -1,23 +1,45 @@
1
1
  let _poly_x_core = require("@poly-x/core");
2
- //#region src/cookie-adapter.ts
2
+ //#region src/cookie-codec.ts
3
3
  /**
4
- * Attributes for the session cookie (ADR-0004 secure custody). `secure` is decided at
5
- * write time from the actual transport: a `Secure` cookie is silently dropped by the
6
- * browser over plain http, so http/localhost dev must set `secure:false` while https
7
- * always sets `secure:true`. This is not a downgrade — `Secure` carries no meaning on
8
- * http; setting it there just breaks the session. httpOnly/sameSite/path are unchanged.
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
- return base64UrlEncode(packed);
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-adapter.ts
1
+ import { PolyXError, SessionExpiredError, SessionRevokedError } from "@poly-x/core";
2
+ //#region src/cookie-codec.ts
3
3
  /**
4
- * Attributes for the session cookie (ADR-0004 secure custody). `secure` is decided at
5
- * write time from the actual transport: a `Secure` cookie is silently dropped by the
6
- * browser over plain http, so http/localhost dev must set `secure:false` while https
7
- * always sets `secure:true`. This is not a downgrade — `Secure` carries no meaning on
8
- * http; setting it there just breaks the session. httpOnly/sameSite/path are unchanged.
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
- return base64UrlEncode(packed);
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 { sealSession as a, openSession as i, ServerSession as n, toAuthContext as r, DEFAULT_SESSION_COOKIE as t };
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-QJLA_Tz-.cjs");
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
- await newSession(adapt(await (0, next_headers.cookies)()), config, secure).establishFromCode(outcome.code, verifier, redirectUri, {
228
- ...collectForwardedDeviceHeaders(request),
229
- ...geoHeaders,
230
- ...deviceContextHeaders
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
- await newSession(adapt(store), config, secure).establishFromCode(code, tx.verifier, tx.redirectUri, collectForwardedDeviceHeaders(request));
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
- declare function sealSession(session: StoredSession, secret: string): Promise<string>;
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
- declare function sealSession(session: StoredSession, secret: string): Promise<string>;
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 sealSession, i as openSession, n as ServerSession, r as toAuthContext, t as DEFAULT_SESSION_COOKIE } from "./server-session-CjP44zFC.mjs";
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
- await newSession(adapt(await cookies()), config, secure).establishFromCode(outcome.code, verifier, redirectUri, {
227
- ...collectForwardedDeviceHeaders(request),
228
- ...geoHeaders,
229
- ...deviceContextHeaders
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
- await newSession(adapt(store), config, secure).establishFromCode(code, tx.verifier, tx.redirectUri, collectForwardedDeviceHeaders(request));
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.0",
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.0",
51
- "@poly-x/react": "0.1.0"
50
+ "@poly-x/core": "0.1.1",
51
+ "@poly-x/react": "0.1.1"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "next": ">=14",