@poly-x/next 0.1.1 → 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 +166 -23
- package/dist/index.cjs +2 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +2 -1
- package/dist/proxy.cjs +24 -4
- package/dist/proxy.d.cts +2 -1
- package/dist/proxy.d.mts +2 -1
- package/dist/proxy.mjs +23 -5
- package/dist/server.cjs +1085 -22
- package/dist/server.d.cts +294 -3
- package/dist/server.d.mts +294 -3
- package/dist/server.mjs +1066 -12
- package/dist/session-cookie-AEnY4PVW.d.cts +21 -0
- package/dist/session-cookie-AEnY4PVW.d.mts +21 -0
- package/dist/session-cookie-BQD10B3D.cjs +34 -0
- package/dist/session-cookie-C23WspJZ.mjs +23 -0
- package/dist/styles.css +151 -0
- package/package.json +3 -3
- package/dist/server-session-Df9HXl_5.cjs +0 -254
- package/dist/server-session-pY-nzh5x.mjs +0 -207
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/session-cookie.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The session cookie's name, in a module with NO imports (F015 / FR-SESS-4).
|
|
4
|
+
*
|
|
5
|
+
* Both consuming apps hard-code `"polyx_session"` rather than importing it, and the
|
|
6
|
+
* reason is mechanical: it lived in `server-session.ts`, so importing it from the edge
|
|
7
|
+
* dragged server-only code into the edge runtime. A hard-coded copy in each app is a
|
|
8
|
+
* silent breakage waiting to happen — if the SDK ever changed the name, route gating
|
|
9
|
+
* would fail open or closed with nothing to point at.
|
|
10
|
+
*
|
|
11
|
+
* This module is safe to import anywhere: edge middleware, the browser, the server.
|
|
12
|
+
*/
|
|
13
|
+
/** The cookie the SDK seals the session into. */
|
|
14
|
+
declare const DEFAULT_SESSION_COOKIE = "polyx_session";
|
|
15
|
+
/**
|
|
16
|
+
* Read the sealed session value out of a raw `Cookie` header, without unsealing it.
|
|
17
|
+
* Useful at the edge, where presence is all that can be checked cheaply.
|
|
18
|
+
*/
|
|
19
|
+
declare function readSessionCookie(cookieHeader: string | null | undefined, cookieName?: string): string | undefined;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { readSessionCookie as n, DEFAULT_SESSION_COOKIE as t };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/session-cookie.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The session cookie's name, in a module with NO imports (F015 / FR-SESS-4).
|
|
4
|
+
*
|
|
5
|
+
* Both consuming apps hard-code `"polyx_session"` rather than importing it, and the
|
|
6
|
+
* reason is mechanical: it lived in `server-session.ts`, so importing it from the edge
|
|
7
|
+
* dragged server-only code into the edge runtime. A hard-coded copy in each app is a
|
|
8
|
+
* silent breakage waiting to happen — if the SDK ever changed the name, route gating
|
|
9
|
+
* would fail open or closed with nothing to point at.
|
|
10
|
+
*
|
|
11
|
+
* This module is safe to import anywhere: edge middleware, the browser, the server.
|
|
12
|
+
*/
|
|
13
|
+
/** The cookie the SDK seals the session into. */
|
|
14
|
+
declare const DEFAULT_SESSION_COOKIE = "polyx_session";
|
|
15
|
+
/**
|
|
16
|
+
* Read the sealed session value out of a raw `Cookie` header, without unsealing it.
|
|
17
|
+
* Useful at the edge, where presence is all that can be checked cheaply.
|
|
18
|
+
*/
|
|
19
|
+
declare function readSessionCookie(cookieHeader: string | null | undefined, cookieName?: string): string | undefined;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { readSessionCookie as n, DEFAULT_SESSION_COOKIE as t };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//#region src/session-cookie.ts
|
|
2
|
+
/**
|
|
3
|
+
* The session cookie's name, in a module with NO imports (F015 / FR-SESS-4).
|
|
4
|
+
*
|
|
5
|
+
* Both consuming apps hard-code `"polyx_session"` rather than importing it, and the
|
|
6
|
+
* reason is mechanical: it lived in `server-session.ts`, so importing it from the edge
|
|
7
|
+
* dragged server-only code into the edge runtime. A hard-coded copy in each app is a
|
|
8
|
+
* silent breakage waiting to happen — if the SDK ever changed the name, route gating
|
|
9
|
+
* would fail open or closed with nothing to point at.
|
|
10
|
+
*
|
|
11
|
+
* This module is safe to import anywhere: edge middleware, the browser, the server.
|
|
12
|
+
*/
|
|
13
|
+
/** The cookie the SDK seals the session into. */
|
|
14
|
+
const DEFAULT_SESSION_COOKIE = "polyx_session";
|
|
15
|
+
/**
|
|
16
|
+
* Read the sealed session value out of a raw `Cookie` header, without unsealing it.
|
|
17
|
+
* Useful at the edge, where presence is all that can be checked cheaply.
|
|
18
|
+
*/
|
|
19
|
+
function readSessionCookie(cookieHeader, cookieName = DEFAULT_SESSION_COOKIE) {
|
|
20
|
+
return cookieHeader?.split(";").map((part) => part.trim()).find((part) => part.startsWith(`${cookieName}=`))?.slice(cookieName.length + 1);
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
Object.defineProperty(exports, "DEFAULT_SESSION_COOKIE", {
|
|
24
|
+
enumerable: true,
|
|
25
|
+
get: function() {
|
|
26
|
+
return DEFAULT_SESSION_COOKIE;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
Object.defineProperty(exports, "readSessionCookie", {
|
|
30
|
+
enumerable: true,
|
|
31
|
+
get: function() {
|
|
32
|
+
return readSessionCookie;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/session-cookie.ts
|
|
2
|
+
/**
|
|
3
|
+
* The session cookie's name, in a module with NO imports (F015 / FR-SESS-4).
|
|
4
|
+
*
|
|
5
|
+
* Both consuming apps hard-code `"polyx_session"` rather than importing it, and the
|
|
6
|
+
* reason is mechanical: it lived in `server-session.ts`, so importing it from the edge
|
|
7
|
+
* dragged server-only code into the edge runtime. A hard-coded copy in each app is a
|
|
8
|
+
* silent breakage waiting to happen — if the SDK ever changed the name, route gating
|
|
9
|
+
* would fail open or closed with nothing to point at.
|
|
10
|
+
*
|
|
11
|
+
* This module is safe to import anywhere: edge middleware, the browser, the server.
|
|
12
|
+
*/
|
|
13
|
+
/** The cookie the SDK seals the session into. */
|
|
14
|
+
const DEFAULT_SESSION_COOKIE = "polyx_session";
|
|
15
|
+
/**
|
|
16
|
+
* Read the sealed session value out of a raw `Cookie` header, without unsealing it.
|
|
17
|
+
* Useful at the edge, where presence is all that can be checked cheaply.
|
|
18
|
+
*/
|
|
19
|
+
function readSessionCookie(cookieHeader, cookieName = DEFAULT_SESSION_COOKIE) {
|
|
20
|
+
return cookieHeader?.split(";").map((part) => part.trim()).find((part) => part.startsWith(`${cookieName}=`))?.slice(cookieName.length + 1);
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
export { readSessionCookie as n, DEFAULT_SESSION_COOKIE as t };
|
package/dist/styles.css
CHANGED
|
@@ -709,6 +709,157 @@
|
|
|
709
709
|
color: var(--polyx-muted-fg);
|
|
710
710
|
}
|
|
711
711
|
|
|
712
|
+
/* --- User profile dialog (v0.4) ------------------------------------------ */
|
|
713
|
+
|
|
714
|
+
.polyx-userprofile__trigger {
|
|
715
|
+
font: inherit;
|
|
716
|
+
font-size: 0.875rem;
|
|
717
|
+
color: var(--polyx-fg);
|
|
718
|
+
background: none;
|
|
719
|
+
border: none;
|
|
720
|
+
padding: 0;
|
|
721
|
+
cursor: pointer;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
.polyx-userprofile__overlay {
|
|
725
|
+
position: fixed;
|
|
726
|
+
inset: 0;
|
|
727
|
+
z-index: 50;
|
|
728
|
+
display: flex;
|
|
729
|
+
align-items: center;
|
|
730
|
+
justify-content: center;
|
|
731
|
+
padding: 1rem;
|
|
732
|
+
background: rgb(0 0 0 / 0.45);
|
|
733
|
+
overflow-y: auto;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
.polyx-userprofile__dialog {
|
|
737
|
+
width: 100%;
|
|
738
|
+
max-width: 28rem;
|
|
739
|
+
padding: 1.5rem;
|
|
740
|
+
background: var(--polyx-bg);
|
|
741
|
+
color: var(--polyx-fg);
|
|
742
|
+
border: 1px solid var(--polyx-border);
|
|
743
|
+
border-radius: var(--polyx-radius);
|
|
744
|
+
box-shadow: var(--polyx-shadow);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
.polyx-userprofile__header {
|
|
748
|
+
margin-bottom: 1.25rem;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
.polyx-userprofile__title {
|
|
752
|
+
margin: 0;
|
|
753
|
+
font-size: 1.0625rem;
|
|
754
|
+
font-weight: 600;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
.polyx-userprofile__description {
|
|
758
|
+
margin: 0.25rem 0 0;
|
|
759
|
+
font-size: 0.8125rem;
|
|
760
|
+
color: var(--polyx-muted-fg);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
.polyx-userprofile__status {
|
|
764
|
+
margin: 0;
|
|
765
|
+
font-size: 0.875rem;
|
|
766
|
+
color: var(--polyx-muted-fg);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
.polyx-userprofile__photo {
|
|
770
|
+
display: flex;
|
|
771
|
+
align-items: center;
|
|
772
|
+
gap: 0.875rem;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
.polyx-userprofile__photo-actions {
|
|
776
|
+
display: flex;
|
|
777
|
+
flex-direction: column;
|
|
778
|
+
gap: 0.25rem;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
.polyx-userprofile__photo-button {
|
|
782
|
+
font: inherit;
|
|
783
|
+
font-size: 0.8125rem;
|
|
784
|
+
font-weight: 500;
|
|
785
|
+
color: var(--polyx-brand);
|
|
786
|
+
background: none;
|
|
787
|
+
border: none;
|
|
788
|
+
padding: 0;
|
|
789
|
+
text-align: left;
|
|
790
|
+
cursor: pointer;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/* Hidden from sight but not from assistive technology — the visible control is the button. */
|
|
794
|
+
.polyx-userprofile__file {
|
|
795
|
+
position: absolute;
|
|
796
|
+
width: 1px;
|
|
797
|
+
height: 1px;
|
|
798
|
+
padding: 0;
|
|
799
|
+
margin: -1px;
|
|
800
|
+
overflow: hidden;
|
|
801
|
+
clip: rect(0, 0, 0, 0);
|
|
802
|
+
white-space: nowrap;
|
|
803
|
+
border: 0;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
.polyx-userprofile__row {
|
|
807
|
+
display: grid;
|
|
808
|
+
grid-template-columns: 1fr 1fr;
|
|
809
|
+
gap: 0.75rem;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
@media (max-width: 24rem) {
|
|
813
|
+
.polyx-userprofile__row {
|
|
814
|
+
grid-template-columns: 1fr;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
.polyx-userprofile__optional {
|
|
819
|
+
font-weight: 400;
|
|
820
|
+
color: var(--polyx-muted-fg);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
.polyx-userprofile__field-error {
|
|
824
|
+
margin: 0;
|
|
825
|
+
font-size: 0.8125rem;
|
|
826
|
+
color: var(--polyx-danger);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
.polyx-userprofile__success {
|
|
830
|
+
margin: 0;
|
|
831
|
+
padding: 0.625rem 0.75rem;
|
|
832
|
+
font-size: 0.8125rem;
|
|
833
|
+
color: var(--polyx-fg);
|
|
834
|
+
background: var(--polyx-panel-bg);
|
|
835
|
+
border: 1px solid var(--polyx-border);
|
|
836
|
+
border-radius: var(--polyx-radius-sm);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
.polyx-userprofile__actions {
|
|
840
|
+
display: flex;
|
|
841
|
+
justify-content: flex-end;
|
|
842
|
+
gap: 0.625rem;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
.polyx-userprofile__cancel {
|
|
846
|
+
font: inherit;
|
|
847
|
+
font-size: 0.875rem;
|
|
848
|
+
font-weight: 500;
|
|
849
|
+
height: 2.5rem;
|
|
850
|
+
padding: 0 0.875rem;
|
|
851
|
+
color: var(--polyx-fg);
|
|
852
|
+
background: none;
|
|
853
|
+
border: 1px solid var(--polyx-border);
|
|
854
|
+
border-radius: var(--polyx-radius-sm);
|
|
855
|
+
cursor: pointer;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
.polyx-userprofile__actions .polyx-signin__submit {
|
|
859
|
+
width: auto;
|
|
860
|
+
padding: 0 1.125rem;
|
|
861
|
+
}
|
|
862
|
+
|
|
712
863
|
/* Closes `@layer polyx` — opened at the top of the file. The rules above are left
|
|
713
864
|
unindented deliberately: wrapping the sheet in a layer is a cascade change, not a
|
|
714
865
|
reformat, and re-indenting 600 lines would bury it in the diff. */
|
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",
|
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
let _poly_x_core = require("@poly-x/core");
|
|
2
|
-
//#region src/cookie-codec.ts
|
|
3
|
-
/**
|
|
4
|
-
* AES-GCM sealing of the session cookie (F007 / ADR-0004). The key is derived
|
|
5
|
-
* (SHA-256) from the consumer's `POLYX_SESSION_SECRET`; AES-GCM gives
|
|
6
|
-
* confidentiality + tamper-detection together, so any corruption, truncation,
|
|
7
|
-
* or wrong-secret input decrypts to `null` rather than a partial session.
|
|
8
|
-
* Web Crypto is universal (Node ≥20 + edge runtimes), so this runs anywhere a
|
|
9
|
-
* Next route handler runs.
|
|
10
|
-
*/
|
|
11
|
-
const IV_BYTES = 12;
|
|
12
|
-
/**
|
|
13
|
-
* Browsers cap a single cookie at roughly 4096 bytes for the whole `Set-Cookie` pair —
|
|
14
|
-
* name, value and attributes together — and enforce it by DISCARDING the cookie without
|
|
15
|
-
* an error. For a session cookie that failure is invisible and badly misleading: the
|
|
16
|
-
* platform authenticates, the handler answers 200, and the user is bounced back to
|
|
17
|
-
* sign-in on the next request because the cookie was never stored.
|
|
18
|
-
*
|
|
19
|
-
* poly-x-sdk/ADR-0006 measured a live Odin session at 2628 bytes and recorded the missing
|
|
20
|
-
* guard as an open follow-up; a per-tenant flag that inflates the JWT is enough to cross
|
|
21
|
-
* the line. We fail loudly here instead.
|
|
22
|
-
*/
|
|
23
|
-
const SESSION_COOKIE_MAX_BYTES = 4096;
|
|
24
|
-
/** Attributes `buildSessionCookieOptions` always sets: `Path=/; HttpOnly; Secure; SameSite=Lax`. */
|
|
25
|
-
const COOKIE_ATTRIBUTE_BYTES = 45;
|
|
26
|
-
/** Bytes the whole `Set-Cookie` pair will occupy for this value under this cookie name. */
|
|
27
|
-
function sealedCookieByteLength(value, cookieName = "polyx_session") {
|
|
28
|
-
return new TextEncoder().encode(cookieName).length + 1 + value.length + COOKIE_ATTRIBUTE_BYTES;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Thrown when a sealed session would exceed what a browser will store. Never includes the
|
|
32
|
-
* session itself — the claims that caused the overflow are exactly the data we must not log.
|
|
33
|
-
*/
|
|
34
|
-
var SessionCookieTooLargeError = class extends _poly_x_core.PolyXError {
|
|
35
|
-
bytes;
|
|
36
|
-
limit;
|
|
37
|
-
constructor(bytes, limit, cookieName) {
|
|
38
|
-
super(`PolyX session cookie is ${bytes} bytes, over the ~${limit}-byte browser limit for "${cookieName}". The browser would discard it silently and the user would appear signed out. Reduce what the platform puts in the session — the usual cause is a tenant configured to embed full permissions in the JWT (includeFullPermissionInJwt).`, { code: "SESSION_COOKIE_TOO_LARGE" });
|
|
39
|
-
this.bytes = bytes;
|
|
40
|
-
this.limit = limit;
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
function base64UrlEncode(bytes) {
|
|
44
|
-
let binary = "";
|
|
45
|
-
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
46
|
-
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
47
|
-
}
|
|
48
|
-
function base64UrlDecode(text) {
|
|
49
|
-
const base64 = text.replace(/-/g, "+").replace(/_/g, "/");
|
|
50
|
-
const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
|
|
51
|
-
const binary = atob(padded);
|
|
52
|
-
const bytes = new Uint8Array(binary.length);
|
|
53
|
-
for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
|
|
54
|
-
return bytes;
|
|
55
|
-
}
|
|
56
|
-
async function deriveKey(secret) {
|
|
57
|
-
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(secret));
|
|
58
|
-
return crypto.subtle.importKey("raw", hash, "AES-GCM", false, ["encrypt", "decrypt"]);
|
|
59
|
-
}
|
|
60
|
-
async function sealSession(session, secret, options = {}) {
|
|
61
|
-
const key = await deriveKey(secret);
|
|
62
|
-
const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
|
|
63
|
-
const plaintext = new TextEncoder().encode(JSON.stringify(session));
|
|
64
|
-
const ciphertext = new Uint8Array(await crypto.subtle.encrypt({
|
|
65
|
-
name: "AES-GCM",
|
|
66
|
-
iv
|
|
67
|
-
}, key, plaintext));
|
|
68
|
-
const packed = new Uint8Array(iv.length + ciphertext.length);
|
|
69
|
-
packed.set(iv);
|
|
70
|
-
packed.set(ciphertext, iv.length);
|
|
71
|
-
const sealed = base64UrlEncode(packed);
|
|
72
|
-
const cookieName = options.cookieName ?? "polyx_session";
|
|
73
|
-
const bytes = sealedCookieByteLength(sealed, cookieName);
|
|
74
|
-
if (bytes > 4096) throw new SessionCookieTooLargeError(bytes, SESSION_COOKIE_MAX_BYTES, cookieName);
|
|
75
|
-
return sealed;
|
|
76
|
-
}
|
|
77
|
-
async function openSession(token, secret) {
|
|
78
|
-
try {
|
|
79
|
-
const packed = base64UrlDecode(token);
|
|
80
|
-
if (packed.length <= IV_BYTES) return null;
|
|
81
|
-
const iv = packed.slice(0, IV_BYTES);
|
|
82
|
-
const ciphertext = packed.slice(IV_BYTES);
|
|
83
|
-
const key = await deriveKey(secret);
|
|
84
|
-
const plaintext = await crypto.subtle.decrypt({
|
|
85
|
-
name: "AES-GCM",
|
|
86
|
-
iv
|
|
87
|
-
}, key, ciphertext);
|
|
88
|
-
return JSON.parse(new TextDecoder().decode(plaintext));
|
|
89
|
-
} catch {
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
//#endregion
|
|
94
|
-
//#region src/cookie-adapter.ts
|
|
95
|
-
/**
|
|
96
|
-
* Attributes for the session cookie (ADR-0004 secure custody). `secure` is decided at
|
|
97
|
-
* write time from the actual transport: a `Secure` cookie is silently dropped by the
|
|
98
|
-
* browser over plain http, so http/localhost dev must set `secure:false` while https
|
|
99
|
-
* always sets `secure:true`. This is not a downgrade — `Secure` carries no meaning on
|
|
100
|
-
* http; setting it there just breaks the session. httpOnly/sameSite/path are unchanged.
|
|
101
|
-
*/
|
|
102
|
-
function buildSessionCookieOptions(secure) {
|
|
103
|
-
return {
|
|
104
|
-
httpOnly: true,
|
|
105
|
-
secure,
|
|
106
|
-
sameSite: "lax",
|
|
107
|
-
path: "/"
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
//#endregion
|
|
111
|
-
//#region src/server-session.ts
|
|
112
|
-
/**
|
|
113
|
-
* The framework-agnostic BFF session controller (F007): tokens sealed in an
|
|
114
|
-
* httpOnly cookie on the consumer's domain, exchanged/refreshed server-side.
|
|
115
|
-
* Composes core's AuthClient with a CookieAdapter seam; fully unit-testable.
|
|
116
|
-
*/
|
|
117
|
-
const DEFAULT_SESSION_COOKIE = "polyx_session";
|
|
118
|
-
var ServerSession = class {
|
|
119
|
-
authClient;
|
|
120
|
-
secret;
|
|
121
|
-
cookies;
|
|
122
|
-
cookieName;
|
|
123
|
-
secure;
|
|
124
|
-
constructor(options) {
|
|
125
|
-
this.authClient = options.authClient;
|
|
126
|
-
this.secret = options.secret;
|
|
127
|
-
this.cookies = options.cookies;
|
|
128
|
-
this.cookieName = options.cookieName ?? "polyx_session";
|
|
129
|
-
this.secure = options.secure ?? true;
|
|
130
|
-
}
|
|
131
|
-
async establishFromCode(code, codeVerifier, redirectUri, forwardedHeaders) {
|
|
132
|
-
const stored = await this.authClient.exchangeCode({
|
|
133
|
-
code,
|
|
134
|
-
codeVerifier,
|
|
135
|
-
redirectUri,
|
|
136
|
-
forwardedHeaders
|
|
137
|
-
});
|
|
138
|
-
await this.write(stored);
|
|
139
|
-
return stored;
|
|
140
|
-
}
|
|
141
|
-
async read() {
|
|
142
|
-
const raw = this.cookies.get(this.cookieName);
|
|
143
|
-
if (!raw) return null;
|
|
144
|
-
return openSession(raw, this.secret);
|
|
145
|
-
}
|
|
146
|
-
/** Rotate the tokens; clears the cookie (returns null) on a terminal refresh failure. */
|
|
147
|
-
async refresh() {
|
|
148
|
-
const current = await this.read();
|
|
149
|
-
if (!current) return null;
|
|
150
|
-
try {
|
|
151
|
-
const next = await this.authClient.createRefresher()(current);
|
|
152
|
-
await this.write(next);
|
|
153
|
-
return next;
|
|
154
|
-
} catch (error) {
|
|
155
|
-
if (error instanceof _poly_x_core.SessionRevokedError || error instanceof _poly_x_core.SessionExpiredError) {
|
|
156
|
-
this.clear();
|
|
157
|
-
return null;
|
|
158
|
-
}
|
|
159
|
-
if (error instanceof SessionCookieTooLargeError) {
|
|
160
|
-
console.error(`[polyx] ${error.message}`);
|
|
161
|
-
this.clear();
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
164
|
-
throw error;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* End the session upstream, then locally. `device` revokes this session; `everywhere`
|
|
169
|
-
* revokes every session for the user (FR-SSO-5).
|
|
170
|
-
*
|
|
171
|
-
* Order is load-bearing: revocation is authenticated with the access token sealed in the
|
|
172
|
-
* cookie, so the cookie must be read before it is dropped. The upstream call is
|
|
173
|
-
* best-effort — an unreachable platform must not strand the user half-signed-in — but the
|
|
174
|
-
* call itself is real: the ecosystem logout the SDK used before resolves its session from
|
|
175
|
-
* a browser cookie, which a server-to-server fetch never carries, so it silently revoked
|
|
176
|
-
* nothing and answered 200.
|
|
177
|
-
*/
|
|
178
|
-
async revoke(scope = "device") {
|
|
179
|
-
const current = await this.read();
|
|
180
|
-
if (current) try {
|
|
181
|
-
await this.authClient.revokeSession(current.tokens.accessToken, scope);
|
|
182
|
-
} catch {}
|
|
183
|
-
this.clear();
|
|
184
|
-
}
|
|
185
|
-
clear() {
|
|
186
|
-
this.cookies.delete(this.cookieName);
|
|
187
|
-
}
|
|
188
|
-
async write(session) {
|
|
189
|
-
const sealed = await sealSession(session, this.secret, { cookieName: this.cookieName });
|
|
190
|
-
this.cookies.set(this.cookieName, sealed, buildSessionCookieOptions(this.secure));
|
|
191
|
-
}
|
|
192
|
-
};
|
|
193
|
-
function toAuthContext(session) {
|
|
194
|
-
if (!session) return {
|
|
195
|
-
isSignedIn: false,
|
|
196
|
-
userId: null,
|
|
197
|
-
claims: null
|
|
198
|
-
};
|
|
199
|
-
return {
|
|
200
|
-
isSignedIn: true,
|
|
201
|
-
userId: session.session.claims.userId,
|
|
202
|
-
organizationId: session.session.claims.organizationId,
|
|
203
|
-
claims: session.session.claims
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
//#endregion
|
|
207
|
-
Object.defineProperty(exports, "DEFAULT_SESSION_COOKIE", {
|
|
208
|
-
enumerable: true,
|
|
209
|
-
get: function() {
|
|
210
|
-
return DEFAULT_SESSION_COOKIE;
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
Object.defineProperty(exports, "SESSION_COOKIE_MAX_BYTES", {
|
|
214
|
-
enumerable: true,
|
|
215
|
-
get: function() {
|
|
216
|
-
return SESSION_COOKIE_MAX_BYTES;
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
Object.defineProperty(exports, "ServerSession", {
|
|
220
|
-
enumerable: true,
|
|
221
|
-
get: function() {
|
|
222
|
-
return ServerSession;
|
|
223
|
-
}
|
|
224
|
-
});
|
|
225
|
-
Object.defineProperty(exports, "SessionCookieTooLargeError", {
|
|
226
|
-
enumerable: true,
|
|
227
|
-
get: function() {
|
|
228
|
-
return SessionCookieTooLargeError;
|
|
229
|
-
}
|
|
230
|
-
});
|
|
231
|
-
Object.defineProperty(exports, "openSession", {
|
|
232
|
-
enumerable: true,
|
|
233
|
-
get: function() {
|
|
234
|
-
return openSession;
|
|
235
|
-
}
|
|
236
|
-
});
|
|
237
|
-
Object.defineProperty(exports, "sealSession", {
|
|
238
|
-
enumerable: true,
|
|
239
|
-
get: function() {
|
|
240
|
-
return sealSession;
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
Object.defineProperty(exports, "sealedCookieByteLength", {
|
|
244
|
-
enumerable: true,
|
|
245
|
-
get: function() {
|
|
246
|
-
return sealedCookieByteLength;
|
|
247
|
-
}
|
|
248
|
-
});
|
|
249
|
-
Object.defineProperty(exports, "toAuthContext", {
|
|
250
|
-
enumerable: true,
|
|
251
|
-
get: function() {
|
|
252
|
-
return toAuthContext;
|
|
253
|
-
}
|
|
254
|
-
});
|