@poly-x/next 0.2.0 → 0.3.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 +39 -0
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/server.cjs +108 -0
- package/dist/server.d.cts +24 -0
- package/dist/server.d.mts +24 -0
- package/dist/server.mjs +108 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -150,6 +150,45 @@ platform's update applies whatever recognised fields it receives — including
|
|
|
150
150
|
turn a profile form into a way to change a credential or grant a role. Only the
|
|
151
151
|
six profile fields ever leave the server.
|
|
152
152
|
|
|
153
|
+
## Cross-application sign-on and switching workspace
|
|
154
|
+
|
|
155
|
+
`createPolyXHandler()` also mounts `/api/polyx/hop` and `/api/polyx/hop-complete`. You do not
|
|
156
|
+
call these directly — `@poly-x/react`'s `useWarmHop()` and `switchWorkspace()` drive them.
|
|
157
|
+
|
|
158
|
+
**The attempt is made by the browser, and it has to be.** The platform recognises a returning
|
|
159
|
+
person from an ecosystem cookie set on *its own* domain. Your server does not have that cookie,
|
|
160
|
+
so a server-side attempt is told "sign-in required" every single time — correctly, and
|
|
161
|
+
indistinguishably from someone who genuinely is signed out. The feature would look implemented
|
|
162
|
+
and never once work.
|
|
163
|
+
|
|
164
|
+
So the routes bracket a browser-made request rather than making one:
|
|
165
|
+
|
|
166
|
+
- **`hop`** mints the PKCE pair, keeps the verifier in an httpOnly cookie, and returns only a
|
|
167
|
+
URL. It makes no request of its own.
|
|
168
|
+
- **`hop-complete`** takes the `code` the browser received and exchanges it here. The code is
|
|
169
|
+
safe for a page to hold precisely because the verifier that redeems it never left your server.
|
|
170
|
+
|
|
171
|
+
Because of that, calling `completeAuthorize()` from server code **throws**
|
|
172
|
+
(`WARM_HOP_ON_SERVER`) rather than returning the plausible answer.
|
|
173
|
+
|
|
174
|
+
Register your `/api/polyx/callback` on the App even though nothing redirects to it during a
|
|
175
|
+
hop — the platform validates `redirect_uri` on both the authorize and the exchange.
|
|
176
|
+
|
|
177
|
+
> **Warm hop needs https end to end** (or a `localhost` dev setup). The platform issues its
|
|
178
|
+
> ecosystem cookie with `Secure`, and a browser silently discards a `Secure` cookie delivered
|
|
179
|
+
> over http. Nothing reports this: sign-in still succeeds, the `Set-Cookie` header is still
|
|
180
|
+
> there, and every hop afterwards reports "sign-in required" — indistinguishable from someone
|
|
181
|
+
> who is genuinely signed out. `localhost` is the one host browsers exempt.
|
|
182
|
+
|
|
183
|
+
### The boundary, stated plainly
|
|
184
|
+
|
|
185
|
+
**This works between applications that share the ecosystem cookie's domain.** Browsers block
|
|
186
|
+
third-party cookies by default, so an application on an unrelated domain cannot see that cookie
|
|
187
|
+
and will fall back to ordinary sign-in. That is a browser policy, not a limitation we can
|
|
188
|
+
configure away — solving it needs a different mechanism entirely. Nothing here claims otherwise.
|
|
189
|
+
|
|
190
|
+
Switching workspace is unaffected by that boundary: it happens inside one application.
|
|
191
|
+
|
|
153
192
|
## Exports
|
|
154
193
|
|
|
155
194
|
- `@poly-x/next` — the client surface (re-exports `@poly-x/react`): `PolyXProvider`,
|
package/dist/index.cjs
CHANGED
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
3
3
|
//#region src/index.ts
|
|
4
4
|
const PACKAGE_NAME = "@poly-x/next";
|
|
5
5
|
/** The published version of this package, injected at build time. */
|
|
6
|
-
const version = "0.
|
|
6
|
+
const version = "0.3.0";
|
|
7
7
|
//#endregion
|
|
8
8
|
exports.PACKAGE_NAME = PACKAGE_NAME;
|
|
9
9
|
exports.version = version;
|
package/dist/index.mjs
CHANGED
|
@@ -3,6 +3,6 @@ export * from "@poly-x/react";
|
|
|
3
3
|
//#region src/index.ts
|
|
4
4
|
const PACKAGE_NAME = "@poly-x/next";
|
|
5
5
|
/** The published version of this package, injected at build time. */
|
|
6
|
-
const version = "0.
|
|
6
|
+
const version = "0.3.0";
|
|
7
7
|
//#endregion
|
|
8
8
|
export { PACKAGE_NAME, version };
|
package/dist/server.cjs
CHANGED
|
@@ -512,6 +512,8 @@ const POLYX_ACTIONS = {
|
|
|
512
512
|
"forgot-password": "forgotPassword",
|
|
513
513
|
"reset-password": "resetPassword",
|
|
514
514
|
callback: "callback",
|
|
515
|
+
hop: "hop",
|
|
516
|
+
"hop-complete": "hopComplete",
|
|
515
517
|
refresh: "refresh",
|
|
516
518
|
signout: "signout",
|
|
517
519
|
session: "session",
|
|
@@ -534,6 +536,8 @@ const POLYX_ACTION_METHODS = {
|
|
|
534
536
|
"subscription-credential": ["GET"],
|
|
535
537
|
"realtime-ticket": ["GET"],
|
|
536
538
|
authenticate: ["POST"],
|
|
539
|
+
hop: ["POST"],
|
|
540
|
+
"hop-complete": ["POST"],
|
|
537
541
|
"forgot-password": ["POST"],
|
|
538
542
|
"reset-password": ["POST"],
|
|
539
543
|
refresh: ["POST"],
|
|
@@ -1125,6 +1129,110 @@ function createAuthHandlers(handlersConfig = {}) {
|
|
|
1125
1129
|
}
|
|
1126
1130
|
return Response.redirect(home, 302);
|
|
1127
1131
|
},
|
|
1132
|
+
async hop(request) {
|
|
1133
|
+
const config = resolveNextConfig(handlersConfig);
|
|
1134
|
+
const secure = resolveCookieSecure(request, config);
|
|
1135
|
+
const redirectUri = `${resolvePublicOrigin(request, config)}${callbackPath}`;
|
|
1136
|
+
let hint = {};
|
|
1137
|
+
try {
|
|
1138
|
+
hint = await request.json() ?? {};
|
|
1139
|
+
} catch {
|
|
1140
|
+
hint = {};
|
|
1141
|
+
}
|
|
1142
|
+
const organizationCode = typeof hint.tenantId === "string" && hint.tenantId !== "" ? hint.tenantId : typeof hint.organizationCode === "string" && hint.organizationCode !== "" ? hint.organizationCode : void 0;
|
|
1143
|
+
const { verifier, challenge } = await (0, _poly_x_core.generatePkce)();
|
|
1144
|
+
const state = (0, _poly_x_core.randomState)();
|
|
1145
|
+
(await (0, next_headers.cookies)()).set(VERIFIER_COOKIE, JSON.stringify({
|
|
1146
|
+
verifier,
|
|
1147
|
+
state,
|
|
1148
|
+
redirectUri,
|
|
1149
|
+
...organizationCode ? { requested: organizationCode } : {}
|
|
1150
|
+
}), {
|
|
1151
|
+
httpOnly: true,
|
|
1152
|
+
secure,
|
|
1153
|
+
sameSite: "lax",
|
|
1154
|
+
path: "/",
|
|
1155
|
+
maxAge: VERIFIER_MAX_AGE
|
|
1156
|
+
});
|
|
1157
|
+
const authorizeUrl = buildServerAuthClient(config).buildAuthorizeUrl({
|
|
1158
|
+
redirectUri,
|
|
1159
|
+
state,
|
|
1160
|
+
challenge,
|
|
1161
|
+
prompt: "none",
|
|
1162
|
+
...organizationCode ? { organizationCode } : {}
|
|
1163
|
+
});
|
|
1164
|
+
return Response.json({
|
|
1165
|
+
status: "ready",
|
|
1166
|
+
authorizeUrl
|
|
1167
|
+
});
|
|
1168
|
+
},
|
|
1169
|
+
async hopComplete(request) {
|
|
1170
|
+
const config = resolveNextConfig(handlersConfig);
|
|
1171
|
+
const secure = resolveCookieSecure(request, config);
|
|
1172
|
+
const store = await (0, next_headers.cookies)();
|
|
1173
|
+
let body;
|
|
1174
|
+
try {
|
|
1175
|
+
body = await request.json() ?? {};
|
|
1176
|
+
} catch {
|
|
1177
|
+
return Response.json({ status: "invalid_request" }, { status: 400 });
|
|
1178
|
+
}
|
|
1179
|
+
const code = typeof body.code === "string" ? body.code : "";
|
|
1180
|
+
const state = typeof body.state === "string" ? body.state : "";
|
|
1181
|
+
const raw = store.get(VERIFIER_COOKIE)?.value;
|
|
1182
|
+
store.delete(VERIFIER_COOKIE);
|
|
1183
|
+
if (!code || !state || !raw) return Response.json({ status: "invalid_request" }, { status: 400 });
|
|
1184
|
+
let tx;
|
|
1185
|
+
try {
|
|
1186
|
+
tx = JSON.parse(raw);
|
|
1187
|
+
} catch {
|
|
1188
|
+
return Response.json({ status: "invalid_request" }, { status: 400 });
|
|
1189
|
+
}
|
|
1190
|
+
if (tx.state !== state) return Response.json({ status: "invalid_request" }, { status: 400 });
|
|
1191
|
+
let entered;
|
|
1192
|
+
try {
|
|
1193
|
+
entered = (await newSession(adapt(store), config, secure).establishFromCode(code, tx.verifier, tx.redirectUri, collectForwardedDeviceHeaders(request))).session.claims.organizationId;
|
|
1194
|
+
} catch (error) {
|
|
1195
|
+
if (error instanceof SessionCookieTooLargeError) {
|
|
1196
|
+
console.error(`[polyx] ${error.message}`);
|
|
1197
|
+
return Response.json({
|
|
1198
|
+
status: "session_too_large",
|
|
1199
|
+
bytes: error.bytes,
|
|
1200
|
+
limit: error.limit
|
|
1201
|
+
}, { status: 400 });
|
|
1202
|
+
}
|
|
1203
|
+
return Response.json({ status: "exchange_failed" }, { status: 400 });
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* The substitution guard (verified against a live platform, 2026-07-29).
|
|
1207
|
+
*
|
|
1208
|
+
* The platform only consults a workspace hint when the person has **more than one**
|
|
1209
|
+
* candidate workspace for that application. With exactly one, the hint is ignored and they
|
|
1210
|
+
* are placed in that one — answering `authorized`, with nothing to say the request was not
|
|
1211
|
+
* honoured. Asking for workspace A and being silently seated in workspace B is precisely
|
|
1212
|
+
* the outcome the scope decisions call worse than a prompt: someone can act in the wrong
|
|
1213
|
+
* tenant without noticing.
|
|
1214
|
+
*
|
|
1215
|
+
* A hint can be a tenant id, an organization code, or a slug, and only the first is
|
|
1216
|
+
* comparable to what the session carries. So an id-shaped hint that does not match is
|
|
1217
|
+
* reported as a mismatch; for the other forms the workspace actually entered is returned
|
|
1218
|
+
* and the caller can decide. Either way the answer is never a bare "success" that hides a
|
|
1219
|
+
* substitution.
|
|
1220
|
+
*
|
|
1221
|
+
* The session IS established at this point and is left in place: the person is signed in
|
|
1222
|
+
* somewhere they are entitled to be. This reports what happened rather than tearing down a
|
|
1223
|
+
* valid session the caller may be perfectly happy with.
|
|
1224
|
+
*/
|
|
1225
|
+
const requested = tx.requested;
|
|
1226
|
+
if (typeof requested === "string" && /^[a-f0-9]{24}$/i.test(requested) && entered && requested.toLowerCase() !== entered.toLowerCase()) return Response.json({
|
|
1227
|
+
status: "workspace_mismatch",
|
|
1228
|
+
requested,
|
|
1229
|
+
workspace: entered
|
|
1230
|
+
});
|
|
1231
|
+
return Response.json({
|
|
1232
|
+
status: "success",
|
|
1233
|
+
...entered ? { workspace: entered } : {}
|
|
1234
|
+
});
|
|
1235
|
+
},
|
|
1128
1236
|
async refresh(request) {
|
|
1129
1237
|
const { isSignedIn } = await refreshSession({
|
|
1130
1238
|
...handlersConfig,
|
package/dist/server.d.cts
CHANGED
|
@@ -221,6 +221,8 @@ declare const POLYX_ACTIONS: {
|
|
|
221
221
|
readonly "forgot-password": "forgotPassword";
|
|
222
222
|
readonly "reset-password": "resetPassword";
|
|
223
223
|
readonly callback: "callback";
|
|
224
|
+
readonly hop: "hop";
|
|
225
|
+
readonly "hop-complete": "hopComplete";
|
|
224
226
|
readonly refresh: "refresh";
|
|
225
227
|
readonly signout: "signout";
|
|
226
228
|
readonly session: "session";
|
|
@@ -383,6 +385,28 @@ interface AuthHandlers {
|
|
|
383
385
|
*/
|
|
384
386
|
resetPassword(request: Request): Promise<Response>;
|
|
385
387
|
callback(request: Request): Promise<Response>;
|
|
388
|
+
/**
|
|
389
|
+
* Prepare a warm hop — the silent sign-in attempt (F026 / FR-HOP-1).
|
|
390
|
+
*
|
|
391
|
+
* Returns the authorize URL for **the browser** to call, and keeps the PKCE verifier in an
|
|
392
|
+
* httpOnly cookie. It deliberately makes no request of its own: the platform resolves this
|
|
393
|
+
* from the browser's ecosystem cookie, which a server-side call does not carry, so a
|
|
394
|
+
* server-side attempt would report "sign-in required" every time and the feature would look
|
|
395
|
+
* built while never working (FR-HOP-5).
|
|
396
|
+
*
|
|
397
|
+
* An optional `{ tenantId }` / `{ organizationCode }` body pins a workspace. The platform
|
|
398
|
+
* matches it **only within the person's own memberships**, so it can narrow a choice but
|
|
399
|
+
* never widen access — which is what also makes this the route the workspace switch uses.
|
|
400
|
+
*/
|
|
401
|
+
hop(request: Request): Promise<Response>;
|
|
402
|
+
/**
|
|
403
|
+
* Finish a warm hop: exchange the `code` the browser received for a session (F026 / FR-HOP-3).
|
|
404
|
+
*
|
|
405
|
+
* The code is safe for a browser to hold because it is worthless without the PKCE verifier,
|
|
406
|
+
* and the verifier never left this server. The exchange happens here, so tokens never touch
|
|
407
|
+
* the page — the same custody path as every other sign-in, not a second mechanism.
|
|
408
|
+
*/
|
|
409
|
+
hopComplete(request: Request): Promise<Response>;
|
|
386
410
|
refresh(request: Request): Promise<Response>;
|
|
387
411
|
signout(request: Request): Promise<Response>;
|
|
388
412
|
/**
|
package/dist/server.d.mts
CHANGED
|
@@ -221,6 +221,8 @@ declare const POLYX_ACTIONS: {
|
|
|
221
221
|
readonly "forgot-password": "forgotPassword";
|
|
222
222
|
readonly "reset-password": "resetPassword";
|
|
223
223
|
readonly callback: "callback";
|
|
224
|
+
readonly hop: "hop";
|
|
225
|
+
readonly "hop-complete": "hopComplete";
|
|
224
226
|
readonly refresh: "refresh";
|
|
225
227
|
readonly signout: "signout";
|
|
226
228
|
readonly session: "session";
|
|
@@ -383,6 +385,28 @@ interface AuthHandlers {
|
|
|
383
385
|
*/
|
|
384
386
|
resetPassword(request: Request): Promise<Response>;
|
|
385
387
|
callback(request: Request): Promise<Response>;
|
|
388
|
+
/**
|
|
389
|
+
* Prepare a warm hop — the silent sign-in attempt (F026 / FR-HOP-1).
|
|
390
|
+
*
|
|
391
|
+
* Returns the authorize URL for **the browser** to call, and keeps the PKCE verifier in an
|
|
392
|
+
* httpOnly cookie. It deliberately makes no request of its own: the platform resolves this
|
|
393
|
+
* from the browser's ecosystem cookie, which a server-side call does not carry, so a
|
|
394
|
+
* server-side attempt would report "sign-in required" every time and the feature would look
|
|
395
|
+
* built while never working (FR-HOP-5).
|
|
396
|
+
*
|
|
397
|
+
* An optional `{ tenantId }` / `{ organizationCode }` body pins a workspace. The platform
|
|
398
|
+
* matches it **only within the person's own memberships**, so it can narrow a choice but
|
|
399
|
+
* never widen access — which is what also makes this the route the workspace switch uses.
|
|
400
|
+
*/
|
|
401
|
+
hop(request: Request): Promise<Response>;
|
|
402
|
+
/**
|
|
403
|
+
* Finish a warm hop: exchange the `code` the browser received for a session (F026 / FR-HOP-3).
|
|
404
|
+
*
|
|
405
|
+
* The code is safe for a browser to hold because it is worthless without the PKCE verifier,
|
|
406
|
+
* and the verifier never left this server. The exchange happens here, so tokens never touch
|
|
407
|
+
* the page — the same custody path as every other sign-in, not a second mechanism.
|
|
408
|
+
*/
|
|
409
|
+
hopComplete(request: Request): Promise<Response>;
|
|
386
410
|
refresh(request: Request): Promise<Response>;
|
|
387
411
|
signout(request: Request): Promise<Response>;
|
|
388
412
|
/**
|
package/dist/server.mjs
CHANGED
|
@@ -511,6 +511,8 @@ const POLYX_ACTIONS = {
|
|
|
511
511
|
"forgot-password": "forgotPassword",
|
|
512
512
|
"reset-password": "resetPassword",
|
|
513
513
|
callback: "callback",
|
|
514
|
+
hop: "hop",
|
|
515
|
+
"hop-complete": "hopComplete",
|
|
514
516
|
refresh: "refresh",
|
|
515
517
|
signout: "signout",
|
|
516
518
|
session: "session",
|
|
@@ -533,6 +535,8 @@ const POLYX_ACTION_METHODS = {
|
|
|
533
535
|
"subscription-credential": ["GET"],
|
|
534
536
|
"realtime-ticket": ["GET"],
|
|
535
537
|
authenticate: ["POST"],
|
|
538
|
+
hop: ["POST"],
|
|
539
|
+
"hop-complete": ["POST"],
|
|
536
540
|
"forgot-password": ["POST"],
|
|
537
541
|
"reset-password": ["POST"],
|
|
538
542
|
refresh: ["POST"],
|
|
@@ -1124,6 +1128,110 @@ function createAuthHandlers(handlersConfig = {}) {
|
|
|
1124
1128
|
}
|
|
1125
1129
|
return Response.redirect(home, 302);
|
|
1126
1130
|
},
|
|
1131
|
+
async hop(request) {
|
|
1132
|
+
const config = resolveNextConfig(handlersConfig);
|
|
1133
|
+
const secure = resolveCookieSecure(request, config);
|
|
1134
|
+
const redirectUri = `${resolvePublicOrigin(request, config)}${callbackPath}`;
|
|
1135
|
+
let hint = {};
|
|
1136
|
+
try {
|
|
1137
|
+
hint = await request.json() ?? {};
|
|
1138
|
+
} catch {
|
|
1139
|
+
hint = {};
|
|
1140
|
+
}
|
|
1141
|
+
const organizationCode = typeof hint.tenantId === "string" && hint.tenantId !== "" ? hint.tenantId : typeof hint.organizationCode === "string" && hint.organizationCode !== "" ? hint.organizationCode : void 0;
|
|
1142
|
+
const { verifier, challenge } = await generatePkce();
|
|
1143
|
+
const state = randomState();
|
|
1144
|
+
(await cookies()).set(VERIFIER_COOKIE, JSON.stringify({
|
|
1145
|
+
verifier,
|
|
1146
|
+
state,
|
|
1147
|
+
redirectUri,
|
|
1148
|
+
...organizationCode ? { requested: organizationCode } : {}
|
|
1149
|
+
}), {
|
|
1150
|
+
httpOnly: true,
|
|
1151
|
+
secure,
|
|
1152
|
+
sameSite: "lax",
|
|
1153
|
+
path: "/",
|
|
1154
|
+
maxAge: VERIFIER_MAX_AGE
|
|
1155
|
+
});
|
|
1156
|
+
const authorizeUrl = buildServerAuthClient(config).buildAuthorizeUrl({
|
|
1157
|
+
redirectUri,
|
|
1158
|
+
state,
|
|
1159
|
+
challenge,
|
|
1160
|
+
prompt: "none",
|
|
1161
|
+
...organizationCode ? { organizationCode } : {}
|
|
1162
|
+
});
|
|
1163
|
+
return Response.json({
|
|
1164
|
+
status: "ready",
|
|
1165
|
+
authorizeUrl
|
|
1166
|
+
});
|
|
1167
|
+
},
|
|
1168
|
+
async hopComplete(request) {
|
|
1169
|
+
const config = resolveNextConfig(handlersConfig);
|
|
1170
|
+
const secure = resolveCookieSecure(request, config);
|
|
1171
|
+
const store = await cookies();
|
|
1172
|
+
let body;
|
|
1173
|
+
try {
|
|
1174
|
+
body = await request.json() ?? {};
|
|
1175
|
+
} catch {
|
|
1176
|
+
return Response.json({ status: "invalid_request" }, { status: 400 });
|
|
1177
|
+
}
|
|
1178
|
+
const code = typeof body.code === "string" ? body.code : "";
|
|
1179
|
+
const state = typeof body.state === "string" ? body.state : "";
|
|
1180
|
+
const raw = store.get(VERIFIER_COOKIE)?.value;
|
|
1181
|
+
store.delete(VERIFIER_COOKIE);
|
|
1182
|
+
if (!code || !state || !raw) return Response.json({ status: "invalid_request" }, { status: 400 });
|
|
1183
|
+
let tx;
|
|
1184
|
+
try {
|
|
1185
|
+
tx = JSON.parse(raw);
|
|
1186
|
+
} catch {
|
|
1187
|
+
return Response.json({ status: "invalid_request" }, { status: 400 });
|
|
1188
|
+
}
|
|
1189
|
+
if (tx.state !== state) return Response.json({ status: "invalid_request" }, { status: 400 });
|
|
1190
|
+
let entered;
|
|
1191
|
+
try {
|
|
1192
|
+
entered = (await newSession(adapt(store), config, secure).establishFromCode(code, tx.verifier, tx.redirectUri, collectForwardedDeviceHeaders(request))).session.claims.organizationId;
|
|
1193
|
+
} catch (error) {
|
|
1194
|
+
if (error instanceof SessionCookieTooLargeError) {
|
|
1195
|
+
console.error(`[polyx] ${error.message}`);
|
|
1196
|
+
return Response.json({
|
|
1197
|
+
status: "session_too_large",
|
|
1198
|
+
bytes: error.bytes,
|
|
1199
|
+
limit: error.limit
|
|
1200
|
+
}, { status: 400 });
|
|
1201
|
+
}
|
|
1202
|
+
return Response.json({ status: "exchange_failed" }, { status: 400 });
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
1205
|
+
* The substitution guard (verified against a live platform, 2026-07-29).
|
|
1206
|
+
*
|
|
1207
|
+
* The platform only consults a workspace hint when the person has **more than one**
|
|
1208
|
+
* candidate workspace for that application. With exactly one, the hint is ignored and they
|
|
1209
|
+
* are placed in that one — answering `authorized`, with nothing to say the request was not
|
|
1210
|
+
* honoured. Asking for workspace A and being silently seated in workspace B is precisely
|
|
1211
|
+
* the outcome the scope decisions call worse than a prompt: someone can act in the wrong
|
|
1212
|
+
* tenant without noticing.
|
|
1213
|
+
*
|
|
1214
|
+
* A hint can be a tenant id, an organization code, or a slug, and only the first is
|
|
1215
|
+
* comparable to what the session carries. So an id-shaped hint that does not match is
|
|
1216
|
+
* reported as a mismatch; for the other forms the workspace actually entered is returned
|
|
1217
|
+
* and the caller can decide. Either way the answer is never a bare "success" that hides a
|
|
1218
|
+
* substitution.
|
|
1219
|
+
*
|
|
1220
|
+
* The session IS established at this point and is left in place: the person is signed in
|
|
1221
|
+
* somewhere they are entitled to be. This reports what happened rather than tearing down a
|
|
1222
|
+
* valid session the caller may be perfectly happy with.
|
|
1223
|
+
*/
|
|
1224
|
+
const requested = tx.requested;
|
|
1225
|
+
if (typeof requested === "string" && /^[a-f0-9]{24}$/i.test(requested) && entered && requested.toLowerCase() !== entered.toLowerCase()) return Response.json({
|
|
1226
|
+
status: "workspace_mismatch",
|
|
1227
|
+
requested,
|
|
1228
|
+
workspace: entered
|
|
1229
|
+
});
|
|
1230
|
+
return Response.json({
|
|
1231
|
+
status: "success",
|
|
1232
|
+
...entered ? { workspace: entered } : {}
|
|
1233
|
+
});
|
|
1234
|
+
},
|
|
1127
1235
|
async refresh(request) {
|
|
1128
1236
|
const { isSignedIn } = await refreshSession({
|
|
1129
1237
|
...handlersConfig,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poly-x/next",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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.
|
|
51
|
-
"@poly-x/react": "0.
|
|
50
|
+
"@poly-x/core": "0.3.0",
|
|
51
|
+
"@poly-x/react": "0.3.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"next": ">=14",
|