@poly-x/next 0.1.0 → 0.2.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 +127 -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 +1005 -25
- package/dist/server.d.cts +302 -4
- package/dist/server.d.mts +302 -4
- package/dist/server.mjs +988 -20
- 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-CjP44zFC.mjs +0 -159
- package/dist/server-session-QJLA_Tz-.cjs +0 -188
|
@@ -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.2.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.2.0",
|
|
51
|
+
"@poly-x/react": "0.2.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"next": ">=14",
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import { SessionExpiredError, SessionRevokedError } from "@poly-x/core";
|
|
2
|
-
//#region src/cookie-adapter.ts
|
|
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
|
-
}
|
|
18
|
-
//#endregion
|
|
19
|
-
//#region src/cookie-codec.ts
|
|
20
|
-
const IV_BYTES = 12;
|
|
21
|
-
function base64UrlEncode(bytes) {
|
|
22
|
-
let binary = "";
|
|
23
|
-
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
24
|
-
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
25
|
-
}
|
|
26
|
-
function base64UrlDecode(text) {
|
|
27
|
-
const base64 = text.replace(/-/g, "+").replace(/_/g, "/");
|
|
28
|
-
const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
|
|
29
|
-
const binary = atob(padded);
|
|
30
|
-
const bytes = new Uint8Array(binary.length);
|
|
31
|
-
for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
|
|
32
|
-
return bytes;
|
|
33
|
-
}
|
|
34
|
-
async function deriveKey(secret) {
|
|
35
|
-
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(secret));
|
|
36
|
-
return crypto.subtle.importKey("raw", hash, "AES-GCM", false, ["encrypt", "decrypt"]);
|
|
37
|
-
}
|
|
38
|
-
async function sealSession(session, secret) {
|
|
39
|
-
const key = await deriveKey(secret);
|
|
40
|
-
const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
|
|
41
|
-
const plaintext = new TextEncoder().encode(JSON.stringify(session));
|
|
42
|
-
const ciphertext = new Uint8Array(await crypto.subtle.encrypt({
|
|
43
|
-
name: "AES-GCM",
|
|
44
|
-
iv
|
|
45
|
-
}, key, plaintext));
|
|
46
|
-
const packed = new Uint8Array(iv.length + ciphertext.length);
|
|
47
|
-
packed.set(iv);
|
|
48
|
-
packed.set(ciphertext, iv.length);
|
|
49
|
-
return base64UrlEncode(packed);
|
|
50
|
-
}
|
|
51
|
-
async function openSession(token, secret) {
|
|
52
|
-
try {
|
|
53
|
-
const packed = base64UrlDecode(token);
|
|
54
|
-
if (packed.length <= IV_BYTES) return null;
|
|
55
|
-
const iv = packed.slice(0, IV_BYTES);
|
|
56
|
-
const ciphertext = packed.slice(IV_BYTES);
|
|
57
|
-
const key = await deriveKey(secret);
|
|
58
|
-
const plaintext = await crypto.subtle.decrypt({
|
|
59
|
-
name: "AES-GCM",
|
|
60
|
-
iv
|
|
61
|
-
}, key, ciphertext);
|
|
62
|
-
return JSON.parse(new TextDecoder().decode(plaintext));
|
|
63
|
-
} catch {
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
//#endregion
|
|
68
|
-
//#region src/server-session.ts
|
|
69
|
-
/**
|
|
70
|
-
* The framework-agnostic BFF session controller (F007): tokens sealed in an
|
|
71
|
-
* httpOnly cookie on the consumer's domain, exchanged/refreshed server-side.
|
|
72
|
-
* Composes core's AuthClient with a CookieAdapter seam; fully unit-testable.
|
|
73
|
-
*/
|
|
74
|
-
const DEFAULT_SESSION_COOKIE = "polyx_session";
|
|
75
|
-
var ServerSession = class {
|
|
76
|
-
authClient;
|
|
77
|
-
secret;
|
|
78
|
-
cookies;
|
|
79
|
-
cookieName;
|
|
80
|
-
secure;
|
|
81
|
-
constructor(options) {
|
|
82
|
-
this.authClient = options.authClient;
|
|
83
|
-
this.secret = options.secret;
|
|
84
|
-
this.cookies = options.cookies;
|
|
85
|
-
this.cookieName = options.cookieName ?? "polyx_session";
|
|
86
|
-
this.secure = options.secure ?? true;
|
|
87
|
-
}
|
|
88
|
-
async establishFromCode(code, codeVerifier, redirectUri, forwardedHeaders) {
|
|
89
|
-
const stored = await this.authClient.exchangeCode({
|
|
90
|
-
code,
|
|
91
|
-
codeVerifier,
|
|
92
|
-
redirectUri,
|
|
93
|
-
forwardedHeaders
|
|
94
|
-
});
|
|
95
|
-
await this.write(stored);
|
|
96
|
-
return stored;
|
|
97
|
-
}
|
|
98
|
-
async read() {
|
|
99
|
-
const raw = this.cookies.get(this.cookieName);
|
|
100
|
-
if (!raw) return null;
|
|
101
|
-
return openSession(raw, this.secret);
|
|
102
|
-
}
|
|
103
|
-
/** Rotate the tokens; clears the cookie (returns null) on a terminal refresh failure. */
|
|
104
|
-
async refresh() {
|
|
105
|
-
const current = await this.read();
|
|
106
|
-
if (!current) return null;
|
|
107
|
-
try {
|
|
108
|
-
const next = await this.authClient.createRefresher()(current);
|
|
109
|
-
await this.write(next);
|
|
110
|
-
return next;
|
|
111
|
-
} catch (error) {
|
|
112
|
-
if (error instanceof SessionRevokedError || error instanceof SessionExpiredError) {
|
|
113
|
-
this.clear();
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
throw error;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* End the session upstream, then locally. `device` revokes this session; `everywhere`
|
|
121
|
-
* revokes every session for the user (FR-SSO-5).
|
|
122
|
-
*
|
|
123
|
-
* Order is load-bearing: revocation is authenticated with the access token sealed in the
|
|
124
|
-
* cookie, so the cookie must be read before it is dropped. The upstream call is
|
|
125
|
-
* best-effort — an unreachable platform must not strand the user half-signed-in — but the
|
|
126
|
-
* call itself is real: the ecosystem logout the SDK used before resolves its session from
|
|
127
|
-
* a browser cookie, which a server-to-server fetch never carries, so it silently revoked
|
|
128
|
-
* nothing and answered 200.
|
|
129
|
-
*/
|
|
130
|
-
async revoke(scope = "device") {
|
|
131
|
-
const current = await this.read();
|
|
132
|
-
if (current) try {
|
|
133
|
-
await this.authClient.revokeSession(current.tokens.accessToken, scope);
|
|
134
|
-
} catch {}
|
|
135
|
-
this.clear();
|
|
136
|
-
}
|
|
137
|
-
clear() {
|
|
138
|
-
this.cookies.delete(this.cookieName);
|
|
139
|
-
}
|
|
140
|
-
async write(session) {
|
|
141
|
-
const sealed = await sealSession(session, this.secret);
|
|
142
|
-
this.cookies.set(this.cookieName, sealed, buildSessionCookieOptions(this.secure));
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
function toAuthContext(session) {
|
|
146
|
-
if (!session) return {
|
|
147
|
-
isSignedIn: false,
|
|
148
|
-
userId: null,
|
|
149
|
-
claims: null
|
|
150
|
-
};
|
|
151
|
-
return {
|
|
152
|
-
isSignedIn: true,
|
|
153
|
-
userId: session.session.claims.userId,
|
|
154
|
-
organizationId: session.session.claims.organizationId,
|
|
155
|
-
claims: session.session.claims
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
//#endregion
|
|
159
|
-
export { sealSession as a, openSession as i, ServerSession as n, toAuthContext as r, DEFAULT_SESSION_COOKIE as t };
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
let _poly_x_core = require("@poly-x/core");
|
|
2
|
-
//#region src/cookie-adapter.ts
|
|
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
|
-
}
|
|
18
|
-
//#endregion
|
|
19
|
-
//#region src/cookie-codec.ts
|
|
20
|
-
const IV_BYTES = 12;
|
|
21
|
-
function base64UrlEncode(bytes) {
|
|
22
|
-
let binary = "";
|
|
23
|
-
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
24
|
-
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
25
|
-
}
|
|
26
|
-
function base64UrlDecode(text) {
|
|
27
|
-
const base64 = text.replace(/-/g, "+").replace(/_/g, "/");
|
|
28
|
-
const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
|
|
29
|
-
const binary = atob(padded);
|
|
30
|
-
const bytes = new Uint8Array(binary.length);
|
|
31
|
-
for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
|
|
32
|
-
return bytes;
|
|
33
|
-
}
|
|
34
|
-
async function deriveKey(secret) {
|
|
35
|
-
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(secret));
|
|
36
|
-
return crypto.subtle.importKey("raw", hash, "AES-GCM", false, ["encrypt", "decrypt"]);
|
|
37
|
-
}
|
|
38
|
-
async function sealSession(session, secret) {
|
|
39
|
-
const key = await deriveKey(secret);
|
|
40
|
-
const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
|
|
41
|
-
const plaintext = new TextEncoder().encode(JSON.stringify(session));
|
|
42
|
-
const ciphertext = new Uint8Array(await crypto.subtle.encrypt({
|
|
43
|
-
name: "AES-GCM",
|
|
44
|
-
iv
|
|
45
|
-
}, key, plaintext));
|
|
46
|
-
const packed = new Uint8Array(iv.length + ciphertext.length);
|
|
47
|
-
packed.set(iv);
|
|
48
|
-
packed.set(ciphertext, iv.length);
|
|
49
|
-
return base64UrlEncode(packed);
|
|
50
|
-
}
|
|
51
|
-
async function openSession(token, secret) {
|
|
52
|
-
try {
|
|
53
|
-
const packed = base64UrlDecode(token);
|
|
54
|
-
if (packed.length <= IV_BYTES) return null;
|
|
55
|
-
const iv = packed.slice(0, IV_BYTES);
|
|
56
|
-
const ciphertext = packed.slice(IV_BYTES);
|
|
57
|
-
const key = await deriveKey(secret);
|
|
58
|
-
const plaintext = await crypto.subtle.decrypt({
|
|
59
|
-
name: "AES-GCM",
|
|
60
|
-
iv
|
|
61
|
-
}, key, ciphertext);
|
|
62
|
-
return JSON.parse(new TextDecoder().decode(plaintext));
|
|
63
|
-
} catch {
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
//#endregion
|
|
68
|
-
//#region src/server-session.ts
|
|
69
|
-
/**
|
|
70
|
-
* The framework-agnostic BFF session controller (F007): tokens sealed in an
|
|
71
|
-
* httpOnly cookie on the consumer's domain, exchanged/refreshed server-side.
|
|
72
|
-
* Composes core's AuthClient with a CookieAdapter seam; fully unit-testable.
|
|
73
|
-
*/
|
|
74
|
-
const DEFAULT_SESSION_COOKIE = "polyx_session";
|
|
75
|
-
var ServerSession = class {
|
|
76
|
-
authClient;
|
|
77
|
-
secret;
|
|
78
|
-
cookies;
|
|
79
|
-
cookieName;
|
|
80
|
-
secure;
|
|
81
|
-
constructor(options) {
|
|
82
|
-
this.authClient = options.authClient;
|
|
83
|
-
this.secret = options.secret;
|
|
84
|
-
this.cookies = options.cookies;
|
|
85
|
-
this.cookieName = options.cookieName ?? "polyx_session";
|
|
86
|
-
this.secure = options.secure ?? true;
|
|
87
|
-
}
|
|
88
|
-
async establishFromCode(code, codeVerifier, redirectUri, forwardedHeaders) {
|
|
89
|
-
const stored = await this.authClient.exchangeCode({
|
|
90
|
-
code,
|
|
91
|
-
codeVerifier,
|
|
92
|
-
redirectUri,
|
|
93
|
-
forwardedHeaders
|
|
94
|
-
});
|
|
95
|
-
await this.write(stored);
|
|
96
|
-
return stored;
|
|
97
|
-
}
|
|
98
|
-
async read() {
|
|
99
|
-
const raw = this.cookies.get(this.cookieName);
|
|
100
|
-
if (!raw) return null;
|
|
101
|
-
return openSession(raw, this.secret);
|
|
102
|
-
}
|
|
103
|
-
/** Rotate the tokens; clears the cookie (returns null) on a terminal refresh failure. */
|
|
104
|
-
async refresh() {
|
|
105
|
-
const current = await this.read();
|
|
106
|
-
if (!current) return null;
|
|
107
|
-
try {
|
|
108
|
-
const next = await this.authClient.createRefresher()(current);
|
|
109
|
-
await this.write(next);
|
|
110
|
-
return next;
|
|
111
|
-
} catch (error) {
|
|
112
|
-
if (error instanceof _poly_x_core.SessionRevokedError || error instanceof _poly_x_core.SessionExpiredError) {
|
|
113
|
-
this.clear();
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
throw error;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* End the session upstream, then locally. `device` revokes this session; `everywhere`
|
|
121
|
-
* revokes every session for the user (FR-SSO-5).
|
|
122
|
-
*
|
|
123
|
-
* Order is load-bearing: revocation is authenticated with the access token sealed in the
|
|
124
|
-
* cookie, so the cookie must be read before it is dropped. The upstream call is
|
|
125
|
-
* best-effort — an unreachable platform must not strand the user half-signed-in — but the
|
|
126
|
-
* call itself is real: the ecosystem logout the SDK used before resolves its session from
|
|
127
|
-
* a browser cookie, which a server-to-server fetch never carries, so it silently revoked
|
|
128
|
-
* nothing and answered 200.
|
|
129
|
-
*/
|
|
130
|
-
async revoke(scope = "device") {
|
|
131
|
-
const current = await this.read();
|
|
132
|
-
if (current) try {
|
|
133
|
-
await this.authClient.revokeSession(current.tokens.accessToken, scope);
|
|
134
|
-
} catch {}
|
|
135
|
-
this.clear();
|
|
136
|
-
}
|
|
137
|
-
clear() {
|
|
138
|
-
this.cookies.delete(this.cookieName);
|
|
139
|
-
}
|
|
140
|
-
async write(session) {
|
|
141
|
-
const sealed = await sealSession(session, this.secret);
|
|
142
|
-
this.cookies.set(this.cookieName, sealed, buildSessionCookieOptions(this.secure));
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
function toAuthContext(session) {
|
|
146
|
-
if (!session) return {
|
|
147
|
-
isSignedIn: false,
|
|
148
|
-
userId: null,
|
|
149
|
-
claims: null
|
|
150
|
-
};
|
|
151
|
-
return {
|
|
152
|
-
isSignedIn: true,
|
|
153
|
-
userId: session.session.claims.userId,
|
|
154
|
-
organizationId: session.session.claims.organizationId,
|
|
155
|
-
claims: session.session.claims
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
//#endregion
|
|
159
|
-
Object.defineProperty(exports, "DEFAULT_SESSION_COOKIE", {
|
|
160
|
-
enumerable: true,
|
|
161
|
-
get: function() {
|
|
162
|
-
return DEFAULT_SESSION_COOKIE;
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
Object.defineProperty(exports, "ServerSession", {
|
|
166
|
-
enumerable: true,
|
|
167
|
-
get: function() {
|
|
168
|
-
return ServerSession;
|
|
169
|
-
}
|
|
170
|
-
});
|
|
171
|
-
Object.defineProperty(exports, "openSession", {
|
|
172
|
-
enumerable: true,
|
|
173
|
-
get: function() {
|
|
174
|
-
return openSession;
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
Object.defineProperty(exports, "sealSession", {
|
|
178
|
-
enumerable: true,
|
|
179
|
-
get: function() {
|
|
180
|
-
return sealSession;
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
Object.defineProperty(exports, "toAuthContext", {
|
|
184
|
-
enumerable: true,
|
|
185
|
-
get: function() {
|
|
186
|
-
return toAuthContext;
|
|
187
|
-
}
|
|
188
|
-
});
|