@poly-x/next 0.1.0-alpha.0 → 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 +177 -13
- package/dist/server.d.cts +96 -1
- package/dist/server.d.mts +96 -1
- package/dist/server.mjs +177 -13
- package/dist/styles.css +635 -0
- package/package.json +9 -6
package/dist/proxy.cjs
CHANGED
package/dist/proxy.mjs
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
let _poly_x_core = require("@poly-x/core");
|
|
2
2
|
//#region src/cookie-adapter.ts
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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.
|
|
9
|
+
*/
|
|
10
|
+
function buildSessionCookieOptions(secure) {
|
|
11
|
+
return {
|
|
12
|
+
httpOnly: true,
|
|
13
|
+
secure,
|
|
14
|
+
sameSite: "lax",
|
|
15
|
+
path: "/"
|
|
16
|
+
};
|
|
17
|
+
}
|
|
10
18
|
//#endregion
|
|
11
19
|
//#region src/cookie-codec.ts
|
|
12
20
|
const IV_BYTES = 12;
|
|
@@ -69,11 +77,13 @@ var ServerSession = class {
|
|
|
69
77
|
secret;
|
|
70
78
|
cookies;
|
|
71
79
|
cookieName;
|
|
80
|
+
secure;
|
|
72
81
|
constructor(options) {
|
|
73
82
|
this.authClient = options.authClient;
|
|
74
83
|
this.secret = options.secret;
|
|
75
84
|
this.cookies = options.cookies;
|
|
76
85
|
this.cookieName = options.cookieName ?? "polyx_session";
|
|
86
|
+
this.secure = options.secure ?? true;
|
|
77
87
|
}
|
|
78
88
|
async establishFromCode(code, codeVerifier, redirectUri) {
|
|
79
89
|
const stored = await this.authClient.exchangeCode({
|
|
@@ -105,12 +115,30 @@ var ServerSession = class {
|
|
|
105
115
|
throw error;
|
|
106
116
|
}
|
|
107
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* End the session upstream, then locally. `device` revokes this session; `everywhere`
|
|
120
|
+
* revokes every session for the user (FR-SSO-5).
|
|
121
|
+
*
|
|
122
|
+
* Order is load-bearing: revocation is authenticated with the access token sealed in the
|
|
123
|
+
* cookie, so the cookie must be read before it is dropped. The upstream call is
|
|
124
|
+
* best-effort — an unreachable platform must not strand the user half-signed-in — but the
|
|
125
|
+
* call itself is real: the ecosystem logout the SDK used before resolves its session from
|
|
126
|
+
* a browser cookie, which a server-to-server fetch never carries, so it silently revoked
|
|
127
|
+
* nothing and answered 200.
|
|
128
|
+
*/
|
|
129
|
+
async revoke(scope = "device") {
|
|
130
|
+
const current = await this.read();
|
|
131
|
+
if (current) try {
|
|
132
|
+
await this.authClient.revokeSession(current.tokens.accessToken, scope);
|
|
133
|
+
} catch {}
|
|
134
|
+
this.clear();
|
|
135
|
+
}
|
|
108
136
|
clear() {
|
|
109
137
|
this.cookies.delete(this.cookieName);
|
|
110
138
|
}
|
|
111
139
|
async write(session) {
|
|
112
140
|
const sealed = await sealSession(session, this.secret);
|
|
113
|
-
this.cookies.set(this.cookieName, sealed,
|
|
141
|
+
this.cookies.set(this.cookieName, sealed, buildSessionCookieOptions(this.secure));
|
|
114
142
|
}
|
|
115
143
|
};
|
|
116
144
|
function toAuthContext(session) {
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { SessionExpiredError, SessionRevokedError } from "@poly-x/core";
|
|
2
2
|
//#region src/cookie-adapter.ts
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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.
|
|
9
|
+
*/
|
|
10
|
+
function buildSessionCookieOptions(secure) {
|
|
11
|
+
return {
|
|
12
|
+
httpOnly: true,
|
|
13
|
+
secure,
|
|
14
|
+
sameSite: "lax",
|
|
15
|
+
path: "/"
|
|
16
|
+
};
|
|
17
|
+
}
|
|
10
18
|
//#endregion
|
|
11
19
|
//#region src/cookie-codec.ts
|
|
12
20
|
const IV_BYTES = 12;
|
|
@@ -69,11 +77,13 @@ var ServerSession = class {
|
|
|
69
77
|
secret;
|
|
70
78
|
cookies;
|
|
71
79
|
cookieName;
|
|
80
|
+
secure;
|
|
72
81
|
constructor(options) {
|
|
73
82
|
this.authClient = options.authClient;
|
|
74
83
|
this.secret = options.secret;
|
|
75
84
|
this.cookies = options.cookies;
|
|
76
85
|
this.cookieName = options.cookieName ?? "polyx_session";
|
|
86
|
+
this.secure = options.secure ?? true;
|
|
77
87
|
}
|
|
78
88
|
async establishFromCode(code, codeVerifier, redirectUri) {
|
|
79
89
|
const stored = await this.authClient.exchangeCode({
|
|
@@ -105,12 +115,30 @@ var ServerSession = class {
|
|
|
105
115
|
throw error;
|
|
106
116
|
}
|
|
107
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* End the session upstream, then locally. `device` revokes this session; `everywhere`
|
|
120
|
+
* revokes every session for the user (FR-SSO-5).
|
|
121
|
+
*
|
|
122
|
+
* Order is load-bearing: revocation is authenticated with the access token sealed in the
|
|
123
|
+
* cookie, so the cookie must be read before it is dropped. The upstream call is
|
|
124
|
+
* best-effort — an unreachable platform must not strand the user half-signed-in — but the
|
|
125
|
+
* call itself is real: the ecosystem logout the SDK used before resolves its session from
|
|
126
|
+
* a browser cookie, which a server-to-server fetch never carries, so it silently revoked
|
|
127
|
+
* nothing and answered 200.
|
|
128
|
+
*/
|
|
129
|
+
async revoke(scope = "device") {
|
|
130
|
+
const current = await this.read();
|
|
131
|
+
if (current) try {
|
|
132
|
+
await this.authClient.revokeSession(current.tokens.accessToken, scope);
|
|
133
|
+
} catch {}
|
|
134
|
+
this.clear();
|
|
135
|
+
}
|
|
108
136
|
clear() {
|
|
109
137
|
this.cookies.delete(this.cookieName);
|
|
110
138
|
}
|
|
111
139
|
async write(session) {
|
|
112
140
|
const sealed = await sealSession(session, this.secret);
|
|
113
|
-
this.cookies.set(this.cookieName, sealed,
|
|
141
|
+
this.cookies.set(this.cookieName, sealed, buildSessionCookieOptions(this.secure));
|
|
114
142
|
}
|
|
115
143
|
};
|
|
116
144
|
function toAuthContext(session) {
|
package/dist/server.cjs
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
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-BpRaJCz9.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
|
|
6
|
+
/** Parse a boolean-ish env var; undefined when unset/unrecognized (→ auto-derive). */
|
|
7
|
+
function parseBoolEnv(value) {
|
|
8
|
+
if (value === void 0) return void 0;
|
|
9
|
+
const v = value.trim().toLowerCase();
|
|
10
|
+
if (v === "true" || v === "1") return true;
|
|
11
|
+
if (v === "false" || v === "0") return false;
|
|
12
|
+
}
|
|
6
13
|
function resolveNextConfig(overrides = {}) {
|
|
7
14
|
const publishableKey = overrides.publishableKey ?? process.env.NEXT_PUBLIC_POLYX_PUBLISHABLE_KEY;
|
|
8
15
|
const secret = overrides.secret ?? process.env.POLYX_SESSION_SECRET;
|
|
@@ -11,7 +18,9 @@ function resolveNextConfig(overrides = {}) {
|
|
|
11
18
|
return {
|
|
12
19
|
publishableKey,
|
|
13
20
|
secret,
|
|
14
|
-
baseUrl: overrides.baseUrl ?? process.env.POLYX_BASE_URL
|
|
21
|
+
baseUrl: overrides.baseUrl ?? process.env.POLYX_BASE_URL,
|
|
22
|
+
signInUrl: overrides.signInUrl ?? process.env.NEXT_PUBLIC_POLYX_SIGN_IN_URL,
|
|
23
|
+
cookieSecure: overrides.cookieSecure ?? parseBoolEnv(process.env.POLYX_COOKIE_SECURE)
|
|
15
24
|
};
|
|
16
25
|
}
|
|
17
26
|
function buildServerAuthClient(config) {
|
|
@@ -19,6 +28,7 @@ function buildServerAuthClient(config) {
|
|
|
19
28
|
config: (0, _poly_x_core.resolveConfig)({
|
|
20
29
|
key: config.publishableKey,
|
|
21
30
|
baseUrl: config.baseUrl,
|
|
31
|
+
signInUrl: config.signInUrl,
|
|
22
32
|
context: "server"
|
|
23
33
|
}),
|
|
24
34
|
transport: new _poly_x_core.FetchTransport()
|
|
@@ -34,6 +44,8 @@ function buildServerAuthClient(config) {
|
|
|
34
44
|
*/
|
|
35
45
|
const VERIFIER_COOKIE = "polyx_pkce";
|
|
36
46
|
const VERIFIER_MAX_AGE = 600;
|
|
47
|
+
/** poly-auth's own floor (Joi: min 6, max 128) — reject early rather than round-trip. */
|
|
48
|
+
const MIN_PASSWORD_LENGTH = 6;
|
|
37
49
|
function adapt(store) {
|
|
38
50
|
return {
|
|
39
51
|
get: (name) => store.get(name)?.value,
|
|
@@ -44,6 +56,22 @@ function adapt(store) {
|
|
|
44
56
|
async function requestCookies() {
|
|
45
57
|
return adapt(await (0, next_headers.cookies)());
|
|
46
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Decide the cookie `Secure` attribute for this request. An explicit `cookieSecure`
|
|
61
|
+
* (from `POLYX_COOKIE_SECURE` or code) wins; otherwise derive from the transport —
|
|
62
|
+
* `x-forwarded-proto` (proxied TLS) then the request URL — and fail secure if unknown.
|
|
63
|
+
* A Secure cookie is dropped by the browser over http, so http/localhost must be false.
|
|
64
|
+
*/
|
|
65
|
+
function resolveCookieSecure(request, config) {
|
|
66
|
+
if (config.cookieSecure !== void 0) return config.cookieSecure;
|
|
67
|
+
const forwarded = request.headers.get("x-forwarded-proto");
|
|
68
|
+
if (forwarded) return forwarded.split(",")[0].trim() === "https";
|
|
69
|
+
try {
|
|
70
|
+
return new URL(request.url).protocol === "https:";
|
|
71
|
+
} catch {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
47
75
|
/** Read the current session — safe in Server Components (no cookie write). */
|
|
48
76
|
async function auth(overrides) {
|
|
49
77
|
const config = resolveNextConfig(overrides);
|
|
@@ -56,16 +84,18 @@ async function auth(overrides) {
|
|
|
56
84
|
function createAuthHandlers(handlersConfig = {}) {
|
|
57
85
|
const callbackPath = handlersConfig.callbackPath ?? "/api/polyx/callback";
|
|
58
86
|
const afterSignInPath = handlersConfig.afterSignInPath ?? "/";
|
|
59
|
-
function newSession(store, config) {
|
|
87
|
+
function newSession(store, config, secure) {
|
|
60
88
|
return new require_server_session.ServerSession({
|
|
61
89
|
authClient: buildServerAuthClient(config),
|
|
62
90
|
secret: config.secret,
|
|
63
|
-
cookies: store
|
|
91
|
+
cookies: store,
|
|
92
|
+
secure
|
|
64
93
|
});
|
|
65
94
|
}
|
|
66
95
|
return {
|
|
67
96
|
async signin(request) {
|
|
68
97
|
const config = resolveNextConfig(handlersConfig);
|
|
98
|
+
const secure = resolveCookieSecure(request, config);
|
|
69
99
|
const redirectUri = `${new URL(request.url).origin}${callbackPath}`;
|
|
70
100
|
const { verifier, challenge } = await (0, _poly_x_core.generatePkce)();
|
|
71
101
|
const state = (0, _poly_x_core.randomState)();
|
|
@@ -75,20 +105,140 @@ function createAuthHandlers(handlersConfig = {}) {
|
|
|
75
105
|
redirectUri
|
|
76
106
|
}), {
|
|
77
107
|
httpOnly: true,
|
|
78
|
-
secure
|
|
108
|
+
secure,
|
|
79
109
|
sameSite: "lax",
|
|
80
110
|
path: "/",
|
|
81
111
|
maxAge: VERIFIER_MAX_AGE
|
|
82
112
|
});
|
|
83
|
-
const url = buildServerAuthClient(config).
|
|
113
|
+
const url = buildServerAuthClient(config).buildSignInUrl({
|
|
84
114
|
redirectUri,
|
|
85
115
|
state,
|
|
86
116
|
challenge
|
|
87
117
|
});
|
|
88
118
|
return Response.redirect(url, 302);
|
|
89
119
|
},
|
|
120
|
+
async authenticate(request) {
|
|
121
|
+
const config = resolveNextConfig(handlersConfig);
|
|
122
|
+
const secure = resolveCookieSecure(request, config);
|
|
123
|
+
const redirectUri = `${new URL(request.url).origin}${callbackPath}`;
|
|
124
|
+
const client = buildServerAuthClient(config);
|
|
125
|
+
let payload;
|
|
126
|
+
try {
|
|
127
|
+
payload = await request.json();
|
|
128
|
+
} catch {
|
|
129
|
+
return Response.json({ status: "invalid_request" }, { status: 400 });
|
|
130
|
+
}
|
|
131
|
+
const email = typeof payload.email === "string" ? payload.email : "";
|
|
132
|
+
const password = typeof payload.password === "string" ? payload.password : "";
|
|
133
|
+
if (!email || !password) return Response.json({ status: "invalid_request" }, { status: 400 });
|
|
134
|
+
const organizationCode = typeof payload.organizationCode === "string" ? payload.organizationCode : void 0;
|
|
135
|
+
const tenantId = typeof payload.tenantId === "string" ? payload.tenantId : void 0;
|
|
136
|
+
const newPassword = typeof payload.newPassword === "string" ? payload.newPassword : void 0;
|
|
137
|
+
const confirmPassword = typeof payload.confirmPassword === "string" ? payload.confirmPassword : void 0;
|
|
138
|
+
/** One authorize attempt, mapped to the wire response (sealing the session on success). */
|
|
139
|
+
async function attempt(withPassword) {
|
|
140
|
+
const { verifier, challenge } = await (0, _poly_x_core.generatePkce)();
|
|
141
|
+
const outcome = await client.authorizeWithPassword({
|
|
142
|
+
email,
|
|
143
|
+
password: withPassword,
|
|
144
|
+
redirectUri,
|
|
145
|
+
state: (0, _poly_x_core.randomState)(),
|
|
146
|
+
challenge,
|
|
147
|
+
organizationCode,
|
|
148
|
+
tenantId
|
|
149
|
+
});
|
|
150
|
+
switch (outcome.type) {
|
|
151
|
+
case "authorized":
|
|
152
|
+
await newSession(adapt(await (0, next_headers.cookies)()), config, secure).establishFromCode(outcome.code, verifier, redirectUri);
|
|
153
|
+
return Response.json({
|
|
154
|
+
status: "success",
|
|
155
|
+
redirectTo: afterSignInPath
|
|
156
|
+
});
|
|
157
|
+
case "select_tenant": return Response.json({
|
|
158
|
+
status: "select_tenant",
|
|
159
|
+
tenants: outcome.tenants,
|
|
160
|
+
state: outcome.state
|
|
161
|
+
});
|
|
162
|
+
case "password_change_required": return Response.json({
|
|
163
|
+
status: "password_change_required",
|
|
164
|
+
userId: outcome.userId
|
|
165
|
+
}, { status: 403 });
|
|
166
|
+
case "no_access": return Response.json({ status: "no_access" }, { status: 403 });
|
|
167
|
+
default: return Response.json({ status: "invalid_credentials" }, { status: 401 });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (newPassword === void 0) return attempt(password);
|
|
171
|
+
if (newPassword.length < MIN_PASSWORD_LENGTH) return Response.json({
|
|
172
|
+
status: "weak_password",
|
|
173
|
+
minLength: MIN_PASSWORD_LENGTH
|
|
174
|
+
}, { status: 400 });
|
|
175
|
+
if (newPassword !== confirmPassword) return Response.json({ status: "password_mismatch" }, { status: 400 });
|
|
176
|
+
const { challenge } = await (0, _poly_x_core.generatePkce)();
|
|
177
|
+
const proof = await client.authorizeWithPassword({
|
|
178
|
+
email,
|
|
179
|
+
password,
|
|
180
|
+
redirectUri,
|
|
181
|
+
state: (0, _poly_x_core.randomState)(),
|
|
182
|
+
challenge,
|
|
183
|
+
organizationCode,
|
|
184
|
+
tenantId
|
|
185
|
+
});
|
|
186
|
+
if (proof.type !== "password_change_required") return Response.json({ status: "invalid_credentials" }, { status: 401 });
|
|
187
|
+
await client.setPassword({
|
|
188
|
+
userId: proof.userId,
|
|
189
|
+
newPassword,
|
|
190
|
+
confirmPassword,
|
|
191
|
+
ticket: proof.ticket
|
|
192
|
+
});
|
|
193
|
+
return attempt(newPassword);
|
|
194
|
+
},
|
|
195
|
+
async forgotPassword(request) {
|
|
196
|
+
const config = resolveNextConfig(handlersConfig);
|
|
197
|
+
let payload;
|
|
198
|
+
try {
|
|
199
|
+
payload = await request.json();
|
|
200
|
+
} catch {
|
|
201
|
+
return Response.json({ status: "invalid_request" }, { status: 400 });
|
|
202
|
+
}
|
|
203
|
+
const email = typeof payload.email === "string" ? payload.email.trim() : "";
|
|
204
|
+
if (!email) return Response.json({ status: "invalid_request" }, { status: 400 });
|
|
205
|
+
try {
|
|
206
|
+
await buildServerAuthClient(config).requestPasswordReset({ email });
|
|
207
|
+
} catch {}
|
|
208
|
+
return Response.json({ status: "accepted" });
|
|
209
|
+
},
|
|
210
|
+
async resetPassword(request) {
|
|
211
|
+
const config = resolveNextConfig(handlersConfig);
|
|
212
|
+
let payload;
|
|
213
|
+
try {
|
|
214
|
+
payload = await request.json();
|
|
215
|
+
} catch {
|
|
216
|
+
return Response.json({ status: "invalid_request" }, { status: 400 });
|
|
217
|
+
}
|
|
218
|
+
const token = typeof payload.token === "string" ? payload.token : "";
|
|
219
|
+
const newPassword = typeof payload.newPassword === "string" ? payload.newPassword : "";
|
|
220
|
+
const confirmPassword = typeof payload.confirmPassword === "string" ? payload.confirmPassword : void 0;
|
|
221
|
+
if (!token) return Response.json({ status: "invalid_request" }, { status: 400 });
|
|
222
|
+
if (newPassword.length < MIN_PASSWORD_LENGTH) return Response.json({
|
|
223
|
+
status: "weak_password",
|
|
224
|
+
minLength: MIN_PASSWORD_LENGTH
|
|
225
|
+
}, { status: 400 });
|
|
226
|
+
if (confirmPassword !== void 0 && newPassword !== confirmPassword) return Response.json({ status: "password_mismatch" }, { status: 400 });
|
|
227
|
+
switch (await buildServerAuthClient(config).resetPassword({
|
|
228
|
+
token,
|
|
229
|
+
newPassword
|
|
230
|
+
})) {
|
|
231
|
+
case "ok": return Response.json({ status: "success" });
|
|
232
|
+
case "weak_password": return Response.json({
|
|
233
|
+
status: "weak_password",
|
|
234
|
+
minLength: MIN_PASSWORD_LENGTH
|
|
235
|
+
}, { status: 400 });
|
|
236
|
+
default: return Response.json({ status: "invalid_or_expired" }, { status: 400 });
|
|
237
|
+
}
|
|
238
|
+
},
|
|
90
239
|
async callback(request) {
|
|
91
240
|
const config = resolveNextConfig(handlersConfig);
|
|
241
|
+
const secure = resolveCookieSecure(request, config);
|
|
92
242
|
const url = new URL(request.url);
|
|
93
243
|
const code = url.searchParams.get("code");
|
|
94
244
|
const state = url.searchParams.get("state");
|
|
@@ -99,21 +249,35 @@ function createAuthHandlers(handlersConfig = {}) {
|
|
|
99
249
|
if (!code || !state || !raw) return Response.redirect(home, 302);
|
|
100
250
|
const tx = JSON.parse(raw);
|
|
101
251
|
if (tx.state !== state) return Response.redirect(home, 302);
|
|
102
|
-
await newSession(adapt(store), config).establishFromCode(code, tx.verifier, tx.redirectUri);
|
|
252
|
+
await newSession(adapt(store), config, secure).establishFromCode(code, tx.verifier, tx.redirectUri);
|
|
103
253
|
return Response.redirect(home, 302);
|
|
104
254
|
},
|
|
105
|
-
async refresh() {
|
|
255
|
+
async refresh(request) {
|
|
106
256
|
const config = resolveNextConfig(handlersConfig);
|
|
107
|
-
const
|
|
257
|
+
const secure = resolveCookieSecure(request, config);
|
|
258
|
+
const session = await newSession(await requestCookies(), config, secure).refresh();
|
|
108
259
|
return Response.json({ isSignedIn: session !== null }, { status: session ? 200 : 401 });
|
|
109
260
|
},
|
|
261
|
+
/**
|
|
262
|
+
* End the session: revoke it upstream (bearer-authenticated, using the sealed token)
|
|
263
|
+
* and unseal the cookie. `?scope=everywhere` signs the user out of every device.
|
|
264
|
+
*
|
|
265
|
+
* Content-negotiated so both callers work: a plain link/form navigation gets the 302 it
|
|
266
|
+
* expects, while `Accept: application/json` (what `<UserButton/>` sends) gets JSON and
|
|
267
|
+
* decides its own destination client-side. Deliberately no server-side redirect target
|
|
268
|
+
* — a caller-supplied one would be an open redirect on the auth path, and the client
|
|
269
|
+
* already knows where it wants to land.
|
|
270
|
+
*/
|
|
110
271
|
async signout(request) {
|
|
111
272
|
const config = resolveNextConfig(handlersConfig);
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
273
|
+
const secure = resolveCookieSecure(request, config);
|
|
274
|
+
const scope = new URL(request.url).searchParams.get("scope") === "everywhere" ? "everywhere" : "device";
|
|
275
|
+
await newSession(adapt(await (0, next_headers.cookies)()), config, secure).revoke(scope);
|
|
276
|
+
if (request.headers.get("accept")?.includes("application/json")) return Response.json({ signedOut: true });
|
|
116
277
|
return Response.redirect(new URL(request.url).origin + "/", 302);
|
|
278
|
+
},
|
|
279
|
+
async session() {
|
|
280
|
+
return Response.json(await auth(handlersConfig));
|
|
117
281
|
}
|
|
118
282
|
};
|
|
119
283
|
}
|
package/dist/server.d.cts
CHANGED
|
@@ -6,6 +6,15 @@ interface NextServerConfig {
|
|
|
6
6
|
/** Secret for encrypting the session cookie (AES-GCM key derivation). */
|
|
7
7
|
secret: string;
|
|
8
8
|
baseUrl?: string;
|
|
9
|
+
/** Hosted sign-in page URL the signin route redirects to. */
|
|
10
|
+
signInUrl?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Force the session/verifier cookie `Secure` attribute. Leave undefined (default) to
|
|
13
|
+
* derive it from the request transport (https → true, http/localhost → false). Set via
|
|
14
|
+
* `POLYX_COOKIE_SECURE=true|false` only for edge cases (e.g. a TLS-terminating proxy
|
|
15
|
+
* that doesn't forward `x-forwarded-proto`).
|
|
16
|
+
*/
|
|
17
|
+
cookieSecure?: boolean;
|
|
9
18
|
}
|
|
10
19
|
declare function resolveNextConfig(overrides?: Partial<NextServerConfig>): NextServerConfig;
|
|
11
20
|
//#endregion
|
|
@@ -35,17 +44,36 @@ interface ServerSessionOptions {
|
|
|
35
44
|
secret: string;
|
|
36
45
|
cookies: CookieAdapter;
|
|
37
46
|
cookieName?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Whether to set the `Secure` attribute on the session cookie. Defaults to `true`
|
|
49
|
+
* (fail secure). Handlers derive it from the request transport so http/localhost dev
|
|
50
|
+
* works (a Secure cookie is dropped over http). See buildSessionCookieOptions.
|
|
51
|
+
*/
|
|
52
|
+
secure?: boolean;
|
|
38
53
|
}
|
|
39
54
|
declare class ServerSession {
|
|
40
55
|
private readonly authClient;
|
|
41
56
|
private readonly secret;
|
|
42
57
|
private readonly cookies;
|
|
43
58
|
private readonly cookieName;
|
|
59
|
+
private readonly secure;
|
|
44
60
|
constructor(options: ServerSessionOptions);
|
|
45
61
|
establishFromCode(code: string, codeVerifier: string, redirectUri: string): Promise<StoredSession>;
|
|
46
62
|
read(): Promise<StoredSession | null>;
|
|
47
63
|
/** Rotate the tokens; clears the cookie (returns null) on a terminal refresh failure. */
|
|
48
64
|
refresh(): Promise<StoredSession | null>;
|
|
65
|
+
/**
|
|
66
|
+
* End the session upstream, then locally. `device` revokes this session; `everywhere`
|
|
67
|
+
* revokes every session for the user (FR-SSO-5).
|
|
68
|
+
*
|
|
69
|
+
* Order is load-bearing: revocation is authenticated with the access token sealed in the
|
|
70
|
+
* cookie, so the cookie must be read before it is dropped. The upstream call is
|
|
71
|
+
* best-effort — an unreachable platform must not strand the user half-signed-in — but the
|
|
72
|
+
* call itself is real: the ecosystem logout the SDK used before resolves its session from
|
|
73
|
+
* a browser cookie, which a server-to-server fetch never carries, so it silently revoked
|
|
74
|
+
* nothing and answered 200.
|
|
75
|
+
*/
|
|
76
|
+
revoke(scope?: "device" | "everywhere"): Promise<void>;
|
|
49
77
|
clear(): void;
|
|
50
78
|
private write;
|
|
51
79
|
}
|
|
@@ -71,10 +99,77 @@ interface AuthHandlersConfig extends Partial<NextServerConfig> {
|
|
|
71
99
|
}
|
|
72
100
|
interface AuthHandlers {
|
|
73
101
|
signin(request: Request): Promise<Response>;
|
|
102
|
+
/**
|
|
103
|
+
* The embedded `<SignIn/>` BFF endpoint: reads `{email, password}` (+ optional
|
|
104
|
+
* `organizationCode`/`tenantId`), runs the password authorize entirely
|
|
105
|
+
* server-side, and — on success — seals the httpOnly session before returning.
|
|
106
|
+
* The browser never sees the code, the tokens, or the credentials after POST.
|
|
107
|
+
*/
|
|
108
|
+
authenticate(request: Request): Promise<Response>;
|
|
109
|
+
/**
|
|
110
|
+
* The `<ForgotPassword/>` BFF endpoint: reads `{email}`, adds this app's publishable
|
|
111
|
+
* key, and asks poly-auth to email a reset link server-side. Always relays a uniform
|
|
112
|
+
* `accepted` — it never reveals whether the account exists.
|
|
113
|
+
*/
|
|
114
|
+
forgotPassword(request: Request): Promise<Response>;
|
|
115
|
+
/**
|
|
116
|
+
* The `<ResetPassword/>` BFF endpoint: reads `{token, newPassword}` and redeems the
|
|
117
|
+
* opaque single-use token. `success` on completion; `invalid_or_expired` otherwise.
|
|
118
|
+
*/
|
|
119
|
+
resetPassword(request: Request): Promise<Response>;
|
|
74
120
|
callback(request: Request): Promise<Response>;
|
|
75
121
|
refresh(request: Request): Promise<Response>;
|
|
76
122
|
signout(request: Request): Promise<Response>;
|
|
123
|
+
/**
|
|
124
|
+
* The client's view of the session: the same `AuthContext` `auth()` returns in a Server
|
|
125
|
+
* Component, as JSON. This is what makes client components work under BFF custody, where
|
|
126
|
+
* the browser holds no token and so cannot derive a session for itself — the hooks read
|
|
127
|
+
* server truth instead. Never carries a token.
|
|
128
|
+
*/
|
|
129
|
+
session(request: Request): Promise<Response>;
|
|
77
130
|
}
|
|
131
|
+
/** The JSON the `forgotPassword` handler returns — always uniform (no enumeration). */
|
|
132
|
+
type ForgotPasswordResult = {
|
|
133
|
+
status: "accepted";
|
|
134
|
+
} | {
|
|
135
|
+
status: "invalid_request";
|
|
136
|
+
};
|
|
137
|
+
/** The JSON the `resetPassword` handler returns — the `<ResetPassword/>` form branches on `status`. */
|
|
138
|
+
type ResetPasswordResult = {
|
|
139
|
+
status: "success";
|
|
140
|
+
} | {
|
|
141
|
+
status: "weak_password";
|
|
142
|
+
minLength: number;
|
|
143
|
+
} | {
|
|
144
|
+
status: "password_mismatch";
|
|
145
|
+
} | {
|
|
146
|
+
status: "invalid_or_expired";
|
|
147
|
+
} | {
|
|
148
|
+
status: "invalid_request";
|
|
149
|
+
};
|
|
150
|
+
/** The JSON the `authenticate` handler returns — the `<SignIn/>` form branches on `status`. */
|
|
151
|
+
type AuthenticateResult = {
|
|
152
|
+
status: "success";
|
|
153
|
+
redirectTo: string;
|
|
154
|
+
} | {
|
|
155
|
+
status: "select_tenant";
|
|
156
|
+
tenants: unknown[];
|
|
157
|
+
state: string;
|
|
158
|
+
} | {
|
|
159
|
+
status: "password_change_required";
|
|
160
|
+
userId: string;
|
|
161
|
+
} | {
|
|
162
|
+
status: "weak_password";
|
|
163
|
+
minLength: number;
|
|
164
|
+
} | {
|
|
165
|
+
status: "password_mismatch";
|
|
166
|
+
} | {
|
|
167
|
+
status: "no_access";
|
|
168
|
+
} | {
|
|
169
|
+
status: "invalid_credentials";
|
|
170
|
+
} | {
|
|
171
|
+
status: "invalid_request";
|
|
172
|
+
};
|
|
78
173
|
declare function createAuthHandlers(handlersConfig?: AuthHandlersConfig): AuthHandlers;
|
|
79
174
|
//#endregion
|
|
80
|
-
export { type AuthContext, AuthHandlers, AuthHandlersConfig, type CookieAdapter, type CookieSetOptions, DEFAULT_SESSION_COOKIE, type NextServerConfig, ServerSession, auth, createAuthHandlers, openSession, resolveNextConfig, sealSession };
|
|
175
|
+
export { type AuthContext, AuthHandlers, AuthHandlersConfig, AuthenticateResult, type CookieAdapter, type CookieSetOptions, DEFAULT_SESSION_COOKIE, ForgotPasswordResult, type NextServerConfig, ResetPasswordResult, ServerSession, auth, createAuthHandlers, openSession, resolveNextConfig, sealSession };
|