@pontive/pontkit-server 1.0.4
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/config.d.ts +72 -0
- package/dist/cookies.d.ts +38 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +391 -0
- package/dist/index.js.map +1 -0
- package/dist/oidc.d.ts +66 -0
- package/dist/refresh.d.ts +28 -0
- package/dist/seal.d.ts +3 -0
- package/dist/server-auth.d.ts +54 -0
- package/package.json +32 -0
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an app tells the SDK, and the defaults for everything it doesn't.
|
|
3
|
+
*
|
|
4
|
+
* Only the first five matter in practice. Everything else exists for the rare
|
|
5
|
+
* app that must differ, and its default is the decision the SDK makes for
|
|
6
|
+
* everyone else.
|
|
7
|
+
*/
|
|
8
|
+
export interface ServerAuthConfig {
|
|
9
|
+
/** The Pontive auth domain, e.g. `https://acme.auth.us.pontive.com`. */
|
|
10
|
+
issuer: string;
|
|
11
|
+
/** The app's id. */
|
|
12
|
+
clientId: string;
|
|
13
|
+
/**
|
|
14
|
+
* Set for a confidential app. It is sent on every token call, and is what
|
|
15
|
+
* lets the app hold its refresh token on its server at all.
|
|
16
|
+
*/
|
|
17
|
+
clientSecret?: string;
|
|
18
|
+
/** Encrypts the session cookie. At least 32 characters; rotating it signs everyone out. */
|
|
19
|
+
sessionSecret: string;
|
|
20
|
+
/**
|
|
21
|
+
* The app's public origin, e.g. `https://app.acme.com`. Set it for any
|
|
22
|
+
* deployment behind a proxy or in a container, where the request's own URL
|
|
23
|
+
* names the address the server binds to rather than the one a browser uses.
|
|
24
|
+
* Without it the origin comes from `x-forwarded-proto`/`x-forwarded-host`,
|
|
25
|
+
* then the request URL.
|
|
26
|
+
*/
|
|
27
|
+
baseUrl?: string;
|
|
28
|
+
/** Default `openid email profile offline_access`. */
|
|
29
|
+
scopes?: string;
|
|
30
|
+
/** Where the SDK's sign-in routes live. Default `/api/auth`. */
|
|
31
|
+
basePath?: string;
|
|
32
|
+
/** Default `${origin}${basePath}/callback`; must be registered on the app verbatim. */
|
|
33
|
+
redirectUri?: string;
|
|
34
|
+
/** The app's own sign-in page: failed sign-ins and signed-out users land here. Default `/login`. */
|
|
35
|
+
signInPath?: string;
|
|
36
|
+
/** Where a sign-in lands when it did not start from a page. Default `/`. */
|
|
37
|
+
afterSignInPath?: string;
|
|
38
|
+
/** An OIDC `prompt` to send, e.g. `login`. */
|
|
39
|
+
prompt?: string;
|
|
40
|
+
/** Default `pontive_session`. */
|
|
41
|
+
cookieName?: string;
|
|
42
|
+
/** Default: whether the public origin is https. */
|
|
43
|
+
secureCookies?: boolean;
|
|
44
|
+
/** The session cookie's lifetime. Default 7 days. */
|
|
45
|
+
sessionMaxAgeSeconds?: number;
|
|
46
|
+
/** Refresh when fewer than this many seconds remain. Default 60. */
|
|
47
|
+
refreshSkewSeconds?: number;
|
|
48
|
+
/** Defaults to the global `fetch`. */
|
|
49
|
+
fetch?: typeof fetch;
|
|
50
|
+
/** Where the SDK reports a failed refresh or sign-in. Defaults to `console`. */
|
|
51
|
+
logger?: Pick<Console, "warn">;
|
|
52
|
+
}
|
|
53
|
+
export interface ResolvedConfig {
|
|
54
|
+
issuer: string;
|
|
55
|
+
clientId: string;
|
|
56
|
+
clientSecret?: string;
|
|
57
|
+
sessionSecret: string;
|
|
58
|
+
baseUrl?: string;
|
|
59
|
+
scopes: string;
|
|
60
|
+
basePath: string;
|
|
61
|
+
redirectUri?: string;
|
|
62
|
+
signInPath: string;
|
|
63
|
+
afterSignInPath: string;
|
|
64
|
+
prompt?: string;
|
|
65
|
+
cookieName: string;
|
|
66
|
+
secureCookies?: boolean;
|
|
67
|
+
sessionMaxAgeSeconds: number;
|
|
68
|
+
refreshSkewSeconds: number;
|
|
69
|
+
fetch: typeof fetch;
|
|
70
|
+
logger: Pick<Console, "warn">;
|
|
71
|
+
}
|
|
72
|
+
export declare function resolveConfig(config: ServerAuthConfig): ResolvedConfig;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cookie reading and writing, on plain header strings so it runs anywhere a
|
|
3
|
+
* Web `Request`/`Response` does.
|
|
4
|
+
*
|
|
5
|
+
* A sealed session outgrows one cookie (browsers cap each at about 4 KB, and an
|
|
6
|
+
* access token, a refresh token and a user fit in about 3–5), so it is split
|
|
7
|
+
* across numbered chunks: `name`, `name.1`, `name.2`, … and joined on read.
|
|
8
|
+
*/
|
|
9
|
+
/** Characters per chunk, leaving room under the ~4096-byte cap for the name and attributes. */
|
|
10
|
+
export declare const CHUNK_SIZE = 3500;
|
|
11
|
+
/** Upper bound on chunks probed when reading or clearing. */
|
|
12
|
+
export declare const MAX_CHUNKS = 16;
|
|
13
|
+
export interface CookieAttributes {
|
|
14
|
+
/** Seconds; 0 deletes the cookie. */
|
|
15
|
+
maxAge: number;
|
|
16
|
+
secure: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** Parses a `Cookie` request header into name → value. */
|
|
19
|
+
export declare function parseCookies(header: string | null | undefined): Map<string, string>;
|
|
20
|
+
/** Serialises a `Cookie` request header from name → value. */
|
|
21
|
+
export declare function formatCookieHeader(cookies: Map<string, string>): string;
|
|
22
|
+
/**
|
|
23
|
+
* A `Set-Cookie` value. Always HttpOnly, host-only, `Path=/` and
|
|
24
|
+
* `SameSite=Lax`: Lax is what lets the cookie ride the top-level redirect back
|
|
25
|
+
* from the sign-in page, and nothing here is ever for script.
|
|
26
|
+
*/
|
|
27
|
+
export declare function serializeCookie(name: string, value: string, attrs: CookieAttributes): string;
|
|
28
|
+
export declare function chunkName(name: string, index: number): string;
|
|
29
|
+
/** Joins a chunked value, or null when its first chunk is absent. */
|
|
30
|
+
export declare function readChunked(cookies: Map<string, string>, name: string): string | null;
|
|
31
|
+
/**
|
|
32
|
+
* The `Set-Cookie` values that store `value` in chunks, and delete any chunk
|
|
33
|
+
* the request carried beyond what it now needs. Also updates `cookies` in
|
|
34
|
+
* place, so a caller can forward the new state on the same request.
|
|
35
|
+
*/
|
|
36
|
+
export declare function writeChunked(cookies: Map<string, string>, name: string, value: string, attrs: CookieAttributes): string[];
|
|
37
|
+
/** The `Set-Cookie` values that delete every chunk the request carried. */
|
|
38
|
+
export declare function clearChunked(cookies: Map<string, string>, name: string, secure: boolean): string[];
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const _=require("jose"),Y=10080*60;function Z(t){const e=["issuer","clientId","sessionSecret"];for(const r of e)if(!t[r])throw new Error(`@pontive/pontkit-server: ${r} is required`);if(t.sessionSecret.length<32)throw new Error("@pontive/pontkit-server: sessionSecret must be at least 32 characters");if(t.baseUrl)try{new URL(t.baseUrl)}catch{throw new Error(`@pontive/pontkit-server: baseUrl "${t.baseUrl}" is not an absolute URL`)}return{issuer:t.issuer.replace(/\/+$/,""),clientId:t.clientId,clientSecret:t.clientSecret||void 0,sessionSecret:t.sessionSecret,baseUrl:t.baseUrl?new URL(t.baseUrl).origin:void 0,scopes:t.scopes??"openid email profile offline_access",basePath:(t.basePath??"/api/auth").replace(/\/+$/,""),redirectUri:t.redirectUri,signInPath:t.signInPath??"/login",afterSignInPath:t.afterSignInPath??"/",prompt:t.prompt,cookieName:t.cookieName??"pontive_session",secureCookies:t.secureCookies,sessionMaxAgeSeconds:t.sessionMaxAgeSeconds??Y,refreshSkewSeconds:t.refreshSkewSeconds??60,fetch:t.fetch??((...r)=>fetch(...r)),logger:t.logger??console}}const T=3500,$=16;function C(t){const e=new Map;if(!t)return e;for(const r of t.split(";")){const n=r.indexOf("=");if(n<0)continue;const s=r.slice(0,n).trim();!s||e.has(s)||e.set(s,r.slice(n+1).trim())}return e}function I(t){return[...t].map(([e,r])=>`${e}=${r}`).join("; ")}function U(t,e,r){const n=[`${t}=${e}`,"Path=/","HttpOnly","SameSite=Lax",`Max-Age=${r.maxAge}`];return r.secure&&n.push("Secure"),n.join("; ")}function y(t,e){return e===0?t:`${t}.${e}`}function Q(t,e){const r=t.get(e);if(r===void 0)return null;let n=r;for(let s=1;s<$;s++){const a=t.get(y(e,s));if(a===void 0)break;n+=a}return n}function q(t,e,r,n){const s=[],a=Math.max(1,Math.ceil(r.length/T));for(let u=0;u<a;u++){const d=r.slice(u*T,(u+1)*T);t.set(y(e,u),d),s.push(U(y(e,u),d,n))}for(let u=a;u<$;u++){const d=y(e,u);if(!t.has(d))break;t.delete(d),s.push(U(d,"",{...n,maxAge:0}))}return s}function R(t,e,r){const n=[];for(let s=0;s<$;s++){const a=y(e,s);if(!t.has(a)){if(s>0)break;continue}t.delete(a),n.push(U(a,"",{maxAge:0,secure:r}))}return n}class W extends Error{constructor(e,r,n){super(`token endpoint ${e} ${r}: ${n}`),this.status=e,this.code=r}status;code}const ee=600*1e3,E=new Map,M=new Map;function te(t){return t.replace(/\/+$/,"")}function A(t,e){const r=te(t);let n=E.get(r);return n||(n=e(`${r}/.well-known/openid-configuration`).then(async s=>{if(!s.ok)throw new Error(`OIDC discovery failed for ${r}: ${s.status}`);return await s.json()}).catch(s=>{throw E.delete(r),s}),E.set(r,n)),n}async function L(t,e,r){const n=M.get(t);if(n&&!r&&Date.now()-n.at<ee)return n.set;const s=await e(t);if(!s.ok)throw new Error(`JWKS fetch failed for ${t}: ${s.status}`);const a=_.createLocalJWKSet(await s.json());return M.set(t,{at:Date.now(),set:a}),a}function F(t){let e="";for(const r of t)e+=String.fromCharCode(r);return btoa(e).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}function v(){return F(crypto.getRandomValues(new Uint8Array(32)))}async function re(t){const e=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(t));return F(new Uint8Array(e))}async function ne(t){return[...new Uint8Array(await crypto.subtle.digest("SHA-256",new TextEncoder().encode(t)))].map(r=>r.toString(16).padStart(2,"0")).join("")}async function se(t,e,r){const n=await A(t.issuer,e),s=new URL(n.authorization_endpoint);return s.searchParams.set("response_type","code"),s.searchParams.set("client_id",t.clientId),s.searchParams.set("redirect_uri",r.redirectUri),s.searchParams.set("scope",r.scopes),s.searchParams.set("state",r.state),s.searchParams.set("nonce",r.nonce),s.searchParams.set("code_challenge",r.codeChallenge),s.searchParams.set("code_challenge_method","S256"),r.prompt&&s.searchParams.set("prompt",r.prompt),s.toString()}async function K(t,e,r){const n=await A(t.issuer,e),s=new URLSearchParams(r);s.set("client_id",t.clientId);const a={"content-type":"application/x-www-form-urlencoded"};if(t.clientSecret){const d=`${encodeURIComponent(t.clientId)}:${encodeURIComponent(t.clientSecret)}`;a.authorization=`Basic ${btoa(d)}`}const u=await e(n.token_endpoint,{method:"POST",headers:a,body:s});if(!u.ok){let d="unknown_error",k="";try{const S=await u.json();d=S.error??d,k=S.error_description??""}catch{}throw new W(u.status,d,k)}return await u.json()}function oe(t,e,r){return K(t,e,{grant_type:"authorization_code",code:r.code,code_verifier:r.codeVerifier,redirect_uri:r.redirectUri})}function ie(t,e,r){return K(t,e,{grant_type:"refresh_token",refresh_token:r})}async function ae(t,e,r,n){const s=await A(t.issuer,e),a={issuer:s.issuer,audience:t.clientId};let u;try{u=await _.jwtVerify(r,await L(s.jwks_uri,e,!1),a)}catch(d){if(d.code!=="ERR_JWKS_NO_MATCHING_KEY")throw d;u=await _.jwtVerify(r,await L(s.jwks_uri,e,!0),a)}if(u.payload.nonce!==n)throw new Error("id_token nonce does not match the sign-in request");return u.payload}async function ce(t,e,r){const n=await A(t.issuer,e);if(!n.end_session_endpoint)return null;const s=new URL(n.end_session_endpoint);return s.searchParams.set("client_id",t.clientId),s.searchParams.set("post_logout_redirect_uri",r.postLogoutRedirectUri),r.sessionId&&s.searchParams.set("logout_hint",r.sessionId),s.toString()}const ue=3e4;class de{constructor(e=ue){this.recentMs=e}recentMs;inFlight=new Map;recent=new Map;async run(e,r){const n=await ne(e);this.sweep();const s=this.recent.get(n);if(s)return s.result;let a=this.inFlight.get(n);return a||(a=r().then(u=>(this.recent.set(n,{at:Date.now(),result:u}),u)).finally(()=>this.inFlight.delete(n)),this.inFlight.set(n,a)),a}sweep(){const e=Date.now()-this.recentMs;for(const[r,n]of this.recent)n.at<e&&this.recent.delete(r)}}const N=new Map;function V(t){let e=N.get(t);return e||(e=crypto.subtle.digest("SHA-256",new TextEncoder().encode(t)).then(r=>new Uint8Array(r)),N.set(t,e)),e}async function j(t,e,r){return new _.EncryptJWT(t).setProtectedHeader({alg:"dir",enc:"A256GCM"}).setIssuedAt().setExpirationTime(`${r}s`).encrypt(await V(e))}async function D(t,e){try{const{payload:r}=await _.jwtDecrypt(t,await V(e));return r}catch{return null}}const le=900,H=600;function he(t,e){return t&&t.startsWith("/")&&!t.startsWith("//")&&!t.startsWith("/\\")?t:e}function m(t,e=[]){const r=new Headers({location:t,"cache-control":"no-store"});for(const n of e)r.append("set-cookie",n);return new Response(null,{status:302,headers:r})}function x(t,e){if(t.baseUrl)return t.baseUrl;const r=e.headers.get("x-forwarded-host")?.split(",")[0]?.trim()||e.headers.get("host")?.trim();if(r){const n=e.headers.get("x-forwarded-proto")?.split(",")[0]?.trim()||new URL(e.url).protocol.replace(":","");try{return new URL(`${n}://${r}`).origin}catch{}}return new URL(e.url).origin}function fe(t){const e=n=>typeof t[n]=="string"?t[n]:void 0,r=e("name")??([e("given_name"),e("family_name")].filter(Boolean).join(" ")||void 0)??e("email");return{id:t.sub??"",email:e("email"),name:r,username:e("preferred_username"),picture:e("picture"),emailVerified:t.email_verified===!0}}function O(t){return Date.now()+(t.expires_in??le)*1e3}function we(t){const e=Z(t),r={issuer:e.issuer,clientId:e.clientId,clientSecret:e.clientSecret},n=`${e.cookieName}_tx`,s=new de,a=o=>e.secureCookies??x(e,o).startsWith("https:"),u=o=>e.redirectUri??`${x(e,o)}${e.basePath}/callback`,d=(o,i)=>{const c=new URL(e.signInPath,x(e,o));return i&&c.searchParams.set("error",i),c.toString()};async function k(o){const i=Q(o,e.cookieName);if(i===null)return null;const c=await D(i,e.sessionSecret);return c||e.logger.warn("[pontive] session cookie could not be read (sealed with another secret, or truncated)"),c}async function S(o,i,c){const l=await j({...i},e.sessionSecret,e.sessionMaxAgeSeconds);return q(o,e.cookieName,l,{maxAge:e.sessionMaxAgeSeconds,secure:a(c)})}function b(o){const{refreshToken:i,...c}=o;return c}async function G(o){const i=new URL(o.url),c={state:v(),verifier:v(),nonce:v(),returnTo:he(i.searchParams.get("returnTo")??i.searchParams.get("next"),e.afterSignInPath)},l=await se(r,e.fetch,{redirectUri:u(o),scopes:e.scopes,state:c.state,nonce:c.nonce,codeChallenge:await re(c.verifier),prompt:e.prompt}),h=await j({...c},e.sessionSecret,H);return m(l,[U(n,h,{maxAge:H,secure:a(o)})])}async function z(o){const i=new URL(o.url),c=C(o.headers.get("cookie")),l=[U(n,"",{maxAge:0,secure:a(o)})],h=i.searchParams.get("error");if(h)return m(d(o,h),l);const f=c.get(n),g=f?await D(f,e.sessionSecret):null,w=i.searchParams.get("code");if(!g||!w||i.searchParams.get("state")!==g.state)return m(d(o,"invalid_state"),l);try{const p=await oe(r,e.fetch,{code:w,codeVerifier:g.verifier,redirectUri:u(o)});if(!p.id_token)throw new Error("the token response carried no id_token; is the openid scope requested?");const P=await ae(r,e.fetch,p.id_token,g.nonce),X={user:fe(P),accessToken:p.access_token,refreshToken:p.refresh_token,expiresAt:O(p),sessionId:typeof P.sid=="string"?P.sid:void 0};l.push(...await S(c,X,o))}catch(p){return e.logger.warn("[pontive] sign-in failed:",p instanceof Error?p.message:String(p)),m(d(o,"exchange_failed"),l)}return m(new URL(g.returnTo,x(e,o)).toString(),l)}async function J(o){const i=C(o.headers.get("cookie")),c=await k(i),l=R(i,e.cookieName,a(o));let h=d(o);try{h=await ce(r,e.fetch,{postLogoutRedirectUri:d(o),sessionId:c?.sessionId})??h}catch(f){e.logger.warn("[pontive] could not reach the issuer to end its session:",f instanceof Error?f.message:String(f))}return m(h,l)}async function B(o){try{const i=await ie(r,e.fetch,o.refreshToken);return{...o,accessToken:i.access_token,refreshToken:i.refresh_token??o.refreshToken,expiresAt:O(i)}}catch(i){if(i instanceof W&&i.status>=400&&i.status<500)return e.logger.warn("[pontive] refresh refused; signing out:",i.message),null;throw i}}return{async handle(o){const{pathname:i}=new URL(o.url);if(!i.startsWith(`${e.basePath}/`))return null;const c=i.slice(e.basePath.length+1);return c==="login"&&o.method==="GET"?G(o):c==="callback"&&o.method==="GET"?z(o):c==="logout"&&(o.method==="GET"||o.method==="POST")?J(o):null},async getSession(o){const i=await k(C(o));return i?Date.now()>=i.expiresAt?(e.logger.warn("[pontive] the session's access token has expired and getSession cannot refresh it. Run the SDK's middleware on this path so it refreshes before the request is handled."),null):b(i):null},async refresh(o){const i=C(o.headers.get("cookie")),c=w=>({session:w,setCookies:[],cookieHeader:o.headers.get("cookie")??""}),l=await k(i);if(!l)return c(null);if(Date.now()<l.expiresAt-e.refreshSkewSeconds*1e3)return c(b(l));const h=()=>({session:null,setCookies:R(i,e.cookieName,a(o)),cookieHeader:I(i)});if(!l.refreshToken)return h();let f;try{f=await s.run(l.refreshToken,()=>B(l))}catch(w){return e.logger.warn("[pontive] refresh failed, will retry:",w instanceof Error?w.message:String(w)),c(Date.now()<l.expiresAt?b(l):null)}if(!f)return h();const g=await S(i,f,o);return{session:b(f),setCookies:g,cookieHeader:I(i)}}}}exports.createServerAuth=we;
|
|
2
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../src/config.ts","../src/cookies.ts","../src/oidc.ts","../src/refresh.ts","../src/seal.ts","../src/server-auth.ts"],"sourcesContent":["/**\n * What an app tells the SDK, and the defaults for everything it doesn't.\n *\n * Only the first five matter in practice. Everything else exists for the rare\n * app that must differ, and its default is the decision the SDK makes for\n * everyone else.\n */\nexport interface ServerAuthConfig {\n /** The Pontive auth domain, e.g. `https://acme.auth.us.pontive.com`. */\n issuer: string;\n /** The app's id. */\n clientId: string;\n /**\n * Set for a confidential app. It is sent on every token call, and is what\n * lets the app hold its refresh token on its server at all.\n */\n clientSecret?: string;\n /** Encrypts the session cookie. At least 32 characters; rotating it signs everyone out. */\n sessionSecret: string;\n /**\n * The app's public origin, e.g. `https://app.acme.com`. Set it for any\n * deployment behind a proxy or in a container, where the request's own URL\n * names the address the server binds to rather than the one a browser uses.\n * Without it the origin comes from `x-forwarded-proto`/`x-forwarded-host`,\n * then the request URL.\n */\n baseUrl?: string;\n\n /** Default `openid email profile offline_access`. */\n scopes?: string;\n /** Where the SDK's sign-in routes live. Default `/api/auth`. */\n basePath?: string;\n /** Default `${origin}${basePath}/callback`; must be registered on the app verbatim. */\n redirectUri?: string;\n /** The app's own sign-in page: failed sign-ins and signed-out users land here. Default `/login`. */\n signInPath?: string;\n /** Where a sign-in lands when it did not start from a page. Default `/`. */\n afterSignInPath?: string;\n /** An OIDC `prompt` to send, e.g. `login`. */\n prompt?: string;\n /** Default `pontive_session`. */\n cookieName?: string;\n /** Default: whether the public origin is https. */\n secureCookies?: boolean;\n /** The session cookie's lifetime. Default 7 days. */\n sessionMaxAgeSeconds?: number;\n /** Refresh when fewer than this many seconds remain. Default 60. */\n refreshSkewSeconds?: number;\n /** Defaults to the global `fetch`. */\n fetch?: typeof fetch;\n /** Where the SDK reports a failed refresh or sign-in. Defaults to `console`. */\n logger?: Pick<Console, \"warn\">;\n}\n\nexport interface ResolvedConfig {\n issuer: string;\n clientId: string;\n clientSecret?: string;\n sessionSecret: string;\n baseUrl?: string;\n scopes: string;\n basePath: string;\n redirectUri?: string;\n signInPath: string;\n afterSignInPath: string;\n prompt?: string;\n cookieName: string;\n secureCookies?: boolean;\n sessionMaxAgeSeconds: number;\n refreshSkewSeconds: number;\n fetch: typeof fetch;\n logger: Pick<Console, \"warn\">;\n}\n\nconst SEVEN_DAYS = 7 * 24 * 60 * 60;\n\nexport function resolveConfig(config: ServerAuthConfig): ResolvedConfig {\n const required: (keyof ServerAuthConfig)[] = [\"issuer\", \"clientId\", \"sessionSecret\"];\n for (const key of required) {\n if (!config[key]) throw new Error(`@pontive/pontkit-server: ${key} is required`);\n }\n\n if (config.sessionSecret.length < 32) {\n throw new Error(\"@pontive/pontkit-server: sessionSecret must be at least 32 characters\");\n }\n\n if (config.baseUrl) {\n try {\n new URL(config.baseUrl);\n } catch {\n throw new Error(`@pontive/pontkit-server: baseUrl \"${config.baseUrl}\" is not an absolute URL`);\n }\n }\n\n return {\n issuer: config.issuer.replace(/\\/+$/, \"\"),\n clientId: config.clientId,\n clientSecret: config.clientSecret || undefined,\n sessionSecret: config.sessionSecret,\n baseUrl: config.baseUrl ? new URL(config.baseUrl).origin : undefined,\n scopes: config.scopes ?? \"openid email profile offline_access\",\n basePath: (config.basePath ?? \"/api/auth\").replace(/\\/+$/, \"\"),\n redirectUri: config.redirectUri,\n signInPath: config.signInPath ?? \"/login\",\n afterSignInPath: config.afterSignInPath ?? \"/\",\n prompt: config.prompt,\n cookieName: config.cookieName ?? \"pontive_session\",\n secureCookies: config.secureCookies,\n sessionMaxAgeSeconds: config.sessionMaxAgeSeconds ?? SEVEN_DAYS,\n refreshSkewSeconds: config.refreshSkewSeconds ?? 60,\n fetch: config.fetch ?? ((...args) => fetch(...args)),\n logger: config.logger ?? console,\n };\n}\n","/**\n * Cookie reading and writing, on plain header strings so it runs anywhere a\n * Web `Request`/`Response` does.\n *\n * A sealed session outgrows one cookie (browsers cap each at about 4 KB, and an\n * access token, a refresh token and a user fit in about 3–5), so it is split\n * across numbered chunks: `name`, `name.1`, `name.2`, … and joined on read.\n */\n\n/** Characters per chunk, leaving room under the ~4096-byte cap for the name and attributes. */\nexport const CHUNK_SIZE = 3500;\n/** Upper bound on chunks probed when reading or clearing. */\nexport const MAX_CHUNKS = 16;\n\nexport interface CookieAttributes {\n /** Seconds; 0 deletes the cookie. */\n maxAge: number;\n secure: boolean;\n}\n\n/** Parses a `Cookie` request header into name → value. */\nexport function parseCookies(header: string | null | undefined): Map<string, string> {\n const out = new Map<string, string>();\n if (!header) return out;\n\n for (const part of header.split(\";\")) {\n const eq = part.indexOf(\"=\");\n if (eq < 0) continue;\n\n const name = part.slice(0, eq).trim();\n if (!name || out.has(name)) continue;\n\n out.set(name, part.slice(eq + 1).trim());\n }\n\n return out;\n}\n\n/** Serialises a `Cookie` request header from name → value. */\nexport function formatCookieHeader(cookies: Map<string, string>): string {\n return [...cookies].map(([name, value]) => `${name}=${value}`).join(\"; \");\n}\n\n/**\n * A `Set-Cookie` value. Always HttpOnly, host-only, `Path=/` and\n * `SameSite=Lax`: Lax is what lets the cookie ride the top-level redirect back\n * from the sign-in page, and nothing here is ever for script.\n */\nexport function serializeCookie(name: string, value: string, attrs: CookieAttributes): string {\n const parts = [`${name}=${value}`, \"Path=/\", \"HttpOnly\", \"SameSite=Lax\", `Max-Age=${attrs.maxAge}`];\n if (attrs.secure) parts.push(\"Secure\");\n\n return parts.join(\"; \");\n}\n\nexport function chunkName(name: string, index: number): string {\n return index === 0 ? name : `${name}.${index}`;\n}\n\n/** Joins a chunked value, or null when its first chunk is absent. */\nexport function readChunked(cookies: Map<string, string>, name: string): string | null {\n const first = cookies.get(name);\n if (first === undefined) return null;\n\n let value = first;\n for (let i = 1; i < MAX_CHUNKS; i++) {\n const part = cookies.get(chunkName(name, i));\n if (part === undefined) break;\n value += part;\n }\n\n return value;\n}\n\n/**\n * The `Set-Cookie` values that store `value` in chunks, and delete any chunk\n * the request carried beyond what it now needs. Also updates `cookies` in\n * place, so a caller can forward the new state on the same request.\n */\nexport function writeChunked(\n cookies: Map<string, string>,\n name: string,\n value: string,\n attrs: CookieAttributes,\n): string[] {\n const out: string[] = [];\n const count = Math.max(1, Math.ceil(value.length / CHUNK_SIZE));\n\n for (let i = 0; i < count; i++) {\n const chunk = value.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);\n cookies.set(chunkName(name, i), chunk);\n out.push(serializeCookie(chunkName(name, i), chunk, attrs));\n }\n\n for (let i = count; i < MAX_CHUNKS; i++) {\n const stale = chunkName(name, i);\n if (!cookies.has(stale)) break;\n cookies.delete(stale);\n out.push(serializeCookie(stale, \"\", { ...attrs, maxAge: 0 }));\n }\n\n return out;\n}\n\n/** The `Set-Cookie` values that delete every chunk the request carried. */\nexport function clearChunked(cookies: Map<string, string>, name: string, secure: boolean): string[] {\n const out: string[] = [];\n\n for (let i = 0; i < MAX_CHUNKS; i++) {\n const chunk = chunkName(name, i);\n if (!cookies.has(chunk)) {\n if (i > 0) break;\n continue;\n }\n cookies.delete(chunk);\n out.push(serializeCookie(chunk, \"\", { maxAge: 0, secure }));\n }\n\n return out;\n}\n","import { createLocalJWKSet, jwtVerify, type JSONWebKeySet, type JWTPayload } from \"jose\";\n\n/**\n * The OIDC relying-party calls: discovery, PKCE, the two token grants and\n * id_token verification. Standard OIDC only, so any compliant issuer works.\n */\n\nexport interface Discovery {\n issuer: string;\n authorization_endpoint: string;\n token_endpoint: string;\n jwks_uri: string;\n end_session_endpoint?: string;\n}\n\nexport interface TokenResponse {\n access_token: string;\n token_type: string;\n expires_in?: number;\n refresh_token?: string;\n id_token?: string;\n scope?: string;\n}\n\nexport interface ClientCredentials {\n issuer: string;\n clientId: string;\n clientSecret?: string;\n}\n\n/** An error from the token endpoint, carrying its OAuth code. */\nexport class TokenEndpointError extends Error {\n constructor(\n readonly status: number,\n readonly code: string,\n description: string,\n ) {\n super(`token endpoint ${status} ${code}: ${description}`);\n }\n}\n\ntype Fetch = typeof fetch;\n\nconst JWKS_TTL_MS = 10 * 60 * 1000;\n\nconst discoveries = new Map<string, Promise<Discovery>>();\nconst jwksCache = new Map<string, { at: number; set: ReturnType<typeof createLocalJWKSet> }>();\n\nfunction trimSlash(value: string): string {\n return value.replace(/\\/+$/, \"\");\n}\n\nexport function discover(issuer: string, fetchImpl: Fetch): Promise<Discovery> {\n const key = trimSlash(issuer);\n let pending = discoveries.get(key);\n\n if (!pending) {\n pending = fetchImpl(`${key}/.well-known/openid-configuration`)\n .then(async (res) => {\n if (!res.ok) throw new Error(`OIDC discovery failed for ${key}: ${res.status}`);\n return (await res.json()) as Discovery;\n })\n .catch((err: unknown) => {\n // A failure is not cached: the next request retries.\n discoveries.delete(key);\n throw err;\n });\n discoveries.set(key, pending);\n }\n\n return pending;\n}\n\nasync function jwks(uri: string, fetchImpl: Fetch, force: boolean): Promise<ReturnType<typeof createLocalJWKSet>> {\n const cached = jwksCache.get(uri);\n if (cached && !force && Date.now() - cached.at < JWKS_TTL_MS) return cached.set;\n\n const res = await fetchImpl(uri);\n if (!res.ok) throw new Error(`JWKS fetch failed for ${uri}: ${res.status}`);\n\n const set = createLocalJWKSet((await res.json()) as JSONWebKeySet);\n jwksCache.set(uri, { at: Date.now(), set });\n\n return set;\n}\n\nfunction base64url(bytes: Uint8Array): string {\n let binary = \"\";\n for (const b of bytes) binary += String.fromCharCode(b);\n\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n/** A random, URL-safe value for state, nonce and the PKCE verifier. */\nexport function randomValue(): string {\n return base64url(crypto.getRandomValues(new Uint8Array(32)));\n}\n\nexport async function pkceChallenge(verifier: string): Promise<string> {\n const digest = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(verifier));\n\n return base64url(new Uint8Array(digest));\n}\n\n/** Hex SHA-256, for keying state by a secret without holding the secret. */\nexport async function sha256Hex(value: string): Promise<string> {\n const digest = new Uint8Array(await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(value)));\n\n return [...digest].map((b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nexport async function authorizationUrl(\n client: ClientCredentials,\n fetchImpl: Fetch,\n input: { redirectUri: string; scopes: string; state: string; nonce: string; codeChallenge: string; prompt?: string },\n): Promise<string> {\n const disc = await discover(client.issuer, fetchImpl);\n const url = new URL(disc.authorization_endpoint);\n\n url.searchParams.set(\"response_type\", \"code\");\n url.searchParams.set(\"client_id\", client.clientId);\n url.searchParams.set(\"redirect_uri\", input.redirectUri);\n url.searchParams.set(\"scope\", input.scopes);\n url.searchParams.set(\"state\", input.state);\n url.searchParams.set(\"nonce\", input.nonce);\n url.searchParams.set(\"code_challenge\", input.codeChallenge);\n url.searchParams.set(\"code_challenge_method\", \"S256\");\n if (input.prompt) url.searchParams.set(\"prompt\", input.prompt);\n\n return url.toString();\n}\n\n/**\n * A token-endpoint call. A confidential client authenticates with HTTP Basic\n * (RFC 6749 §2.3.1); `client_id` is always in the form too, which a public\n * client needs and a confidential one is allowed.\n */\nasync function tokenRequest(\n client: ClientCredentials,\n fetchImpl: Fetch,\n params: Record<string, string>,\n): Promise<TokenResponse> {\n const disc = await discover(client.issuer, fetchImpl);\n const body = new URLSearchParams(params);\n body.set(\"client_id\", client.clientId);\n\n const headers: Record<string, string> = { \"content-type\": \"application/x-www-form-urlencoded\" };\n if (client.clientSecret) {\n const credentials = `${encodeURIComponent(client.clientId)}:${encodeURIComponent(client.clientSecret)}`;\n headers.authorization = `Basic ${btoa(credentials)}`;\n }\n\n const res = await fetchImpl(disc.token_endpoint, { method: \"POST\", headers, body });\n if (!res.ok) {\n let code = \"unknown_error\";\n let description = \"\";\n\n try {\n const err = (await res.json()) as { error?: string; error_description?: string };\n code = err.error ?? code;\n description = err.error_description ?? \"\";\n } catch {\n // Not JSON; the status says enough.\n }\n\n throw new TokenEndpointError(res.status, code, description);\n }\n\n return (await res.json()) as TokenResponse;\n}\n\nexport function exchangeCode(\n client: ClientCredentials,\n fetchImpl: Fetch,\n input: { code: string; codeVerifier: string; redirectUri: string },\n): Promise<TokenResponse> {\n return tokenRequest(client, fetchImpl, {\n grant_type: \"authorization_code\",\n code: input.code,\n code_verifier: input.codeVerifier,\n redirect_uri: input.redirectUri,\n });\n}\n\nexport function refreshTokens(client: ClientCredentials, fetchImpl: Fetch, refreshToken: string): Promise<TokenResponse> {\n return tokenRequest(client, fetchImpl, { grant_type: \"refresh_token\", refresh_token: refreshToken });\n}\n\n/**\n * Verifies an id_token's signature, issuer, audience and nonce. A key the\n * cached set does not know refetches the set once, which is how a key rotation\n * at the issuer is picked up without waiting out the cache.\n */\nexport async function verifyIdToken(\n client: ClientCredentials,\n fetchImpl: Fetch,\n idToken: string,\n nonce: string,\n): Promise<JWTPayload> {\n const disc = await discover(client.issuer, fetchImpl);\n const options = { issuer: disc.issuer, audience: client.clientId };\n\n let result;\n try {\n result = await jwtVerify(idToken, await jwks(disc.jwks_uri, fetchImpl, false), options);\n } catch (err) {\n if ((err as { code?: string }).code !== \"ERR_JWKS_NO_MATCHING_KEY\") throw err;\n result = await jwtVerify(idToken, await jwks(disc.jwks_uri, fetchImpl, true), options);\n }\n\n if (result.payload.nonce !== nonce) throw new Error(\"id_token nonce does not match the sign-in request\");\n\n return result.payload;\n}\n\n/** RP-initiated logout (OIDC RP-Initiated Logout 1.0), or null when the issuer offers none. */\nexport async function endSessionUrl(\n client: ClientCredentials,\n fetchImpl: Fetch,\n input: { postLogoutRedirectUri: string; sessionId?: string },\n): Promise<string | null> {\n const disc = await discover(client.issuer, fetchImpl);\n if (!disc.end_session_endpoint) return null;\n\n const url = new URL(disc.end_session_endpoint);\n url.searchParams.set(\"client_id\", client.clientId);\n url.searchParams.set(\"post_logout_redirect_uri\", input.postLogoutRedirectUri);\n // logout_hint is what lets the issuer end its session without asking \"Sign\n // out?\": only an app holding tokens from that session knows the sid.\n if (input.sessionId) url.searchParams.set(\"logout_hint\", input.sessionId);\n\n return url.toString();\n}\n\n/** For tests: forget cached discovery documents and key sets. */\nexport function resetOidcCaches(): void {\n discoveries.clear();\n jwksCache.clear();\n}\n","import { sha256Hex } from \"./oidc\";\n\n/**\n * One refresh per session, however many requests ask for it.\n *\n * With rotation on, every refresh spends the token it presents. A page that\n * fires several requests the moment its access token expires would otherwise\n * refresh once per request, and every request after the first would present a\n * token the first had already spent — which the issuer reads as theft.\n *\n * Two layers, in this process:\n *\n * - In flight: requests for the same refresh token share one call.\n * - Recently done: a request that arrives a moment later still holding the old\n * cookie (its browser had not yet stored the new one) is handed the same\n * result rather than spending the old token again.\n *\n * Across processes — several replicas of the app — the issuer's reuse grace\n * window covers the same race, so no shared lock is needed.\n */\n\n/** How long a result is handed to late arrivals. Matches the issuer's grace window. */\nexport const RECENT_RESULT_MS = 30_000;\n\nexport class RefreshCoordinator<T> {\n private readonly inFlight = new Map<string, Promise<T>>();\n private readonly recent = new Map<string, { at: number; result: T }>();\n\n constructor(private readonly recentMs: number = RECENT_RESULT_MS) {}\n\n async run(refreshToken: string, refresh: () => Promise<T>): Promise<T> {\n const key = await sha256Hex(refreshToken);\n this.sweep();\n\n const done = this.recent.get(key);\n if (done) return done.result;\n\n let pending = this.inFlight.get(key);\n if (!pending) {\n pending = refresh()\n .then((result) => {\n this.recent.set(key, { at: Date.now(), result });\n return result;\n })\n .finally(() => this.inFlight.delete(key));\n this.inFlight.set(key, pending);\n }\n\n return pending;\n }\n\n private sweep(): void {\n const cutoff = Date.now() - this.recentMs;\n for (const [key, entry] of this.recent) {\n if (entry.at < cutoff) this.recent.delete(key);\n }\n }\n}\n","import { EncryptJWT, jwtDecrypt } from \"jose\";\n\n/**\n * Sealing: authenticated encryption (JWE, `dir` + A256GCM) of whatever the\n * server stores in a cookie. The browser holds ciphertext it can neither read\n * nor alter, which is what lets a refresh token live in a cookie at all.\n */\n\nconst keys = new Map<string, Promise<Uint8Array>>();\n\n/** A 256-bit key derived from the configured secret, whatever its length. */\nfunction keyFor(secret: string): Promise<Uint8Array> {\n let key = keys.get(secret);\n if (!key) {\n key = crypto.subtle\n .digest(\"SHA-256\", new TextEncoder().encode(secret))\n .then((digest) => new Uint8Array(digest));\n keys.set(secret, key);\n }\n\n return key;\n}\n\nexport async function seal(payload: Record<string, unknown>, secret: string, maxAgeSeconds: number): Promise<string> {\n return new EncryptJWT(payload)\n .setProtectedHeader({ alg: \"dir\", enc: \"A256GCM\" })\n .setIssuedAt()\n .setExpirationTime(`${maxAgeSeconds}s`)\n .encrypt(await keyFor(secret));\n}\n\n/** The payload, or null when the value is not one this secret sealed, or has expired. */\nexport async function unseal<T>(value: string, secret: string): Promise<T | null> {\n try {\n const { payload } = await jwtDecrypt(value, await keyFor(secret));\n return payload as T;\n } catch {\n return null;\n }\n}\n","import type { JWTPayload } from \"jose\";\n\nimport { resolveConfig, type ResolvedConfig, type ServerAuthConfig } from \"./config\";\nimport {\n clearChunked,\n formatCookieHeader,\n parseCookies,\n readChunked,\n serializeCookie,\n writeChunked,\n} from \"./cookies\";\nimport {\n authorizationUrl,\n endSessionUrl,\n exchangeCode,\n pkceChallenge,\n randomValue,\n refreshTokens,\n TokenEndpointError,\n verifyIdToken,\n type ClientCredentials,\n type TokenResponse,\n} from \"./oidc\";\nimport { RefreshCoordinator } from \"./refresh\";\nimport { seal, unseal } from \"./seal\";\n\nexport interface SessionUser {\n id: string;\n email?: string;\n name?: string;\n username?: string;\n picture?: string;\n emailVerified: boolean;\n}\n\n/** What `getSession` hands the app. */\nexport interface Session {\n user: SessionUser;\n /** A valid access token, for calling APIs as this user. */\n accessToken: string;\n /** When the access token expires, epoch milliseconds. */\n expiresAt: number;\n /** The issuer's session id (`sid`), when it sent one. */\n sessionId?: string;\n}\n\n/** What the cookie holds: the session plus the refresh token, which the app never sees. */\ninterface StoredSession extends Session {\n refreshToken?: string;\n}\n\n/** A sign-in in progress, sealed into its own short-lived cookie. */\ninterface SignInTransaction {\n state: string;\n verifier: string;\n nonce: string;\n returnTo: string;\n}\n\n/** What a refresh produced, for the middleware to apply. */\nexport interface RefreshOutcome {\n /** The session after the refresh, or null when there is none (or it could not be refreshed). */\n session: Session | null;\n /** `Set-Cookie` values for the response. Empty when nothing changed. */\n setCookies: string[];\n /**\n * The request's `Cookie` header as it should be seen by whatever handles\n * this request next, carrying the refreshed session. Forward it, so a page\n * rendered on this very request reads the new token rather than the expired\n * one the browser sent.\n */\n cookieHeader: string;\n}\n\nexport interface ServerAuth {\n /**\n * Handles the SDK's routes under `basePath`: `/login`, `/callback` and\n * `/logout`. Null for any other path.\n */\n handle(request: Request): Promise<Response | null>;\n /**\n * The session in a `Cookie` header, or null. Read-only and never refreshes:\n * any server code can call it, including code that cannot set cookies.\n * Returns null for a session whose access token has expired — `refresh`,\n * run in middleware ahead of the request, is what keeps it current.\n */\n getSession(cookieHeader: string | null | undefined): Promise<Session | null>;\n /**\n * Refreshes the session when its access token is about to expire. For\n * middleware: it runs before anything renders, and its response can always\n * carry the new cookie. Concurrent calls for one session share one refresh.\n */\n refresh(request: Request): Promise<RefreshOutcome>;\n}\n\nconst DEFAULT_ACCESS_TTL_SECONDS = 900;\nconst TRANSACTION_MAX_AGE = 600;\n\n/** Only a same-origin path may be returned to after sign-in: no open redirect. */\nfunction safePath(value: string | null | undefined, fallback: string): string {\n if (value && value.startsWith(\"/\") && !value.startsWith(\"//\") && !value.startsWith(\"/\\\\\")) return value;\n return fallback;\n}\n\nfunction redirect(location: string, setCookies: string[] = []): Response {\n const headers = new Headers({ location, \"cache-control\": \"no-store\" });\n for (const cookie of setCookies) headers.append(\"set-cookie\", cookie);\n\n return new Response(null, { status: 302, headers });\n}\n\nfunction publicOrigin(cfg: ResolvedConfig, request: Request): string {\n if (cfg.baseUrl) return cfg.baseUrl;\n\n const host = request.headers.get(\"x-forwarded-host\")?.split(\",\")[0]?.trim() || request.headers.get(\"host\")?.trim();\n if (host) {\n const proto =\n request.headers.get(\"x-forwarded-proto\")?.split(\",\")[0]?.trim() || new URL(request.url).protocol.replace(\":\", \"\");\n try {\n return new URL(`${proto}://${host}`).origin;\n } catch {\n // Fall through to the request URL.\n }\n }\n\n return new URL(request.url).origin;\n}\n\nfunction userFrom(claims: JWTPayload): SessionUser {\n const str = (key: string) => (typeof claims[key] === \"string\" ? (claims[key] as string) : undefined);\n const name =\n str(\"name\") ?? ([str(\"given_name\"), str(\"family_name\")].filter(Boolean).join(\" \") || undefined) ?? str(\"email\");\n\n return {\n id: claims.sub ?? \"\",\n email: str(\"email\"),\n name,\n username: str(\"preferred_username\"),\n picture: str(\"picture\"),\n emailVerified: claims.email_verified === true,\n };\n}\n\nfunction expiresAtFrom(token: TokenResponse): number {\n return Date.now() + (token.expires_in ?? DEFAULT_ACCESS_TTL_SECONDS) * 1000;\n}\n\nexport function createServerAuth(config: ServerAuthConfig): ServerAuth {\n const cfg = resolveConfig(config);\n const client: ClientCredentials = { issuer: cfg.issuer, clientId: cfg.clientId, clientSecret: cfg.clientSecret };\n const txCookie = `${cfg.cookieName}_tx`;\n const coordinator = new RefreshCoordinator<StoredSession | null>();\n\n const secure = (request: Request) => cfg.secureCookies ?? publicOrigin(cfg, request).startsWith(\"https:\");\n const redirectUri = (request: Request) => cfg.redirectUri ?? `${publicOrigin(cfg, request)}${cfg.basePath}/callback`;\n const toSignIn = (request: Request, error?: string) => {\n const url = new URL(cfg.signInPath, publicOrigin(cfg, request));\n if (error) url.searchParams.set(\"error\", error);\n return url.toString();\n };\n\n async function readStored(cookies: Map<string, string>): Promise<StoredSession | null> {\n const raw = readChunked(cookies, cfg.cookieName);\n if (raw === null) return null;\n\n const stored = await unseal<StoredSession>(raw, cfg.sessionSecret);\n if (!stored) cfg.logger.warn(\"[pontive] session cookie could not be read (sealed with another secret, or truncated)\");\n\n return stored;\n }\n\n async function writeStored(cookies: Map<string, string>, stored: StoredSession, request: Request): Promise<string[]> {\n const sealed = await seal({ ...stored }, cfg.sessionSecret, cfg.sessionMaxAgeSeconds);\n\n return writeChunked(cookies, cfg.cookieName, sealed, { maxAge: cfg.sessionMaxAgeSeconds, secure: secure(request) });\n }\n\n function publicSession(stored: StoredSession): Session {\n const { refreshToken: _refreshToken, ...session } = stored;\n return session;\n }\n\n async function signIn(request: Request): Promise<Response> {\n const url = new URL(request.url);\n const tx: SignInTransaction = {\n state: randomValue(),\n verifier: randomValue(),\n nonce: randomValue(),\n returnTo: safePath(url.searchParams.get(\"returnTo\") ?? url.searchParams.get(\"next\"), cfg.afterSignInPath),\n };\n\n const location = await authorizationUrl(client, cfg.fetch, {\n redirectUri: redirectUri(request),\n scopes: cfg.scopes,\n state: tx.state,\n nonce: tx.nonce,\n codeChallenge: await pkceChallenge(tx.verifier),\n prompt: cfg.prompt,\n });\n\n const sealed = await seal({ ...tx }, cfg.sessionSecret, TRANSACTION_MAX_AGE);\n\n return redirect(location, [serializeCookie(txCookie, sealed, { maxAge: TRANSACTION_MAX_AGE, secure: secure(request) })]);\n }\n\n async function callback(request: Request): Promise<Response> {\n const url = new URL(request.url);\n const cookies = parseCookies(request.headers.get(\"cookie\"));\n // Single use, whatever happens next.\n const setCookies = [serializeCookie(txCookie, \"\", { maxAge: 0, secure: secure(request) })];\n\n const issuerError = url.searchParams.get(\"error\");\n if (issuerError) return redirect(toSignIn(request, issuerError), setCookies);\n\n const raw = cookies.get(txCookie);\n const tx = raw ? await unseal<SignInTransaction>(raw, cfg.sessionSecret) : null;\n const code = url.searchParams.get(\"code\");\n\n if (!tx || !code || url.searchParams.get(\"state\") !== tx.state) {\n return redirect(toSignIn(request, \"invalid_state\"), setCookies);\n }\n\n try {\n const tokens = await exchangeCode(client, cfg.fetch, {\n code,\n codeVerifier: tx.verifier,\n redirectUri: redirectUri(request),\n });\n if (!tokens.id_token) throw new Error(\"the token response carried no id_token; is the openid scope requested?\");\n\n const claims = await verifyIdToken(client, cfg.fetch, tokens.id_token, tx.nonce);\n const stored: StoredSession = {\n user: userFrom(claims),\n accessToken: tokens.access_token,\n refreshToken: tokens.refresh_token,\n expiresAt: expiresAtFrom(tokens),\n sessionId: typeof claims.sid === \"string\" ? claims.sid : undefined,\n };\n\n setCookies.push(...(await writeStored(cookies, stored, request)));\n } catch (err) {\n cfg.logger.warn(\"[pontive] sign-in failed:\", err instanceof Error ? err.message : String(err));\n return redirect(toSignIn(request, \"exchange_failed\"), setCookies);\n }\n\n return redirect(new URL(tx.returnTo, publicOrigin(cfg, request)).toString(), setCookies);\n }\n\n async function signOut(request: Request): Promise<Response> {\n const cookies = parseCookies(request.headers.get(\"cookie\"));\n // Read before clearing: the session holds the sid the issuer's logout needs.\n const stored = await readStored(cookies);\n const setCookies = clearChunked(cookies, cfg.cookieName, secure(request));\n\n let location = toSignIn(request);\n try {\n location =\n (await endSessionUrl(client, cfg.fetch, {\n postLogoutRedirectUri: toSignIn(request),\n sessionId: stored?.sessionId,\n })) ?? location;\n } catch (err) {\n // An unreachable issuer must not stop the local sign-out.\n cfg.logger.warn(\"[pontive] could not reach the issuer to end its session:\", err instanceof Error ? err.message : String(err));\n }\n\n return redirect(location, setCookies);\n }\n\n /**\n * Spends the refresh token once, returning the refreshed session, or null\n * when the issuer refused it for good. Anything transient throws instead, so\n * the coordinator does not hand a passing failure to other requests.\n */\n async function refreshStored(stored: StoredSession): Promise<StoredSession | null> {\n try {\n const tokens = await refreshTokens(client, cfg.fetch, stored.refreshToken!);\n\n return {\n ...stored,\n accessToken: tokens.access_token,\n // A response without one means rotation is off: the token in hand is still current.\n refreshToken: tokens.refresh_token ?? stored.refreshToken,\n expiresAt: expiresAtFrom(tokens),\n };\n } catch (err) {\n if (err instanceof TokenEndpointError && err.status >= 400 && err.status < 500) {\n cfg.logger.warn(\"[pontive] refresh refused; signing out:\", err.message);\n return null;\n }\n\n throw err;\n }\n }\n\n return {\n async handle(request) {\n const { pathname } = new URL(request.url);\n if (!pathname.startsWith(`${cfg.basePath}/`)) return null;\n\n const route = pathname.slice(cfg.basePath.length + 1);\n if (route === \"login\" && request.method === \"GET\") return signIn(request);\n if (route === \"callback\" && request.method === \"GET\") return callback(request);\n if (route === \"logout\" && (request.method === \"GET\" || request.method === \"POST\")) return signOut(request);\n\n return null;\n },\n\n async getSession(cookieHeader) {\n const stored = await readStored(parseCookies(cookieHeader));\n if (!stored) return null;\n\n if (Date.now() >= stored.expiresAt) {\n cfg.logger.warn(\n \"[pontive] the session's access token has expired and getSession cannot refresh it. \" +\n \"Run the SDK's middleware on this path so it refreshes before the request is handled.\",\n );\n return null;\n }\n\n return publicSession(stored);\n },\n\n async refresh(request) {\n const cookies = parseCookies(request.headers.get(\"cookie\"));\n const unchanged = (session: Session | null): RefreshOutcome => ({\n session,\n setCookies: [],\n cookieHeader: request.headers.get(\"cookie\") ?? \"\",\n });\n\n const stored = await readStored(cookies);\n if (!stored) return unchanged(null);\n\n if (Date.now() < stored.expiresAt - cfg.refreshSkewSeconds * 1000) return unchanged(publicSession(stored));\n\n const signOutNow = (): RefreshOutcome => {\n const setCookies = clearChunked(cookies, cfg.cookieName, secure(request));\n return { session: null, setCookies, cookieHeader: formatCookieHeader(cookies) };\n };\n\n if (!stored.refreshToken) return signOutNow();\n\n let refreshed: StoredSession | null;\n try {\n refreshed = await coordinator.run(stored.refreshToken, () => refreshStored(stored));\n } catch (err) {\n // Transient: keep the session and let the next request try again. The\n // token may still have a few seconds of the skew left.\n cfg.logger.warn(\"[pontive] refresh failed, will retry:\", err instanceof Error ? err.message : String(err));\n return unchanged(Date.now() < stored.expiresAt ? publicSession(stored) : null);\n }\n\n if (!refreshed) return signOutNow();\n\n const setCookies = await writeStored(cookies, refreshed, request);\n\n return { session: publicSession(refreshed), setCookies, cookieHeader: formatCookieHeader(cookies) };\n },\n };\n}\n"],"names":["SEVEN_DAYS","resolveConfig","config","required","key","args","CHUNK_SIZE","MAX_CHUNKS","parseCookies","header","out","part","eq","name","formatCookieHeader","cookies","value","serializeCookie","attrs","parts","chunkName","index","readChunked","first","i","writeChunked","count","chunk","stale","clearChunked","secure","TokenEndpointError","status","code","description","JWKS_TTL_MS","discoveries","jwksCache","trimSlash","discover","issuer","fetchImpl","pending","res","err","jwks","uri","force","cached","set","createLocalJWKSet","base64url","bytes","binary","b","randomValue","pkceChallenge","verifier","digest","sha256Hex","authorizationUrl","client","input","disc","url","tokenRequest","params","body","headers","credentials","exchangeCode","refreshTokens","refreshToken","verifyIdToken","idToken","nonce","options","result","jwtVerify","endSessionUrl","RECENT_RESULT_MS","RefreshCoordinator","recentMs","refresh","done","cutoff","entry","keys","keyFor","secret","seal","payload","maxAgeSeconds","EncryptJWT","unseal","jwtDecrypt","DEFAULT_ACCESS_TTL_SECONDS","TRANSACTION_MAX_AGE","safePath","fallback","redirect","location","setCookies","cookie","publicOrigin","cfg","request","host","proto","userFrom","claims","str","expiresAtFrom","token","createServerAuth","txCookie","coordinator","redirectUri","toSignIn","error","readStored","raw","stored","writeStored","sealed","publicSession","_refreshToken","session","signIn","tx","callback","issuerError","tokens","signOut","refreshStored","pathname","route","cookieHeader","unchanged","signOutNow","refreshed"],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side sessions for Pontive apps, on Web `Request`/`Response`.
|
|
3
|
+
*
|
|
4
|
+
* `createServerAuth` gives a server three things: routes for signing in and
|
|
5
|
+
* out, a read-only `getSession`, and a `refresh` for middleware that keeps the
|
|
6
|
+
* session's access token current without ever spending a refresh token twice.
|
|
7
|
+
* Framework packages (such as `@pontive/pontkit-nextjs/server`) wrap it; use it
|
|
8
|
+
* directly in anything else that speaks Request and Response.
|
|
9
|
+
*/
|
|
10
|
+
export { createServerAuth } from './server-auth';
|
|
11
|
+
export type { RefreshOutcome, ServerAuth, Session, SessionUser } from './server-auth';
|
|
12
|
+
export type { ServerAuthConfig } from './config';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { jwtVerify as v, createLocalJWKSet as Y, jwtDecrypt as Z, EncryptJWT as Q } from "jose";
|
|
2
|
+
const q = 10080 * 60;
|
|
3
|
+
function ee(t) {
|
|
4
|
+
const e = ["issuer", "clientId", "sessionSecret"];
|
|
5
|
+
for (const r of e)
|
|
6
|
+
if (!t[r]) throw new Error(`@pontive/pontkit-server: ${r} is required`);
|
|
7
|
+
if (t.sessionSecret.length < 32)
|
|
8
|
+
throw new Error("@pontive/pontkit-server: sessionSecret must be at least 32 characters");
|
|
9
|
+
if (t.baseUrl)
|
|
10
|
+
try {
|
|
11
|
+
new URL(t.baseUrl);
|
|
12
|
+
} catch {
|
|
13
|
+
throw new Error(`@pontive/pontkit-server: baseUrl "${t.baseUrl}" is not an absolute URL`);
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
issuer: t.issuer.replace(/\/+$/, ""),
|
|
17
|
+
clientId: t.clientId,
|
|
18
|
+
clientSecret: t.clientSecret || void 0,
|
|
19
|
+
sessionSecret: t.sessionSecret,
|
|
20
|
+
baseUrl: t.baseUrl ? new URL(t.baseUrl).origin : void 0,
|
|
21
|
+
scopes: t.scopes ?? "openid email profile offline_access",
|
|
22
|
+
basePath: (t.basePath ?? "/api/auth").replace(/\/+$/, ""),
|
|
23
|
+
redirectUri: t.redirectUri,
|
|
24
|
+
signInPath: t.signInPath ?? "/login",
|
|
25
|
+
afterSignInPath: t.afterSignInPath ?? "/",
|
|
26
|
+
prompt: t.prompt,
|
|
27
|
+
cookieName: t.cookieName ?? "pontive_session",
|
|
28
|
+
secureCookies: t.secureCookies,
|
|
29
|
+
sessionMaxAgeSeconds: t.sessionMaxAgeSeconds ?? q,
|
|
30
|
+
refreshSkewSeconds: t.refreshSkewSeconds ?? 60,
|
|
31
|
+
fetch: t.fetch ?? ((...r) => fetch(...r)),
|
|
32
|
+
logger: t.logger ?? console
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const P = 3500, $ = 16;
|
|
36
|
+
function x(t) {
|
|
37
|
+
const e = /* @__PURE__ */ new Map();
|
|
38
|
+
if (!t) return e;
|
|
39
|
+
for (const r of t.split(";")) {
|
|
40
|
+
const n = r.indexOf("=");
|
|
41
|
+
if (n < 0) continue;
|
|
42
|
+
const s = r.slice(0, n).trim();
|
|
43
|
+
!s || e.has(s) || e.set(s, r.slice(n + 1).trim());
|
|
44
|
+
}
|
|
45
|
+
return e;
|
|
46
|
+
}
|
|
47
|
+
function I(t) {
|
|
48
|
+
return [...t].map(([e, r]) => `${e}=${r}`).join("; ");
|
|
49
|
+
}
|
|
50
|
+
function _(t, e, r) {
|
|
51
|
+
const n = [`${t}=${e}`, "Path=/", "HttpOnly", "SameSite=Lax", `Max-Age=${r.maxAge}`];
|
|
52
|
+
return r.secure && n.push("Secure"), n.join("; ");
|
|
53
|
+
}
|
|
54
|
+
function y(t, e) {
|
|
55
|
+
return e === 0 ? t : `${t}.${e}`;
|
|
56
|
+
}
|
|
57
|
+
function te(t, e) {
|
|
58
|
+
const r = t.get(e);
|
|
59
|
+
if (r === void 0) return null;
|
|
60
|
+
let n = r;
|
|
61
|
+
for (let s = 1; s < $; s++) {
|
|
62
|
+
const a = t.get(y(e, s));
|
|
63
|
+
if (a === void 0) break;
|
|
64
|
+
n += a;
|
|
65
|
+
}
|
|
66
|
+
return n;
|
|
67
|
+
}
|
|
68
|
+
function re(t, e, r, n) {
|
|
69
|
+
const s = [], a = Math.max(1, Math.ceil(r.length / P));
|
|
70
|
+
for (let u = 0; u < a; u++) {
|
|
71
|
+
const d = r.slice(u * P, (u + 1) * P);
|
|
72
|
+
t.set(y(e, u), d), s.push(_(y(e, u), d, n));
|
|
73
|
+
}
|
|
74
|
+
for (let u = a; u < $; u++) {
|
|
75
|
+
const d = y(e, u);
|
|
76
|
+
if (!t.has(d)) break;
|
|
77
|
+
t.delete(d), s.push(_(d, "", { ...n, maxAge: 0 }));
|
|
78
|
+
}
|
|
79
|
+
return s;
|
|
80
|
+
}
|
|
81
|
+
function R(t, e, r) {
|
|
82
|
+
const n = [];
|
|
83
|
+
for (let s = 0; s < $; s++) {
|
|
84
|
+
const a = y(e, s);
|
|
85
|
+
if (!t.has(a)) {
|
|
86
|
+
if (s > 0) break;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
t.delete(a), n.push(_(a, "", { maxAge: 0, secure: r }));
|
|
90
|
+
}
|
|
91
|
+
return n;
|
|
92
|
+
}
|
|
93
|
+
class W extends Error {
|
|
94
|
+
constructor(e, r, n) {
|
|
95
|
+
super(`token endpoint ${e} ${r}: ${n}`), this.status = e, this.code = r;
|
|
96
|
+
}
|
|
97
|
+
status;
|
|
98
|
+
code;
|
|
99
|
+
}
|
|
100
|
+
const ne = 600 * 1e3, T = /* @__PURE__ */ new Map(), L = /* @__PURE__ */ new Map();
|
|
101
|
+
function se(t) {
|
|
102
|
+
return t.replace(/\/+$/, "");
|
|
103
|
+
}
|
|
104
|
+
function b(t, e) {
|
|
105
|
+
const r = se(t);
|
|
106
|
+
let n = T.get(r);
|
|
107
|
+
return n || (n = e(`${r}/.well-known/openid-configuration`).then(async (s) => {
|
|
108
|
+
if (!s.ok) throw new Error(`OIDC discovery failed for ${r}: ${s.status}`);
|
|
109
|
+
return await s.json();
|
|
110
|
+
}).catch((s) => {
|
|
111
|
+
throw T.delete(r), s;
|
|
112
|
+
}), T.set(r, n)), n;
|
|
113
|
+
}
|
|
114
|
+
async function M(t, e, r) {
|
|
115
|
+
const n = L.get(t);
|
|
116
|
+
if (n && !r && Date.now() - n.at < ne) return n.set;
|
|
117
|
+
const s = await e(t);
|
|
118
|
+
if (!s.ok) throw new Error(`JWKS fetch failed for ${t}: ${s.status}`);
|
|
119
|
+
const a = Y(await s.json());
|
|
120
|
+
return L.set(t, { at: Date.now(), set: a }), a;
|
|
121
|
+
}
|
|
122
|
+
function F(t) {
|
|
123
|
+
let e = "";
|
|
124
|
+
for (const r of t) e += String.fromCharCode(r);
|
|
125
|
+
return btoa(e).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
126
|
+
}
|
|
127
|
+
function E() {
|
|
128
|
+
return F(crypto.getRandomValues(new Uint8Array(32)));
|
|
129
|
+
}
|
|
130
|
+
async function oe(t) {
|
|
131
|
+
const e = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(t));
|
|
132
|
+
return F(new Uint8Array(e));
|
|
133
|
+
}
|
|
134
|
+
async function ie(t) {
|
|
135
|
+
return [...new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(t)))].map((r) => r.toString(16).padStart(2, "0")).join("");
|
|
136
|
+
}
|
|
137
|
+
async function ae(t, e, r) {
|
|
138
|
+
const n = await b(t.issuer, e), s = new URL(n.authorization_endpoint);
|
|
139
|
+
return s.searchParams.set("response_type", "code"), s.searchParams.set("client_id", t.clientId), s.searchParams.set("redirect_uri", r.redirectUri), s.searchParams.set("scope", r.scopes), s.searchParams.set("state", r.state), s.searchParams.set("nonce", r.nonce), s.searchParams.set("code_challenge", r.codeChallenge), s.searchParams.set("code_challenge_method", "S256"), r.prompt && s.searchParams.set("prompt", r.prompt), s.toString();
|
|
140
|
+
}
|
|
141
|
+
async function K(t, e, r) {
|
|
142
|
+
const n = await b(t.issuer, e), s = new URLSearchParams(r);
|
|
143
|
+
s.set("client_id", t.clientId);
|
|
144
|
+
const a = { "content-type": "application/x-www-form-urlencoded" };
|
|
145
|
+
if (t.clientSecret) {
|
|
146
|
+
const d = `${encodeURIComponent(t.clientId)}:${encodeURIComponent(t.clientSecret)}`;
|
|
147
|
+
a.authorization = `Basic ${btoa(d)}`;
|
|
148
|
+
}
|
|
149
|
+
const u = await e(n.token_endpoint, { method: "POST", headers: a, body: s });
|
|
150
|
+
if (!u.ok) {
|
|
151
|
+
let d = "unknown_error", k = "";
|
|
152
|
+
try {
|
|
153
|
+
const S = await u.json();
|
|
154
|
+
d = S.error ?? d, k = S.error_description ?? "";
|
|
155
|
+
} catch {
|
|
156
|
+
}
|
|
157
|
+
throw new W(u.status, d, k);
|
|
158
|
+
}
|
|
159
|
+
return await u.json();
|
|
160
|
+
}
|
|
161
|
+
function ce(t, e, r) {
|
|
162
|
+
return K(t, e, {
|
|
163
|
+
grant_type: "authorization_code",
|
|
164
|
+
code: r.code,
|
|
165
|
+
code_verifier: r.codeVerifier,
|
|
166
|
+
redirect_uri: r.redirectUri
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
function ue(t, e, r) {
|
|
170
|
+
return K(t, e, { grant_type: "refresh_token", refresh_token: r });
|
|
171
|
+
}
|
|
172
|
+
async function de(t, e, r, n) {
|
|
173
|
+
const s = await b(t.issuer, e), a = { issuer: s.issuer, audience: t.clientId };
|
|
174
|
+
let u;
|
|
175
|
+
try {
|
|
176
|
+
u = await v(r, await M(s.jwks_uri, e, !1), a);
|
|
177
|
+
} catch (d) {
|
|
178
|
+
if (d.code !== "ERR_JWKS_NO_MATCHING_KEY") throw d;
|
|
179
|
+
u = await v(r, await M(s.jwks_uri, e, !0), a);
|
|
180
|
+
}
|
|
181
|
+
if (u.payload.nonce !== n) throw new Error("id_token nonce does not match the sign-in request");
|
|
182
|
+
return u.payload;
|
|
183
|
+
}
|
|
184
|
+
async function le(t, e, r) {
|
|
185
|
+
const n = await b(t.issuer, e);
|
|
186
|
+
if (!n.end_session_endpoint) return null;
|
|
187
|
+
const s = new URL(n.end_session_endpoint);
|
|
188
|
+
return s.searchParams.set("client_id", t.clientId), s.searchParams.set("post_logout_redirect_uri", r.postLogoutRedirectUri), r.sessionId && s.searchParams.set("logout_hint", r.sessionId), s.toString();
|
|
189
|
+
}
|
|
190
|
+
const he = 3e4;
|
|
191
|
+
class fe {
|
|
192
|
+
constructor(e = he) {
|
|
193
|
+
this.recentMs = e;
|
|
194
|
+
}
|
|
195
|
+
recentMs;
|
|
196
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
197
|
+
recent = /* @__PURE__ */ new Map();
|
|
198
|
+
async run(e, r) {
|
|
199
|
+
const n = await ie(e);
|
|
200
|
+
this.sweep();
|
|
201
|
+
const s = this.recent.get(n);
|
|
202
|
+
if (s) return s.result;
|
|
203
|
+
let a = this.inFlight.get(n);
|
|
204
|
+
return a || (a = r().then((u) => (this.recent.set(n, { at: Date.now(), result: u }), u)).finally(() => this.inFlight.delete(n)), this.inFlight.set(n, a)), a;
|
|
205
|
+
}
|
|
206
|
+
sweep() {
|
|
207
|
+
const e = Date.now() - this.recentMs;
|
|
208
|
+
for (const [r, n] of this.recent)
|
|
209
|
+
n.at < e && this.recent.delete(r);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const N = /* @__PURE__ */ new Map();
|
|
213
|
+
function V(t) {
|
|
214
|
+
let e = N.get(t);
|
|
215
|
+
return e || (e = crypto.subtle.digest("SHA-256", new TextEncoder().encode(t)).then((r) => new Uint8Array(r)), N.set(t, e)), e;
|
|
216
|
+
}
|
|
217
|
+
async function j(t, e, r) {
|
|
218
|
+
return new Q(t).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${r}s`).encrypt(await V(e));
|
|
219
|
+
}
|
|
220
|
+
async function D(t, e) {
|
|
221
|
+
try {
|
|
222
|
+
const { payload: r } = await Z(t, await V(e));
|
|
223
|
+
return r;
|
|
224
|
+
} catch {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const we = 900, H = 600;
|
|
229
|
+
function pe(t, e) {
|
|
230
|
+
return t && t.startsWith("/") && !t.startsWith("//") && !t.startsWith("/\\") ? t : e;
|
|
231
|
+
}
|
|
232
|
+
function m(t, e = []) {
|
|
233
|
+
const r = new Headers({ location: t, "cache-control": "no-store" });
|
|
234
|
+
for (const n of e) r.append("set-cookie", n);
|
|
235
|
+
return new Response(null, { status: 302, headers: r });
|
|
236
|
+
}
|
|
237
|
+
function C(t, e) {
|
|
238
|
+
if (t.baseUrl) return t.baseUrl;
|
|
239
|
+
const r = e.headers.get("x-forwarded-host")?.split(",")[0]?.trim() || e.headers.get("host")?.trim();
|
|
240
|
+
if (r) {
|
|
241
|
+
const n = e.headers.get("x-forwarded-proto")?.split(",")[0]?.trim() || new URL(e.url).protocol.replace(":", "");
|
|
242
|
+
try {
|
|
243
|
+
return new URL(`${n}://${r}`).origin;
|
|
244
|
+
} catch {
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return new URL(e.url).origin;
|
|
248
|
+
}
|
|
249
|
+
function ge(t) {
|
|
250
|
+
const e = (n) => typeof t[n] == "string" ? t[n] : void 0, r = e("name") ?? ([e("given_name"), e("family_name")].filter(Boolean).join(" ") || void 0) ?? e("email");
|
|
251
|
+
return {
|
|
252
|
+
id: t.sub ?? "",
|
|
253
|
+
email: e("email"),
|
|
254
|
+
name: r,
|
|
255
|
+
username: e("preferred_username"),
|
|
256
|
+
picture: e("picture"),
|
|
257
|
+
emailVerified: t.email_verified === !0
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function O(t) {
|
|
261
|
+
return Date.now() + (t.expires_in ?? we) * 1e3;
|
|
262
|
+
}
|
|
263
|
+
function me(t) {
|
|
264
|
+
const e = ee(t), r = { issuer: e.issuer, clientId: e.clientId, clientSecret: e.clientSecret }, n = `${e.cookieName}_tx`, s = new fe(), a = (o) => e.secureCookies ?? C(e, o).startsWith("https:"), u = (o) => e.redirectUri ?? `${C(e, o)}${e.basePath}/callback`, d = (o, i) => {
|
|
265
|
+
const c = new URL(e.signInPath, C(e, o));
|
|
266
|
+
return i && c.searchParams.set("error", i), c.toString();
|
|
267
|
+
};
|
|
268
|
+
async function k(o) {
|
|
269
|
+
const i = te(o, e.cookieName);
|
|
270
|
+
if (i === null) return null;
|
|
271
|
+
const c = await D(i, e.sessionSecret);
|
|
272
|
+
return c || e.logger.warn("[pontive] session cookie could not be read (sealed with another secret, or truncated)"), c;
|
|
273
|
+
}
|
|
274
|
+
async function S(o, i, c) {
|
|
275
|
+
const l = await j({ ...i }, e.sessionSecret, e.sessionMaxAgeSeconds);
|
|
276
|
+
return re(o, e.cookieName, l, { maxAge: e.sessionMaxAgeSeconds, secure: a(c) });
|
|
277
|
+
}
|
|
278
|
+
function U(o) {
|
|
279
|
+
const { refreshToken: i, ...c } = o;
|
|
280
|
+
return c;
|
|
281
|
+
}
|
|
282
|
+
async function G(o) {
|
|
283
|
+
const i = new URL(o.url), c = {
|
|
284
|
+
state: E(),
|
|
285
|
+
verifier: E(),
|
|
286
|
+
nonce: E(),
|
|
287
|
+
returnTo: pe(i.searchParams.get("returnTo") ?? i.searchParams.get("next"), e.afterSignInPath)
|
|
288
|
+
}, l = await ae(r, e.fetch, {
|
|
289
|
+
redirectUri: u(o),
|
|
290
|
+
scopes: e.scopes,
|
|
291
|
+
state: c.state,
|
|
292
|
+
nonce: c.nonce,
|
|
293
|
+
codeChallenge: await oe(c.verifier),
|
|
294
|
+
prompt: e.prompt
|
|
295
|
+
}), h = await j({ ...c }, e.sessionSecret, H);
|
|
296
|
+
return m(l, [_(n, h, { maxAge: H, secure: a(o) })]);
|
|
297
|
+
}
|
|
298
|
+
async function z(o) {
|
|
299
|
+
const i = new URL(o.url), c = x(o.headers.get("cookie")), l = [_(n, "", { maxAge: 0, secure: a(o) })], h = i.searchParams.get("error");
|
|
300
|
+
if (h) return m(d(o, h), l);
|
|
301
|
+
const f = c.get(n), g = f ? await D(f, e.sessionSecret) : null, w = i.searchParams.get("code");
|
|
302
|
+
if (!g || !w || i.searchParams.get("state") !== g.state)
|
|
303
|
+
return m(d(o, "invalid_state"), l);
|
|
304
|
+
try {
|
|
305
|
+
const p = await ce(r, e.fetch, {
|
|
306
|
+
code: w,
|
|
307
|
+
codeVerifier: g.verifier,
|
|
308
|
+
redirectUri: u(o)
|
|
309
|
+
});
|
|
310
|
+
if (!p.id_token) throw new Error("the token response carried no id_token; is the openid scope requested?");
|
|
311
|
+
const A = await de(r, e.fetch, p.id_token, g.nonce), X = {
|
|
312
|
+
user: ge(A),
|
|
313
|
+
accessToken: p.access_token,
|
|
314
|
+
refreshToken: p.refresh_token,
|
|
315
|
+
expiresAt: O(p),
|
|
316
|
+
sessionId: typeof A.sid == "string" ? A.sid : void 0
|
|
317
|
+
};
|
|
318
|
+
l.push(...await S(c, X, o));
|
|
319
|
+
} catch (p) {
|
|
320
|
+
return e.logger.warn("[pontive] sign-in failed:", p instanceof Error ? p.message : String(p)), m(d(o, "exchange_failed"), l);
|
|
321
|
+
}
|
|
322
|
+
return m(new URL(g.returnTo, C(e, o)).toString(), l);
|
|
323
|
+
}
|
|
324
|
+
async function J(o) {
|
|
325
|
+
const i = x(o.headers.get("cookie")), c = await k(i), l = R(i, e.cookieName, a(o));
|
|
326
|
+
let h = d(o);
|
|
327
|
+
try {
|
|
328
|
+
h = await le(r, e.fetch, {
|
|
329
|
+
postLogoutRedirectUri: d(o),
|
|
330
|
+
sessionId: c?.sessionId
|
|
331
|
+
}) ?? h;
|
|
332
|
+
} catch (f) {
|
|
333
|
+
e.logger.warn("[pontive] could not reach the issuer to end its session:", f instanceof Error ? f.message : String(f));
|
|
334
|
+
}
|
|
335
|
+
return m(h, l);
|
|
336
|
+
}
|
|
337
|
+
async function B(o) {
|
|
338
|
+
try {
|
|
339
|
+
const i = await ue(r, e.fetch, o.refreshToken);
|
|
340
|
+
return {
|
|
341
|
+
...o,
|
|
342
|
+
accessToken: i.access_token,
|
|
343
|
+
// A response without one means rotation is off: the token in hand is still current.
|
|
344
|
+
refreshToken: i.refresh_token ?? o.refreshToken,
|
|
345
|
+
expiresAt: O(i)
|
|
346
|
+
};
|
|
347
|
+
} catch (i) {
|
|
348
|
+
if (i instanceof W && i.status >= 400 && i.status < 500)
|
|
349
|
+
return e.logger.warn("[pontive] refresh refused; signing out:", i.message), null;
|
|
350
|
+
throw i;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
async handle(o) {
|
|
355
|
+
const { pathname: i } = new URL(o.url);
|
|
356
|
+
if (!i.startsWith(`${e.basePath}/`)) return null;
|
|
357
|
+
const c = i.slice(e.basePath.length + 1);
|
|
358
|
+
return c === "login" && o.method === "GET" ? G(o) : c === "callback" && o.method === "GET" ? z(o) : c === "logout" && (o.method === "GET" || o.method === "POST") ? J(o) : null;
|
|
359
|
+
},
|
|
360
|
+
async getSession(o) {
|
|
361
|
+
const i = await k(x(o));
|
|
362
|
+
return i ? Date.now() >= i.expiresAt ? (e.logger.warn(
|
|
363
|
+
"[pontive] the session's access token has expired and getSession cannot refresh it. Run the SDK's middleware on this path so it refreshes before the request is handled."
|
|
364
|
+
), null) : U(i) : null;
|
|
365
|
+
},
|
|
366
|
+
async refresh(o) {
|
|
367
|
+
const i = x(o.headers.get("cookie")), c = (w) => ({
|
|
368
|
+
session: w,
|
|
369
|
+
setCookies: [],
|
|
370
|
+
cookieHeader: o.headers.get("cookie") ?? ""
|
|
371
|
+
}), l = await k(i);
|
|
372
|
+
if (!l) return c(null);
|
|
373
|
+
if (Date.now() < l.expiresAt - e.refreshSkewSeconds * 1e3) return c(U(l));
|
|
374
|
+
const h = () => ({ session: null, setCookies: R(i, e.cookieName, a(o)), cookieHeader: I(i) });
|
|
375
|
+
if (!l.refreshToken) return h();
|
|
376
|
+
let f;
|
|
377
|
+
try {
|
|
378
|
+
f = await s.run(l.refreshToken, () => B(l));
|
|
379
|
+
} catch (w) {
|
|
380
|
+
return e.logger.warn("[pontive] refresh failed, will retry:", w instanceof Error ? w.message : String(w)), c(Date.now() < l.expiresAt ? U(l) : null);
|
|
381
|
+
}
|
|
382
|
+
if (!f) return h();
|
|
383
|
+
const g = await S(i, f, o);
|
|
384
|
+
return { session: U(f), setCookies: g, cookieHeader: I(i) };
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
export {
|
|
389
|
+
me as createServerAuth
|
|
390
|
+
};
|
|
391
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/config.ts","../src/cookies.ts","../src/oidc.ts","../src/refresh.ts","../src/seal.ts","../src/server-auth.ts"],"sourcesContent":["/**\n * What an app tells the SDK, and the defaults for everything it doesn't.\n *\n * Only the first five matter in practice. Everything else exists for the rare\n * app that must differ, and its default is the decision the SDK makes for\n * everyone else.\n */\nexport interface ServerAuthConfig {\n /** The Pontive auth domain, e.g. `https://acme.auth.us.pontive.com`. */\n issuer: string;\n /** The app's id. */\n clientId: string;\n /**\n * Set for a confidential app. It is sent on every token call, and is what\n * lets the app hold its refresh token on its server at all.\n */\n clientSecret?: string;\n /** Encrypts the session cookie. At least 32 characters; rotating it signs everyone out. */\n sessionSecret: string;\n /**\n * The app's public origin, e.g. `https://app.acme.com`. Set it for any\n * deployment behind a proxy or in a container, where the request's own URL\n * names the address the server binds to rather than the one a browser uses.\n * Without it the origin comes from `x-forwarded-proto`/`x-forwarded-host`,\n * then the request URL.\n */\n baseUrl?: string;\n\n /** Default `openid email profile offline_access`. */\n scopes?: string;\n /** Where the SDK's sign-in routes live. Default `/api/auth`. */\n basePath?: string;\n /** Default `${origin}${basePath}/callback`; must be registered on the app verbatim. */\n redirectUri?: string;\n /** The app's own sign-in page: failed sign-ins and signed-out users land here. Default `/login`. */\n signInPath?: string;\n /** Where a sign-in lands when it did not start from a page. Default `/`. */\n afterSignInPath?: string;\n /** An OIDC `prompt` to send, e.g. `login`. */\n prompt?: string;\n /** Default `pontive_session`. */\n cookieName?: string;\n /** Default: whether the public origin is https. */\n secureCookies?: boolean;\n /** The session cookie's lifetime. Default 7 days. */\n sessionMaxAgeSeconds?: number;\n /** Refresh when fewer than this many seconds remain. Default 60. */\n refreshSkewSeconds?: number;\n /** Defaults to the global `fetch`. */\n fetch?: typeof fetch;\n /** Where the SDK reports a failed refresh or sign-in. Defaults to `console`. */\n logger?: Pick<Console, \"warn\">;\n}\n\nexport interface ResolvedConfig {\n issuer: string;\n clientId: string;\n clientSecret?: string;\n sessionSecret: string;\n baseUrl?: string;\n scopes: string;\n basePath: string;\n redirectUri?: string;\n signInPath: string;\n afterSignInPath: string;\n prompt?: string;\n cookieName: string;\n secureCookies?: boolean;\n sessionMaxAgeSeconds: number;\n refreshSkewSeconds: number;\n fetch: typeof fetch;\n logger: Pick<Console, \"warn\">;\n}\n\nconst SEVEN_DAYS = 7 * 24 * 60 * 60;\n\nexport function resolveConfig(config: ServerAuthConfig): ResolvedConfig {\n const required: (keyof ServerAuthConfig)[] = [\"issuer\", \"clientId\", \"sessionSecret\"];\n for (const key of required) {\n if (!config[key]) throw new Error(`@pontive/pontkit-server: ${key} is required`);\n }\n\n if (config.sessionSecret.length < 32) {\n throw new Error(\"@pontive/pontkit-server: sessionSecret must be at least 32 characters\");\n }\n\n if (config.baseUrl) {\n try {\n new URL(config.baseUrl);\n } catch {\n throw new Error(`@pontive/pontkit-server: baseUrl \"${config.baseUrl}\" is not an absolute URL`);\n }\n }\n\n return {\n issuer: config.issuer.replace(/\\/+$/, \"\"),\n clientId: config.clientId,\n clientSecret: config.clientSecret || undefined,\n sessionSecret: config.sessionSecret,\n baseUrl: config.baseUrl ? new URL(config.baseUrl).origin : undefined,\n scopes: config.scopes ?? \"openid email profile offline_access\",\n basePath: (config.basePath ?? \"/api/auth\").replace(/\\/+$/, \"\"),\n redirectUri: config.redirectUri,\n signInPath: config.signInPath ?? \"/login\",\n afterSignInPath: config.afterSignInPath ?? \"/\",\n prompt: config.prompt,\n cookieName: config.cookieName ?? \"pontive_session\",\n secureCookies: config.secureCookies,\n sessionMaxAgeSeconds: config.sessionMaxAgeSeconds ?? SEVEN_DAYS,\n refreshSkewSeconds: config.refreshSkewSeconds ?? 60,\n fetch: config.fetch ?? ((...args) => fetch(...args)),\n logger: config.logger ?? console,\n };\n}\n","/**\n * Cookie reading and writing, on plain header strings so it runs anywhere a\n * Web `Request`/`Response` does.\n *\n * A sealed session outgrows one cookie (browsers cap each at about 4 KB, and an\n * access token, a refresh token and a user fit in about 3–5), so it is split\n * across numbered chunks: `name`, `name.1`, `name.2`, … and joined on read.\n */\n\n/** Characters per chunk, leaving room under the ~4096-byte cap for the name and attributes. */\nexport const CHUNK_SIZE = 3500;\n/** Upper bound on chunks probed when reading or clearing. */\nexport const MAX_CHUNKS = 16;\n\nexport interface CookieAttributes {\n /** Seconds; 0 deletes the cookie. */\n maxAge: number;\n secure: boolean;\n}\n\n/** Parses a `Cookie` request header into name → value. */\nexport function parseCookies(header: string | null | undefined): Map<string, string> {\n const out = new Map<string, string>();\n if (!header) return out;\n\n for (const part of header.split(\";\")) {\n const eq = part.indexOf(\"=\");\n if (eq < 0) continue;\n\n const name = part.slice(0, eq).trim();\n if (!name || out.has(name)) continue;\n\n out.set(name, part.slice(eq + 1).trim());\n }\n\n return out;\n}\n\n/** Serialises a `Cookie` request header from name → value. */\nexport function formatCookieHeader(cookies: Map<string, string>): string {\n return [...cookies].map(([name, value]) => `${name}=${value}`).join(\"; \");\n}\n\n/**\n * A `Set-Cookie` value. Always HttpOnly, host-only, `Path=/` and\n * `SameSite=Lax`: Lax is what lets the cookie ride the top-level redirect back\n * from the sign-in page, and nothing here is ever for script.\n */\nexport function serializeCookie(name: string, value: string, attrs: CookieAttributes): string {\n const parts = [`${name}=${value}`, \"Path=/\", \"HttpOnly\", \"SameSite=Lax\", `Max-Age=${attrs.maxAge}`];\n if (attrs.secure) parts.push(\"Secure\");\n\n return parts.join(\"; \");\n}\n\nexport function chunkName(name: string, index: number): string {\n return index === 0 ? name : `${name}.${index}`;\n}\n\n/** Joins a chunked value, or null when its first chunk is absent. */\nexport function readChunked(cookies: Map<string, string>, name: string): string | null {\n const first = cookies.get(name);\n if (first === undefined) return null;\n\n let value = first;\n for (let i = 1; i < MAX_CHUNKS; i++) {\n const part = cookies.get(chunkName(name, i));\n if (part === undefined) break;\n value += part;\n }\n\n return value;\n}\n\n/**\n * The `Set-Cookie` values that store `value` in chunks, and delete any chunk\n * the request carried beyond what it now needs. Also updates `cookies` in\n * place, so a caller can forward the new state on the same request.\n */\nexport function writeChunked(\n cookies: Map<string, string>,\n name: string,\n value: string,\n attrs: CookieAttributes,\n): string[] {\n const out: string[] = [];\n const count = Math.max(1, Math.ceil(value.length / CHUNK_SIZE));\n\n for (let i = 0; i < count; i++) {\n const chunk = value.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);\n cookies.set(chunkName(name, i), chunk);\n out.push(serializeCookie(chunkName(name, i), chunk, attrs));\n }\n\n for (let i = count; i < MAX_CHUNKS; i++) {\n const stale = chunkName(name, i);\n if (!cookies.has(stale)) break;\n cookies.delete(stale);\n out.push(serializeCookie(stale, \"\", { ...attrs, maxAge: 0 }));\n }\n\n return out;\n}\n\n/** The `Set-Cookie` values that delete every chunk the request carried. */\nexport function clearChunked(cookies: Map<string, string>, name: string, secure: boolean): string[] {\n const out: string[] = [];\n\n for (let i = 0; i < MAX_CHUNKS; i++) {\n const chunk = chunkName(name, i);\n if (!cookies.has(chunk)) {\n if (i > 0) break;\n continue;\n }\n cookies.delete(chunk);\n out.push(serializeCookie(chunk, \"\", { maxAge: 0, secure }));\n }\n\n return out;\n}\n","import { createLocalJWKSet, jwtVerify, type JSONWebKeySet, type JWTPayload } from \"jose\";\n\n/**\n * The OIDC relying-party calls: discovery, PKCE, the two token grants and\n * id_token verification. Standard OIDC only, so any compliant issuer works.\n */\n\nexport interface Discovery {\n issuer: string;\n authorization_endpoint: string;\n token_endpoint: string;\n jwks_uri: string;\n end_session_endpoint?: string;\n}\n\nexport interface TokenResponse {\n access_token: string;\n token_type: string;\n expires_in?: number;\n refresh_token?: string;\n id_token?: string;\n scope?: string;\n}\n\nexport interface ClientCredentials {\n issuer: string;\n clientId: string;\n clientSecret?: string;\n}\n\n/** An error from the token endpoint, carrying its OAuth code. */\nexport class TokenEndpointError extends Error {\n constructor(\n readonly status: number,\n readonly code: string,\n description: string,\n ) {\n super(`token endpoint ${status} ${code}: ${description}`);\n }\n}\n\ntype Fetch = typeof fetch;\n\nconst JWKS_TTL_MS = 10 * 60 * 1000;\n\nconst discoveries = new Map<string, Promise<Discovery>>();\nconst jwksCache = new Map<string, { at: number; set: ReturnType<typeof createLocalJWKSet> }>();\n\nfunction trimSlash(value: string): string {\n return value.replace(/\\/+$/, \"\");\n}\n\nexport function discover(issuer: string, fetchImpl: Fetch): Promise<Discovery> {\n const key = trimSlash(issuer);\n let pending = discoveries.get(key);\n\n if (!pending) {\n pending = fetchImpl(`${key}/.well-known/openid-configuration`)\n .then(async (res) => {\n if (!res.ok) throw new Error(`OIDC discovery failed for ${key}: ${res.status}`);\n return (await res.json()) as Discovery;\n })\n .catch((err: unknown) => {\n // A failure is not cached: the next request retries.\n discoveries.delete(key);\n throw err;\n });\n discoveries.set(key, pending);\n }\n\n return pending;\n}\n\nasync function jwks(uri: string, fetchImpl: Fetch, force: boolean): Promise<ReturnType<typeof createLocalJWKSet>> {\n const cached = jwksCache.get(uri);\n if (cached && !force && Date.now() - cached.at < JWKS_TTL_MS) return cached.set;\n\n const res = await fetchImpl(uri);\n if (!res.ok) throw new Error(`JWKS fetch failed for ${uri}: ${res.status}`);\n\n const set = createLocalJWKSet((await res.json()) as JSONWebKeySet);\n jwksCache.set(uri, { at: Date.now(), set });\n\n return set;\n}\n\nfunction base64url(bytes: Uint8Array): string {\n let binary = \"\";\n for (const b of bytes) binary += String.fromCharCode(b);\n\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n/** A random, URL-safe value for state, nonce and the PKCE verifier. */\nexport function randomValue(): string {\n return base64url(crypto.getRandomValues(new Uint8Array(32)));\n}\n\nexport async function pkceChallenge(verifier: string): Promise<string> {\n const digest = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(verifier));\n\n return base64url(new Uint8Array(digest));\n}\n\n/** Hex SHA-256, for keying state by a secret without holding the secret. */\nexport async function sha256Hex(value: string): Promise<string> {\n const digest = new Uint8Array(await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(value)));\n\n return [...digest].map((b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nexport async function authorizationUrl(\n client: ClientCredentials,\n fetchImpl: Fetch,\n input: { redirectUri: string; scopes: string; state: string; nonce: string; codeChallenge: string; prompt?: string },\n): Promise<string> {\n const disc = await discover(client.issuer, fetchImpl);\n const url = new URL(disc.authorization_endpoint);\n\n url.searchParams.set(\"response_type\", \"code\");\n url.searchParams.set(\"client_id\", client.clientId);\n url.searchParams.set(\"redirect_uri\", input.redirectUri);\n url.searchParams.set(\"scope\", input.scopes);\n url.searchParams.set(\"state\", input.state);\n url.searchParams.set(\"nonce\", input.nonce);\n url.searchParams.set(\"code_challenge\", input.codeChallenge);\n url.searchParams.set(\"code_challenge_method\", \"S256\");\n if (input.prompt) url.searchParams.set(\"prompt\", input.prompt);\n\n return url.toString();\n}\n\n/**\n * A token-endpoint call. A confidential client authenticates with HTTP Basic\n * (RFC 6749 §2.3.1); `client_id` is always in the form too, which a public\n * client needs and a confidential one is allowed.\n */\nasync function tokenRequest(\n client: ClientCredentials,\n fetchImpl: Fetch,\n params: Record<string, string>,\n): Promise<TokenResponse> {\n const disc = await discover(client.issuer, fetchImpl);\n const body = new URLSearchParams(params);\n body.set(\"client_id\", client.clientId);\n\n const headers: Record<string, string> = { \"content-type\": \"application/x-www-form-urlencoded\" };\n if (client.clientSecret) {\n const credentials = `${encodeURIComponent(client.clientId)}:${encodeURIComponent(client.clientSecret)}`;\n headers.authorization = `Basic ${btoa(credentials)}`;\n }\n\n const res = await fetchImpl(disc.token_endpoint, { method: \"POST\", headers, body });\n if (!res.ok) {\n let code = \"unknown_error\";\n let description = \"\";\n\n try {\n const err = (await res.json()) as { error?: string; error_description?: string };\n code = err.error ?? code;\n description = err.error_description ?? \"\";\n } catch {\n // Not JSON; the status says enough.\n }\n\n throw new TokenEndpointError(res.status, code, description);\n }\n\n return (await res.json()) as TokenResponse;\n}\n\nexport function exchangeCode(\n client: ClientCredentials,\n fetchImpl: Fetch,\n input: { code: string; codeVerifier: string; redirectUri: string },\n): Promise<TokenResponse> {\n return tokenRequest(client, fetchImpl, {\n grant_type: \"authorization_code\",\n code: input.code,\n code_verifier: input.codeVerifier,\n redirect_uri: input.redirectUri,\n });\n}\n\nexport function refreshTokens(client: ClientCredentials, fetchImpl: Fetch, refreshToken: string): Promise<TokenResponse> {\n return tokenRequest(client, fetchImpl, { grant_type: \"refresh_token\", refresh_token: refreshToken });\n}\n\n/**\n * Verifies an id_token's signature, issuer, audience and nonce. A key the\n * cached set does not know refetches the set once, which is how a key rotation\n * at the issuer is picked up without waiting out the cache.\n */\nexport async function verifyIdToken(\n client: ClientCredentials,\n fetchImpl: Fetch,\n idToken: string,\n nonce: string,\n): Promise<JWTPayload> {\n const disc = await discover(client.issuer, fetchImpl);\n const options = { issuer: disc.issuer, audience: client.clientId };\n\n let result;\n try {\n result = await jwtVerify(idToken, await jwks(disc.jwks_uri, fetchImpl, false), options);\n } catch (err) {\n if ((err as { code?: string }).code !== \"ERR_JWKS_NO_MATCHING_KEY\") throw err;\n result = await jwtVerify(idToken, await jwks(disc.jwks_uri, fetchImpl, true), options);\n }\n\n if (result.payload.nonce !== nonce) throw new Error(\"id_token nonce does not match the sign-in request\");\n\n return result.payload;\n}\n\n/** RP-initiated logout (OIDC RP-Initiated Logout 1.0), or null when the issuer offers none. */\nexport async function endSessionUrl(\n client: ClientCredentials,\n fetchImpl: Fetch,\n input: { postLogoutRedirectUri: string; sessionId?: string },\n): Promise<string | null> {\n const disc = await discover(client.issuer, fetchImpl);\n if (!disc.end_session_endpoint) return null;\n\n const url = new URL(disc.end_session_endpoint);\n url.searchParams.set(\"client_id\", client.clientId);\n url.searchParams.set(\"post_logout_redirect_uri\", input.postLogoutRedirectUri);\n // logout_hint is what lets the issuer end its session without asking \"Sign\n // out?\": only an app holding tokens from that session knows the sid.\n if (input.sessionId) url.searchParams.set(\"logout_hint\", input.sessionId);\n\n return url.toString();\n}\n\n/** For tests: forget cached discovery documents and key sets. */\nexport function resetOidcCaches(): void {\n discoveries.clear();\n jwksCache.clear();\n}\n","import { sha256Hex } from \"./oidc\";\n\n/**\n * One refresh per session, however many requests ask for it.\n *\n * With rotation on, every refresh spends the token it presents. A page that\n * fires several requests the moment its access token expires would otherwise\n * refresh once per request, and every request after the first would present a\n * token the first had already spent — which the issuer reads as theft.\n *\n * Two layers, in this process:\n *\n * - In flight: requests for the same refresh token share one call.\n * - Recently done: a request that arrives a moment later still holding the old\n * cookie (its browser had not yet stored the new one) is handed the same\n * result rather than spending the old token again.\n *\n * Across processes — several replicas of the app — the issuer's reuse grace\n * window covers the same race, so no shared lock is needed.\n */\n\n/** How long a result is handed to late arrivals. Matches the issuer's grace window. */\nexport const RECENT_RESULT_MS = 30_000;\n\nexport class RefreshCoordinator<T> {\n private readonly inFlight = new Map<string, Promise<T>>();\n private readonly recent = new Map<string, { at: number; result: T }>();\n\n constructor(private readonly recentMs: number = RECENT_RESULT_MS) {}\n\n async run(refreshToken: string, refresh: () => Promise<T>): Promise<T> {\n const key = await sha256Hex(refreshToken);\n this.sweep();\n\n const done = this.recent.get(key);\n if (done) return done.result;\n\n let pending = this.inFlight.get(key);\n if (!pending) {\n pending = refresh()\n .then((result) => {\n this.recent.set(key, { at: Date.now(), result });\n return result;\n })\n .finally(() => this.inFlight.delete(key));\n this.inFlight.set(key, pending);\n }\n\n return pending;\n }\n\n private sweep(): void {\n const cutoff = Date.now() - this.recentMs;\n for (const [key, entry] of this.recent) {\n if (entry.at < cutoff) this.recent.delete(key);\n }\n }\n}\n","import { EncryptJWT, jwtDecrypt } from \"jose\";\n\n/**\n * Sealing: authenticated encryption (JWE, `dir` + A256GCM) of whatever the\n * server stores in a cookie. The browser holds ciphertext it can neither read\n * nor alter, which is what lets a refresh token live in a cookie at all.\n */\n\nconst keys = new Map<string, Promise<Uint8Array>>();\n\n/** A 256-bit key derived from the configured secret, whatever its length. */\nfunction keyFor(secret: string): Promise<Uint8Array> {\n let key = keys.get(secret);\n if (!key) {\n key = crypto.subtle\n .digest(\"SHA-256\", new TextEncoder().encode(secret))\n .then((digest) => new Uint8Array(digest));\n keys.set(secret, key);\n }\n\n return key;\n}\n\nexport async function seal(payload: Record<string, unknown>, secret: string, maxAgeSeconds: number): Promise<string> {\n return new EncryptJWT(payload)\n .setProtectedHeader({ alg: \"dir\", enc: \"A256GCM\" })\n .setIssuedAt()\n .setExpirationTime(`${maxAgeSeconds}s`)\n .encrypt(await keyFor(secret));\n}\n\n/** The payload, or null when the value is not one this secret sealed, or has expired. */\nexport async function unseal<T>(value: string, secret: string): Promise<T | null> {\n try {\n const { payload } = await jwtDecrypt(value, await keyFor(secret));\n return payload as T;\n } catch {\n return null;\n }\n}\n","import type { JWTPayload } from \"jose\";\n\nimport { resolveConfig, type ResolvedConfig, type ServerAuthConfig } from \"./config\";\nimport {\n clearChunked,\n formatCookieHeader,\n parseCookies,\n readChunked,\n serializeCookie,\n writeChunked,\n} from \"./cookies\";\nimport {\n authorizationUrl,\n endSessionUrl,\n exchangeCode,\n pkceChallenge,\n randomValue,\n refreshTokens,\n TokenEndpointError,\n verifyIdToken,\n type ClientCredentials,\n type TokenResponse,\n} from \"./oidc\";\nimport { RefreshCoordinator } from \"./refresh\";\nimport { seal, unseal } from \"./seal\";\n\nexport interface SessionUser {\n id: string;\n email?: string;\n name?: string;\n username?: string;\n picture?: string;\n emailVerified: boolean;\n}\n\n/** What `getSession` hands the app. */\nexport interface Session {\n user: SessionUser;\n /** A valid access token, for calling APIs as this user. */\n accessToken: string;\n /** When the access token expires, epoch milliseconds. */\n expiresAt: number;\n /** The issuer's session id (`sid`), when it sent one. */\n sessionId?: string;\n}\n\n/** What the cookie holds: the session plus the refresh token, which the app never sees. */\ninterface StoredSession extends Session {\n refreshToken?: string;\n}\n\n/** A sign-in in progress, sealed into its own short-lived cookie. */\ninterface SignInTransaction {\n state: string;\n verifier: string;\n nonce: string;\n returnTo: string;\n}\n\n/** What a refresh produced, for the middleware to apply. */\nexport interface RefreshOutcome {\n /** The session after the refresh, or null when there is none (or it could not be refreshed). */\n session: Session | null;\n /** `Set-Cookie` values for the response. Empty when nothing changed. */\n setCookies: string[];\n /**\n * The request's `Cookie` header as it should be seen by whatever handles\n * this request next, carrying the refreshed session. Forward it, so a page\n * rendered on this very request reads the new token rather than the expired\n * one the browser sent.\n */\n cookieHeader: string;\n}\n\nexport interface ServerAuth {\n /**\n * Handles the SDK's routes under `basePath`: `/login`, `/callback` and\n * `/logout`. Null for any other path.\n */\n handle(request: Request): Promise<Response | null>;\n /**\n * The session in a `Cookie` header, or null. Read-only and never refreshes:\n * any server code can call it, including code that cannot set cookies.\n * Returns null for a session whose access token has expired — `refresh`,\n * run in middleware ahead of the request, is what keeps it current.\n */\n getSession(cookieHeader: string | null | undefined): Promise<Session | null>;\n /**\n * Refreshes the session when its access token is about to expire. For\n * middleware: it runs before anything renders, and its response can always\n * carry the new cookie. Concurrent calls for one session share one refresh.\n */\n refresh(request: Request): Promise<RefreshOutcome>;\n}\n\nconst DEFAULT_ACCESS_TTL_SECONDS = 900;\nconst TRANSACTION_MAX_AGE = 600;\n\n/** Only a same-origin path may be returned to after sign-in: no open redirect. */\nfunction safePath(value: string | null | undefined, fallback: string): string {\n if (value && value.startsWith(\"/\") && !value.startsWith(\"//\") && !value.startsWith(\"/\\\\\")) return value;\n return fallback;\n}\n\nfunction redirect(location: string, setCookies: string[] = []): Response {\n const headers = new Headers({ location, \"cache-control\": \"no-store\" });\n for (const cookie of setCookies) headers.append(\"set-cookie\", cookie);\n\n return new Response(null, { status: 302, headers });\n}\n\nfunction publicOrigin(cfg: ResolvedConfig, request: Request): string {\n if (cfg.baseUrl) return cfg.baseUrl;\n\n const host = request.headers.get(\"x-forwarded-host\")?.split(\",\")[0]?.trim() || request.headers.get(\"host\")?.trim();\n if (host) {\n const proto =\n request.headers.get(\"x-forwarded-proto\")?.split(\",\")[0]?.trim() || new URL(request.url).protocol.replace(\":\", \"\");\n try {\n return new URL(`${proto}://${host}`).origin;\n } catch {\n // Fall through to the request URL.\n }\n }\n\n return new URL(request.url).origin;\n}\n\nfunction userFrom(claims: JWTPayload): SessionUser {\n const str = (key: string) => (typeof claims[key] === \"string\" ? (claims[key] as string) : undefined);\n const name =\n str(\"name\") ?? ([str(\"given_name\"), str(\"family_name\")].filter(Boolean).join(\" \") || undefined) ?? str(\"email\");\n\n return {\n id: claims.sub ?? \"\",\n email: str(\"email\"),\n name,\n username: str(\"preferred_username\"),\n picture: str(\"picture\"),\n emailVerified: claims.email_verified === true,\n };\n}\n\nfunction expiresAtFrom(token: TokenResponse): number {\n return Date.now() + (token.expires_in ?? DEFAULT_ACCESS_TTL_SECONDS) * 1000;\n}\n\nexport function createServerAuth(config: ServerAuthConfig): ServerAuth {\n const cfg = resolveConfig(config);\n const client: ClientCredentials = { issuer: cfg.issuer, clientId: cfg.clientId, clientSecret: cfg.clientSecret };\n const txCookie = `${cfg.cookieName}_tx`;\n const coordinator = new RefreshCoordinator<StoredSession | null>();\n\n const secure = (request: Request) => cfg.secureCookies ?? publicOrigin(cfg, request).startsWith(\"https:\");\n const redirectUri = (request: Request) => cfg.redirectUri ?? `${publicOrigin(cfg, request)}${cfg.basePath}/callback`;\n const toSignIn = (request: Request, error?: string) => {\n const url = new URL(cfg.signInPath, publicOrigin(cfg, request));\n if (error) url.searchParams.set(\"error\", error);\n return url.toString();\n };\n\n async function readStored(cookies: Map<string, string>): Promise<StoredSession | null> {\n const raw = readChunked(cookies, cfg.cookieName);\n if (raw === null) return null;\n\n const stored = await unseal<StoredSession>(raw, cfg.sessionSecret);\n if (!stored) cfg.logger.warn(\"[pontive] session cookie could not be read (sealed with another secret, or truncated)\");\n\n return stored;\n }\n\n async function writeStored(cookies: Map<string, string>, stored: StoredSession, request: Request): Promise<string[]> {\n const sealed = await seal({ ...stored }, cfg.sessionSecret, cfg.sessionMaxAgeSeconds);\n\n return writeChunked(cookies, cfg.cookieName, sealed, { maxAge: cfg.sessionMaxAgeSeconds, secure: secure(request) });\n }\n\n function publicSession(stored: StoredSession): Session {\n const { refreshToken: _refreshToken, ...session } = stored;\n return session;\n }\n\n async function signIn(request: Request): Promise<Response> {\n const url = new URL(request.url);\n const tx: SignInTransaction = {\n state: randomValue(),\n verifier: randomValue(),\n nonce: randomValue(),\n returnTo: safePath(url.searchParams.get(\"returnTo\") ?? url.searchParams.get(\"next\"), cfg.afterSignInPath),\n };\n\n const location = await authorizationUrl(client, cfg.fetch, {\n redirectUri: redirectUri(request),\n scopes: cfg.scopes,\n state: tx.state,\n nonce: tx.nonce,\n codeChallenge: await pkceChallenge(tx.verifier),\n prompt: cfg.prompt,\n });\n\n const sealed = await seal({ ...tx }, cfg.sessionSecret, TRANSACTION_MAX_AGE);\n\n return redirect(location, [serializeCookie(txCookie, sealed, { maxAge: TRANSACTION_MAX_AGE, secure: secure(request) })]);\n }\n\n async function callback(request: Request): Promise<Response> {\n const url = new URL(request.url);\n const cookies = parseCookies(request.headers.get(\"cookie\"));\n // Single use, whatever happens next.\n const setCookies = [serializeCookie(txCookie, \"\", { maxAge: 0, secure: secure(request) })];\n\n const issuerError = url.searchParams.get(\"error\");\n if (issuerError) return redirect(toSignIn(request, issuerError), setCookies);\n\n const raw = cookies.get(txCookie);\n const tx = raw ? await unseal<SignInTransaction>(raw, cfg.sessionSecret) : null;\n const code = url.searchParams.get(\"code\");\n\n if (!tx || !code || url.searchParams.get(\"state\") !== tx.state) {\n return redirect(toSignIn(request, \"invalid_state\"), setCookies);\n }\n\n try {\n const tokens = await exchangeCode(client, cfg.fetch, {\n code,\n codeVerifier: tx.verifier,\n redirectUri: redirectUri(request),\n });\n if (!tokens.id_token) throw new Error(\"the token response carried no id_token; is the openid scope requested?\");\n\n const claims = await verifyIdToken(client, cfg.fetch, tokens.id_token, tx.nonce);\n const stored: StoredSession = {\n user: userFrom(claims),\n accessToken: tokens.access_token,\n refreshToken: tokens.refresh_token,\n expiresAt: expiresAtFrom(tokens),\n sessionId: typeof claims.sid === \"string\" ? claims.sid : undefined,\n };\n\n setCookies.push(...(await writeStored(cookies, stored, request)));\n } catch (err) {\n cfg.logger.warn(\"[pontive] sign-in failed:\", err instanceof Error ? err.message : String(err));\n return redirect(toSignIn(request, \"exchange_failed\"), setCookies);\n }\n\n return redirect(new URL(tx.returnTo, publicOrigin(cfg, request)).toString(), setCookies);\n }\n\n async function signOut(request: Request): Promise<Response> {\n const cookies = parseCookies(request.headers.get(\"cookie\"));\n // Read before clearing: the session holds the sid the issuer's logout needs.\n const stored = await readStored(cookies);\n const setCookies = clearChunked(cookies, cfg.cookieName, secure(request));\n\n let location = toSignIn(request);\n try {\n location =\n (await endSessionUrl(client, cfg.fetch, {\n postLogoutRedirectUri: toSignIn(request),\n sessionId: stored?.sessionId,\n })) ?? location;\n } catch (err) {\n // An unreachable issuer must not stop the local sign-out.\n cfg.logger.warn(\"[pontive] could not reach the issuer to end its session:\", err instanceof Error ? err.message : String(err));\n }\n\n return redirect(location, setCookies);\n }\n\n /**\n * Spends the refresh token once, returning the refreshed session, or null\n * when the issuer refused it for good. Anything transient throws instead, so\n * the coordinator does not hand a passing failure to other requests.\n */\n async function refreshStored(stored: StoredSession): Promise<StoredSession | null> {\n try {\n const tokens = await refreshTokens(client, cfg.fetch, stored.refreshToken!);\n\n return {\n ...stored,\n accessToken: tokens.access_token,\n // A response without one means rotation is off: the token in hand is still current.\n refreshToken: tokens.refresh_token ?? stored.refreshToken,\n expiresAt: expiresAtFrom(tokens),\n };\n } catch (err) {\n if (err instanceof TokenEndpointError && err.status >= 400 && err.status < 500) {\n cfg.logger.warn(\"[pontive] refresh refused; signing out:\", err.message);\n return null;\n }\n\n throw err;\n }\n }\n\n return {\n async handle(request) {\n const { pathname } = new URL(request.url);\n if (!pathname.startsWith(`${cfg.basePath}/`)) return null;\n\n const route = pathname.slice(cfg.basePath.length + 1);\n if (route === \"login\" && request.method === \"GET\") return signIn(request);\n if (route === \"callback\" && request.method === \"GET\") return callback(request);\n if (route === \"logout\" && (request.method === \"GET\" || request.method === \"POST\")) return signOut(request);\n\n return null;\n },\n\n async getSession(cookieHeader) {\n const stored = await readStored(parseCookies(cookieHeader));\n if (!stored) return null;\n\n if (Date.now() >= stored.expiresAt) {\n cfg.logger.warn(\n \"[pontive] the session's access token has expired and getSession cannot refresh it. \" +\n \"Run the SDK's middleware on this path so it refreshes before the request is handled.\",\n );\n return null;\n }\n\n return publicSession(stored);\n },\n\n async refresh(request) {\n const cookies = parseCookies(request.headers.get(\"cookie\"));\n const unchanged = (session: Session | null): RefreshOutcome => ({\n session,\n setCookies: [],\n cookieHeader: request.headers.get(\"cookie\") ?? \"\",\n });\n\n const stored = await readStored(cookies);\n if (!stored) return unchanged(null);\n\n if (Date.now() < stored.expiresAt - cfg.refreshSkewSeconds * 1000) return unchanged(publicSession(stored));\n\n const signOutNow = (): RefreshOutcome => {\n const setCookies = clearChunked(cookies, cfg.cookieName, secure(request));\n return { session: null, setCookies, cookieHeader: formatCookieHeader(cookies) };\n };\n\n if (!stored.refreshToken) return signOutNow();\n\n let refreshed: StoredSession | null;\n try {\n refreshed = await coordinator.run(stored.refreshToken, () => refreshStored(stored));\n } catch (err) {\n // Transient: keep the session and let the next request try again. The\n // token may still have a few seconds of the skew left.\n cfg.logger.warn(\"[pontive] refresh failed, will retry:\", err instanceof Error ? err.message : String(err));\n return unchanged(Date.now() < stored.expiresAt ? publicSession(stored) : null);\n }\n\n if (!refreshed) return signOutNow();\n\n const setCookies = await writeStored(cookies, refreshed, request);\n\n return { session: publicSession(refreshed), setCookies, cookieHeader: formatCookieHeader(cookies) };\n },\n };\n}\n"],"names":["SEVEN_DAYS","resolveConfig","config","required","key","args","CHUNK_SIZE","MAX_CHUNKS","parseCookies","header","out","part","eq","name","formatCookieHeader","cookies","value","serializeCookie","attrs","parts","chunkName","index","readChunked","first","i","writeChunked","count","chunk","stale","clearChunked","secure","TokenEndpointError","status","code","description","JWKS_TTL_MS","discoveries","jwksCache","trimSlash","discover","issuer","fetchImpl","pending","res","err","jwks","uri","force","cached","set","createLocalJWKSet","base64url","bytes","binary","b","randomValue","pkceChallenge","verifier","digest","sha256Hex","authorizationUrl","client","input","disc","url","tokenRequest","params","body","headers","credentials","exchangeCode","refreshTokens","refreshToken","verifyIdToken","idToken","nonce","options","result","jwtVerify","endSessionUrl","RECENT_RESULT_MS","RefreshCoordinator","recentMs","refresh","done","cutoff","entry","keys","keyFor","secret","seal","payload","maxAgeSeconds","EncryptJWT","unseal","jwtDecrypt","DEFAULT_ACCESS_TTL_SECONDS","TRANSACTION_MAX_AGE","safePath","fallback","redirect","location","setCookies","cookie","publicOrigin","cfg","request","host","proto","userFrom","claims","str","expiresAtFrom","token","createServerAuth","txCookie","coordinator","redirectUri","toSignIn","error","readStored","raw","stored","writeStored","sealed","publicSession","_refreshToken","session","signIn","tx","callback","issuerError","tokens","signOut","refreshStored","pathname","route","cookieHeader","unchanged","signOutNow","refreshed"],"mappings":"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"}
|
package/dist/oidc.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { JWTPayload } from 'jose';
|
|
2
|
+
/**
|
|
3
|
+
* The OIDC relying-party calls: discovery, PKCE, the two token grants and
|
|
4
|
+
* id_token verification. Standard OIDC only, so any compliant issuer works.
|
|
5
|
+
*/
|
|
6
|
+
export interface Discovery {
|
|
7
|
+
issuer: string;
|
|
8
|
+
authorization_endpoint: string;
|
|
9
|
+
token_endpoint: string;
|
|
10
|
+
jwks_uri: string;
|
|
11
|
+
end_session_endpoint?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface TokenResponse {
|
|
14
|
+
access_token: string;
|
|
15
|
+
token_type: string;
|
|
16
|
+
expires_in?: number;
|
|
17
|
+
refresh_token?: string;
|
|
18
|
+
id_token?: string;
|
|
19
|
+
scope?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ClientCredentials {
|
|
22
|
+
issuer: string;
|
|
23
|
+
clientId: string;
|
|
24
|
+
clientSecret?: string;
|
|
25
|
+
}
|
|
26
|
+
/** An error from the token endpoint, carrying its OAuth code. */
|
|
27
|
+
export declare class TokenEndpointError extends Error {
|
|
28
|
+
readonly status: number;
|
|
29
|
+
readonly code: string;
|
|
30
|
+
constructor(status: number, code: string, description: string);
|
|
31
|
+
}
|
|
32
|
+
type Fetch = typeof fetch;
|
|
33
|
+
export declare function discover(issuer: string, fetchImpl: Fetch): Promise<Discovery>;
|
|
34
|
+
/** A random, URL-safe value for state, nonce and the PKCE verifier. */
|
|
35
|
+
export declare function randomValue(): string;
|
|
36
|
+
export declare function pkceChallenge(verifier: string): Promise<string>;
|
|
37
|
+
/** Hex SHA-256, for keying state by a secret without holding the secret. */
|
|
38
|
+
export declare function sha256Hex(value: string): Promise<string>;
|
|
39
|
+
export declare function authorizationUrl(client: ClientCredentials, fetchImpl: Fetch, input: {
|
|
40
|
+
redirectUri: string;
|
|
41
|
+
scopes: string;
|
|
42
|
+
state: string;
|
|
43
|
+
nonce: string;
|
|
44
|
+
codeChallenge: string;
|
|
45
|
+
prompt?: string;
|
|
46
|
+
}): Promise<string>;
|
|
47
|
+
export declare function exchangeCode(client: ClientCredentials, fetchImpl: Fetch, input: {
|
|
48
|
+
code: string;
|
|
49
|
+
codeVerifier: string;
|
|
50
|
+
redirectUri: string;
|
|
51
|
+
}): Promise<TokenResponse>;
|
|
52
|
+
export declare function refreshTokens(client: ClientCredentials, fetchImpl: Fetch, refreshToken: string): Promise<TokenResponse>;
|
|
53
|
+
/**
|
|
54
|
+
* Verifies an id_token's signature, issuer, audience and nonce. A key the
|
|
55
|
+
* cached set does not know refetches the set once, which is how a key rotation
|
|
56
|
+
* at the issuer is picked up without waiting out the cache.
|
|
57
|
+
*/
|
|
58
|
+
export declare function verifyIdToken(client: ClientCredentials, fetchImpl: Fetch, idToken: string, nonce: string): Promise<JWTPayload>;
|
|
59
|
+
/** RP-initiated logout (OIDC RP-Initiated Logout 1.0), or null when the issuer offers none. */
|
|
60
|
+
export declare function endSessionUrl(client: ClientCredentials, fetchImpl: Fetch, input: {
|
|
61
|
+
postLogoutRedirectUri: string;
|
|
62
|
+
sessionId?: string;
|
|
63
|
+
}): Promise<string | null>;
|
|
64
|
+
/** For tests: forget cached discovery documents and key sets. */
|
|
65
|
+
export declare function resetOidcCaches(): void;
|
|
66
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One refresh per session, however many requests ask for it.
|
|
3
|
+
*
|
|
4
|
+
* With rotation on, every refresh spends the token it presents. A page that
|
|
5
|
+
* fires several requests the moment its access token expires would otherwise
|
|
6
|
+
* refresh once per request, and every request after the first would present a
|
|
7
|
+
* token the first had already spent — which the issuer reads as theft.
|
|
8
|
+
*
|
|
9
|
+
* Two layers, in this process:
|
|
10
|
+
*
|
|
11
|
+
* - In flight: requests for the same refresh token share one call.
|
|
12
|
+
* - Recently done: a request that arrives a moment later still holding the old
|
|
13
|
+
* cookie (its browser had not yet stored the new one) is handed the same
|
|
14
|
+
* result rather than spending the old token again.
|
|
15
|
+
*
|
|
16
|
+
* Across processes — several replicas of the app — the issuer's reuse grace
|
|
17
|
+
* window covers the same race, so no shared lock is needed.
|
|
18
|
+
*/
|
|
19
|
+
/** How long a result is handed to late arrivals. Matches the issuer's grace window. */
|
|
20
|
+
export declare const RECENT_RESULT_MS = 30000;
|
|
21
|
+
export declare class RefreshCoordinator<T> {
|
|
22
|
+
private readonly recentMs;
|
|
23
|
+
private readonly inFlight;
|
|
24
|
+
private readonly recent;
|
|
25
|
+
constructor(recentMs?: number);
|
|
26
|
+
run(refreshToken: string, refresh: () => Promise<T>): Promise<T>;
|
|
27
|
+
private sweep;
|
|
28
|
+
}
|
package/dist/seal.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function seal(payload: Record<string, unknown>, secret: string, maxAgeSeconds: number): Promise<string>;
|
|
2
|
+
/** The payload, or null when the value is not one this secret sealed, or has expired. */
|
|
3
|
+
export declare function unseal<T>(value: string, secret: string): Promise<T | null>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ServerAuthConfig } from './config';
|
|
2
|
+
export interface SessionUser {
|
|
3
|
+
id: string;
|
|
4
|
+
email?: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
username?: string;
|
|
7
|
+
picture?: string;
|
|
8
|
+
emailVerified: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** What `getSession` hands the app. */
|
|
11
|
+
export interface Session {
|
|
12
|
+
user: SessionUser;
|
|
13
|
+
/** A valid access token, for calling APIs as this user. */
|
|
14
|
+
accessToken: string;
|
|
15
|
+
/** When the access token expires, epoch milliseconds. */
|
|
16
|
+
expiresAt: number;
|
|
17
|
+
/** The issuer's session id (`sid`), when it sent one. */
|
|
18
|
+
sessionId?: string;
|
|
19
|
+
}
|
|
20
|
+
/** What a refresh produced, for the middleware to apply. */
|
|
21
|
+
export interface RefreshOutcome {
|
|
22
|
+
/** The session after the refresh, or null when there is none (or it could not be refreshed). */
|
|
23
|
+
session: Session | null;
|
|
24
|
+
/** `Set-Cookie` values for the response. Empty when nothing changed. */
|
|
25
|
+
setCookies: string[];
|
|
26
|
+
/**
|
|
27
|
+
* The request's `Cookie` header as it should be seen by whatever handles
|
|
28
|
+
* this request next, carrying the refreshed session. Forward it, so a page
|
|
29
|
+
* rendered on this very request reads the new token rather than the expired
|
|
30
|
+
* one the browser sent.
|
|
31
|
+
*/
|
|
32
|
+
cookieHeader: string;
|
|
33
|
+
}
|
|
34
|
+
export interface ServerAuth {
|
|
35
|
+
/**
|
|
36
|
+
* Handles the SDK's routes under `basePath`: `/login`, `/callback` and
|
|
37
|
+
* `/logout`. Null for any other path.
|
|
38
|
+
*/
|
|
39
|
+
handle(request: Request): Promise<Response | null>;
|
|
40
|
+
/**
|
|
41
|
+
* The session in a `Cookie` header, or null. Read-only and never refreshes:
|
|
42
|
+
* any server code can call it, including code that cannot set cookies.
|
|
43
|
+
* Returns null for a session whose access token has expired — `refresh`,
|
|
44
|
+
* run in middleware ahead of the request, is what keeps it current.
|
|
45
|
+
*/
|
|
46
|
+
getSession(cookieHeader: string | null | undefined): Promise<Session | null>;
|
|
47
|
+
/**
|
|
48
|
+
* Refreshes the session when its access token is about to expire. For
|
|
49
|
+
* middleware: it runs before anything renders, and its response can always
|
|
50
|
+
* carry the new cookie. Concurrent calls for one session share one refresh.
|
|
51
|
+
*/
|
|
52
|
+
refresh(request: Request): Promise<RefreshOutcome>;
|
|
53
|
+
}
|
|
54
|
+
export declare function createServerAuth(config: ServerAuthConfig): ServerAuth;
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pontive/pontkit-server",
|
|
3
|
+
"version": "1.0.4",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "Server-side sessions for Pontive apps: sign-in, a sealed session cookie and safe token refresh, on Web Request/Response.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.cjs",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"require": "./dist/index.cjs"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "vite build",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"typecheck": "tsc --noEmit"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"jose": "^6.2.3"
|
|
31
|
+
}
|
|
32
|
+
}
|