@poly-x/next 0.1.0-alpha.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/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # @poly-x/next
2
+
3
+ PolyX authentication for the Next.js App Router. Everything in `@poly-x/react`,
4
+ plus a server side: tokens live **AES-GCM-encrypted in an httpOnly cookie on your
5
+ own domain** — browser JavaScript never sees them.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @poly-x/next
11
+ ```
12
+
13
+ Set two env vars: `NEXT_PUBLIC_POLYX_PUBLISHABLE_KEY` (the `pk_…` key) and
14
+ `POLYX_SESSION_SECRET` (used to encrypt the session cookie).
15
+
16
+ ## Client
17
+
18
+ The client entry re-exports `@poly-x/react` — `PolyXProvider`, hooks, `<SignIn>`,
19
+ `<Protected>`, and friends:
20
+
21
+ ```tsx
22
+ import { PolyXProvider } from "@poly-x/next";
23
+ ```
24
+
25
+ ## Server
26
+
27
+ Read the session in a Server Component or route handler (read-only — Server
28
+ Components can't set cookies):
29
+
30
+ ```ts
31
+ import { auth } from "@poly-x/next/server";
32
+
33
+ export default async function Page() {
34
+ const { isSignedIn, userId } = await auth();
35
+ // …
36
+ }
37
+ ```
38
+
39
+ Mount the route handlers that own every cookie write (login / callback / refresh
40
+ / signout):
41
+
42
+ ```ts
43
+ // app/api/polyx/[...action]/route.ts
44
+ import { createAuthHandlers } from "@poly-x/next/server";
45
+ export const { signin: GET } = createAuthHandlers();
46
+ ```
47
+
48
+ Gate routes with the proxy (Next 16 `proxy.ts`; `middleware.ts` on ≤15):
49
+
50
+ ```ts
51
+ import { proxy } from "@poly-x/next/proxy";
52
+ export const proxy = proxyHandler; // from proxy({ protectedRoutes: ["/dashboard"] })
53
+ ```
54
+
55
+ ## Exports
56
+
57
+ `@poly-x/next` (client) · `@poly-x/next/server` (`auth`, `createAuthHandlers`,
58
+ `ServerSession`) · `@poly-x/next/proxy` (`proxy`).
59
+
60
+ MIT licensed.
package/dist/index.cjs ADDED
@@ -0,0 +1,17 @@
1
+ "use client";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ //#region src/index.ts
4
+ const PACKAGE_NAME = "@poly-x/next";
5
+ const version = "0.0.0";
6
+ //#endregion
7
+ exports.PACKAGE_NAME = PACKAGE_NAME;
8
+ exports.version = version;
9
+ var _poly_x_react = require("@poly-x/react");
10
+ Object.keys(_poly_x_react).forEach(function(k) {
11
+ if (k !== "default" && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
12
+ enumerable: true,
13
+ get: function() {
14
+ return _poly_x_react[k];
15
+ }
16
+ });
17
+ });
@@ -0,0 +1,7 @@
1
+ export * from "@poly-x/react";
2
+
3
+ //#region src/index.d.ts
4
+ declare const PACKAGE_NAME = "@poly-x/next";
5
+ declare const version = "0.0.0";
6
+ //#endregion
7
+ export { PACKAGE_NAME, version };
@@ -0,0 +1,7 @@
1
+ export * from "@poly-x/react";
2
+
3
+ //#region src/index.d.ts
4
+ declare const PACKAGE_NAME = "@poly-x/next";
5
+ declare const version = "0.0.0";
6
+ //#endregion
7
+ export { PACKAGE_NAME, version };
package/dist/index.mjs ADDED
@@ -0,0 +1,7 @@
1
+ "use client";
2
+ export * from "@poly-x/react";
3
+ //#region src/index.ts
4
+ const PACKAGE_NAME = "@poly-x/next";
5
+ const version = "0.0.0";
6
+ //#endregion
7
+ export { PACKAGE_NAME, version };
package/dist/proxy.cjs ADDED
@@ -0,0 +1,24 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ require("./server-session-YbnXBt3c.cjs");
3
+ let next_server = require("next/server");
4
+ //#region src/proxy.ts
5
+ /**
6
+ * @poly-x/next/proxy — route gating (F007). In Next 16 this is wired via
7
+ * `proxy.ts` (`export { proxy as proxy }`); on ≤15 it's `middleware.ts`. It
8
+ * gates on the session cookie's *presence* (a fast edge check) and redirects
9
+ * signed-out users to sign-in; `auth()` does the real decrypt server-side.
10
+ */
11
+ function isProtected(pathname, routes) {
12
+ return routes.some((route) => typeof route === "string" ? pathname.startsWith(route) : route.test(pathname));
13
+ }
14
+ function proxy(config) {
15
+ const cookieName = config.cookieName ?? "polyx_session";
16
+ const signInPath = config.signInPath ?? "/api/polyx/signin";
17
+ return (request) => {
18
+ if (!isProtected(request.nextUrl.pathname, config.protectedRoutes)) return next_server.NextResponse.next();
19
+ if (request.cookies.get(cookieName)?.value) return next_server.NextResponse.next();
20
+ return next_server.NextResponse.redirect(new URL(signInPath, request.url));
21
+ };
22
+ }
23
+ //#endregion
24
+ exports.proxy = proxy;
@@ -0,0 +1,13 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+
3
+ //#region src/proxy.d.ts
4
+ interface ProxyConfig {
5
+ /** Path prefixes or patterns that require a session. */
6
+ protectedRoutes: Array<string | RegExp>;
7
+ /** Where to send signed-out users. Default `/api/polyx/signin`. */
8
+ signInPath?: string;
9
+ cookieName?: string;
10
+ }
11
+ declare function proxy(config: ProxyConfig): (request: NextRequest) => NextResponse;
12
+ //#endregion
13
+ export { ProxyConfig, proxy };
@@ -0,0 +1,13 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+
3
+ //#region src/proxy.d.ts
4
+ interface ProxyConfig {
5
+ /** Path prefixes or patterns that require a session. */
6
+ protectedRoutes: Array<string | RegExp>;
7
+ /** Where to send signed-out users. Default `/api/polyx/signin`. */
8
+ signInPath?: string;
9
+ cookieName?: string;
10
+ }
11
+ declare function proxy(config: ProxyConfig): (request: NextRequest) => NextResponse;
12
+ //#endregion
13
+ export { ProxyConfig, proxy };
package/dist/proxy.mjs ADDED
@@ -0,0 +1,23 @@
1
+ import "./server-session-B3zJ7CS8.mjs";
2
+ import { NextResponse } from "next/server";
3
+ //#region src/proxy.ts
4
+ /**
5
+ * @poly-x/next/proxy — route gating (F007). In Next 16 this is wired via
6
+ * `proxy.ts` (`export { proxy as proxy }`); on ≤15 it's `middleware.ts`. It
7
+ * gates on the session cookie's *presence* (a fast edge check) and redirects
8
+ * signed-out users to sign-in; `auth()` does the real decrypt server-side.
9
+ */
10
+ function isProtected(pathname, routes) {
11
+ return routes.some((route) => typeof route === "string" ? pathname.startsWith(route) : route.test(pathname));
12
+ }
13
+ function proxy(config) {
14
+ const cookieName = config.cookieName ?? "polyx_session";
15
+ const signInPath = config.signInPath ?? "/api/polyx/signin";
16
+ return (request) => {
17
+ if (!isProtected(request.nextUrl.pathname, config.protectedRoutes)) return NextResponse.next();
18
+ if (request.cookies.get(cookieName)?.value) return NextResponse.next();
19
+ return NextResponse.redirect(new URL(signInPath, request.url));
20
+ };
21
+ }
22
+ //#endregion
23
+ export { proxy };
@@ -0,0 +1,130 @@
1
+ import { SessionExpiredError, SessionRevokedError } from "@poly-x/core";
2
+ //#region src/cookie-adapter.ts
3
+ /** Default attributes for the session cookie (ADR-0004 secure custody). */
4
+ const SESSION_COOKIE_OPTIONS = {
5
+ httpOnly: true,
6
+ secure: true,
7
+ sameSite: "lax",
8
+ path: "/"
9
+ };
10
+ //#endregion
11
+ //#region src/cookie-codec.ts
12
+ const IV_BYTES = 12;
13
+ function base64UrlEncode(bytes) {
14
+ let binary = "";
15
+ for (const byte of bytes) binary += String.fromCharCode(byte);
16
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
17
+ }
18
+ function base64UrlDecode(text) {
19
+ const base64 = text.replace(/-/g, "+").replace(/_/g, "/");
20
+ const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
21
+ const binary = atob(padded);
22
+ const bytes = new Uint8Array(binary.length);
23
+ for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
24
+ return bytes;
25
+ }
26
+ async function deriveKey(secret) {
27
+ const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(secret));
28
+ return crypto.subtle.importKey("raw", hash, "AES-GCM", false, ["encrypt", "decrypt"]);
29
+ }
30
+ async function sealSession(session, secret) {
31
+ const key = await deriveKey(secret);
32
+ const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
33
+ const plaintext = new TextEncoder().encode(JSON.stringify(session));
34
+ const ciphertext = new Uint8Array(await crypto.subtle.encrypt({
35
+ name: "AES-GCM",
36
+ iv
37
+ }, key, plaintext));
38
+ const packed = new Uint8Array(iv.length + ciphertext.length);
39
+ packed.set(iv);
40
+ packed.set(ciphertext, iv.length);
41
+ return base64UrlEncode(packed);
42
+ }
43
+ async function openSession(token, secret) {
44
+ try {
45
+ const packed = base64UrlDecode(token);
46
+ if (packed.length <= IV_BYTES) return null;
47
+ const iv = packed.slice(0, IV_BYTES);
48
+ const ciphertext = packed.slice(IV_BYTES);
49
+ const key = await deriveKey(secret);
50
+ const plaintext = await crypto.subtle.decrypt({
51
+ name: "AES-GCM",
52
+ iv
53
+ }, key, ciphertext);
54
+ return JSON.parse(new TextDecoder().decode(plaintext));
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+ //#endregion
60
+ //#region src/server-session.ts
61
+ /**
62
+ * The framework-agnostic BFF session controller (F007): tokens sealed in an
63
+ * httpOnly cookie on the consumer's domain, exchanged/refreshed server-side.
64
+ * Composes core's AuthClient with a CookieAdapter seam; fully unit-testable.
65
+ */
66
+ const DEFAULT_SESSION_COOKIE = "polyx_session";
67
+ var ServerSession = class {
68
+ authClient;
69
+ secret;
70
+ cookies;
71
+ cookieName;
72
+ constructor(options) {
73
+ this.authClient = options.authClient;
74
+ this.secret = options.secret;
75
+ this.cookies = options.cookies;
76
+ this.cookieName = options.cookieName ?? "polyx_session";
77
+ }
78
+ async establishFromCode(code, codeVerifier, redirectUri) {
79
+ const stored = await this.authClient.exchangeCode({
80
+ code,
81
+ codeVerifier,
82
+ redirectUri
83
+ });
84
+ await this.write(stored);
85
+ return stored;
86
+ }
87
+ async read() {
88
+ const raw = this.cookies.get(this.cookieName);
89
+ if (!raw) return null;
90
+ return openSession(raw, this.secret);
91
+ }
92
+ /** Rotate the tokens; clears the cookie (returns null) on a terminal refresh failure. */
93
+ async refresh() {
94
+ const current = await this.read();
95
+ if (!current) return null;
96
+ try {
97
+ const next = await this.authClient.createRefresher()(current);
98
+ await this.write(next);
99
+ return next;
100
+ } catch (error) {
101
+ if (error instanceof SessionRevokedError || error instanceof SessionExpiredError) {
102
+ this.clear();
103
+ return null;
104
+ }
105
+ throw error;
106
+ }
107
+ }
108
+ clear() {
109
+ this.cookies.delete(this.cookieName);
110
+ }
111
+ async write(session) {
112
+ const sealed = await sealSession(session, this.secret);
113
+ this.cookies.set(this.cookieName, sealed, SESSION_COOKIE_OPTIONS);
114
+ }
115
+ };
116
+ function toAuthContext(session) {
117
+ if (!session) return {
118
+ isSignedIn: false,
119
+ userId: null,
120
+ claims: null
121
+ };
122
+ return {
123
+ isSignedIn: true,
124
+ userId: session.session.claims.userId,
125
+ organizationId: session.session.claims.organizationId,
126
+ claims: session.session.claims
127
+ };
128
+ }
129
+ //#endregion
130
+ export { sealSession as a, openSession as i, ServerSession as n, toAuthContext as r, DEFAULT_SESSION_COOKIE as t };
@@ -0,0 +1,159 @@
1
+ let _poly_x_core = require("@poly-x/core");
2
+ //#region src/cookie-adapter.ts
3
+ /** Default attributes for the session cookie (ADR-0004 secure custody). */
4
+ const SESSION_COOKIE_OPTIONS = {
5
+ httpOnly: true,
6
+ secure: true,
7
+ sameSite: "lax",
8
+ path: "/"
9
+ };
10
+ //#endregion
11
+ //#region src/cookie-codec.ts
12
+ const IV_BYTES = 12;
13
+ function base64UrlEncode(bytes) {
14
+ let binary = "";
15
+ for (const byte of bytes) binary += String.fromCharCode(byte);
16
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
17
+ }
18
+ function base64UrlDecode(text) {
19
+ const base64 = text.replace(/-/g, "+").replace(/_/g, "/");
20
+ const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
21
+ const binary = atob(padded);
22
+ const bytes = new Uint8Array(binary.length);
23
+ for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
24
+ return bytes;
25
+ }
26
+ async function deriveKey(secret) {
27
+ const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(secret));
28
+ return crypto.subtle.importKey("raw", hash, "AES-GCM", false, ["encrypt", "decrypt"]);
29
+ }
30
+ async function sealSession(session, secret) {
31
+ const key = await deriveKey(secret);
32
+ const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
33
+ const plaintext = new TextEncoder().encode(JSON.stringify(session));
34
+ const ciphertext = new Uint8Array(await crypto.subtle.encrypt({
35
+ name: "AES-GCM",
36
+ iv
37
+ }, key, plaintext));
38
+ const packed = new Uint8Array(iv.length + ciphertext.length);
39
+ packed.set(iv);
40
+ packed.set(ciphertext, iv.length);
41
+ return base64UrlEncode(packed);
42
+ }
43
+ async function openSession(token, secret) {
44
+ try {
45
+ const packed = base64UrlDecode(token);
46
+ if (packed.length <= IV_BYTES) return null;
47
+ const iv = packed.slice(0, IV_BYTES);
48
+ const ciphertext = packed.slice(IV_BYTES);
49
+ const key = await deriveKey(secret);
50
+ const plaintext = await crypto.subtle.decrypt({
51
+ name: "AES-GCM",
52
+ iv
53
+ }, key, ciphertext);
54
+ return JSON.parse(new TextDecoder().decode(plaintext));
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+ //#endregion
60
+ //#region src/server-session.ts
61
+ /**
62
+ * The framework-agnostic BFF session controller (F007): tokens sealed in an
63
+ * httpOnly cookie on the consumer's domain, exchanged/refreshed server-side.
64
+ * Composes core's AuthClient with a CookieAdapter seam; fully unit-testable.
65
+ */
66
+ const DEFAULT_SESSION_COOKIE = "polyx_session";
67
+ var ServerSession = class {
68
+ authClient;
69
+ secret;
70
+ cookies;
71
+ cookieName;
72
+ constructor(options) {
73
+ this.authClient = options.authClient;
74
+ this.secret = options.secret;
75
+ this.cookies = options.cookies;
76
+ this.cookieName = options.cookieName ?? "polyx_session";
77
+ }
78
+ async establishFromCode(code, codeVerifier, redirectUri) {
79
+ const stored = await this.authClient.exchangeCode({
80
+ code,
81
+ codeVerifier,
82
+ redirectUri
83
+ });
84
+ await this.write(stored);
85
+ return stored;
86
+ }
87
+ async read() {
88
+ const raw = this.cookies.get(this.cookieName);
89
+ if (!raw) return null;
90
+ return openSession(raw, this.secret);
91
+ }
92
+ /** Rotate the tokens; clears the cookie (returns null) on a terminal refresh failure. */
93
+ async refresh() {
94
+ const current = await this.read();
95
+ if (!current) return null;
96
+ try {
97
+ const next = await this.authClient.createRefresher()(current);
98
+ await this.write(next);
99
+ return next;
100
+ } catch (error) {
101
+ if (error instanceof _poly_x_core.SessionRevokedError || error instanceof _poly_x_core.SessionExpiredError) {
102
+ this.clear();
103
+ return null;
104
+ }
105
+ throw error;
106
+ }
107
+ }
108
+ clear() {
109
+ this.cookies.delete(this.cookieName);
110
+ }
111
+ async write(session) {
112
+ const sealed = await sealSession(session, this.secret);
113
+ this.cookies.set(this.cookieName, sealed, SESSION_COOKIE_OPTIONS);
114
+ }
115
+ };
116
+ function toAuthContext(session) {
117
+ if (!session) return {
118
+ isSignedIn: false,
119
+ userId: null,
120
+ claims: null
121
+ };
122
+ return {
123
+ isSignedIn: true,
124
+ userId: session.session.claims.userId,
125
+ organizationId: session.session.claims.organizationId,
126
+ claims: session.session.claims
127
+ };
128
+ }
129
+ //#endregion
130
+ Object.defineProperty(exports, "DEFAULT_SESSION_COOKIE", {
131
+ enumerable: true,
132
+ get: function() {
133
+ return DEFAULT_SESSION_COOKIE;
134
+ }
135
+ });
136
+ Object.defineProperty(exports, "ServerSession", {
137
+ enumerable: true,
138
+ get: function() {
139
+ return ServerSession;
140
+ }
141
+ });
142
+ Object.defineProperty(exports, "openSession", {
143
+ enumerable: true,
144
+ get: function() {
145
+ return openSession;
146
+ }
147
+ });
148
+ Object.defineProperty(exports, "sealSession", {
149
+ enumerable: true,
150
+ get: function() {
151
+ return sealSession;
152
+ }
153
+ });
154
+ Object.defineProperty(exports, "toAuthContext", {
155
+ enumerable: true,
156
+ get: function() {
157
+ return toAuthContext;
158
+ }
159
+ });
@@ -0,0 +1,127 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_server_session = require("./server-session-YbnXBt3c.cjs");
3
+ let _poly_x_core = require("@poly-x/core");
4
+ let next_headers = require("next/headers");
5
+ //#region src/config.ts
6
+ function resolveNextConfig(overrides = {}) {
7
+ const publishableKey = overrides.publishableKey ?? process.env.NEXT_PUBLIC_POLYX_PUBLISHABLE_KEY;
8
+ const secret = overrides.secret ?? process.env.POLYX_SESSION_SECRET;
9
+ if (!publishableKey) throw new Error("PolyX: missing publishable key (set NEXT_PUBLIC_POLYX_PUBLISHABLE_KEY or pass config.publishableKey).");
10
+ if (!secret) throw new Error("PolyX: missing POLYX_SESSION_SECRET (needed to encrypt the session cookie).");
11
+ return {
12
+ publishableKey,
13
+ secret,
14
+ baseUrl: overrides.baseUrl ?? process.env.POLYX_BASE_URL
15
+ };
16
+ }
17
+ function buildServerAuthClient(config) {
18
+ return new _poly_x_core.AuthClient({
19
+ config: (0, _poly_x_core.resolveConfig)({
20
+ key: config.publishableKey,
21
+ baseUrl: config.baseUrl,
22
+ context: "server"
23
+ }),
24
+ transport: new _poly_x_core.FetchTransport()
25
+ });
26
+ }
27
+ //#endregion
28
+ //#region src/server.ts
29
+ /**
30
+ * @poly-x/next/server — App Router server helpers (F007). `auth()` reads the
31
+ * httpOnly session cookie (usable in RSC — read-only); `createAuthHandlers()`
32
+ * returns the route handlers that do every cookie write (login/callback/refresh/
33
+ * signout), since Server Components can't set cookies.
34
+ */
35
+ const VERIFIER_COOKIE = "polyx_pkce";
36
+ const VERIFIER_MAX_AGE = 600;
37
+ function adapt(store) {
38
+ return {
39
+ get: (name) => store.get(name)?.value,
40
+ set: (name, value, options) => store.set(name, value, options),
41
+ delete: (name) => store.delete(name)
42
+ };
43
+ }
44
+ async function requestCookies() {
45
+ return adapt(await (0, next_headers.cookies)());
46
+ }
47
+ /** Read the current session — safe in Server Components (no cookie write). */
48
+ async function auth(overrides) {
49
+ const config = resolveNextConfig(overrides);
50
+ return require_server_session.toAuthContext(await new require_server_session.ServerSession({
51
+ authClient: buildServerAuthClient(config),
52
+ secret: config.secret,
53
+ cookies: await requestCookies()
54
+ }).read());
55
+ }
56
+ function createAuthHandlers(handlersConfig = {}) {
57
+ const callbackPath = handlersConfig.callbackPath ?? "/api/polyx/callback";
58
+ const afterSignInPath = handlersConfig.afterSignInPath ?? "/";
59
+ function newSession(store, config) {
60
+ return new require_server_session.ServerSession({
61
+ authClient: buildServerAuthClient(config),
62
+ secret: config.secret,
63
+ cookies: store
64
+ });
65
+ }
66
+ return {
67
+ async signin(request) {
68
+ const config = resolveNextConfig(handlersConfig);
69
+ const redirectUri = `${new URL(request.url).origin}${callbackPath}`;
70
+ const { verifier, challenge } = await (0, _poly_x_core.generatePkce)();
71
+ const state = (0, _poly_x_core.randomState)();
72
+ (await (0, next_headers.cookies)()).set(VERIFIER_COOKIE, JSON.stringify({
73
+ verifier,
74
+ state,
75
+ redirectUri
76
+ }), {
77
+ httpOnly: true,
78
+ secure: true,
79
+ sameSite: "lax",
80
+ path: "/",
81
+ maxAge: VERIFIER_MAX_AGE
82
+ });
83
+ const url = buildServerAuthClient(config).buildAuthorizeUrl({
84
+ redirectUri,
85
+ state,
86
+ challenge
87
+ });
88
+ return Response.redirect(url, 302);
89
+ },
90
+ async callback(request) {
91
+ const config = resolveNextConfig(handlersConfig);
92
+ const url = new URL(request.url);
93
+ const code = url.searchParams.get("code");
94
+ const state = url.searchParams.get("state");
95
+ const store = await (0, next_headers.cookies)();
96
+ const raw = store.get(VERIFIER_COOKIE)?.value;
97
+ store.delete(VERIFIER_COOKIE);
98
+ const home = `${url.origin}${afterSignInPath}`;
99
+ if (!code || !state || !raw) return Response.redirect(home, 302);
100
+ const tx = JSON.parse(raw);
101
+ if (tx.state !== state) return Response.redirect(home, 302);
102
+ await newSession(adapt(store), config).establishFromCode(code, tx.verifier, tx.redirectUri);
103
+ return Response.redirect(home, 302);
104
+ },
105
+ async refresh() {
106
+ const config = resolveNextConfig(handlersConfig);
107
+ const session = await newSession(await requestCookies(), config).refresh();
108
+ return Response.json({ isSignedIn: session !== null }, { status: session ? 200 : 401 });
109
+ },
110
+ async signout(request) {
111
+ const config = resolveNextConfig(handlersConfig);
112
+ newSession(adapt(await (0, next_headers.cookies)()), config).clear();
113
+ try {
114
+ await buildServerAuthClient(config).logout("device");
115
+ } catch {}
116
+ return Response.redirect(new URL(request.url).origin + "/", 302);
117
+ }
118
+ };
119
+ }
120
+ //#endregion
121
+ exports.DEFAULT_SESSION_COOKIE = require_server_session.DEFAULT_SESSION_COOKIE;
122
+ exports.ServerSession = require_server_session.ServerSession;
123
+ exports.auth = auth;
124
+ exports.createAuthHandlers = createAuthHandlers;
125
+ exports.openSession = require_server_session.openSession;
126
+ exports.resolveNextConfig = resolveNextConfig;
127
+ exports.sealSession = require_server_session.sealSession;
@@ -0,0 +1,80 @@
1
+ import { AuthClient, StoredSession } from "@poly-x/core";
2
+
3
+ //#region src/config.d.ts
4
+ interface NextServerConfig {
5
+ publishableKey: string;
6
+ /** Secret for encrypting the session cookie (AES-GCM key derivation). */
7
+ secret: string;
8
+ baseUrl?: string;
9
+ }
10
+ declare function resolveNextConfig(overrides?: Partial<NextServerConfig>): NextServerConfig;
11
+ //#endregion
12
+ //#region src/cookie-adapter.d.ts
13
+ /**
14
+ * Cookie access behind a seam so the server-session core is testable without a
15
+ * Next runtime. The Next adapter (built from an awaited `cookies()` store) lives
16
+ * in server.ts / proxy.ts; a fake backs the unit tests.
17
+ */
18
+ interface CookieSetOptions {
19
+ httpOnly?: boolean;
20
+ secure?: boolean;
21
+ sameSite?: "lax" | "strict" | "none";
22
+ path?: string;
23
+ maxAge?: number;
24
+ }
25
+ interface CookieAdapter {
26
+ get(name: string): string | undefined;
27
+ set(name: string, value: string, options?: CookieSetOptions): void;
28
+ delete(name: string): void;
29
+ }
30
+ //#endregion
31
+ //#region src/server-session.d.ts
32
+ declare const DEFAULT_SESSION_COOKIE = "polyx_session";
33
+ interface ServerSessionOptions {
34
+ authClient: AuthClient;
35
+ secret: string;
36
+ cookies: CookieAdapter;
37
+ cookieName?: string;
38
+ }
39
+ declare class ServerSession {
40
+ private readonly authClient;
41
+ private readonly secret;
42
+ private readonly cookies;
43
+ private readonly cookieName;
44
+ constructor(options: ServerSessionOptions);
45
+ establishFromCode(code: string, codeVerifier: string, redirectUri: string): Promise<StoredSession>;
46
+ read(): Promise<StoredSession | null>;
47
+ /** Rotate the tokens; clears the cookie (returns null) on a terminal refresh failure. */
48
+ refresh(): Promise<StoredSession | null>;
49
+ clear(): void;
50
+ private write;
51
+ }
52
+ interface AuthContext {
53
+ isSignedIn: boolean;
54
+ userId: string | null;
55
+ organizationId?: string;
56
+ claims: StoredSession["session"]["claims"] | null;
57
+ }
58
+ //#endregion
59
+ //#region src/cookie-codec.d.ts
60
+ declare function sealSession(session: StoredSession, secret: string): Promise<string>;
61
+ declare function openSession(token: string, secret: string): Promise<StoredSession | null>;
62
+ //#endregion
63
+ //#region src/server.d.ts
64
+ /** Read the current session — safe in Server Components (no cookie write). */
65
+ declare function auth(overrides?: Partial<NextServerConfig>): Promise<AuthContext>;
66
+ interface AuthHandlersConfig extends Partial<NextServerConfig> {
67
+ /** Where to send the user after a successful sign-in. Default `/`. */
68
+ afterSignInPath?: string;
69
+ /** The callback route path this app mounts. Default `/api/polyx/callback`. */
70
+ callbackPath?: string;
71
+ }
72
+ interface AuthHandlers {
73
+ signin(request: Request): Promise<Response>;
74
+ callback(request: Request): Promise<Response>;
75
+ refresh(request: Request): Promise<Response>;
76
+ signout(request: Request): Promise<Response>;
77
+ }
78
+ declare function createAuthHandlers(handlersConfig?: AuthHandlersConfig): AuthHandlers;
79
+ //#endregion
80
+ export { type AuthContext, AuthHandlers, AuthHandlersConfig, type CookieAdapter, type CookieSetOptions, DEFAULT_SESSION_COOKIE, type NextServerConfig, ServerSession, auth, createAuthHandlers, openSession, resolveNextConfig, sealSession };
@@ -0,0 +1,80 @@
1
+ import { AuthClient, StoredSession } from "@poly-x/core";
2
+
3
+ //#region src/config.d.ts
4
+ interface NextServerConfig {
5
+ publishableKey: string;
6
+ /** Secret for encrypting the session cookie (AES-GCM key derivation). */
7
+ secret: string;
8
+ baseUrl?: string;
9
+ }
10
+ declare function resolveNextConfig(overrides?: Partial<NextServerConfig>): NextServerConfig;
11
+ //#endregion
12
+ //#region src/cookie-adapter.d.ts
13
+ /**
14
+ * Cookie access behind a seam so the server-session core is testable without a
15
+ * Next runtime. The Next adapter (built from an awaited `cookies()` store) lives
16
+ * in server.ts / proxy.ts; a fake backs the unit tests.
17
+ */
18
+ interface CookieSetOptions {
19
+ httpOnly?: boolean;
20
+ secure?: boolean;
21
+ sameSite?: "lax" | "strict" | "none";
22
+ path?: string;
23
+ maxAge?: number;
24
+ }
25
+ interface CookieAdapter {
26
+ get(name: string): string | undefined;
27
+ set(name: string, value: string, options?: CookieSetOptions): void;
28
+ delete(name: string): void;
29
+ }
30
+ //#endregion
31
+ //#region src/server-session.d.ts
32
+ declare const DEFAULT_SESSION_COOKIE = "polyx_session";
33
+ interface ServerSessionOptions {
34
+ authClient: AuthClient;
35
+ secret: string;
36
+ cookies: CookieAdapter;
37
+ cookieName?: string;
38
+ }
39
+ declare class ServerSession {
40
+ private readonly authClient;
41
+ private readonly secret;
42
+ private readonly cookies;
43
+ private readonly cookieName;
44
+ constructor(options: ServerSessionOptions);
45
+ establishFromCode(code: string, codeVerifier: string, redirectUri: string): Promise<StoredSession>;
46
+ read(): Promise<StoredSession | null>;
47
+ /** Rotate the tokens; clears the cookie (returns null) on a terminal refresh failure. */
48
+ refresh(): Promise<StoredSession | null>;
49
+ clear(): void;
50
+ private write;
51
+ }
52
+ interface AuthContext {
53
+ isSignedIn: boolean;
54
+ userId: string | null;
55
+ organizationId?: string;
56
+ claims: StoredSession["session"]["claims"] | null;
57
+ }
58
+ //#endregion
59
+ //#region src/cookie-codec.d.ts
60
+ declare function sealSession(session: StoredSession, secret: string): Promise<string>;
61
+ declare function openSession(token: string, secret: string): Promise<StoredSession | null>;
62
+ //#endregion
63
+ //#region src/server.d.ts
64
+ /** Read the current session — safe in Server Components (no cookie write). */
65
+ declare function auth(overrides?: Partial<NextServerConfig>): Promise<AuthContext>;
66
+ interface AuthHandlersConfig extends Partial<NextServerConfig> {
67
+ /** Where to send the user after a successful sign-in. Default `/`. */
68
+ afterSignInPath?: string;
69
+ /** The callback route path this app mounts. Default `/api/polyx/callback`. */
70
+ callbackPath?: string;
71
+ }
72
+ interface AuthHandlers {
73
+ signin(request: Request): Promise<Response>;
74
+ callback(request: Request): Promise<Response>;
75
+ refresh(request: Request): Promise<Response>;
76
+ signout(request: Request): Promise<Response>;
77
+ }
78
+ declare function createAuthHandlers(handlersConfig?: AuthHandlersConfig): AuthHandlers;
79
+ //#endregion
80
+ export { type AuthContext, AuthHandlers, AuthHandlersConfig, type CookieAdapter, type CookieSetOptions, DEFAULT_SESSION_COOKIE, type NextServerConfig, ServerSession, auth, createAuthHandlers, openSession, resolveNextConfig, sealSession };
@@ -0,0 +1,120 @@
1
+ import { a as sealSession, i as openSession, n as ServerSession, r as toAuthContext, t as DEFAULT_SESSION_COOKIE } from "./server-session-B3zJ7CS8.mjs";
2
+ import { AuthClient, FetchTransport, generatePkce, randomState, resolveConfig } from "@poly-x/core";
3
+ import { cookies } from "next/headers";
4
+ //#region src/config.ts
5
+ function resolveNextConfig(overrides = {}) {
6
+ const publishableKey = overrides.publishableKey ?? process.env.NEXT_PUBLIC_POLYX_PUBLISHABLE_KEY;
7
+ const secret = overrides.secret ?? process.env.POLYX_SESSION_SECRET;
8
+ if (!publishableKey) throw new Error("PolyX: missing publishable key (set NEXT_PUBLIC_POLYX_PUBLISHABLE_KEY or pass config.publishableKey).");
9
+ if (!secret) throw new Error("PolyX: missing POLYX_SESSION_SECRET (needed to encrypt the session cookie).");
10
+ return {
11
+ publishableKey,
12
+ secret,
13
+ baseUrl: overrides.baseUrl ?? process.env.POLYX_BASE_URL
14
+ };
15
+ }
16
+ function buildServerAuthClient(config) {
17
+ return new AuthClient({
18
+ config: resolveConfig({
19
+ key: config.publishableKey,
20
+ baseUrl: config.baseUrl,
21
+ context: "server"
22
+ }),
23
+ transport: new FetchTransport()
24
+ });
25
+ }
26
+ //#endregion
27
+ //#region src/server.ts
28
+ /**
29
+ * @poly-x/next/server — App Router server helpers (F007). `auth()` reads the
30
+ * httpOnly session cookie (usable in RSC — read-only); `createAuthHandlers()`
31
+ * returns the route handlers that do every cookie write (login/callback/refresh/
32
+ * signout), since Server Components can't set cookies.
33
+ */
34
+ const VERIFIER_COOKIE = "polyx_pkce";
35
+ const VERIFIER_MAX_AGE = 600;
36
+ function adapt(store) {
37
+ return {
38
+ get: (name) => store.get(name)?.value,
39
+ set: (name, value, options) => store.set(name, value, options),
40
+ delete: (name) => store.delete(name)
41
+ };
42
+ }
43
+ async function requestCookies() {
44
+ return adapt(await cookies());
45
+ }
46
+ /** Read the current session — safe in Server Components (no cookie write). */
47
+ async function auth(overrides) {
48
+ const config = resolveNextConfig(overrides);
49
+ return toAuthContext(await new ServerSession({
50
+ authClient: buildServerAuthClient(config),
51
+ secret: config.secret,
52
+ cookies: await requestCookies()
53
+ }).read());
54
+ }
55
+ function createAuthHandlers(handlersConfig = {}) {
56
+ const callbackPath = handlersConfig.callbackPath ?? "/api/polyx/callback";
57
+ const afterSignInPath = handlersConfig.afterSignInPath ?? "/";
58
+ function newSession(store, config) {
59
+ return new ServerSession({
60
+ authClient: buildServerAuthClient(config),
61
+ secret: config.secret,
62
+ cookies: store
63
+ });
64
+ }
65
+ return {
66
+ async signin(request) {
67
+ const config = resolveNextConfig(handlersConfig);
68
+ const redirectUri = `${new URL(request.url).origin}${callbackPath}`;
69
+ const { verifier, challenge } = await generatePkce();
70
+ const state = randomState();
71
+ (await cookies()).set(VERIFIER_COOKIE, JSON.stringify({
72
+ verifier,
73
+ state,
74
+ redirectUri
75
+ }), {
76
+ httpOnly: true,
77
+ secure: true,
78
+ sameSite: "lax",
79
+ path: "/",
80
+ maxAge: VERIFIER_MAX_AGE
81
+ });
82
+ const url = buildServerAuthClient(config).buildAuthorizeUrl({
83
+ redirectUri,
84
+ state,
85
+ challenge
86
+ });
87
+ return Response.redirect(url, 302);
88
+ },
89
+ async callback(request) {
90
+ const config = resolveNextConfig(handlersConfig);
91
+ const url = new URL(request.url);
92
+ const code = url.searchParams.get("code");
93
+ const state = url.searchParams.get("state");
94
+ const store = await cookies();
95
+ const raw = store.get(VERIFIER_COOKIE)?.value;
96
+ store.delete(VERIFIER_COOKIE);
97
+ const home = `${url.origin}${afterSignInPath}`;
98
+ if (!code || !state || !raw) return Response.redirect(home, 302);
99
+ const tx = JSON.parse(raw);
100
+ if (tx.state !== state) return Response.redirect(home, 302);
101
+ await newSession(adapt(store), config).establishFromCode(code, tx.verifier, tx.redirectUri);
102
+ return Response.redirect(home, 302);
103
+ },
104
+ async refresh() {
105
+ const config = resolveNextConfig(handlersConfig);
106
+ const session = await newSession(await requestCookies(), config).refresh();
107
+ return Response.json({ isSignedIn: session !== null }, { status: session ? 200 : 401 });
108
+ },
109
+ async signout(request) {
110
+ const config = resolveNextConfig(handlersConfig);
111
+ newSession(adapt(await cookies()), config).clear();
112
+ try {
113
+ await buildServerAuthClient(config).logout("device");
114
+ } catch {}
115
+ return Response.redirect(new URL(request.url).origin + "/", 302);
116
+ }
117
+ };
118
+ }
119
+ //#endregion
120
+ export { DEFAULT_SESSION_COOKIE, ServerSession, auth, createAuthHandlers, openSession, resolveNextConfig, sealSession };
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@poly-x/next",
3
+ "version": "0.1.0-alpha.0",
4
+ "description": "PolyX SDK for Next.js App Router - components plus server helpers",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "exports": {
12
+ ".": {
13
+ "import": {
14
+ "types": "./dist/index.d.mts",
15
+ "default": "./dist/index.mjs"
16
+ },
17
+ "require": {
18
+ "types": "./dist/index.d.cts",
19
+ "default": "./dist/index.cjs"
20
+ }
21
+ },
22
+ "./server": {
23
+ "import": {
24
+ "types": "./dist/server.d.mts",
25
+ "default": "./dist/server.mjs"
26
+ },
27
+ "require": {
28
+ "types": "./dist/server.d.cts",
29
+ "default": "./dist/server.cjs"
30
+ }
31
+ },
32
+ "./proxy": {
33
+ "import": {
34
+ "types": "./dist/proxy.d.mts",
35
+ "default": "./dist/proxy.mjs"
36
+ },
37
+ "require": {
38
+ "types": "./dist/proxy.d.cts",
39
+ "default": "./dist/proxy.cjs"
40
+ }
41
+ }
42
+ },
43
+ "main": "./dist/index.cjs",
44
+ "module": "./dist/index.mjs",
45
+ "types": "./dist/index.d.cts",
46
+ "dependencies": {
47
+ "@poly-x/react": "0.1.0-alpha.0",
48
+ "@poly-x/core": "0.1.0-alpha.0"
49
+ },
50
+ "peerDependencies": {
51
+ "next": ">=14",
52
+ "react": ">=18"
53
+ },
54
+ "publishConfig": {
55
+ "access": "public"
56
+ },
57
+ "repository": {
58
+ "type": "git",
59
+ "url": "https://dev.azure.com/waiindustries/wAI%20Engineering/_git/poly-x-sdk"
60
+ },
61
+ "engines": {
62
+ "node": ">=20"
63
+ },
64
+ "devDependencies": {
65
+ "@types/react": "^19.2.17",
66
+ "next": "^16.2.10",
67
+ "react": "^19.2.7",
68
+ "react-dom": "^19.2.7"
69
+ },
70
+ "scripts": {
71
+ "build": "tsdown",
72
+ "test": "vitest run",
73
+ "lint": "eslint src"
74
+ }
75
+ }