aui-auth 0.0.2 → 0.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/README.md +3 -1
- package/dist/accounts.d.ts +2 -2
- package/dist/accounts.js +1 -1
- package/dist/cache-BWpH93Oi.js +1 -0
- package/dist/codec-BcgaGRoP.js +1 -0
- package/dist/database/index.d.ts +2 -0
- package/dist/database/index.js +1 -0
- package/dist/{pg.d.ts → database/pg.d.ts} +3 -6
- package/dist/{pg.js → database/pg.js} +1 -1
- package/dist/database/redis.d.ts +34 -0
- package/dist/database/redis.js +43 -0
- package/dist/{index-CICWi97l.d.ts → index-CZZ3DRlf.d.ts} +35 -10
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/next.d.ts +3 -3
- package/dist/next.js +1 -1
- package/dist/testing.d.ts +35 -0
- package/dist/testing.js +1 -0
- package/dist/{types-CRiqvt6N.d.ts → types-BcXM3ZEI.d.ts} +42 -6
- package/package.json +20 -7
- package/dist/cookies-D3lpM5ER.js +0 -1
- package/dist/oidc-BIfcHoyS.js +0 -1
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
# aui-auth
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Server-side sessions for web apps that sign in through assistant-ui accounts: OpenID Connect with PKCE, tokens kept in the app's own database, single-flight refresh, and an encrypted cache cookie. Internal to assistant-ui; the manual is `docs/first-party-auth.md` and the design is `packages/aui-auth/ARCHITECTURE.md` in the assistant-internal repository.
|
|
4
|
+
|
|
5
|
+
Entry points: `aui-auth`, `aui-auth/accounts`, `aui-auth/next`, `aui-auth/database`, `aui-auth/database/pg`.
|
package/dist/accounts.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
//#region src/accounts.d.ts
|
|
1
|
+
import { m as SignInInput, p as RevalidateInput } from "./types-BcXM3ZEI.js";
|
|
2
|
+
//#region src/accounts/index.d.ts
|
|
3
3
|
type AccountsOrganization = {
|
|
4
4
|
id: string;
|
|
5
5
|
name: string;
|
package/dist/accounts.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{a as e,m as t}from"./cache-BWpH93Oi.js";async function n(n,r){let i=await(n.fetch??globalThis.fetch.bind(globalThis))(e(n.issuer,`/api/oidc/organizations`),{headers:{Authorization:`Bearer ${r}`},cache:`no-store`});if(!i.ok)throw new t(`Failed to load organizations: ${i.status}`,i.status);return await i.json()}function r(e,t){if(t){let n=e.find(e=>e.slug===t);if(n)return n}return e[0]??null}function i(e){let[,t]=new URL(e,`http://localhost`).pathname.split(`/`);return t&&t!==`org`?t:null}function a(e){let a=e.preferredSlug??i;async function o(t){return{activeOrganization:r(await n(e,t.accessToken),a(t.redirectPath))}}async function s(i){let a;try{a=await n(e,i.accessToken)}catch(e){if(e instanceof t&&e.status>=400&&e.status<500)return null;throw e}let o=i.session.data.activeOrganization;if(!o)return{activeOrganization:r(a)};let s=a.find(e=>e.id===o.id)??null;return s?{activeOrganization:s}:null}return{onSignIn:o,onRevalidate:s}}export{n as fetchOrganizations,a as organizationHooks,i as preferredOrganizationSlug,r as selectOrganization};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{t as e}from"./codec-BcgaGRoP.js";function t(e){if(!e)return;let t=e.split(`,`);return t[t.length-1].trim()||void 0}function n(e){let n=new URL(e.url);return`${t(e.headers.get(`x-forwarded-proto`))||n.protocol.replace(`:`,``)}://${t(e.headers.get(`x-forwarded-host`))||e.headers.get(`host`)||n.host}`}function r(e){return new URL(e).protocol===`https:`}function i(e,t){return t?`__Secure-${e}`:e}function a(e){let t=new Map;if(!e)return t;for(let n of e.split(`;`)){let e=n.indexOf(`=`);if(e===-1)continue;let r=n.slice(0,e).trim();!r||t.has(r)||t.set(r,n.slice(e+1).trim())}return t}function o(e,t){let n=a(e.headers.get(`cookie`));return n.get(i(t,!0))||n.get(i(t,!1))||null}function s(e,t,n,r){let i=[`${e}=${t}`,`Path=/`,`HttpOnly`,`SameSite=Lax`,`Max-Age=${r}`];return n&&i.push(`Secure`),i.join(`; `)}function c(e,t,n,a,o){let c=r(a);e.append(`set-cookie`,s(i(t,c),n,c,Math.floor(o/1e3)))}function l(e,t){let n=r(t);return[n,!n].map(t=>s(i(e,t),``,t,0))}function u(e,t,n){for(let r of l(t,n))e.append(`set-cookie`,r)}function d(e,t=307){return new Response(null,{status:t,headers:{location:e.toString()}})}function f(e,t){return`${e}_flow_${t}`}function p(e){return btoa(JSON.stringify(e)).replace(/\+/g,`-`).replace(/\//g,`_`).replace(/=+$/g,``)}function m(e){if(!e)return null;try{let t=e.replace(/-/g,`+`).replace(/_/g,`/`),n=JSON.parse(atob(t));return typeof n.verifier==`string`?{verifier:n.verifier,redirectPath:typeof n.redirectPath==`string`?n.redirectPath:`/`,silent:n.silent===!0}:null}catch{return null}}function h(e,t){if(!e)return`/`;let n;try{n=new URL(e,t)}catch{return`/`}return n.origin===t?`${n.pathname}${n.search}${n.hash}`:`/`}const g={login:`/api/auth/login`,callback:`/api/auth/callback`,logout:`/api/auth/logout`,logoutComplete:`/api/auth/logout-complete`,error:`/auth/error`},_={absoluteMaxAgeMs:2592e6,idleMaxAgeMs:6048e5,revalidateIntervalMs:12e4,refreshBeforeExpiryMs:3e5,flowMaxAgeMs:6e5,refreshLeaseMs:2e4,leaseWaitMs:5e3},v=Object.freeze({});function y(e){return{...e,scope:e.scope??`openid profile email offline_access`,onSignIn:e.onSignIn??(async()=>v),onRevalidate:e.onRevalidate??(async()=>v),paths:{...g,...e.paths},lifetimes:{..._,...e.lifetimes},fetch:e.fetch??globalThis.fetch.bind(globalThis),now:e.now??Date.now,cache:e.cache?{codec:F(e.cache.secret),maxAgeMs:e.cache.maxAgeMs??6e4}:null,audience:e.audience===!1?null:e.audience??new URL(`/api/auth`,e.issuer).toString()}}var b=class extends Error{status;constructor(e,t){super(e),this.name=`AccountsRequestError`,this.status=t}},x=class extends b{code;description;constructor(e,t,n){super(`Token request failed: ${e} ${t} ${n}`,e),this.name=`OAuthTokenError`,this.code=t,this.description=n}};function S(e){let t=``;for(let n of e)t+=String.fromCharCode(n);return btoa(t).replace(/\+/g,`-`).replace(/\//g,`_`).replace(/=+$/g,``)}function C(e){return S(crypto.getRandomValues(new Uint8Array(e)))}async function w(e){let t=await crypto.subtle.digest(`SHA-256`,new TextEncoder().encode(e));return S(new Uint8Array(t))}function T(e,t){return new URL(t,e).toString()}async function E(e,t){let n=C(24),r=C(32),i=new URL(T(e.issuer,`/api/auth/oauth2/authorize`));return i.searchParams.set(`client_id`,e.clientId),i.searchParams.set(`redirect_uri`,new URL(e.paths.callback,t.origin).toString()),i.searchParams.set(`response_type`,`code`),i.searchParams.set(`scope`,e.scope),i.searchParams.set(`state`,n),i.searchParams.set(`code_challenge`,await w(r)),i.searchParams.set(`code_challenge_method`,`S256`),t.silent&&i.searchParams.set(`prompt`,`none`),{url:i.toString(),state:n,verifier:r}}function D(e,t){let n=new URL(T(e.issuer,`/api/auth/oauth2/end-session`));return n.searchParams.set(`id_token_hint`,t.idToken),n.searchParams.set(`client_id`,e.clientId),n.searchParams.set(`post_logout_redirect_uri`,new URL(e.paths.logoutComplete,t.origin).toString()),n.toString()}async function O(e,t,n,r=!0){let i=new URLSearchParams({client_id:e.clientId,...t});e.clientSecret&&i.set(`client_secret`,e.clientSecret),e.audience&&r&&i.set(`resource`,e.audience);let a=n===void 0?void 0:n-Date.now();if(a!==void 0&&a<=0)throw new b(`Token request deadline passed`,0);let o=await e.fetch(T(e.issuer,`/api/auth/oauth2/token`),{method:`POST`,headers:{"Content-Type":`application/x-www-form-urlencoded`},body:i,cache:`no-store`,...a?{signal:AbortSignal.timeout(a)}:{}});if(!o.ok){let i=await o.text().catch(()=>``),a=`unknown_error`,s=i;try{let e=JSON.parse(i);a=e.error??a,s=e.error_description??s}catch{}if(r&&e.audience&&a===`invalid_request`&&/resource/i.test(s))return O(e,t,n,!1);throw new x(o.status,a,s)}return await o.json()}function k(e,t){return O(e,{grant_type:`authorization_code`,code:t.code,code_verifier:t.verifier,redirect_uri:t.redirectUri})}function A(e,t,n){return O(e,{grant_type:`refresh_token`,refresh_token:t},n===void 0?void 0:Date.now()+n)}async function j(e,t){let n=await(e.fetch??globalThis.fetch.bind(globalThis))(T(e.issuer,`/api/auth/oauth2/userinfo`),{headers:{Authorization:`Bearer ${t}`},cache:`no-store`});if(!n.ok)throw new b(`Failed to load user info: ${n.status}`,n.status);let r=await n.json();return{id:r.sub,email:r.email??``,name:r.name??r.email??r.sub,image:r.picture??null}}function M(e,t,n){return{accessToken:e.access_token,refreshToken:e.refresh_token??t?.refreshToken??null,idToken:e.id_token??t?.idToken??null,accessTokenExpiresAt:typeof e.expires_at==`number`?e.expires_at*1e3:n+(e.expires_in??3600)*1e3}}function N(e){return`${e}_cache`}function P(e){let t=``;for(let n of e)t+=n.toString(16).padStart(2,`0`);return t}function F(t){let n=null;return()=>n??=crypto.subtle.digest(`SHA-256`,new TextEncoder().encode(t)).then(t=>e(P(new Uint8Array(t))))}function I(e,t,n){if(!e.cache)return 0;let{lifetimes:r}=e;return Math.min(e.cache.maxAgeMs,t.lastValidatedAt+r.revalidateIntervalMs-n,t.tokens.accessTokenExpiresAt-r.refreshBeforeExpiryMs-n,t.createdAt+r.absoluteMaxAgeMs-n,t.lastValidatedAt+r.idleMaxAgeMs-n)}async function L(e,t,n){if(!e.cache)return null;let r=I(e,t,n);if(r<1e3)return null;let i={v:1,id:t.id,u:t.user,d:t.data,at:t.tokens.accessToken,ate:t.tokens.accessTokenExpiresAt,ca:t.createdAt,lv:t.lastValidatedAt,exp:n+r};return{value:await(await e.cache.codec()).encrypt(JSON.stringify(i)),maxAgeMs:r}}async function R(e,t,n,r){if(!e.cache||!t||!n)return null;let i;try{let n=await e.cache.codec();i=JSON.parse(await n.decrypt(t))}catch{return null}return i.v!==1||typeof i.exp!=`number`||i.exp<=r||i.id!==await w(n)?null:{id:i.id,user:i.u,tokens:{accessToken:i.at,refreshToken:null,idToken:null,accessTokenExpiresAt:i.ate},data:i.d,createdAt:i.ca,lastValidatedAt:i.lv,refreshLeaseUntil:null}}async function z(e,t,n,a){let o=await L(e,t,a);if(!o)return null;let c=r(n);return s(i(N(e.cookieName),c),o.value,c,Math.floor(o.maxAgeMs/1e3))}function B(e,t){return e.cache?l(N(e.cookieName),t):[]}export{o as C,c as D,h as E,f as S,n as T,u as _,T as a,m as b,k as c,A as d,w as f,y as g,x as h,R as i,j as l,b as m,N as n,E as o,M as p,z as r,D as s,B as t,C as u,l as v,d as w,p as x,i as y};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function e(e){let t=new Uint8Array(e.length/2);for(let n=0;n<e.length;n+=2)t[n/2]=Number.parseInt(e.slice(n,n+2),16);return t}function t(e){let t=``;for(let n of e)t+=String.fromCharCode(n);return btoa(t).replace(/\+/g,`-`).replace(/\//g,`_`).replace(/=+$/g,``)}function n(e){let t=e.replace(/-/g,`+`).replace(/_/g,`/`),n=t+`=`.repeat((4-t.length%4)%4),r;try{r=atob(n)}catch{throw Error(`Malformed encrypted value`)}let i=new Uint8Array(r.length);for(let e=0;e<r.length;e++)i[e]=r.charCodeAt(e);return i}function r(r){if(!/^[0-9a-fA-F]{64}$/.test(r))throw Error(`AES-GCM codec key must be 64 hex characters (32 bytes)`);let i=e(r),a=null,o=()=>a??=crypto.subtle.importKey(`raw`,i,{name:`AES-GCM`},!1,[`encrypt`,`decrypt`]);return{async encrypt(e){let n=crypto.getRandomValues(new Uint8Array(12)),r=new Uint8Array(await crypto.subtle.encrypt({name:`AES-GCM`,iv:n},await o(),new TextEncoder().encode(e))),i=new Uint8Array(12+r.length);return i.set(n),i.set(r,12),t(i)},async decrypt(e){let t=n(e);if(t.length<28)throw Error(`Malformed encrypted value`);let r=await crypto.subtle.decrypt({name:`AES-GCM`,iv:t.subarray(0,12)},await o(),t.subarray(12));return new TextDecoder().decode(r)}}}export{r as t};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { c as AccountsSessionStore, g as createAesGcmCodec, h as SessionCodec, l as AccountsTokens, o as AccountsSession, s as AccountsSessionPatch, u as AccountsUser } from "../types-BcXM3ZEI.js";
|
|
2
|
+
export { type AccountsSession, type AccountsSessionPatch, type AccountsSessionStore, type AccountsTokens, type AccountsUser, type SessionCodec, createAesGcmCodec };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{t as e}from"../codec-BcgaGRoP.js";export{e as createAesGcmCodec};
|
|
@@ -1,15 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
//#region src/pg.d.ts
|
|
1
|
+
import { c as AccountsSessionStore, h as SessionCodec } from "../types-BcXM3ZEI.js";
|
|
2
|
+
//#region src/database/pg.d.ts
|
|
3
3
|
type PgQuery = (text: string, values?: unknown[]) => Promise<{
|
|
4
4
|
rows: Record<string, unknown>[];
|
|
5
5
|
}>;
|
|
6
6
|
type PgSessionStoreOptions = {
|
|
7
7
|
query: PgQuery;
|
|
8
8
|
table?: string;
|
|
9
|
-
codec?:
|
|
10
|
-
encrypt(value: string): Promise<string>;
|
|
11
|
-
decrypt(value: string): Promise<string>;
|
|
12
|
-
};
|
|
9
|
+
codec?: SessionCodec;
|
|
13
10
|
};
|
|
14
11
|
declare const PG_SESSION_TABLE_SQL = "CREATE TABLE \"accounts_sessions\" (\n \"id\" text PRIMARY KEY,\n \"user_id\" text NOT NULL,\n \"user\" jsonb NOT NULL,\n \"access_token\" text NOT NULL,\n \"refresh_token\" text,\n \"id_token\" text,\n \"access_token_expires_at\" timestamp with time zone NOT NULL,\n \"data\" jsonb NOT NULL,\n \"created_at\" timestamp with time zone NOT NULL,\n \"last_validated_at\" timestamp with time zone NOT NULL,\n \"refresh_lease_until\" timestamp with time zone\n);\nCREATE INDEX \"accounts_sessions_user_id_idx\" ON \"accounts_sessions\" (\"user_id\");";
|
|
15
12
|
declare function createPgSessionStore<Data>(options: PgSessionStoreOptions): AccountsSessionStore<Data>;
|
|
@@ -11,4 +11,4 @@ const e=`CREATE TABLE "accounts_sessions" (
|
|
|
11
11
|
"last_validated_at" timestamp with time zone NOT NULL,
|
|
12
12
|
"refresh_lease_until" timestamp with time zone
|
|
13
13
|
);
|
|
14
|
-
CREATE INDEX "accounts_sessions_user_id_idx" ON "accounts_sessions" ("user_id");`,t={encrypt:async e=>e,decrypt:async e=>e};function n(e){if(e instanceof Date)return e.getTime();if(typeof e==`string`)return new Date(e).getTime();if(typeof e==`number`)return e;throw Error(`Unexpected timestamp value in accounts session row`)}function r(e){return new Date(e)}function i(e){let i=e.table??`accounts_sessions`;if(!/^[a-z_][a-z0-9_]*$/.test(i))throw Error(`Invalid accounts session table name: ${i}`);let a=`"${i}"`,o=e.codec??t;async function s(e){return{accessToken:await o.encrypt(e.accessToken),refreshToken:e.refreshToken?await o.encrypt(e.refreshToken):null,idToken:e.idToken?await o.encrypt(e.idToken):null}}async function c(e){return{id:e.id,user:e.user,tokens:{accessToken:await o.decrypt(e.access_token),refreshToken:e.refresh_token?await o.decrypt(e.refresh_token):null,idToken:e.id_token?await o.decrypt(e.id_token):null,accessTokenExpiresAt:n(e.access_token_expires_at)},data:e.data,createdAt:n(e.created_at),lastValidatedAt:n(e.last_validated_at),refreshLeaseUntil:e.refresh_lease_until===null||e.refresh_lease_until===void 0?null:n(e.refresh_lease_until)}}return{async create(t){let n=await s(t.tokens);await e.query(`INSERT INTO ${a} ("id", "user_id", "user", "access_token", "refresh_token", "id_token", "access_token_expires_at", "data", "created_at", "last_validated_at", "refresh_lease_until") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NULL)`,[t.id,t.user.id,
|
|
14
|
+
CREATE INDEX "accounts_sessions_user_id_idx" ON "accounts_sessions" ("user_id");`,t={encrypt:async e=>e,decrypt:async e=>e};function n(e){if(e instanceof Date)return e.getTime();if(typeof e==`string`)return new Date(e).getTime();if(typeof e==`number`)return e;throw Error(`Unexpected timestamp value in accounts session row`)}function r(e){return new Date(e)}function i(e){let i=e.table??`accounts_sessions`;if(!/^[a-z_][a-z0-9_]*$/.test(i))throw Error(`Invalid accounts session table name: ${i}`);let a=`"${i}"`,o=e.codec??t;async function s(e){return{accessToken:await o.encrypt(e.accessToken),refreshToken:e.refreshToken?await o.encrypt(e.refreshToken):null,idToken:e.idToken?await o.encrypt(e.idToken):null}}async function c(e){return{id:e.id,user:e.user,tokens:{accessToken:await o.decrypt(e.access_token),refreshToken:e.refresh_token?await o.decrypt(e.refresh_token):null,idToken:e.id_token?await o.decrypt(e.id_token):null,accessTokenExpiresAt:n(e.access_token_expires_at)},data:e.data,createdAt:n(e.created_at),lastValidatedAt:n(e.last_validated_at),refreshLeaseUntil:e.refresh_lease_until===null||e.refresh_lease_until===void 0?null:n(e.refresh_lease_until)}}return{async create(t){let n=await s(t.tokens);await e.query(`INSERT INTO ${a} ("id", "user_id", "user", "access_token", "refresh_token", "id_token", "access_token_expires_at", "data", "created_at", "last_validated_at", "refresh_lease_until") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NULL)`,[t.id,t.user.id,t.user,n.accessToken,n.refreshToken,n.idToken,r(t.tokens.accessTokenExpiresAt),t.data,r(t.createdAt),r(t.lastValidatedAt)])},async read(t){let{rows:n}=await e.query(`SELECT * FROM ${a} WHERE "id" = $1`,[t]);return n[0]?c(n[0]):null},async update(t,n,i){let o=[],l=[t],u=(e,t)=>{l.push(t),o.push(`"${e}" = $${l.length}`)};if(n.user!==void 0&&(u(`user_id`,n.user.id),u(`user`,n.user)),n.tokens!==void 0){let e=await s(n.tokens);u(`access_token`,e.accessToken),u(`refresh_token`,e.refreshToken),u(`id_token`,e.idToken),u(`access_token_expires_at`,r(n.tokens.accessTokenExpiresAt))}if(n.data!==void 0&&u(`data`,n.data),n.lastValidatedAt!==void 0&&u(`last_validated_at`,r(n.lastValidatedAt)),n.refreshLeaseUntil!==void 0&&u(`refresh_lease_until`,n.refreshLeaseUntil===null?null:r(n.refreshLeaseUntil)),o.length===0)return this.read(t);let d=`"id" = $1`;i!==void 0&&(l.push(r(i)),d+=` AND "refresh_lease_until" = $${l.length}`);let{rows:f}=await e.query(`UPDATE ${a} SET ${o.join(`, `)} WHERE ${d} RETURNING *`,l);return f[0]?c(f[0]):null},async claimRefreshLease(t,n,i){let{rows:o}=await e.query(`UPDATE ${a} SET "refresh_lease_until" = $2 WHERE "id" = $1 AND ("refresh_lease_until" IS NULL OR "refresh_lease_until" <= $3) RETURNING *`,[t,r(n),r(i)]);return o[0]?c(o[0]):null},async delete(t,n){if(n===void 0){let{rows:n}=await e.query(`DELETE FROM ${a} WHERE "id" = $1 RETURNING "id"`,[t]);return n.length>0}let{rows:i}=await e.query(`DELETE FROM ${a} WHERE "id" = $1 AND "refresh_lease_until" = $2 RETURNING "id"`,[t,r(n)]);return i.length>0}}}export{e as PG_SESSION_TABLE_SQL,i as createPgSessionStore};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { c as AccountsSessionStore, h as SessionCodec } from "../types-BcXM3ZEI.js";
|
|
2
|
+
//#region src/database/redis.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The slice of a Redis client this store uses, so the package takes no Redis
|
|
5
|
+
* dependency of its own. The shape is `@upstash/redis`, which satisfies it
|
|
6
|
+
* directly. A client whose `eval` takes a key count and a flat argument list,
|
|
7
|
+
* ioredis for one, needs an adapter:
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* const redis = {
|
|
11
|
+
* hgetall: (key) => client.hgetall(key),
|
|
12
|
+
* eval: (script, keys, args) =>
|
|
13
|
+
* client.eval(script, keys.length, ...keys, ...args),
|
|
14
|
+
* };
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
type RedisSessionClient = {
|
|
18
|
+
hgetall(key: string): Promise<Record<string, unknown> | null>;
|
|
19
|
+
eval(script: string, keys: string[], args: string[]): Promise<unknown>;
|
|
20
|
+
};
|
|
21
|
+
type RedisSessionStoreOptions = {
|
|
22
|
+
redis: RedisSessionClient;
|
|
23
|
+
codec?: SessionCodec;
|
|
24
|
+
/** Key prefix. One prefix per app, the way one table is one app. */
|
|
25
|
+
prefix?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Key expiry, garbage collection only: it is renewed on every write and
|
|
28
|
+
* sessions carry their own limits, so it only has to exceed `idleMaxAgeMs`.
|
|
29
|
+
*/
|
|
30
|
+
ttlMs?: number;
|
|
31
|
+
};
|
|
32
|
+
declare function createRedisSessionStore<Data>(options: RedisSessionStoreOptions): AccountsSessionStore<Data>;
|
|
33
|
+
//#endregion
|
|
34
|
+
export { RedisSessionClient, RedisSessionStoreOptions, createRedisSessionStore };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const e=`refreshLeaseUntil`,t=`
|
|
2
|
+
local key, checkLease, expectedLease, ttl = KEYS[1], ARGV[1], ARGV[2], ARGV[3]
|
|
3
|
+
if redis.call('EXISTS', key) == 0 then return 0 end
|
|
4
|
+
if checkLease == '1' then
|
|
5
|
+
local current = redis.call('HGET', key, '${e}')
|
|
6
|
+
if (current or '') ~= expectedLease then return 0 end
|
|
7
|
+
end
|
|
8
|
+
for index = 4, #ARGV, 2 do
|
|
9
|
+
if ARGV[index + 1] == '' then
|
|
10
|
+
redis.call('HDEL', key, ARGV[index])
|
|
11
|
+
else
|
|
12
|
+
redis.call('HSET', key, ARGV[index], ARGV[index + 1])
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
redis.call('PEXPIRE', key, ttl)
|
|
16
|
+
return 1
|
|
17
|
+
`,n=`
|
|
18
|
+
local key, leaseUntil, now, ttl = KEYS[1], ARGV[1], ARGV[2], ARGV[3]
|
|
19
|
+
if redis.call('EXISTS', key) == 0 then return nil end
|
|
20
|
+
local current = redis.call('HGET', key, '${e}')
|
|
21
|
+
if current and current ~= '' and tonumber(current) > tonumber(now) then
|
|
22
|
+
return nil
|
|
23
|
+
end
|
|
24
|
+
redis.call('HSET', key, '${e}', leaseUntil)
|
|
25
|
+
redis.call('PEXPIRE', key, ttl)
|
|
26
|
+
return redis.call('HGETALL', key)
|
|
27
|
+
`,r=`
|
|
28
|
+
local key, checkLease, expectedLease = KEYS[1], ARGV[1], ARGV[2]
|
|
29
|
+
if redis.call('EXISTS', key) == 0 then return 0 end
|
|
30
|
+
if checkLease == '1' then
|
|
31
|
+
local current = redis.call('HGET', key, '${e}')
|
|
32
|
+
if (current or '') ~= expectedLease then return 0 end
|
|
33
|
+
end
|
|
34
|
+
redis.call('DEL', key)
|
|
35
|
+
return 1
|
|
36
|
+
`,i={encrypt:async e=>e,decrypt:async e=>e};function a(e){return e==null||e===``?null:typeof e==`string`?e:JSON.stringify(e)}function o(e){let t=a(e),n=t===null?NaN:Number(t);if(!Number.isFinite(n))throw Error(`Unexpected timestamp in accounts session hash`);return n}function s(e){if(typeof e!=`string`)return e;try{return JSON.parse(e)}catch{return e}}function c(e){if(!e)return null;if(Array.isArray(e)){if(e.length===0)return null;let t={};for(let n=0;n<e.length;n+=2)t[String(e[n])]=e[n+1];return t}let t=e;return Object.keys(t).length>0?t:null}function l(l){let{redis:u}=l,d=l.codec??i,f=l.prefix??`accounts_sessions:`,p=l.ttlMs??26784e5,m=e=>`${f}${e}`;async function h(t){let n={};if(t.user&&(n.user=JSON.stringify(t.user)),t.data!==void 0&&(n.data=JSON.stringify(t.data)),t.lastValidatedAt!==void 0&&(n.lastValidatedAt=String(t.lastValidatedAt)),t.refreshLeaseUntil!==void 0&&(n[e]=t.refreshLeaseUntil===null?``:String(t.refreshLeaseUntil)),t.tokens){let{accessToken:e,refreshToken:r,idToken:i,accessTokenExpiresAt:a}=t.tokens;n.accessToken=await d.encrypt(e),n.refreshToken=r?await d.encrypt(r):``,n.idToken=i?await d.encrypt(i):``,n.accessTokenExpiresAt=String(a)}return n}async function g(t,n){let r=a(n.refreshToken),i=a(n.idToken),c=a(n[e]);return{id:t,user:s(n.user),tokens:{accessToken:await d.decrypt(a(n.accessToken)??``),refreshToken:r?await d.decrypt(r):null,idToken:i?await d.decrypt(i):null,accessTokenExpiresAt:o(n.accessTokenExpiresAt)},data:s(n.data),createdAt:o(n.createdAt),lastValidatedAt:o(n.lastValidatedAt),refreshLeaseUntil:c===null?null:Number(c)}}async function _(e){let t=c(await u.hgetall(m(e)));return t?g(e,t):null}return{async create(e){let t=await h(e);t.createdAt=String(e.createdAt);let n=Object.entries(t).filter(([,e])=>e!==``);await u.eval(`
|
|
37
|
+
local key, ttl = KEYS[1], ARGV[1]
|
|
38
|
+
for index = 2, #ARGV, 2 do
|
|
39
|
+
redis.call('HSET', key, ARGV[index], ARGV[index + 1])
|
|
40
|
+
end
|
|
41
|
+
redis.call('PEXPIRE', key, ttl)
|
|
42
|
+
return 1
|
|
43
|
+
`,[m(e.id)],[String(p),...n.flat()])},read:_,async update(e,n,r){let i=await h(n),a=await u.eval(t,[m(e)],[r===void 0?`0`:`1`,r===void 0?``:String(r),String(p),...Object.entries(i).flat()]);return Number(a)===1?_(e):null},async claimRefreshLease(e,t,r){let i=c(await u.eval(n,[m(e)],[String(t),String(r),String(p)]));return i?g(e,i):null},async delete(e,t){let n=await u.eval(r,[m(e)],[t===void 0?`0`:`1`,t===void 0?``:String(t)]);return Number(n)===1}}}export{l as createRedisSessionStore};
|
|
@@ -1,5 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
//#region src/
|
|
1
|
+
import { c as AccountsSessionStore, f as ResolvedAccountsAuthConfig, l as AccountsTokens, o as AccountsSession, t as AccountsAuthConfig, u as AccountsUser } from "./types-BcXM3ZEI.js";
|
|
2
|
+
//#region src/core/cookies.d.ts
|
|
3
|
+
declare function requestOrigin(request: Request): string;
|
|
4
|
+
declare function sanitizeRedirectPath(rawRedirect: string | null | undefined, origin: string): string;
|
|
5
|
+
//#endregion
|
|
6
|
+
//#region src/core/oidc.d.ts
|
|
7
|
+
declare function accountsEndpoint(issuer: string, path: string): string;
|
|
8
|
+
declare function fetchUserInfo(options: {
|
|
9
|
+
issuer: string;
|
|
10
|
+
fetch?: typeof fetch;
|
|
11
|
+
}, accessToken: string): Promise<AccountsUser>;
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/core/cache.d.ts
|
|
14
|
+
declare function cacheCookieBase(sessionCookie: string): string;
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/core/guard.d.ts
|
|
3
17
|
type GuardOptions = {
|
|
4
18
|
publicPaths?: string[];
|
|
5
19
|
apiPaths?: string[];
|
|
@@ -8,24 +22,32 @@ type GuardOptions = {
|
|
|
8
22
|
type GuardResult<Data> = {
|
|
9
23
|
kind: "ok";
|
|
10
24
|
session: AccountsSession<Data> | null;
|
|
25
|
+
cookies?: string[];
|
|
11
26
|
} | {
|
|
12
27
|
kind: "response";
|
|
13
28
|
response: Response;
|
|
14
29
|
};
|
|
15
30
|
//#endregion
|
|
16
|
-
//#region src/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
31
|
+
//#region src/core/resolve.d.ts
|
|
32
|
+
type SessionResolution<Data> = {
|
|
33
|
+
session: AccountsSession<Data> | null;
|
|
34
|
+
/**
|
|
35
|
+
* The Set-Cookie headers to attach to the response: the cache cookie while
|
|
36
|
+
* the session is alive, the cookies that clear a session that is gone.
|
|
37
|
+
*/
|
|
38
|
+
cookies: string[];
|
|
39
|
+
};
|
|
22
40
|
//#endregion
|
|
23
|
-
//#region src/store/memory.d.ts
|
|
41
|
+
//#region src/core/store/memory.d.ts
|
|
24
42
|
declare function createMemorySessionStore<Data>(): AccountsSessionStore<Data> & {
|
|
25
43
|
rows: Map<string, AccountsSession<Data>>;
|
|
26
44
|
};
|
|
27
45
|
//#endregion
|
|
28
46
|
//#region src/index.d.ts
|
|
47
|
+
type SessionCookies = {
|
|
48
|
+
session: string | null;
|
|
49
|
+
cache: string | null;
|
|
50
|
+
};
|
|
29
51
|
declare function createAccountsAuth<Data>(input: AccountsAuthConfig<Data>): {
|
|
30
52
|
config: ResolvedAccountsAuthConfig<Data>;
|
|
31
53
|
sessions: {
|
|
@@ -48,12 +70,15 @@ declare function createAccountsAuth<Data>(input: AccountsAuthConfig<Data>): {
|
|
|
48
70
|
logout: (request: Request) => Promise<Response>;
|
|
49
71
|
};
|
|
50
72
|
guard: (request: Request, options?: GuardOptions) => Promise<GuardResult<Data>>;
|
|
73
|
+
resolveSession: (request: Request) => Promise<SessionResolution<Data>>;
|
|
51
74
|
sessionFromCookie: (cookieValue: string | null) => Promise<AccountsSession<Data> | null>;
|
|
75
|
+
sessionFromCookies: (cookies: SessionCookies) => Promise<AccountsSession<Data> | null>;
|
|
52
76
|
sessionFromRequest: (request: Request) => Promise<AccountsSession<Data> | null>;
|
|
53
77
|
accessTokenFromCookie: (cookieValue: string | null) => Promise<string | null>;
|
|
78
|
+
accessTokenFromCookies: (cookies: SessionCookies) => Promise<string | null>;
|
|
54
79
|
accessTokenFromRequest: (request: Request) => Promise<string | null>;
|
|
55
80
|
updateSessionData: (cookieValue: string | null, update: (data: Data) => Data) => Promise<AccountsSession<Data> | null>;
|
|
56
81
|
};
|
|
57
82
|
type AccountsAuth<Data> = ReturnType<typeof createAccountsAuth<Data>>;
|
|
58
83
|
//#endregion
|
|
59
|
-
export {
|
|
84
|
+
export { SessionResolution as a, cacheCookieBase as c, requestOrigin as d, sanitizeRedirectPath as f, createMemorySessionStore as i, accountsEndpoint as l, SessionCookies as n, GuardOptions as o, createAccountsAuth as r, GuardResult as s, AccountsAuth as t, fetchUserInfo as u };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import { a as
|
|
3
|
-
export { AccountsAuth, type AccountsAuthConfig, type AccountsLifetimes, type AccountsPaths, AccountsRequestError, type AccountsSession, type AccountsSessionPatch, type AccountsSessionStore, type AccountsTokens, type AccountsUser, type GuardOptions, type GuardResult, OAuthTokenError, type RevalidateInput, type SignInInput, accountsEndpoint, createAccountsAuth, createMemorySessionStore, requestOrigin, sanitizeRedirectPath };
|
|
1
|
+
import { a as AccountsRequestError, c as AccountsSessionStore, d as OAuthTokenError, i as AccountsPaths, l as AccountsTokens, m as SignInInput, n as AccountsCacheConfig, o as AccountsSession, p as RevalidateInput, r as AccountsLifetimes, s as AccountsSessionPatch, t as AccountsAuthConfig, u as AccountsUser } from "./types-BcXM3ZEI.js";
|
|
2
|
+
import { a as SessionResolution, c as cacheCookieBase, d as requestOrigin, f as sanitizeRedirectPath, i as createMemorySessionStore, l as accountsEndpoint, n as SessionCookies, o as GuardOptions, r as createAccountsAuth, s as GuardResult, t as AccountsAuth, u as fetchUserInfo } from "./index-CZZ3DRlf.js";
|
|
3
|
+
export { AccountsAuth, type AccountsAuthConfig, type AccountsCacheConfig, type AccountsLifetimes, type AccountsPaths, AccountsRequestError, type AccountsSession, type AccountsSessionPatch, type AccountsSessionStore, type AccountsTokens, type AccountsUser, type GuardOptions, type GuardResult, OAuthTokenError, type RevalidateInput, SessionCookies, type SessionResolution, type SignInInput, accountsEndpoint, cacheCookieBase, createAccountsAuth, createMemorySessionStore, fetchUserInfo, requestOrigin, sanitizeRedirectPath };
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{C as e,D as t,E as n,S as r,T as i,_ as a,a as o,b as s,c,d as l,f as u,g as d,h as f,i as p,l as m,m as h,n as g,o as _,p as v,r as y,s as b,t as x,u as S,v as C,w,x as T}from"./cache-BWpH93Oi.js";function E(e,t){return[...C(e.cookieName,t),...x(e,t)]}function D(t,n){return async function(r){let a=i(r),o=e(r,t.cookieName),s=e(r,g(t.cookieName)),c=await p(t,s,o,t.now());if(c)return{session:c,cookies:[]};let l={session:null,cookies:o!==null||s!==null?E(t,a):[]},u=await n.load(o);if(!u)return l;let d=u;try{let e=await n.revalidate(u);if(!e)return l;d=e}catch{return{session:d,cookies:[]}}if(d.tokens.accessTokenExpiresAt<=t.now()){let e=await n.ensureFreshTokens(d).catch(()=>d);if(!e)return l;d=e}let f=await y(t,d,a,t.now());return{session:d,cookies:f?[f]:[]}}}function O(e,t){let n=D(e,t);return async function(t,i={}){let a=new URL(t.url);if((i.publicPaths??[e.paths.login,e.paths.callback,e.paths.error]).some(e=>a.pathname.startsWith(e)))return{kind:`ok`,session:null};let{session:o,cookies:s}=await n(t);return o?s.length>0?{kind:`ok`,session:o,cookies:s}:{kind:`ok`,session:o}:r(t,i,s)};function r(e,t,n){let r=n.length>0,i=new URL(e.url),o=(t.apiPaths??[`/api`,`/trpc`]).some(e=>i.pathname.startsWith(e))?Response.json({error:`Unauthorized`},{status:401}):w(a(e,t,r));for(let e of n)o.headers.append(`set-cookie`,e);return{kind:`response`,response:o}}function a(t,n,r){let a=new URL(t.url),o=new URL(e.paths.login,i(t));return o.searchParams.set(`redirect`,n.redirectPath?.(t)??`${a.pathname}${a.search}`),r&&o.searchParams.set(`silent`,`1`),o}}const k=/^[A-Za-z0-9_-]{1,128}$/,A=new Set([`login_required`,`consent_required`,`interaction_required`,`account_selection_required`]);function j(o,l){function u(e,t){let n=new URL(o.paths.login,e);return n.searchParams.set(`redirect`,t),n}function d(e,t,n){let r=new URL(o.paths.error,e);return r.searchParams.set(`reason`,t),n&&r.searchParams.set(`description`,n),r}async function p(e){let a=i(e),s=new URL(e.url).searchParams,c=n(s.get(`redirect`),a),l=s.get(`silent`)===`1`,{url:u,state:d,verifier:f}=await _(o,{origin:a,silent:l}),p=w(u);return t(p.headers,r(o.cookieName,d),T({verifier:f,redirectPath:c,silent:l}),a,o.lifetimes.flowMaxAgeMs),p}async function h(p){let h=i(p),g=new URL(p.url).searchParams,_=g.get(`state`),b=_&&k.test(_)?r(o.cookieName,_):null,S=b?s(e(p,b)):null,C=e=>(b&&a(e.headers,b,h),e);if(!S)return C(w(d(h,`invalid_state`,null)));let T=n(S.redirectPath,h),E=g.get(`error`);if(E)return S.silent&&A.has(E)?C(w(u(h,T))):C(w(d(h,E,g.get(`error_description`))));let D=g.get(`code`);if(!D)return C(w(d(h,`invalid_request`,null)));let O,j,M;try{let e=await c(o,{code:D,verifier:S.verifier,redirectUri:new URL(o.paths.callback,h).toString()});O=v(e,null,o.now()),j=await m(o,O.accessToken),M=await o.onSignIn({user:j,accessToken:O.accessToken,redirectPath:T,previous:null})}catch(e){return e instanceof f?C(w(d(h,e.code,e.description||null))):C(w(d(h,`accounts_unavailable`,null)))}if(M===null){let t=w(d(h,`access_denied`,null)),n=e(p,o.cookieName),r=await l.load(n);r&&await l.end(r),n!==null&&a(t.headers,o.cookieName,h);for(let e of x(o,h))t.headers.append(`set-cookie`,e);return C(t)}let N,P;try{({cookieValue:N,session:P}=await l.start({user:j,tokens:O,data:M}))}catch{return C(w(d(h,`session_store_error`,null)))}let F=w(new URL(T,h));t(F.headers,o.cookieName,N,h,o.lifetimes.absoluteMaxAgeMs);let I=await y(o,P,h,o.now()).catch(()=>null);return I&&F.headers.append(`set-cookie`,I),C(F)}async function g(t){let n=i(t),r=await l.load(e(t,o.cookieName));r&&await l.end(r);let a=r?.tokens.idToken?b(o,{origin:n,idToken:r.tokens.idToken}):new URL(o.paths.logoutComplete,n).toString(),s=w(a,303);for(let e of E(o,n))s.headers.append(`set-cookie`,e);return s}return{login:p,callback:h,logout:g}}const M=e=>new Promise(t=>setTimeout(t,e));function N(e){let{store:t,lifetimes:n}=e;function r(e,t){return t-e.createdAt>=n.absoluteMaxAgeMs||t-e.lastValidatedAt>=n.idleMaxAgeMs}function i(e,t){return e.accessTokenExpiresAt-t<=n.refreshBeforeExpiryMs}async function a(n){if(!n)return null;let i=await t.read(await u(n));return i?r(i,e.now())?(await t.delete(i.id),null):i:null}async function o(n){let r=S(32),i=e.now(),a={id:await u(r),user:n.user,tokens:n.tokens,data:n.data,createdAt:i,lastValidatedAt:i,refreshLeaseUntil:null};return await t.create(a),{session:a,cookieValue:r}}async function s(r){let a=e.now();if(!i(r.tokens,a))return r;if(!r.tokens.refreshToken)return await t.delete(r.id),null;let o=await t.claimRefreshLease(r.id,a+n.refreshLeaseMs,a);if(!o)return c(r.id,a);let s=o.refreshLeaseUntil??void 0;if(!i(o.tokens,a))return await t.update(o.id,{refreshLeaseUntil:null},s)??t.read(o.id);try{let r=await l(e,o.tokens.refreshToken,Math.max(n.refreshLeaseMs-1e3,n.refreshLeaseMs/2));return await t.update(o.id,{tokens:v(r,o.tokens,e.now()),refreshLeaseUntil:null},s)??t.read(o.id)}catch(n){if(n instanceof f&&n.code===`invalid_grant`)return await t.delete(o.id,s)?null:await t.read(o.id)??null;let r=await t.update(o.id,{refreshLeaseUntil:null},s);if(o.tokens.accessTokenExpiresAt>e.now())return r??o;throw n}}async function c(r,a){for(;;){await M(100);let o=await t.read(r);if(!o)return null;let c=e.now();if(!i(o.tokens,c))return o;if(o.refreshLeaseUntil===null||o.refreshLeaseUntil<=c)return s(o);if(c-a>=n.leaseWaitMs){if(o.tokens.accessTokenExpiresAt>c)return o;throw new h(`Timed out waiting for the session refresh lease`,0)}}}async function d(r){if(e.now()-r.lastValidatedAt<n.revalidateIntervalMs)return r;let i=await s(r);if(!i)return null;let a=await e.onRevalidate({session:i,accessToken:i.tokens.accessToken});return a===null?(await t.delete(i.id),null):t.update(i.id,{data:a,lastValidatedAt:e.now()})}async function p(e){await t.delete(e.id)}return{load:a,start:o,ensureFreshTokens:s,revalidate:d,end:p}}function P(){let e=new Map;return{rows:e,async create(t){e.set(t.id,structuredClone(t))},async read(t){let n=e.get(t);return n?structuredClone(n):null},async update(t,n,r){let i=e.get(t);if(!i||r!==void 0&&i.refreshLeaseUntil!==r)return null;let a=structuredClone({...i,...n});return e.set(t,a),structuredClone(a)},async claimRefreshLease(t,n,r){let i=e.get(t);if(!i||i.refreshLeaseUntil!==null&&i.refreshLeaseUntil>r)return null;let a={...i,refreshLeaseUntil:n};return e.set(t,a),structuredClone(a)},async delete(t,n){let r=e.get(t);return!r||n!==void 0&&r.refreshLeaseUntil!==n?!1:(e.delete(t),!0)}}}function F(t){let n=d(t),r=N(n),i=j(n,r),a=O(n,r),o=D(n,r);function s(t){return{session:e(t,n.cookieName),cache:e(t,g(n.cookieName))}}async function c(e){return r.load(e)}async function l(e){return await p(n,e.cache,e.session,n.now())??r.load(e.session)}async function u(e){return l(s(e))}async function f(e){let t=await r.load(e);return t?(await r.ensureFreshTokens(t))?.tokens.accessToken??null:null}async function m(e){let t=await p(n,e.cache,e.session,n.now());return t&&t.tokens.accessTokenExpiresAt-n.now()>n.lifetimes.refreshBeforeExpiryMs?t.tokens.accessToken:f(e.session)}async function h(e){return m(s(e))}async function _(e,t){let i=await r.load(e);return i?n.store.update(i.id,{data:t(i.data)}):null}return{config:n,sessions:r,handlers:i,guard:a,resolveSession:o,sessionFromCookie:c,sessionFromCookies:l,sessionFromRequest:u,accessTokenFromCookie:f,accessTokenFromCookies:m,accessTokenFromRequest:h,updateSessionData:_}}export{h as AccountsRequestError,f as OAuthTokenError,o as accountsEndpoint,g as cacheCookieBase,F as createAccountsAuth,P as createMemorySessionStore,m as fetchUserInfo,i as requestOrigin,n as sanitizeRedirectPath};
|
package/dist/next.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as AccountsAuth } from "./index-
|
|
3
|
-
//#region src/next.d.ts
|
|
1
|
+
import { o as AccountsSession } from "./types-BcXM3ZEI.js";
|
|
2
|
+
import { t as AccountsAuth } from "./index-CZZ3DRlf.js";
|
|
3
|
+
//#region src/next/index.d.ts
|
|
4
4
|
declare function withNextRequestScope<Data>(auth: AccountsAuth<Data>): {
|
|
5
5
|
getSession(): Promise<AccountsSession<Data> | null>;
|
|
6
6
|
getAccessToken(): Promise<string | null>;
|
package/dist/next.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{n as e}from"./
|
|
1
|
+
import{n as e,y as t}from"./cache-BWpH93Oi.js";import{cookies as n}from"next/headers";import{connection as r}from"next/server";async function i(i){await r();let a=await n(),o=e=>a.get(t(e,!0))?.value||a.get(t(e,!1))?.value||null;return{session:o(i),cache:o(e(i))}}async function a(r){let i=await n(),a=e(r);for(let e of[t(a,!0),t(a,!1)])try{i.delete(e)}catch{}}function o(e){let t=e.config.cookieName;return{async getSession(){return e.sessionFromCookies(await i(t))},async getAccessToken(){return e.accessTokenFromCookies(await i(t))},async updateSessionData(n){let r=await e.updateSessionData((await i(t)).session,n);return r&&await a(t),r}}}export{o as withNextRequestScope};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { c as AccountsSessionStore } from "./types-BcXM3ZEI.js";
|
|
2
|
+
//#region src/testing/index.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* A behavioural conformance suite for the session store contract.
|
|
5
|
+
*
|
|
6
|
+
* The store is the one piece every deployment reimplements, and its hardest
|
|
7
|
+
* part is invisible from the type: the takeover comparison in
|
|
8
|
+
* `claimRefreshLease` is not the equality fence `update` and `delete` use,
|
|
9
|
+
* and getting it wrong means two workers refresh at once, which accounts
|
|
10
|
+
* answers by revoking the whole token family. This suite is the contract, so
|
|
11
|
+
* a new store is checked rather than derived by reading the shipped ones.
|
|
12
|
+
*
|
|
13
|
+
* It returns cases instead of registering them, so it runs under node:test,
|
|
14
|
+
* vitest, or anything else:
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* for (const c of sessionStoreConformance({ createStore })) {
|
|
18
|
+
* test(c.name, c.run);
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
type ConformanceCase = {
|
|
23
|
+
name: string;
|
|
24
|
+
run: () => Promise<void>;
|
|
25
|
+
};
|
|
26
|
+
type StoreConformanceOptions<Data> = {
|
|
27
|
+
/** A store backed by empty storage. Called once per case. */
|
|
28
|
+
createStore: () => AccountsSessionStore<Data> | Promise<AccountsSessionStore<Data>>;
|
|
29
|
+
/** Two distinguishable app payloads. Defaults suit a store generic in Data. */
|
|
30
|
+
data?: Data;
|
|
31
|
+
otherData?: Data;
|
|
32
|
+
};
|
|
33
|
+
declare function sessionStoreConformance<Data = Record<string, never>>(options: StoreConformanceOptions<Data>): ConformanceCase[];
|
|
34
|
+
//#endregion
|
|
35
|
+
export { ConformanceCase, StoreConformanceOptions, sessionStoreConformance };
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import e from"node:assert/strict";const t=17e11;function n(e,n){return{id:e,user:{id:`user_1`,email:`u@test`,name:`U`,image:null},tokens:{accessToken:`at_1`,refreshToken:`rt_1`,idToken:`idt_1`,accessTokenExpiresAt:17000036e5},data:n,createdAt:t,lastValidatedAt:t,refreshLeaseUntil:null}}function r(r){let i=r.data??{seed:1},a=r.otherData??{seed:2},o=()=>Promise.resolve(r.createStore()),s=async(e=`sess_1`)=>{let t=await o();return await t.create(n(e,i)),t};return[{name:`read returns every field create was given`,run:async()=>{let t=await s();e.deepEqual(await t.read(`sess_1`),n(`sess_1`,i))}},{name:`read answers null for an id that was never created`,run:async()=>{let t=await o();e.equal(await t.read(`missing`),null)}},{name:`create keeps a null refresh token null rather than empty`,run:async()=>{let t=await o(),r=n(`sess_1`,i);r.tokens={...r.tokens,refreshToken:null,idToken:null},await t.create(r);let a=await t.read(`sess_1`);e.equal(a?.tokens.refreshToken,null),e.equal(a?.tokens.idToken,null)}},{name:`update writes only the patched fields`,run:async()=>{let n=await(await s()).update(`sess_1`,{data:a});e.deepEqual(n?.data,a),e.equal(n?.lastValidatedAt,t),e.equal(n?.tokens.accessToken,`at_1`)}},{name:`update answers null for an id that is not there`,run:async()=>{let t=await o();e.equal(await t.update(`missing`,{lastValidatedAt:1}),null)}},{name:`update applies when ifLeaseUntil is the lease on the row`,run:async()=>{let n=await s();await n.claimRefreshLease(`sess_1`,170000002e4,t);let r=await n.update(`sess_1`,{lastValidatedAt:1700000000005},170000002e4);e.equal(r?.lastValidatedAt,1700000000005)}},{name:`update writes nothing when ifLeaseUntil is not the lease on the row`,run:async()=>{let n=await s();await n.claimRefreshLease(`sess_1`,170000002e4,t);let r=await n.update(`sess_1`,{lastValidatedAt:1700000000005},1700000999999);e.equal(r,null),e.equal((await n.read(`sess_1`))?.lastValidatedAt,t)}},{name:`update releases the lease when the patch sets it null`,run:async()=>{let n=await s();await n.claimRefreshLease(`sess_1`,170000002e4,t),await n.update(`sess_1`,{refreshLeaseUntil:null},170000002e4),e.equal((await n.read(`sess_1`))?.refreshLeaseUntil,null)}},{name:`claimRefreshLease takes a lease when the row holds none`,run:async()=>{let n=await s(),r=await n.claimRefreshLease(`sess_1`,170000002e4,t);e.equal(r?.refreshLeaseUntil,170000002e4),e.equal((await n.read(`sess_1`))?.refreshLeaseUntil,170000002e4)}},{name:`claimRefreshLease refuses while another lease is still live`,run:async()=>{let n=await s();await n.claimRefreshLease(`sess_1`,170000002e4,t),e.equal(await n.claimRefreshLease(`sess_1`,170000004e4,1700000001e3),null),e.equal((await n.read(`sess_1`))?.refreshLeaseUntil,170000002e4)}},{name:`claimRefreshLease takes over a lease that has expired`,run:async()=>{let n=await s();await n.claimRefreshLease(`sess_1`,170000002e4,t);let r=await n.claimRefreshLease(`sess_1`,170000006e4,1700000020001);e.equal(r?.refreshLeaseUntil,170000006e4)}},{name:`claimRefreshLease answers null for an id that is not there`,run:async()=>{let n=await o();e.equal(await n.claimRefreshLease(`missing`,170000002e4,t),null)}},{name:`delete without a lease removes the session and reports true`,run:async()=>{let t=await s();e.equal(await t.delete(`sess_1`),!0),e.equal(await t.read(`sess_1`),null)}},{name:`delete reports false for an id that is not there`,run:async()=>{let t=await o();e.equal(await t.delete(`missing`),!1)}},{name:`delete applies when ifLeaseUntil is the lease on the row`,run:async()=>{let n=await s();await n.claimRefreshLease(`sess_1`,170000002e4,t),e.equal(await n.delete(`sess_1`,170000002e4),!0),e.equal(await n.read(`sess_1`),null)}},{name:`delete keeps the session when ifLeaseUntil is not its lease`,run:async()=>{let n=await s();await n.claimRefreshLease(`sess_1`,170000002e4,t),e.equal(await n.delete(`sess_1`,1700000999999),!1),e.notEqual(await n.read(`sess_1`),null)}},{name:`sessions are addressed independently`,run:async()=>{let t=await s(`sess_1`);await t.create(n(`sess_2`,a)),await t.delete(`sess_1`),e.equal(await t.read(`sess_1`),null),e.deepEqual((await t.read(`sess_2`))?.data,a)}}]}export{r as sessionStoreConformance};
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
//#region src/
|
|
1
|
+
//#region src/core/codec.d.ts
|
|
2
|
+
type SessionCodec = {
|
|
3
|
+
encrypt(value: string): Promise<string>;
|
|
4
|
+
decrypt(value: string): Promise<string>;
|
|
5
|
+
};
|
|
6
|
+
declare function createAesGcmCodec(hexKey: string): SessionCodec;
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region src/core/types.d.ts
|
|
2
9
|
type AccountsUser = {
|
|
3
10
|
id: string;
|
|
4
11
|
email: string;
|
|
@@ -26,7 +33,13 @@ interface AccountsSessionStore<Data> {
|
|
|
26
33
|
read(id: string): Promise<AccountsSession<Data> | null>;
|
|
27
34
|
update(id: string, patch: AccountsSessionPatch<Data>, ifLeaseUntil?: number): Promise<AccountsSession<Data> | null>;
|
|
28
35
|
claimRefreshLease(id: string, until: number, now: number): Promise<AccountsSession<Data> | null>;
|
|
29
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Removes the session. With `ifLeaseUntil`, removes it only while that
|
|
38
|
+
* refresh lease is still the one on the row, and resolves false when it is
|
|
39
|
+
* not, so a refresher that lost its lease can tell the difference between
|
|
40
|
+
* "gone" and "someone else owns it".
|
|
41
|
+
*/
|
|
42
|
+
delete(id: string, ifLeaseUntil?: number): Promise<boolean>;
|
|
30
43
|
}
|
|
31
44
|
type AccountsPaths = {
|
|
32
45
|
login: string;
|
|
@@ -63,17 +76,39 @@ type AccountsAuthConfig<Data> = {
|
|
|
63
76
|
store: AccountsSessionStore<Data>;
|
|
64
77
|
paths?: Partial<AccountsPaths>;
|
|
65
78
|
lifetimes?: Partial<AccountsLifetimes>;
|
|
66
|
-
|
|
67
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Returns the app data to store on the session, or null to deny the sign-in.
|
|
81
|
+
* Omit it when the app keeps nothing of its own on the session.
|
|
82
|
+
*/
|
|
83
|
+
onSignIn?(input: SignInInput<Data>): Promise<Data | null>;
|
|
84
|
+
/**
|
|
85
|
+
* Re-derives the app data, or returns null to end the session. Throwing
|
|
86
|
+
* keeps it, because a failure to reach accounts says nothing about the user.
|
|
87
|
+
* Omit it when the app keeps nothing of its own on the session.
|
|
88
|
+
*/
|
|
89
|
+
onRevalidate?(input: RevalidateInput<Data>): Promise<Data | null>;
|
|
68
90
|
fetch?: typeof fetch;
|
|
69
91
|
now?: () => number;
|
|
92
|
+
cache?: AccountsCacheConfig;
|
|
93
|
+
audience?: string | false;
|
|
70
94
|
};
|
|
71
|
-
type
|
|
95
|
+
type AccountsCacheConfig = {
|
|
96
|
+
secret: string;
|
|
97
|
+
maxAgeMs?: number;
|
|
98
|
+
};
|
|
99
|
+
type ResolvedAccountsAuthConfig<Data> = Omit<AccountsAuthConfig<Data>, "paths" | "lifetimes" | "scope" | "fetch" | "now" | "cache" | "audience" | "onSignIn" | "onRevalidate"> & {
|
|
100
|
+
onSignIn(input: SignInInput<Data>): Promise<Data | null>;
|
|
101
|
+
onRevalidate(input: RevalidateInput<Data>): Promise<Data | null>;
|
|
72
102
|
scope: string;
|
|
73
103
|
paths: AccountsPaths;
|
|
74
104
|
lifetimes: AccountsLifetimes;
|
|
75
105
|
fetch: typeof fetch;
|
|
76
106
|
now: () => number;
|
|
107
|
+
cache: {
|
|
108
|
+
codec: () => Promise<SessionCodec>;
|
|
109
|
+
maxAgeMs: number;
|
|
110
|
+
} | null;
|
|
111
|
+
audience: string | null;
|
|
77
112
|
};
|
|
78
113
|
declare class AccountsRequestError extends Error {
|
|
79
114
|
readonly status: number;
|
|
@@ -81,7 +116,8 @@ declare class AccountsRequestError extends Error {
|
|
|
81
116
|
}
|
|
82
117
|
declare class OAuthTokenError extends AccountsRequestError {
|
|
83
118
|
readonly code: string;
|
|
119
|
+
readonly description: string;
|
|
84
120
|
constructor(status: number, code: string, description: string);
|
|
85
121
|
}
|
|
86
122
|
//#endregion
|
|
87
|
-
export {
|
|
123
|
+
export { AccountsRequestError as a, AccountsSessionStore as c, OAuthTokenError as d, ResolvedAccountsAuthConfig as f, createAesGcmCodec as g, SessionCodec as h, AccountsPaths as i, AccountsTokens as l, SignInInput as m, AccountsCacheConfig as n, AccountsSession as o, RevalidateInput as p, AccountsLifetimes as r, AccountsSessionPatch as s, AccountsAuthConfig as t, AccountsUser as u };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aui-auth",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Server side sessions for web apps that sign in through assistant-ui accounts: OIDC code flow with PKCE, tokens kept in the app's database, single flight refresh",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -13,17 +13,29 @@
|
|
|
13
13
|
"types": "./dist/index.d.ts",
|
|
14
14
|
"default": "./dist/index.js"
|
|
15
15
|
},
|
|
16
|
+
"./accounts": {
|
|
17
|
+
"types": "./dist/accounts.d.ts",
|
|
18
|
+
"default": "./dist/accounts.js"
|
|
19
|
+
},
|
|
16
20
|
"./next": {
|
|
17
21
|
"types": "./dist/next.d.ts",
|
|
18
22
|
"default": "./dist/next.js"
|
|
19
23
|
},
|
|
20
|
-
"./
|
|
21
|
-
"types": "./dist/
|
|
22
|
-
"default": "./dist/
|
|
24
|
+
"./database": {
|
|
25
|
+
"types": "./dist/database/index.d.ts",
|
|
26
|
+
"default": "./dist/database/index.js"
|
|
23
27
|
},
|
|
24
|
-
"./
|
|
25
|
-
"types": "./dist/
|
|
26
|
-
"default": "./dist/
|
|
28
|
+
"./database/pg": {
|
|
29
|
+
"types": "./dist/database/pg.d.ts",
|
|
30
|
+
"default": "./dist/database/pg.js"
|
|
31
|
+
},
|
|
32
|
+
"./database/redis": {
|
|
33
|
+
"types": "./dist/database/redis.d.ts",
|
|
34
|
+
"default": "./dist/database/redis.js"
|
|
35
|
+
},
|
|
36
|
+
"./testing": {
|
|
37
|
+
"types": "./dist/testing.d.ts",
|
|
38
|
+
"default": "./dist/testing.js"
|
|
27
39
|
}
|
|
28
40
|
},
|
|
29
41
|
"peerDependencies": {
|
|
@@ -36,6 +48,7 @@
|
|
|
36
48
|
},
|
|
37
49
|
"devDependencies": {
|
|
38
50
|
"@types/node": "^26.1.2",
|
|
51
|
+
"ioredis": "^6.0.0",
|
|
39
52
|
"next": "^16.3.0",
|
|
40
53
|
"tsdown": "^0.22.14",
|
|
41
54
|
"typescript": "^7.0.2"
|
package/dist/cookies-D3lpM5ER.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
function e(e){if(!e)return;let t=e.split(`,`);return t[t.length-1].trim()||void 0}function t(t){let n=new URL(t.url);return`${e(t.headers.get(`x-forwarded-proto`))||n.protocol.replace(`:`,``)}://${e(t.headers.get(`x-forwarded-host`))||t.headers.get(`host`)||n.host}`}function n(e){return new URL(e).protocol===`https:`}function r(e,t){return t?`__Secure-${e}`:e}function i(e){let t=new Map;if(!e)return t;for(let n of e.split(`;`)){let e=n.indexOf(`=`);if(e===-1)continue;let r=n.slice(0,e).trim();!r||t.has(r)||t.set(r,n.slice(e+1).trim())}return t}function a(e,t){let n=i(e.headers.get(`cookie`));return n.get(r(t,!0))||n.get(r(t,!1))||null}function o(e,t,n,r){let i=[`${e}=${t}`,`Path=/`,`HttpOnly`,`SameSite=Lax`,`Max-Age=${r}`];return n&&i.push(`Secure`),i.join(`; `)}function s(e,t,i,a,s){let c=n(a);e.append(`set-cookie`,o(r(t,c),i,c,Math.floor(s/1e3)))}function c(e,t,i){let a=n(i);for(let n of[a,!a])e.append(`set-cookie`,o(r(t,n),``,n,0))}function l(e,t=307){return new Response(null,{status:t,headers:{location:e.toString()}})}function u(e,t){return`${e}_flow_${t}`}function d(e){return btoa(JSON.stringify(e)).replace(/\+/g,`-`).replace(/\//g,`_`).replace(/=+$/g,``)}function f(e){if(!e)return null;try{let t=e.replace(/-/g,`+`).replace(/_/g,`/`),n=JSON.parse(atob(t));return typeof n.verifier==`string`?{verifier:n.verifier,redirectPath:typeof n.redirectPath==`string`?n.redirectPath:`/`,silent:n.silent===!0}:null}catch{return null}}function p(e,t){if(!e)return`/`;let n;try{n=new URL(e,t)}catch{return`/`}return n.origin===t?`${n.pathname}${n.search}${n.hash}`:`/`}export{u as a,t as c,d as i,p as l,r as n,a as o,f as r,l as s,c as t,s as u};
|
package/dist/oidc-BIfcHoyS.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
const e={login:`/api/auth/login`,callback:`/api/auth/callback`,logout:`/api/auth/logout`,logoutComplete:`/api/auth/logout-complete`,error:`/auth/error`},t={absoluteMaxAgeMs:2592e6,idleMaxAgeMs:6048e5,revalidateIntervalMs:12e4,refreshBeforeExpiryMs:3e5,flowMaxAgeMs:6e5,refreshLeaseMs:2e4,leaseWaitMs:5e3};function n(n){return{...n,scope:n.scope??`openid profile email offline_access`,paths:{...e,...n.paths},lifetimes:{...t,...n.lifetimes},fetch:n.fetch??globalThis.fetch.bind(globalThis),now:n.now??Date.now}}var r=class extends Error{status;constructor(e,t){super(e),this.name=`AccountsRequestError`,this.status=t}},i=class extends r{code;constructor(e,t,n){super(`Token request failed: ${e} ${t} ${n}`,e),this.name=`OAuthTokenError`,this.code=t}};function a(e){let t=``;for(let n of e)t+=String.fromCharCode(n);return btoa(t).replace(/\+/g,`-`).replace(/\//g,`_`).replace(/=+$/g,``)}function o(e){return a(crypto.getRandomValues(new Uint8Array(e)))}async function s(e){let t=await crypto.subtle.digest(`SHA-256`,new TextEncoder().encode(e));return a(new Uint8Array(t))}function c(e,t){return new URL(t,e).toString()}async function l(e,t){let n=o(24),r=o(32),i=new URL(c(e.issuer,`/api/auth/oauth2/authorize`));return i.searchParams.set(`client_id`,e.clientId),i.searchParams.set(`redirect_uri`,new URL(e.paths.callback,t.origin).toString()),i.searchParams.set(`response_type`,`code`),i.searchParams.set(`scope`,e.scope),i.searchParams.set(`state`,n),i.searchParams.set(`code_challenge`,await s(r)),i.searchParams.set(`code_challenge_method`,`S256`),t.silent&&i.searchParams.set(`prompt`,`none`),{url:i.toString(),state:n,verifier:r}}function u(e,t){let n=new URL(c(e.issuer,`/api/auth/oauth2/end-session`));return n.searchParams.set(`id_token_hint`,t.idToken),n.searchParams.set(`client_id`,e.clientId),n.searchParams.set(`post_logout_redirect_uri`,new URL(e.paths.logoutComplete,t.origin).toString()),n.toString()}async function d(e,t,n){let r=new URLSearchParams({client_id:e.clientId,...t});e.clientSecret&&r.set(`client_secret`,e.clientSecret);let a=await e.fetch(c(e.issuer,`/api/auth/oauth2/token`),{method:`POST`,headers:{"Content-Type":`application/x-www-form-urlencoded`},body:r,cache:`no-store`,...n?{signal:AbortSignal.timeout(n)}:{}});if(!a.ok){let e=await a.text().catch(()=>``),t=`unknown_error`,n=e;try{let r=JSON.parse(e);t=r.error??t,n=r.error_description??n}catch{}throw new i(a.status,t,n)}return await a.json()}function f(e,t){return d(e,{grant_type:`authorization_code`,code:t.code,code_verifier:t.verifier,redirect_uri:t.redirectUri})}function p(e,t,n){return d(e,{grant_type:`refresh_token`,refresh_token:t},n)}async function m(e,t){let n=await e.fetch(c(e.issuer,`/api/auth/oauth2/userinfo`),{headers:{Authorization:`Bearer ${t}`},cache:`no-store`});if(!n.ok)throw new r(`Failed to load user info: ${n.status}`,n.status);let i=await n.json();return{id:i.sub,email:i.email??``,name:i.name??i.email??i.sub,image:i.picture??null}}function h(e,t,n){return{accessToken:e.access_token,refreshToken:e.refresh_token??t?.refreshToken??null,idToken:e.id_token??t?.idToken??null,accessTokenExpiresAt:typeof e.expires_at==`number`?e.expires_at*1e3:n+(e.expires_in??3600)*1e3}}export{m as a,s as c,i as d,n as f,f as i,h as l,l as n,o,u as r,p as s,c as t,r as u};
|