@poly-x/next 0.1.0-alpha.3 → 0.1.0-alpha.5
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/server.cjs +53 -23
- package/dist/server.d.cts +5 -0
- package/dist/server.d.mts +5 -0
- package/dist/server.mjs +53 -23
- package/dist/styles.css +105 -15
- package/package.json +3 -3
package/dist/server.cjs
CHANGED
|
@@ -36,6 +36,8 @@ function buildServerAuthClient(config) {
|
|
|
36
36
|
*/
|
|
37
37
|
const VERIFIER_COOKIE = "polyx_pkce";
|
|
38
38
|
const VERIFIER_MAX_AGE = 600;
|
|
39
|
+
/** poly-auth's own floor (Joi: min 6, max 128) — reject early rather than round-trip. */
|
|
40
|
+
const MIN_PASSWORD_LENGTH = 6;
|
|
39
41
|
function adapt(store) {
|
|
40
42
|
return {
|
|
41
43
|
get: (name) => store.get(name)?.value,
|
|
@@ -92,6 +94,7 @@ function createAuthHandlers(handlersConfig = {}) {
|
|
|
92
94
|
async authenticate(request) {
|
|
93
95
|
const config = resolveNextConfig(handlersConfig);
|
|
94
96
|
const redirectUri = `${new URL(request.url).origin}${callbackPath}`;
|
|
97
|
+
const client = buildServerAuthClient(config);
|
|
95
98
|
let payload;
|
|
96
99
|
try {
|
|
97
100
|
payload = await request.json();
|
|
@@ -103,36 +106,63 @@ function createAuthHandlers(handlersConfig = {}) {
|
|
|
103
106
|
if (!email || !password) return Response.json({ status: "invalid_request" }, { status: 400 });
|
|
104
107
|
const organizationCode = typeof payload.organizationCode === "string" ? payload.organizationCode : void 0;
|
|
105
108
|
const tenantId = typeof payload.tenantId === "string" ? payload.tenantId : void 0;
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
+
const newPassword = typeof payload.newPassword === "string" ? payload.newPassword : void 0;
|
|
110
|
+
const confirmPassword = typeof payload.confirmPassword === "string" ? payload.confirmPassword : void 0;
|
|
111
|
+
/** One authorize attempt, mapped to the wire response (sealing the session on success). */
|
|
112
|
+
async function attempt(withPassword) {
|
|
113
|
+
const { verifier, challenge } = await (0, _poly_x_core.generatePkce)();
|
|
114
|
+
const outcome = await client.authorizeWithPassword({
|
|
115
|
+
email,
|
|
116
|
+
password: withPassword,
|
|
117
|
+
redirectUri,
|
|
118
|
+
state: (0, _poly_x_core.randomState)(),
|
|
119
|
+
challenge,
|
|
120
|
+
organizationCode,
|
|
121
|
+
tenantId
|
|
122
|
+
});
|
|
123
|
+
switch (outcome.type) {
|
|
124
|
+
case "authorized":
|
|
125
|
+
await newSession(adapt(await (0, next_headers.cookies)()), config).establishFromCode(outcome.code, verifier, redirectUri);
|
|
126
|
+
return Response.json({
|
|
127
|
+
status: "success",
|
|
128
|
+
redirectTo: afterSignInPath
|
|
129
|
+
});
|
|
130
|
+
case "select_tenant": return Response.json({
|
|
131
|
+
status: "select_tenant",
|
|
132
|
+
tenants: outcome.tenants,
|
|
133
|
+
state: outcome.state
|
|
134
|
+
});
|
|
135
|
+
case "password_change_required": return Response.json({
|
|
136
|
+
status: "password_change_required",
|
|
137
|
+
userId: outcome.userId
|
|
138
|
+
}, { status: 403 });
|
|
139
|
+
case "no_access": return Response.json({ status: "no_access" }, { status: 403 });
|
|
140
|
+
default: return Response.json({ status: "invalid_credentials" }, { status: 401 });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (newPassword === void 0) return attempt(password);
|
|
144
|
+
if (newPassword.length < MIN_PASSWORD_LENGTH) return Response.json({
|
|
145
|
+
status: "weak_password",
|
|
146
|
+
minLength: MIN_PASSWORD_LENGTH
|
|
147
|
+
}, { status: 400 });
|
|
148
|
+
if (newPassword !== confirmPassword) return Response.json({ status: "password_mismatch" }, { status: 400 });
|
|
149
|
+
const { challenge } = await (0, _poly_x_core.generatePkce)();
|
|
150
|
+
const proof = await client.authorizeWithPassword({
|
|
109
151
|
email,
|
|
110
152
|
password,
|
|
111
153
|
redirectUri,
|
|
112
|
-
state,
|
|
154
|
+
state: (0, _poly_x_core.randomState)(),
|
|
113
155
|
challenge,
|
|
114
156
|
organizationCode,
|
|
115
157
|
tenantId
|
|
116
158
|
});
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
case "select_tenant": return Response.json({
|
|
125
|
-
status: "select_tenant",
|
|
126
|
-
tenants: outcome.tenants,
|
|
127
|
-
state: outcome.state
|
|
128
|
-
});
|
|
129
|
-
case "password_change_required": return Response.json({
|
|
130
|
-
status: "password_change_required",
|
|
131
|
-
userId: outcome.userId
|
|
132
|
-
}, { status: 403 });
|
|
133
|
-
case "no_access": return Response.json({ status: "no_access" }, { status: 403 });
|
|
134
|
-
default: return Response.json({ status: "invalid_credentials" }, { status: 401 });
|
|
135
|
-
}
|
|
159
|
+
if (proof.type !== "password_change_required") return Response.json({ status: "invalid_credentials" }, { status: 401 });
|
|
160
|
+
await client.setPassword({
|
|
161
|
+
userId: proof.userId,
|
|
162
|
+
newPassword,
|
|
163
|
+
confirmPassword
|
|
164
|
+
});
|
|
165
|
+
return attempt(newPassword);
|
|
136
166
|
},
|
|
137
167
|
async callback(request) {
|
|
138
168
|
const config = resolveNextConfig(handlersConfig);
|
package/dist/server.d.cts
CHANGED
package/dist/server.d.mts
CHANGED
package/dist/server.mjs
CHANGED
|
@@ -35,6 +35,8 @@ function buildServerAuthClient(config) {
|
|
|
35
35
|
*/
|
|
36
36
|
const VERIFIER_COOKIE = "polyx_pkce";
|
|
37
37
|
const VERIFIER_MAX_AGE = 600;
|
|
38
|
+
/** poly-auth's own floor (Joi: min 6, max 128) — reject early rather than round-trip. */
|
|
39
|
+
const MIN_PASSWORD_LENGTH = 6;
|
|
38
40
|
function adapt(store) {
|
|
39
41
|
return {
|
|
40
42
|
get: (name) => store.get(name)?.value,
|
|
@@ -91,6 +93,7 @@ function createAuthHandlers(handlersConfig = {}) {
|
|
|
91
93
|
async authenticate(request) {
|
|
92
94
|
const config = resolveNextConfig(handlersConfig);
|
|
93
95
|
const redirectUri = `${new URL(request.url).origin}${callbackPath}`;
|
|
96
|
+
const client = buildServerAuthClient(config);
|
|
94
97
|
let payload;
|
|
95
98
|
try {
|
|
96
99
|
payload = await request.json();
|
|
@@ -102,36 +105,63 @@ function createAuthHandlers(handlersConfig = {}) {
|
|
|
102
105
|
if (!email || !password) return Response.json({ status: "invalid_request" }, { status: 400 });
|
|
103
106
|
const organizationCode = typeof payload.organizationCode === "string" ? payload.organizationCode : void 0;
|
|
104
107
|
const tenantId = typeof payload.tenantId === "string" ? payload.tenantId : void 0;
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
+
const newPassword = typeof payload.newPassword === "string" ? payload.newPassword : void 0;
|
|
109
|
+
const confirmPassword = typeof payload.confirmPassword === "string" ? payload.confirmPassword : void 0;
|
|
110
|
+
/** One authorize attempt, mapped to the wire response (sealing the session on success). */
|
|
111
|
+
async function attempt(withPassword) {
|
|
112
|
+
const { verifier, challenge } = await generatePkce();
|
|
113
|
+
const outcome = await client.authorizeWithPassword({
|
|
114
|
+
email,
|
|
115
|
+
password: withPassword,
|
|
116
|
+
redirectUri,
|
|
117
|
+
state: randomState(),
|
|
118
|
+
challenge,
|
|
119
|
+
organizationCode,
|
|
120
|
+
tenantId
|
|
121
|
+
});
|
|
122
|
+
switch (outcome.type) {
|
|
123
|
+
case "authorized":
|
|
124
|
+
await newSession(adapt(await cookies()), config).establishFromCode(outcome.code, verifier, redirectUri);
|
|
125
|
+
return Response.json({
|
|
126
|
+
status: "success",
|
|
127
|
+
redirectTo: afterSignInPath
|
|
128
|
+
});
|
|
129
|
+
case "select_tenant": return Response.json({
|
|
130
|
+
status: "select_tenant",
|
|
131
|
+
tenants: outcome.tenants,
|
|
132
|
+
state: outcome.state
|
|
133
|
+
});
|
|
134
|
+
case "password_change_required": return Response.json({
|
|
135
|
+
status: "password_change_required",
|
|
136
|
+
userId: outcome.userId
|
|
137
|
+
}, { status: 403 });
|
|
138
|
+
case "no_access": return Response.json({ status: "no_access" }, { status: 403 });
|
|
139
|
+
default: return Response.json({ status: "invalid_credentials" }, { status: 401 });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (newPassword === void 0) return attempt(password);
|
|
143
|
+
if (newPassword.length < MIN_PASSWORD_LENGTH) return Response.json({
|
|
144
|
+
status: "weak_password",
|
|
145
|
+
minLength: MIN_PASSWORD_LENGTH
|
|
146
|
+
}, { status: 400 });
|
|
147
|
+
if (newPassword !== confirmPassword) return Response.json({ status: "password_mismatch" }, { status: 400 });
|
|
148
|
+
const { challenge } = await generatePkce();
|
|
149
|
+
const proof = await client.authorizeWithPassword({
|
|
108
150
|
email,
|
|
109
151
|
password,
|
|
110
152
|
redirectUri,
|
|
111
|
-
state,
|
|
153
|
+
state: randomState(),
|
|
112
154
|
challenge,
|
|
113
155
|
organizationCode,
|
|
114
156
|
tenantId
|
|
115
157
|
});
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
case "select_tenant": return Response.json({
|
|
124
|
-
status: "select_tenant",
|
|
125
|
-
tenants: outcome.tenants,
|
|
126
|
-
state: outcome.state
|
|
127
|
-
});
|
|
128
|
-
case "password_change_required": return Response.json({
|
|
129
|
-
status: "password_change_required",
|
|
130
|
-
userId: outcome.userId
|
|
131
|
-
}, { status: 403 });
|
|
132
|
-
case "no_access": return Response.json({ status: "no_access" }, { status: 403 });
|
|
133
|
-
default: return Response.json({ status: "invalid_credentials" }, { status: 401 });
|
|
134
|
-
}
|
|
158
|
+
if (proof.type !== "password_change_required") return Response.json({ status: "invalid_credentials" }, { status: 401 });
|
|
159
|
+
await client.setPassword({
|
|
160
|
+
userId: proof.userId,
|
|
161
|
+
newPassword,
|
|
162
|
+
confirmPassword
|
|
163
|
+
});
|
|
164
|
+
return attempt(newPassword);
|
|
135
165
|
},
|
|
136
166
|
async callback(request) {
|
|
137
167
|
const config = resolveNextConfig(handlersConfig);
|
package/dist/styles.css
CHANGED
|
@@ -25,7 +25,10 @@
|
|
|
25
25
|
--polyx-ring: #a5b4fc;
|
|
26
26
|
--polyx-danger: #dc2626;
|
|
27
27
|
--polyx-danger-bg: #fef2f2;
|
|
28
|
-
--polyx-page-bg: #
|
|
28
|
+
--polyx-page-bg: #ffffff;
|
|
29
|
+
/* The `page` variant's left panel — a quiet surface, a shade off the form side. */
|
|
30
|
+
--polyx-panel-bg: #fafafa;
|
|
31
|
+
--polyx-panel-fg: #18181b;
|
|
29
32
|
|
|
30
33
|
/* Shape + type */
|
|
31
34
|
--polyx-radius: 0.75rem;
|
|
@@ -59,7 +62,9 @@
|
|
|
59
62
|
--polyx-ring: #4f46e5;
|
|
60
63
|
--polyx-danger: #f87171;
|
|
61
64
|
--polyx-danger-bg: #2a1616;
|
|
62
|
-
--polyx-page-bg: #
|
|
65
|
+
--polyx-page-bg: #18181b;
|
|
66
|
+
--polyx-panel-bg: #09090b;
|
|
67
|
+
--polyx-panel-fg: #fafafa;
|
|
63
68
|
--polyx-shadow: 0 1px 3px rgb(0 0 0 / 0.4), 0 10px 30px rgb(0 0 0 / 0.35);
|
|
64
69
|
}
|
|
65
70
|
}
|
|
@@ -77,7 +82,9 @@
|
|
|
77
82
|
--polyx-ring: #4f46e5;
|
|
78
83
|
--polyx-danger: #f87171;
|
|
79
84
|
--polyx-danger-bg: #2a1616;
|
|
80
|
-
--polyx-page-bg: #
|
|
85
|
+
--polyx-page-bg: #18181b;
|
|
86
|
+
--polyx-panel-bg: #09090b;
|
|
87
|
+
--polyx-panel-fg: #fafafa;
|
|
81
88
|
--polyx-shadow: 0 1px 3px rgb(0 0 0 / 0.4), 0 10px 30px rgb(0 0 0 / 0.35);
|
|
82
89
|
}
|
|
83
90
|
|
|
@@ -92,21 +99,14 @@
|
|
|
92
99
|
--polyx-ring: #a5b4fc;
|
|
93
100
|
--polyx-danger: #dc2626;
|
|
94
101
|
--polyx-danger-bg: #fef2f2;
|
|
95
|
-
--polyx-page-bg: #
|
|
102
|
+
--polyx-page-bg: #ffffff;
|
|
103
|
+
--polyx-panel-bg: #fafafa;
|
|
104
|
+
--polyx-panel-fg: #18181b;
|
|
96
105
|
--polyx-shadow: 0 1px 3px rgb(0 0 0 / 0.06), 0 10px 30px rgb(0 0 0 / 0.07);
|
|
97
106
|
}
|
|
98
107
|
|
|
99
108
|
/* --- Variants ------------------------------------------------------------ */
|
|
100
109
|
|
|
101
|
-
/* `page`: owns the viewport — centered card on a full-bleed background. */
|
|
102
|
-
.polyx-signin--page {
|
|
103
|
-
display: grid;
|
|
104
|
-
place-items: center;
|
|
105
|
-
min-height: 100dvh;
|
|
106
|
-
padding: 1.5rem;
|
|
107
|
-
background: var(--polyx-page-bg);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
110
|
/* `card`: just the card — drop it anywhere in your own layout. */
|
|
111
111
|
.polyx-signin--card {
|
|
112
112
|
display: block;
|
|
@@ -114,15 +114,86 @@
|
|
|
114
114
|
|
|
115
115
|
.polyx-signin__card {
|
|
116
116
|
width: 100%;
|
|
117
|
-
max-width:
|
|
117
|
+
max-width: 27rem;
|
|
118
118
|
margin-inline: auto;
|
|
119
|
-
padding:
|
|
119
|
+
padding: 2.25rem;
|
|
120
120
|
background: var(--polyx-bg);
|
|
121
121
|
border: 1px solid var(--polyx-border);
|
|
122
122
|
border-radius: var(--polyx-radius);
|
|
123
123
|
box-shadow: var(--polyx-shadow);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/**
|
|
127
|
+
* `page`: owns the viewport — a left/right split. The form sits on the right; the
|
|
128
|
+
* left is a quiet surface holding whatever the consumer gives it (a `logo`, a
|
|
129
|
+
* `tagline`, or a custom `panel`). Deliberately minimal: no gradient, no invented
|
|
130
|
+
* copy. An empty panel reads as intentional negative space, and never fights the
|
|
131
|
+
* host app's brand. Below 64rem the panel drops away entirely and the form takes
|
|
132
|
+
* the screen on its own.
|
|
133
|
+
*/
|
|
134
|
+
.polyx-signin--page {
|
|
135
|
+
display: grid;
|
|
136
|
+
grid-template-columns: 1fr;
|
|
137
|
+
min-height: 100dvh;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@media (min-width: 64rem) {
|
|
141
|
+
.polyx-signin--page {
|
|
142
|
+
grid-template-columns: 1fr 1fr;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.polyx-signin__panel {
|
|
147
|
+
display: none;
|
|
148
|
+
flex-direction: column;
|
|
149
|
+
justify-content: space-between;
|
|
150
|
+
gap: 2rem;
|
|
151
|
+
padding: 3.5rem;
|
|
152
|
+
color: var(--polyx-panel-fg);
|
|
153
|
+
background: var(--polyx-panel-bg);
|
|
154
|
+
border-right: 1px solid var(--polyx-border);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
@media (min-width: 64rem) {
|
|
158
|
+
.polyx-signin__panel {
|
|
159
|
+
display: flex;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* The tagline is the panel's headline — it only earns its space on wide screens. */
|
|
164
|
+
.polyx-signin__tagline {
|
|
165
|
+
display: none;
|
|
166
|
+
margin: 0;
|
|
167
|
+
max-width: 18ch;
|
|
168
|
+
font-size: 2rem;
|
|
169
|
+
font-weight: 600;
|
|
170
|
+
line-height: 1.2;
|
|
171
|
+
letter-spacing: -0.02em;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@media (min-width: 64rem) {
|
|
175
|
+
.polyx-signin__tagline {
|
|
176
|
+
display: block;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.polyx-signin__main {
|
|
181
|
+
display: grid;
|
|
182
|
+
place-items: center;
|
|
183
|
+
padding: 2.5rem 1.5rem;
|
|
184
|
+
background: var(--polyx-page-bg);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* On the page variant the form isn't a floating card — it sits on the page itself. */
|
|
188
|
+
.polyx-signin--page .polyx-signin__card {
|
|
189
|
+
max-width: 25rem;
|
|
190
|
+
padding: 0;
|
|
191
|
+
background: transparent;
|
|
192
|
+
border: none;
|
|
193
|
+
box-shadow: none;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
|
|
126
197
|
/* --- Header -------------------------------------------------------------- */
|
|
127
198
|
|
|
128
199
|
.polyx-signin__header {
|
|
@@ -249,6 +320,25 @@
|
|
|
249
320
|
border-radius: var(--polyx-radius-sm);
|
|
250
321
|
}
|
|
251
322
|
|
|
323
|
+
/* --- Footer -------------------------------------------------------------- */
|
|
324
|
+
|
|
325
|
+
.polyx-signin__footer {
|
|
326
|
+
display: flex;
|
|
327
|
+
align-items: center;
|
|
328
|
+
justify-content: center;
|
|
329
|
+
gap: 0.375rem;
|
|
330
|
+
margin-top: 1.5rem;
|
|
331
|
+
font-size: 0.75rem;
|
|
332
|
+
color: var(--polyx-muted-fg);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.polyx-signin__shield {
|
|
336
|
+
flex: none;
|
|
337
|
+
width: 0.875rem;
|
|
338
|
+
height: 0.875rem;
|
|
339
|
+
opacity: 0.85;
|
|
340
|
+
}
|
|
341
|
+
|
|
252
342
|
/* --- Workspace picker ---------------------------------------------------- */
|
|
253
343
|
|
|
254
344
|
.polyx-signin__tenants {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poly-x/next",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.5",
|
|
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-alpha.
|
|
51
|
-
"@poly-x/react": "0.1.0-alpha.
|
|
50
|
+
"@poly-x/core": "0.1.0-alpha.5",
|
|
51
|
+
"@poly-x/react": "0.1.0-alpha.5"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"next": ">=14",
|