@poly-x/next 0.1.0-alpha.1 → 0.1.0-alpha.10
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-YbnXBt3c.cjs → server-session-BpRaJCz9.cjs} +36 -8
- package/dist/{server-session-B3zJ7CS8.mjs → server-session-DedPwl4b.mjs} +36 -8
- package/dist/server.cjs +174 -12
- package/dist/server.d.cts +94 -1
- package/dist/server.d.mts +94 -1
- package/dist/server.mjs +174 -12
- package/dist/styles.css +635 -0
- package/package.json +9 -6
package/dist/server.mjs
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
import { a as sealSession, i as openSession, n as ServerSession, r as toAuthContext, t as DEFAULT_SESSION_COOKIE } from "./server-session-
|
|
1
|
+
import { a as sealSession, i as openSession, n as ServerSession, r as toAuthContext, t as DEFAULT_SESSION_COOKIE } from "./server-session-DedPwl4b.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
|
|
5
|
+
/** Parse a boolean-ish env var; undefined when unset/unrecognized (→ auto-derive). */
|
|
6
|
+
function parseBoolEnv(value) {
|
|
7
|
+
if (value === void 0) return void 0;
|
|
8
|
+
const v = value.trim().toLowerCase();
|
|
9
|
+
if (v === "true" || v === "1") return true;
|
|
10
|
+
if (v === "false" || v === "0") return false;
|
|
11
|
+
}
|
|
5
12
|
function resolveNextConfig(overrides = {}) {
|
|
6
13
|
const publishableKey = overrides.publishableKey ?? process.env.NEXT_PUBLIC_POLYX_PUBLISHABLE_KEY;
|
|
7
14
|
const secret = overrides.secret ?? process.env.POLYX_SESSION_SECRET;
|
|
@@ -11,7 +18,8 @@ function resolveNextConfig(overrides = {}) {
|
|
|
11
18
|
publishableKey,
|
|
12
19
|
secret,
|
|
13
20
|
baseUrl: overrides.baseUrl ?? process.env.POLYX_BASE_URL,
|
|
14
|
-
signInUrl: overrides.signInUrl ?? process.env.NEXT_PUBLIC_POLYX_SIGN_IN_URL
|
|
21
|
+
signInUrl: overrides.signInUrl ?? process.env.NEXT_PUBLIC_POLYX_SIGN_IN_URL,
|
|
22
|
+
cookieSecure: overrides.cookieSecure ?? parseBoolEnv(process.env.POLYX_COOKIE_SECURE)
|
|
15
23
|
};
|
|
16
24
|
}
|
|
17
25
|
function buildServerAuthClient(config) {
|
|
@@ -35,6 +43,8 @@ function buildServerAuthClient(config) {
|
|
|
35
43
|
*/
|
|
36
44
|
const VERIFIER_COOKIE = "polyx_pkce";
|
|
37
45
|
const VERIFIER_MAX_AGE = 600;
|
|
46
|
+
/** poly-auth's own floor (Joi: min 6, max 128) — reject early rather than round-trip. */
|
|
47
|
+
const MIN_PASSWORD_LENGTH = 6;
|
|
38
48
|
function adapt(store) {
|
|
39
49
|
return {
|
|
40
50
|
get: (name) => store.get(name)?.value,
|
|
@@ -45,6 +55,22 @@ function adapt(store) {
|
|
|
45
55
|
async function requestCookies() {
|
|
46
56
|
return adapt(await cookies());
|
|
47
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Decide the cookie `Secure` attribute for this request. An explicit `cookieSecure`
|
|
60
|
+
* (from `POLYX_COOKIE_SECURE` or code) wins; otherwise derive from the transport —
|
|
61
|
+
* `x-forwarded-proto` (proxied TLS) then the request URL — and fail secure if unknown.
|
|
62
|
+
* A Secure cookie is dropped by the browser over http, so http/localhost must be false.
|
|
63
|
+
*/
|
|
64
|
+
function resolveCookieSecure(request, config) {
|
|
65
|
+
if (config.cookieSecure !== void 0) return config.cookieSecure;
|
|
66
|
+
const forwarded = request.headers.get("x-forwarded-proto");
|
|
67
|
+
if (forwarded) return forwarded.split(",")[0].trim() === "https";
|
|
68
|
+
try {
|
|
69
|
+
return new URL(request.url).protocol === "https:";
|
|
70
|
+
} catch {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
48
74
|
/** Read the current session — safe in Server Components (no cookie write). */
|
|
49
75
|
async function auth(overrides) {
|
|
50
76
|
const config = resolveNextConfig(overrides);
|
|
@@ -57,16 +83,18 @@ async function auth(overrides) {
|
|
|
57
83
|
function createAuthHandlers(handlersConfig = {}) {
|
|
58
84
|
const callbackPath = handlersConfig.callbackPath ?? "/api/polyx/callback";
|
|
59
85
|
const afterSignInPath = handlersConfig.afterSignInPath ?? "/";
|
|
60
|
-
function newSession(store, config) {
|
|
86
|
+
function newSession(store, config, secure) {
|
|
61
87
|
return new ServerSession({
|
|
62
88
|
authClient: buildServerAuthClient(config),
|
|
63
89
|
secret: config.secret,
|
|
64
|
-
cookies: store
|
|
90
|
+
cookies: store,
|
|
91
|
+
secure
|
|
65
92
|
});
|
|
66
93
|
}
|
|
67
94
|
return {
|
|
68
95
|
async signin(request) {
|
|
69
96
|
const config = resolveNextConfig(handlersConfig);
|
|
97
|
+
const secure = resolveCookieSecure(request, config);
|
|
70
98
|
const redirectUri = `${new URL(request.url).origin}${callbackPath}`;
|
|
71
99
|
const { verifier, challenge } = await generatePkce();
|
|
72
100
|
const state = randomState();
|
|
@@ -76,7 +104,7 @@ function createAuthHandlers(handlersConfig = {}) {
|
|
|
76
104
|
redirectUri
|
|
77
105
|
}), {
|
|
78
106
|
httpOnly: true,
|
|
79
|
-
secure
|
|
107
|
+
secure,
|
|
80
108
|
sameSite: "lax",
|
|
81
109
|
path: "/",
|
|
82
110
|
maxAge: VERIFIER_MAX_AGE
|
|
@@ -88,8 +116,128 @@ function createAuthHandlers(handlersConfig = {}) {
|
|
|
88
116
|
});
|
|
89
117
|
return Response.redirect(url, 302);
|
|
90
118
|
},
|
|
119
|
+
async authenticate(request) {
|
|
120
|
+
const config = resolveNextConfig(handlersConfig);
|
|
121
|
+
const secure = resolveCookieSecure(request, config);
|
|
122
|
+
const redirectUri = `${new URL(request.url).origin}${callbackPath}`;
|
|
123
|
+
const client = buildServerAuthClient(config);
|
|
124
|
+
let payload;
|
|
125
|
+
try {
|
|
126
|
+
payload = await request.json();
|
|
127
|
+
} catch {
|
|
128
|
+
return Response.json({ status: "invalid_request" }, { status: 400 });
|
|
129
|
+
}
|
|
130
|
+
const email = typeof payload.email === "string" ? payload.email : "";
|
|
131
|
+
const password = typeof payload.password === "string" ? payload.password : "";
|
|
132
|
+
if (!email || !password) return Response.json({ status: "invalid_request" }, { status: 400 });
|
|
133
|
+
const organizationCode = typeof payload.organizationCode === "string" ? payload.organizationCode : void 0;
|
|
134
|
+
const tenantId = typeof payload.tenantId === "string" ? payload.tenantId : void 0;
|
|
135
|
+
const newPassword = typeof payload.newPassword === "string" ? payload.newPassword : void 0;
|
|
136
|
+
const confirmPassword = typeof payload.confirmPassword === "string" ? payload.confirmPassword : void 0;
|
|
137
|
+
/** One authorize attempt, mapped to the wire response (sealing the session on success). */
|
|
138
|
+
async function attempt(withPassword) {
|
|
139
|
+
const { verifier, challenge } = await generatePkce();
|
|
140
|
+
const outcome = await client.authorizeWithPassword({
|
|
141
|
+
email,
|
|
142
|
+
password: withPassword,
|
|
143
|
+
redirectUri,
|
|
144
|
+
state: randomState(),
|
|
145
|
+
challenge,
|
|
146
|
+
organizationCode,
|
|
147
|
+
tenantId
|
|
148
|
+
});
|
|
149
|
+
switch (outcome.type) {
|
|
150
|
+
case "authorized":
|
|
151
|
+
await newSession(adapt(await cookies()), config, secure).establishFromCode(outcome.code, verifier, redirectUri);
|
|
152
|
+
return Response.json({
|
|
153
|
+
status: "success",
|
|
154
|
+
redirectTo: afterSignInPath
|
|
155
|
+
});
|
|
156
|
+
case "select_tenant": return Response.json({
|
|
157
|
+
status: "select_tenant",
|
|
158
|
+
tenants: outcome.tenants,
|
|
159
|
+
state: outcome.state
|
|
160
|
+
});
|
|
161
|
+
case "password_change_required": return Response.json({
|
|
162
|
+
status: "password_change_required",
|
|
163
|
+
userId: outcome.userId
|
|
164
|
+
}, { status: 403 });
|
|
165
|
+
case "no_access": return Response.json({ status: "no_access" }, { status: 403 });
|
|
166
|
+
default: return Response.json({ status: "invalid_credentials" }, { status: 401 });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (newPassword === void 0) return attempt(password);
|
|
170
|
+
if (newPassword.length < MIN_PASSWORD_LENGTH) return Response.json({
|
|
171
|
+
status: "weak_password",
|
|
172
|
+
minLength: MIN_PASSWORD_LENGTH
|
|
173
|
+
}, { status: 400 });
|
|
174
|
+
if (newPassword !== confirmPassword) return Response.json({ status: "password_mismatch" }, { status: 400 });
|
|
175
|
+
const { challenge } = await generatePkce();
|
|
176
|
+
const proof = await client.authorizeWithPassword({
|
|
177
|
+
email,
|
|
178
|
+
password,
|
|
179
|
+
redirectUri,
|
|
180
|
+
state: randomState(),
|
|
181
|
+
challenge,
|
|
182
|
+
organizationCode,
|
|
183
|
+
tenantId
|
|
184
|
+
});
|
|
185
|
+
if (proof.type !== "password_change_required") return Response.json({ status: "invalid_credentials" }, { status: 401 });
|
|
186
|
+
await client.setPassword({
|
|
187
|
+
userId: proof.userId,
|
|
188
|
+
newPassword,
|
|
189
|
+
confirmPassword,
|
|
190
|
+
ticket: proof.ticket
|
|
191
|
+
});
|
|
192
|
+
return attempt(newPassword);
|
|
193
|
+
},
|
|
194
|
+
async forgotPassword(request) {
|
|
195
|
+
const config = resolveNextConfig(handlersConfig);
|
|
196
|
+
let payload;
|
|
197
|
+
try {
|
|
198
|
+
payload = await request.json();
|
|
199
|
+
} catch {
|
|
200
|
+
return Response.json({ status: "invalid_request" }, { status: 400 });
|
|
201
|
+
}
|
|
202
|
+
const email = typeof payload.email === "string" ? payload.email.trim() : "";
|
|
203
|
+
if (!email) return Response.json({ status: "invalid_request" }, { status: 400 });
|
|
204
|
+
try {
|
|
205
|
+
await buildServerAuthClient(config).requestPasswordReset({ email });
|
|
206
|
+
} catch {}
|
|
207
|
+
return Response.json({ status: "accepted" });
|
|
208
|
+
},
|
|
209
|
+
async resetPassword(request) {
|
|
210
|
+
const config = resolveNextConfig(handlersConfig);
|
|
211
|
+
let payload;
|
|
212
|
+
try {
|
|
213
|
+
payload = await request.json();
|
|
214
|
+
} catch {
|
|
215
|
+
return Response.json({ status: "invalid_request" }, { status: 400 });
|
|
216
|
+
}
|
|
217
|
+
const token = typeof payload.token === "string" ? payload.token : "";
|
|
218
|
+
const newPassword = typeof payload.newPassword === "string" ? payload.newPassword : "";
|
|
219
|
+
const confirmPassword = typeof payload.confirmPassword === "string" ? payload.confirmPassword : void 0;
|
|
220
|
+
if (!token) return Response.json({ status: "invalid_request" }, { status: 400 });
|
|
221
|
+
if (newPassword.length < MIN_PASSWORD_LENGTH) return Response.json({
|
|
222
|
+
status: "weak_password",
|
|
223
|
+
minLength: MIN_PASSWORD_LENGTH
|
|
224
|
+
}, { status: 400 });
|
|
225
|
+
if (confirmPassword !== void 0 && newPassword !== confirmPassword) return Response.json({ status: "password_mismatch" }, { status: 400 });
|
|
226
|
+
switch (await buildServerAuthClient(config).resetPassword({
|
|
227
|
+
token,
|
|
228
|
+
newPassword
|
|
229
|
+
})) {
|
|
230
|
+
case "ok": return Response.json({ status: "success" });
|
|
231
|
+
case "weak_password": return Response.json({
|
|
232
|
+
status: "weak_password",
|
|
233
|
+
minLength: MIN_PASSWORD_LENGTH
|
|
234
|
+
}, { status: 400 });
|
|
235
|
+
default: return Response.json({ status: "invalid_or_expired" }, { status: 400 });
|
|
236
|
+
}
|
|
237
|
+
},
|
|
91
238
|
async callback(request) {
|
|
92
239
|
const config = resolveNextConfig(handlersConfig);
|
|
240
|
+
const secure = resolveCookieSecure(request, config);
|
|
93
241
|
const url = new URL(request.url);
|
|
94
242
|
const code = url.searchParams.get("code");
|
|
95
243
|
const state = url.searchParams.get("state");
|
|
@@ -100,21 +248,35 @@ function createAuthHandlers(handlersConfig = {}) {
|
|
|
100
248
|
if (!code || !state || !raw) return Response.redirect(home, 302);
|
|
101
249
|
const tx = JSON.parse(raw);
|
|
102
250
|
if (tx.state !== state) return Response.redirect(home, 302);
|
|
103
|
-
await newSession(adapt(store), config).establishFromCode(code, tx.verifier, tx.redirectUri);
|
|
251
|
+
await newSession(adapt(store), config, secure).establishFromCode(code, tx.verifier, tx.redirectUri);
|
|
104
252
|
return Response.redirect(home, 302);
|
|
105
253
|
},
|
|
106
|
-
async refresh() {
|
|
254
|
+
async refresh(request) {
|
|
107
255
|
const config = resolveNextConfig(handlersConfig);
|
|
108
|
-
const
|
|
256
|
+
const secure = resolveCookieSecure(request, config);
|
|
257
|
+
const session = await newSession(await requestCookies(), config, secure).refresh();
|
|
109
258
|
return Response.json({ isSignedIn: session !== null }, { status: session ? 200 : 401 });
|
|
110
259
|
},
|
|
260
|
+
/**
|
|
261
|
+
* End the session: revoke it upstream (bearer-authenticated, using the sealed token)
|
|
262
|
+
* and unseal the cookie. `?scope=everywhere` signs the user out of every device.
|
|
263
|
+
*
|
|
264
|
+
* Content-negotiated so both callers work: a plain link/form navigation gets the 302 it
|
|
265
|
+
* expects, while `Accept: application/json` (what `<UserButton/>` sends) gets JSON and
|
|
266
|
+
* decides its own destination client-side. Deliberately no server-side redirect target
|
|
267
|
+
* — a caller-supplied one would be an open redirect on the auth path, and the client
|
|
268
|
+
* already knows where it wants to land.
|
|
269
|
+
*/
|
|
111
270
|
async signout(request) {
|
|
112
271
|
const config = resolveNextConfig(handlersConfig);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
272
|
+
const secure = resolveCookieSecure(request, config);
|
|
273
|
+
const scope = new URL(request.url).searchParams.get("scope") === "everywhere" ? "everywhere" : "device";
|
|
274
|
+
await newSession(adapt(await cookies()), config, secure).revoke(scope);
|
|
275
|
+
if (request.headers.get("accept")?.includes("application/json")) return Response.json({ signedOut: true });
|
|
117
276
|
return Response.redirect(new URL(request.url).origin + "/", 302);
|
|
277
|
+
},
|
|
278
|
+
async session() {
|
|
279
|
+
return Response.json(await auth(handlersConfig));
|
|
118
280
|
}
|
|
119
281
|
};
|
|
120
282
|
}
|