@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 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 { verifier, challenge } = await (0, _poly_x_core.generatePkce)();
107
- const state = (0, _poly_x_core.randomState)();
108
- const outcome = await buildServerAuthClient(config).authorizeWithPassword({
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
- switch (outcome.type) {
118
- case "authorized":
119
- await newSession(adapt(await (0, next_headers.cookies)()), config).establishFromCode(outcome.code, verifier, redirectUri);
120
- return Response.json({
121
- status: "success",
122
- redirectTo: afterSignInPath
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
@@ -95,6 +95,11 @@ type AuthenticateResult = {
95
95
  } | {
96
96
  status: "password_change_required";
97
97
  userId: string;
98
+ } | {
99
+ status: "weak_password";
100
+ minLength: number;
101
+ } | {
102
+ status: "password_mismatch";
98
103
  } | {
99
104
  status: "no_access";
100
105
  } | {
package/dist/server.d.mts CHANGED
@@ -95,6 +95,11 @@ type AuthenticateResult = {
95
95
  } | {
96
96
  status: "password_change_required";
97
97
  userId: string;
98
+ } | {
99
+ status: "weak_password";
100
+ minLength: number;
101
+ } | {
102
+ status: "password_mismatch";
98
103
  } | {
99
104
  status: "no_access";
100
105
  } | {
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 { verifier, challenge } = await generatePkce();
106
- const state = randomState();
107
- const outcome = await buildServerAuthClient(config).authorizeWithPassword({
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
- switch (outcome.type) {
117
- case "authorized":
118
- await newSession(adapt(await cookies()), config).establishFromCode(outcome.code, verifier, redirectUri);
119
- return Response.json({
120
- status: "success",
121
- redirectTo: afterSignInPath
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: #fafafa;
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: #09090b;
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: #09090b;
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: #fafafa;
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: 24rem;
117
+ max-width: 27rem;
118
118
  margin-inline: auto;
119
- padding: 2rem;
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",
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.3",
51
- "@poly-x/react": "0.1.0-alpha.3"
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",