aui-auth 0.0.4 → 0.0.6

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 CHANGED
@@ -2,4 +2,4 @@
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
4
 
5
- Entry points: `aui-auth`, `aui-auth/accounts`, `aui-auth/next`, `aui-auth/database`, `aui-auth/database/pg`.
5
+ Entry points: `aui-auth`, `aui-auth/accounts`, `aui-auth/next`, `aui-auth/database`, `aui-auth/database/pg`, `aui-auth/database/redis`, `aui-auth/testing`.
@@ -1,4 +1,5 @@
1
- import { m as SignInInput, p as RevalidateInput } from "./types-BcXM3ZEI.js";
1
+ import { h as RevalidateInput, v as SignInInput } from "./types-CEuhm7Nv.js";
2
+ import { t as AccountsFetchOptions } from "./oidc-CaYOiMFO.js";
2
3
  //#region src/accounts/index.d.ts
3
4
  type AccountsOrganization = {
4
5
  id: string;
@@ -11,15 +12,10 @@ type AccountsOrganization = {
11
12
  type OrganizationSessionData = {
12
13
  activeOrganization: AccountsOrganization | null;
13
14
  };
14
- type OrganizationHooksOptions = {
15
- issuer: string;
16
- fetch?: typeof fetch;
15
+ type OrganizationHooksOptions = AccountsFetchOptions & {
17
16
  preferredSlug?: (redirectPath: string) => string | null;
18
17
  };
19
- declare function fetchOrganizations(options: {
20
- issuer: string;
21
- fetch?: typeof fetch;
22
- }, accessToken: string): Promise<AccountsOrganization[]>;
18
+ declare function fetchOrganizations(options: AccountsFetchOptions, accessToken: string): Promise<AccountsOrganization[]>;
23
19
  declare function selectOrganization(organizations: AccountsOrganization[], preferredSlug?: string | null): AccountsOrganization | null;
24
20
  declare function preferredOrganizationSlug(redirectPath: string): string | null;
25
21
  declare function organizationHooks(options: OrganizationHooksOptions): {
package/dist/accounts.js CHANGED
@@ -1 +1 @@
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};
1
+ import{a as e,t}from"./types-ICH7-e-H.js";import{n}from"./oidc-D2VwAdd3.js";async function r(e,r){let i=await n(e,`/api/oidc/organizations`,{headers:{Authorization:`Bearer ${r}`}});if(!i.ok)throw new t(`Failed to load organizations: ${i.status}`,i.status);return await i.json()}function i(e,t){if(t){let n=e.find(e=>e.slug===t);if(n)return n}return e[0]??null}function a(e){let[,t]=new URL(e,`http://localhost`).pathname.split(`/`);return t&&t!==`org`?t:null}function o(t){let n=t.preferredSlug??a;async function o(e){return{activeOrganization:i(await r(t,e.accessToken),n(e.redirectPath))}}async function s(n){let a;try{a=await r(t,n.accessToken)}catch(t){if(e(t))return null;throw t}let o=n.session.data.activeOrganization;if(!o)return{activeOrganization:i(a)};let s=a.find(e=>e.id===o.id)??null;return s?{activeOrganization:s}:null}return{onSignIn:o,onRevalidate:s}}export{r as fetchOrganizations,o as organizationHooks,a as preferredOrganizationSlug,i as selectOrganization};
@@ -0,0 +1 @@
1
+ import{o as e}from"./types-ICH7-e-H.js";import{u as t}from"./oidc-D2VwAdd3.js";function n(e){if(!e)return;let t=e.split(`,`);return t[t.length-1].trim()||void 0}function r(e){let t=new URL(e.url);return`${n(e.headers.get(`x-forwarded-proto`))||t.protocol.replace(`:`,``)}://${n(e.headers.get(`x-forwarded-host`))||e.headers.get(`host`)||t.host}`}function i(e){return new URL(e).protocol===`https:`}function a(e,t){return t?`__Secure-${e}`:e}function o(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 s(e,t){let n=o(e.headers.get(`cookie`));return n.get(a(t,!0))||n.get(a(t,!1))||null}function c(e,t,n,r){let i=[`${e}=${t}`,`Path=/`,`HttpOnly`,`SameSite=Lax`,`Max-Age=${r}`];return n&&i.push(`Secure`),i.join(`; `)}function l(e,t,n,r,o){let s=i(r);e.append(`set-cookie`,c(a(t,s),n,s,Math.floor(o/1e3)))}function u(e,t){if(t.length===0)return;let n=o(e.get(`cookie`));for(let e of t){let t=e.split(`;`)[0],r=t.indexOf(`=`);if(r<1)continue;let i=t.slice(0,r).trim(),a=t.slice(r+1).trim();a?n.set(i,a):n.delete(i)}e.set(`cookie`,[...n].map(([e,t])=>`${e}=${t}`).join(`; `))}function d(e,t){let n=i(t);return[n,!n].map(t=>c(a(e,t),``,t,0))}function f(e,t,n){for(let r of d(t,n))e.append(`set-cookie`,r)}function p(e,t=307){return new Response(null,{status:t,headers:{location:e.toString()}})}function m(e,t){return`${e}_flow_${t}`}function h(e){return btoa(JSON.stringify(e)).replace(/\+/g,`-`).replace(/\//g,`_`).replace(/=+$/g,``)}function g(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 _(e,t){if(!e)return`/`;let n;try{n=new URL(e,t)}catch{return`/`}return n.origin!==t||n.pathname.startsWith(`//`)?`/`:`${n.pathname}${n.search}${n.hash}`}const v=4096;function y(e){return`${e}_cache`}function b(e,t,n){if(!e.cache)return 0;let{lifetimes:r}=e,i=Math.min(t.lastValidatedAt+r.revalidateIntervalMs,t.tokens.accessTokenExpiresAt-r.refreshBeforeExpiryMs);return Math.min(e.cache.maxAgeMs,(i>n?i:n+r.retryIntervalMs)-n,t.createdAt+r.absoluteMaxAgeMs-n,t.lastValidatedAt+r.idleMaxAgeMs-n)}async function x(e,t,n){if(!e.cache)return null;let r=b(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 S(e,n,r,i){if(!e.cache||!n||!r)return null;let a;try{let t=await e.cache.codec();a=JSON.parse(await t.decrypt(n))}catch{return null}return a.v!==1||typeof a.exp!=`number`||a.exp<=i||a.id!==await t(r)?null:{id:a.id,user:a.u,tokens:{accessToken:a.at,refreshToken:null,idToken:null,accessTokenExpiresAt:a.ate},data:a.d,createdAt:a.ca,lastValidatedAt:a.lv,refreshLeaseUntil:null}}async function C(t,n,r,o){try{let e=await x(t,n,o);if(!e)return null;let s=i(r),l=c(a(y(t.cookieName),s),e.value,s,Math.floor(e.maxAgeMs/1e3));if(l.length>v)throw Error(`Cache cookie is ${l.length} bytes and browsers drop cookies over ${v}; the session data is too large to cache`);return l}catch(n){return e(t,`cache_cookie`,n),null}}function w(e,t){return e.cache?d(y(e.cookieName),t):[]}export{c as _,f as a,g as c,u as d,o as f,_ as g,r as h,S as i,h as l,p as m,y as n,d as o,s as p,C as r,a as s,w as t,m as u,l as v};
@@ -0,0 +1,8 @@
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
+ export { createAesGcmCodec as n, SessionCodec as t };
@@ -1,2 +1,3 @@
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 };
1
+ import { n as createAesGcmCodec, t as SessionCodec } from "../codec-BPnk_EfF.js";
2
+ import { _ as SessionSweepCutoffs, c as AccountsSession, d as AccountsTokens, f as AccountsUser, g as SessionDecodeError, l as AccountsSessionPatch, u as AccountsSessionStore } from "../types-CEuhm7Nv.js";
3
+ export { type AccountsSession, type AccountsSessionPatch, type AccountsSessionStore, type AccountsTokens, type AccountsUser, type SessionCodec, SessionDecodeError, type SessionSweepCutoffs, createAesGcmCodec };
@@ -1 +1 @@
1
- import{t as e}from"../codec-BcgaGRoP.js";export{e as createAesGcmCodec};
1
+ import{c as e,i as t}from"../types-ICH7-e-H.js";export{t as SessionDecodeError,e as createAesGcmCodec};
@@ -1,4 +1,5 @@
1
- import { c as AccountsSessionStore, h as SessionCodec } from "../types-BcXM3ZEI.js";
1
+ import { t as SessionCodec } from "../codec-BPnk_EfF.js";
2
+ import { u as AccountsSessionStore } from "../types-CEuhm7Nv.js";
2
3
  //#region src/database/pg.d.ts
3
4
  type PgQuery = (text: string, values?: unknown[]) => Promise<{
4
5
  rows: Record<string, unknown>[];
@@ -1,4 +1,4 @@
1
- const e=`CREATE TABLE "accounts_sessions" (
1
+ import{i as e}from"../types-ICH7-e-H.js";const t=`CREATE TABLE "accounts_sessions" (
2
2
  "id" text PRIMARY KEY,
3
3
  "user_id" text NOT NULL,
4
4
  "user" jsonb NOT NULL,
@@ -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,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};
14
+ CREATE INDEX "accounts_sessions_user_id_idx" ON "accounts_sessions" ("user_id");`,n={encrypt:async e=>e,decrypt:async e=>e};function r(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 i(e){return new Date(e)}function a(t){let a=t.table??`accounts_sessions`;if(!/^[a-z_][a-z0-9_]*$/.test(a))throw Error(`Invalid accounts session table name: ${a}`);let o=`"${a}"`,s=t.codec??n;async function c(e){return{accessToken:await s.encrypt(e.accessToken),refreshToken:e.refreshToken?await s.encrypt(e.refreshToken):null,idToken:e.idToken?await s.encrypt(e.idToken):null}}async function l(t){try{return{id:t.id,user:t.user,tokens:{accessToken:await s.decrypt(t.access_token),refreshToken:t.refresh_token?await s.decrypt(t.refresh_token):null,idToken:t.id_token?await s.decrypt(t.id_token):null,accessTokenExpiresAt:r(t.access_token_expires_at)},data:t.data,createdAt:r(t.created_at),lastValidatedAt:r(t.last_validated_at),refreshLeaseUntil:t.refresh_lease_until===null||t.refresh_lease_until===void 0?null:r(t.refresh_lease_until)}}catch(t){throw new e(t)}}return{async create(e){let n=await c(e.tokens);await t.query(`INSERT INTO ${o} ("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)`,[e.id,e.user.id,e.user,n.accessToken,n.refreshToken,n.idToken,i(e.tokens.accessTokenExpiresAt),e.data,i(e.createdAt),i(e.lastValidatedAt)])},async read(e){let{rows:n}=await t.query(`SELECT * FROM ${o} WHERE "id" = $1`,[e]);return n[0]?l(n[0]):null},async update(e,n,r){let a=[],s=[e],u=(e,t)=>{s.push(t),a.push(`"${e}" = $${s.length}`)};if(n.user!==void 0&&(u(`user_id`,n.user.id),u(`user`,n.user)),n.tokens!==void 0){let e=await c(n.tokens);u(`access_token`,e.accessToken),u(`refresh_token`,e.refreshToken),u(`id_token`,e.idToken),u(`access_token_expires_at`,i(n.tokens.accessTokenExpiresAt))}if(n.data!==void 0&&u(`data`,n.data),n.lastValidatedAt!==void 0&&u(`last_validated_at`,i(n.lastValidatedAt)),n.refreshLeaseUntil!==void 0&&u(`refresh_lease_until`,n.refreshLeaseUntil===null?null:i(n.refreshLeaseUntil)),a.length===0){if(r===void 0)return this.read(e);let n=await t.query(`SELECT * FROM ${o} WHERE "id" = $1 AND "refresh_lease_until" = $2`,[e,i(r)]);return n.rows[0]?l(n.rows[0]):null}let d=`"id" = $1`;r!==void 0&&(s.push(i(r)),d+=` AND "refresh_lease_until" = $${s.length}`);let{rows:f}=await t.query(`UPDATE ${o} SET ${a.join(`, `)} WHERE ${d} RETURNING *`,s);return f[0]?l(f[0]):null},async claimRefreshLease(e,n,r){let{rows:a}=await t.query(`UPDATE ${o} SET "refresh_lease_until" = $2 WHERE "id" = $1 AND ("refresh_lease_until" IS NULL OR "refresh_lease_until" <= $3) RETURNING *`,[e,i(n),i(r)]);return a[0]?l(a[0]):null},async delete(e,n){if(n===void 0){let{rows:n}=await t.query(`DELETE FROM ${o} WHERE "id" = $1 RETURNING "id"`,[e]);return n.length>0}let{rows:r}=await t.query(`DELETE FROM ${o} WHERE "id" = $1 AND "refresh_lease_until" = $2 RETURNING "id"`,[e,i(n)]);return r.length>0},async sweep(e){let{rows:n}=await t.query(`WITH "removed" AS (DELETE FROM ${o} WHERE "created_at" < $1 OR "last_validated_at" < $2 RETURNING 1) SELECT count(*)::int AS "count" FROM "removed"`,[i(e.createdBefore),i(e.validatedBefore)]);return Number(n[0]?.count??0)}}}export{t as PG_SESSION_TABLE_SQL,a as createPgSessionStore};
@@ -1,4 +1,5 @@
1
- import { c as AccountsSessionStore, h as SessionCodec } from "../types-BcXM3ZEI.js";
1
+ import { t as SessionCodec } from "../codec-BPnk_EfF.js";
2
+ import { u as AccountsSessionStore } from "../types-CEuhm7Nv.js";
2
3
  //#region src/database/redis.d.ts
3
4
  /**
4
5
  * The slice of a Redis client this store uses, so the package takes no Redis
@@ -1,8 +1,8 @@
1
- const e=`refreshLeaseUntil`,t=`
1
+ import{i as e}from"../types-ICH7-e-H.js";const t=`refreshLeaseUntil`,n=`
2
2
  local key, checkLease, expectedLease, ttl = KEYS[1], ARGV[1], ARGV[2], ARGV[3]
3
3
  if redis.call('EXISTS', key) == 0 then return 0 end
4
4
  if checkLease == '1' then
5
- local current = redis.call('HGET', key, '${e}')
5
+ local current = redis.call('HGET', key, '${t}')
6
6
  if (current or '') ~= expectedLease then return 0 end
7
7
  end
8
8
  for index = 4, #ARGV, 2 do
@@ -14,30 +14,30 @@ for index = 4, #ARGV, 2 do
14
14
  end
15
15
  redis.call('PEXPIRE', key, ttl)
16
16
  return 1
17
- `,n=`
17
+ `,r=`
18
18
  local key, leaseUntil, now, ttl = KEYS[1], ARGV[1], ARGV[2], ARGV[3]
19
19
  if redis.call('EXISTS', key) == 0 then return nil end
20
- local current = redis.call('HGET', key, '${e}')
20
+ local current = redis.call('HGET', key, '${t}')
21
21
  if current and current ~= '' and tonumber(current) > tonumber(now) then
22
22
  return nil
23
23
  end
24
- redis.call('HSET', key, '${e}', leaseUntil)
24
+ redis.call('HSET', key, '${t}', leaseUntil)
25
25
  redis.call('PEXPIRE', key, ttl)
26
26
  return redis.call('HGETALL', key)
27
- `,r=`
27
+ `,i=`
28
28
  local key, checkLease, expectedLease = KEYS[1], ARGV[1], ARGV[2]
29
29
  if redis.call('EXISTS', key) == 0 then return 0 end
30
30
  if checkLease == '1' then
31
- local current = redis.call('HGET', key, '${e}')
31
+ local current = redis.call('HGET', key, '${t}')
32
32
  if (current or '') ~= expectedLease then return 0 end
33
33
  end
34
34
  redis.call('DEL', key)
35
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(`
36
+ `,a={encrypt:async e=>e,decrypt:async e=>e};function o(e){return e==null||e===``?null:typeof e==`string`?e:JSON.stringify(e)}function s(e){let t=o(e),n=t===null?NaN:Number(t);if(!Number.isFinite(n))throw Error(`Unexpected timestamp in accounts session hash`);return n}function c(e){if(typeof e!=`string`)return e;try{return JSON.parse(e)}catch{return e}}function l(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 u(u){let{redis:d}=u,f=u.codec??a,p=u.prefix??`accounts_sessions:`,m=u.ttlMs??26784e5,h=e=>`${p}${e}`;async function g(e){let n={};if(e.user&&(n.user=JSON.stringify(e.user)),e.data!==void 0&&(n.data=JSON.stringify(e.data)),e.lastValidatedAt!==void 0&&(n.lastValidatedAt=String(e.lastValidatedAt)),e.refreshLeaseUntil!==void 0&&(n[t]=e.refreshLeaseUntil===null?``:String(e.refreshLeaseUntil)),e.tokens){let{accessToken:t,refreshToken:r,idToken:i,accessTokenExpiresAt:a}=e.tokens;n.accessToken=await f.encrypt(t),n.refreshToken=r?await f.encrypt(r):``,n.idToken=i?await f.encrypt(i):``,n.accessTokenExpiresAt=String(a)}return n}async function _(n,r){try{let e=o(r.refreshToken),i=o(r.idToken),a=o(r[t]);return{id:n,user:c(r.user),tokens:{accessToken:await f.decrypt(o(r.accessToken)??``),refreshToken:e?await f.decrypt(e):null,idToken:i?await f.decrypt(i):null,accessTokenExpiresAt:s(r.accessTokenExpiresAt)},data:c(r.data),createdAt:s(r.createdAt),lastValidatedAt:s(r.lastValidatedAt),refreshLeaseUntil:a===null?null:Number(a)}}catch(t){throw new e(t)}}async function v(e){let t=l(await d.hgetall(h(e)));return t?_(e,t):null}return{async create(e){let t=await g(e);t.createdAt=String(e.createdAt);let n=Object.entries(t).filter(([,e])=>e!==``);await d.eval(`
37
37
  local key, ttl = KEYS[1], ARGV[1]
38
38
  for index = 2, #ARGV, 2 do
39
39
  redis.call('HSET', key, ARGV[index], ARGV[index + 1])
40
40
  end
41
41
  redis.call('PEXPIRE', key, ttl)
42
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};
43
+ `,[h(e.id)],[String(m),...n.flat()])},read:v,async update(e,t,r){let i=await g(t),a=await d.eval(n,[h(e)],[r===void 0?`0`:`1`,r===void 0?``:String(r),String(m),...Object.entries(i).flat()]);return Number(a)===1?v(e):null},async claimRefreshLease(e,t,n){let i=l(await d.eval(r,[h(e)],[String(t),String(n),String(m)]));return i?_(e,i):null},async delete(e,t){let n=await d.eval(i,[h(e)],[t===void 0?`0`:`1`,t===void 0?``:String(t)]);return Number(n)===1}}}export{u as createRedisSessionStore};
@@ -1,20 +1,21 @@
1
- import { c as AccountsSessionStore, f as ResolvedAccountsAuthConfig, l as AccountsTokens, o as AccountsSession, t as AccountsAuthConfig, u as AccountsUser } from "./types-BcXM3ZEI.js";
1
+ import { c as AccountsSession, t as AccountsAuthConfig, u as AccountsSessionStore } from "./types-CEuhm7Nv.js";
2
2
  //#region src/core/cookies.d.ts
3
3
  declare function requestOrigin(request: Request): string;
4
+ /**
5
+ * Rewrites a request's `cookie` header to what the browser would send once it
6
+ * has applied the given Set-Cookie headers. A proxy that resolved a session
7
+ * hands the cookies it issued to the render this way, so the render reads
8
+ * what the next request would carry instead of going back to the store.
9
+ */
10
+ declare function forwardCookies(headers: Headers, issued: readonly string[]): void;
4
11
  declare function sanitizeRedirectPath(rawRedirect: string | null | undefined, origin: string): string;
5
12
  //#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
13
  //#region src/core/cache.d.ts
14
14
  declare function cacheCookieBase(sessionCookie: string): string;
15
15
  //#endregion
16
16
  //#region src/core/guard.d.ts
17
17
  type GuardOptions = {
18
+ /** Paths that need no session, on top of the auth routes and the error page. */
18
19
  publicPaths?: string[];
19
20
  apiPaths?: string[];
20
21
  redirectPath?: (request: Request) => string;
@@ -49,21 +50,7 @@ type SessionCookies = {
49
50
  cache: string | null;
50
51
  };
51
52
  declare function createAccountsAuth<Data>(input: AccountsAuthConfig<Data>): {
52
- config: ResolvedAccountsAuthConfig<Data>;
53
- sessions: {
54
- load: (cookieValue: string | null) => Promise<AccountsSession<Data> | null>;
55
- start: (input: {
56
- user: AccountsUser;
57
- tokens: AccountsTokens;
58
- data: Data;
59
- }) => Promise<{
60
- session: AccountsSession<Data>;
61
- cookieValue: string;
62
- }>;
63
- ensureFreshTokens: (session: AccountsSession<Data>) => Promise<AccountsSession<Data> | null>;
64
- revalidate: (session: AccountsSession<Data>) => Promise<AccountsSession<Data> | null>;
65
- end: (session: AccountsSession<Data>) => Promise<void>;
66
- };
53
+ cookieName: string;
67
54
  handlers: {
68
55
  login: (request: Request) => Promise<Response>;
69
56
  callback: (request: Request) => Promise<Response>;
@@ -71,14 +58,13 @@ declare function createAccountsAuth<Data>(input: AccountsAuthConfig<Data>): {
71
58
  };
72
59
  guard: (request: Request, options?: GuardOptions) => Promise<GuardResult<Data>>;
73
60
  resolveSession: (request: Request) => Promise<SessionResolution<Data>>;
74
- sessionFromCookie: (cookieValue: string | null) => Promise<AccountsSession<Data> | null>;
75
61
  sessionFromCookies: (cookies: SessionCookies) => Promise<AccountsSession<Data> | null>;
76
62
  sessionFromRequest: (request: Request) => Promise<AccountsSession<Data> | null>;
77
- accessTokenFromCookie: (cookieValue: string | null) => Promise<string | null>;
78
63
  accessTokenFromCookies: (cookies: SessionCookies) => Promise<string | null>;
79
64
  accessTokenFromRequest: (request: Request) => Promise<string | null>;
80
- updateSessionData: (cookieValue: string | null, update: (data: Data) => Data) => Promise<AccountsSession<Data> | null>;
65
+ updateSessionData: (cookies: SessionCookies, update: (data: Data) => Data) => Promise<AccountsSession<Data> | null>;
66
+ sweepSessions: () => Promise<number>;
81
67
  };
82
68
  type AccountsAuth<Data> = ReturnType<typeof createAccountsAuth<Data>>;
83
69
  //#endregion
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 };
70
+ export { SessionResolution as a, cacheCookieBase as c, sanitizeRedirectPath as d, createMemorySessionStore as i, forwardCookies as l, SessionCookies as n, GuardOptions as o, createAccountsAuth as r, GuardResult as s, AccountsAuth as t, requestOrigin as u };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
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 };
1
+ import { _ as SessionSweepCutoffs, a as AccountsLifetimes, c as AccountsSession, d as AccountsTokens, f as AccountsUser, g as SessionDecodeError, h as RevalidateInput, i as AccountsErrorPhase, l as AccountsSessionPatch, m as OAuthTokenError, n as AccountsCacheConfig, o as AccountsPaths, p as DEFAULT_LIFETIMES, r as AccountsErrorEvent, s as AccountsRequestError, t as AccountsAuthConfig, u as AccountsSessionStore, v as SignInInput, y as isAccessDenied } from "./types-CEuhm7Nv.js";
2
+ import { n as accountsEndpoint, r as fetchUserInfo, t as AccountsFetchOptions } from "./oidc-CaYOiMFO.js";
3
+ import { a as SessionResolution, c as cacheCookieBase, d as sanitizeRedirectPath, i as createMemorySessionStore, l as forwardCookies, n as SessionCookies, o as GuardOptions, r as createAccountsAuth, s as GuardResult, t as AccountsAuth, u as requestOrigin } from "./index-wc8vz71h.js";
4
+ export { AccountsAuth, type AccountsAuthConfig, type AccountsCacheConfig, type AccountsErrorEvent, type AccountsErrorPhase, type AccountsFetchOptions, type AccountsLifetimes, type AccountsPaths, AccountsRequestError, type AccountsSession, type AccountsSessionPatch, type AccountsSessionStore, type AccountsTokens, type AccountsUser, DEFAULT_LIFETIMES, type GuardOptions, type GuardResult, OAuthTokenError, type RevalidateInput, SessionCookies, SessionDecodeError, type SessionResolution, type SessionSweepCutoffs, type SignInInput, accountsEndpoint, cacheCookieBase, createAccountsAuth, createMemorySessionStore, fetchUserInfo, forwardCookies, isAccessDenied, requestOrigin, sanitizeRedirectPath };
package/dist/index.js CHANGED
@@ -1 +1 @@
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};
1
+ import{_ as e,a as t,c as n,d as r,f as i,g as a,h as o,i as s,l as c,m as l,n as u,o as d,p as f,r as p,t as m,u as h,v as g}from"./cache-kH1-Y-mz.js";import{a as _,i as v,n as y,o as b,r as x,s as S,t as C}from"./types-ICH7-e-H.js";import{a as w,c as T,d as E,i as D,l as O,o as k,r as A,s as j,t as M,u as N}from"./oidc-D2VwAdd3.js";function P(e,t){return[...d(e.cookieName,t),...m(e,t)]}function F(e,t){return async function(n){let r=o(n),i=f(n,e.cookieName),a=f(n,u(e.cookieName)),c=await s(e,a,i,e.now());if(c)return{session:c,cookies:[]};let l={session:null,cookies:i!==null||a!==null?P(e,r):[]},d=await t.load(i);if(!d)return l;let{deferRevalidation:m}=e;if(m&&t.revalidationDue(d)&&I(e,m,t,d)){let t=await p(e,d,r,e.now());return{session:d,cookies:t?[t]:[]}}let h=d;try{let e=await t.revalidate(d);if(!e)return l;h=e}catch(t){b(e,`revalidate`,t)}let g=await p(e,h,r,e.now());return{session:h,cookies:g?[g]:[]}}}function I(e,t,n,r){try{return t(async()=>{try{await n.revalidate(r)}catch(t){b(e,`revalidate`,t)}}),!0}catch(t){return b(e,`revalidate`,t),!1}}function L(e,t){let n=F(e,t);return async function(t,i={}){let a=new URL(t.url);if([e.paths.login,e.paths.callback,e.paths.logout,e.paths.logoutComplete,e.paths.error,...i.publicPaths??[]].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,a=new URL(e.url),o=(t.apiPaths??[`/api`,`/trpc`]).some(e=>a.pathname.startsWith(e))?Response.json({error:`Unauthorized`},{status:401}):l(i(e,t,r));for(let e of n)o.headers.append(`set-cookie`,e);return{kind:`response`,response:o}}function i(t,n,r){let i=new URL(t.url),a=new URL(e.paths.login,o(t));return a.searchParams.set(`redirect`,n.redirectPath?.(t)??`${i.pathname}${i.search}`),r&&a.searchParams.set(`silent`,`1`),a}}const R=/^[A-Za-z0-9_-]{1,128}$/,z=new Set([`login_required`,`consent_required`,`interaction_required`,`account_selection_required`]);function B(r,s){function u(e,t){let n=new URL(r.paths.login,e);return n.searchParams.set(`redirect`,t),n}function d(e,t,n){let i=new URL(r.paths.error,e);return i.searchParams.set(`reason`,t),n&&i.searchParams.set(`description`,n),i}function _(t,n){let a=h(r.cookieName,``),o=[...i(t.headers.get(`cookie`)).keys()].filter(e=>e.replace(/^__Secure-/,``).startsWith(a)),s=o.length-4;for(let t of o.slice(0,Math.max(0,s)))n.headers.append(`set-cookie`,e(t,``,t.startsWith(`__Secure-`),0))}async function v(e){try{let t=await s.load(e);t&&await s.end(t)}catch(e){b(r,`session_store`,e)}}async function y(e){let t=o(e),n=new URL(e.url).searchParams,i=a(n.get(`redirect`),t),s=n.get(`silent`)===`1`,{url:u,state:d,verifier:f}=await A(r,{origin:t,silent:s}),p=l(u);return _(e,p),g(p.headers,h(r.cookieName,d),c({verifier:f,redirectPath:i,silent:s}),t,r.lifetimes.flowMaxAgeMs),p}async function S(e){let i=o(e),c=new URL(e.url).searchParams,_=c.get(`state`),y=_&&R.test(_)?h(r.cookieName,_):null,S=y?n(f(e,y)):null,C=e=>(y&&t(e.headers,y,i),e);if(!S)return C(l(d(i,`invalid_state`,null)));let T=a(S.redirectPath,i),D=c.get(`error`);if(D)return S.silent&&z.has(D)?C(l(u(i,T))):C(l(d(i,D,c.get(`error_description`))));let O=c.get(`code`);if(!O)return C(l(d(i,`invalid_request`,null)));let A,M,N;try{let e=await w(r,{code:O,verifier:S.verifier,redirectUri:new URL(r.paths.callback,i).toString()});A=E(e,null,r.now()),M=await j(k(r),A.accessToken),N=await r.onSignIn({user:M,accessToken:A.accessToken,redirectPath:T,previous:null})}catch(e){return b(r,`sign_in`,e),e instanceof x?C(l(d(i,e.code,e.description||null))):C(l(d(i,`accounts_unavailable`,null)))}let P=f(e,r.cookieName);if(N===null){let e=l(d(i,`access_denied`,null));P!==null&&(await v(P),t(e.headers,r.cookieName,i));for(let t of m(r,i))e.headers.append(`set-cookie`,t);return C(e)}let F,I;try{({cookieValue:F,session:I}=await s.start({user:M,tokens:A,data:N}))}catch(e){return b(r,`session_store`,e),C(l(d(i,`session_store_error`,null)))}P!==null&&await v(P);let L=l(new URL(T,i));g(L.headers,r.cookieName,F,i,r.lifetimes.absoluteMaxAgeMs);let B=await p(r,I,i,r.now());return B&&L.headers.append(`set-cookie`,B),C(L)}async function C(e){let t=o(e),n=await s.load(f(e,r.cookieName));n&&await s.end(n);let i=n?.tokens.idToken?D(r,{origin:t,idToken:n.tokens.idToken}):new URL(r.paths.logoutComplete,t).toString(),a=l(i,303);for(let e of P(r,t))a.headers.append(`set-cookie`,e);return a}return{login:y,callback:S,logout:C}}const V=e=>new Promise(t=>setTimeout(t,e));function H(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}function a(t){let r=e.now();return!i(t.tokens,r)&&r-t.lastValidatedAt>=n.revalidateIntervalMs}async function o(n){if(!n)return null;let i=await N(n),a;try{a=await t.read(i)}catch(n){if(!(n instanceof v))throw n;return b(e,`session_store`,n),await t.delete(i),null}return a?r(a,e.now())?(await t.delete(a.id),null):a:null}async function s(n){let r=T(32),i=e.now(),a={id:await N(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 c(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 l(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 O(e,o.tokens.refreshToken,Math.max(n.refreshLeaseMs-1e3,n.refreshLeaseMs/2));return await t.update(o.id,{tokens:E(r,o.tokens,e.now()),refreshLeaseUntil:null},s)??t.read(o.id)}catch(n){if(n instanceof x&&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 b(e,`refresh`,n),r??o;throw n}}async function l(r,a){for(;;){await V(100);let o=await t.read(r);if(!o)return null;let s=e.now();if(!i(o.tokens,s))return o;if(o.refreshLeaseUntil===null||o.refreshLeaseUntil<=s)return c(o);if(s-a>=n.leaseWaitMs){if(o.tokens.accessTokenExpiresAt>s)return o;throw new C(`Timed out waiting for the session refresh lease`,0)}}}async function u(r){let i;try{i=await c(r)}catch(t){b(e,`refresh`,t),i=r}if(!i)return null;if(e.now()-i.lastValidatedAt<n.revalidateIntervalMs||!d(i.tokens))return i;let a=e.revalidateUser?await p(i):i.user;if(!a)return f(i);if(!d(i.tokens))return i;let o=await e.onRevalidate({session:i,accessToken:i.tokens.accessToken,user:a});if(o===null)return f(i);let s={data:o,lastValidatedAt:e.now()};return a!==i.user&&(s.user=a),t.update(i.id,s)}function d(t){return t.accessTokenExpiresAt-e.now()>n.requestTimeoutMs}async function f(n){return n.tokens.accessTokenExpiresAt<=e.now()?n:(await t.delete(n.id),null)}async function p(t){try{return await j(k(e),t.tokens.accessToken)}catch(n){return _(n)?null:(b(e,`revalidate`,n),t.user)}}async function m(e){await t.delete(e.id)}async function h(){if(!t.sweep)return 0;let r=e.now();return t.sweep({createdBefore:r-n.absoluteMaxAgeMs,validatedBefore:r-n.idleMaxAgeMs})}return{load:o,start:s,ensureFreshTokens:c,revalidate:u,revalidationDue:a,end:m,sweep:h}}function U(){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)},async sweep(t){let n=0;for(let[r,i]of e)(i.createdAt<t.createdBefore||i.lastValidatedAt<t.validatedBefore)&&(e.delete(r),n+=1);return n}}}function W(e){let t=S(e),n=H(t),r=B(t,n),i=L(t,n),a=F(t,n);function o(e){return{session:f(e,t.cookieName),cache:f(e,u(t.cookieName))}}async function c(e){return await s(t,e.cache,e.session,t.now())??n.load(e.session)}async function l(e){return c(o(e))}async function d(e){let r=await s(t,e.cache,e.session,t.now());if(r&&r.tokens.accessTokenExpiresAt>t.now())return r.tokens.accessToken;let i=await n.load(e.session);return i?(await n.ensureFreshTokens(i))?.tokens.accessToken??null:null}async function p(e){return d(o(e))}async function m(e,r){let i=await n.load(e.session);return i?t.store.update(i.id,{data:r(i.data)}):null}function h(){return n.sweep()}return{cookieName:t.cookieName,handlers:r,guard:i,resolveSession:a,sessionFromCookies:c,sessionFromRequest:l,accessTokenFromCookies:d,accessTokenFromRequest:p,updateSessionData:m,sweepSessions:h}}export{C as AccountsRequestError,y as DEFAULT_LIFETIMES,x as OAuthTokenError,v as SessionDecodeError,M as accountsEndpoint,u as cacheCookieBase,W as createAccountsAuth,U as createMemorySessionStore,j as fetchUserInfo,r as forwardCookies,_ as isAccessDenied,o as requestOrigin,a as sanitizeRedirectPath};
package/dist/next.d.ts CHANGED
@@ -1,10 +1,18 @@
1
- import { o as AccountsSession } from "./types-BcXM3ZEI.js";
2
- import { t as AccountsAuth } from "./index-CZZ3DRlf.js";
1
+ import { c as AccountsSession } from "./types-CEuhm7Nv.js";
2
+ import { t as AccountsAuth } from "./index-wc8vz71h.js";
3
+ import { NextRequest, NextResponse } from "next/server";
3
4
  //#region src/next/index.d.ts
5
+ /**
6
+ * The response a proxy returns to let a request through with the cookies the
7
+ * guard or the resolver issued: appended for the browser, and applied to the
8
+ * request as well, so the render reads the session the proxy just resolved
9
+ * instead of going back to the store for it.
10
+ */
11
+ declare function continueRequest(request: NextRequest, issued?: readonly string[]): NextResponse;
4
12
  declare function withNextRequestScope<Data>(auth: AccountsAuth<Data>): {
5
13
  getSession(): Promise<AccountsSession<Data> | null>;
6
14
  getAccessToken(): Promise<string | null>;
7
15
  updateSessionData(update: (data: Data) => Data): Promise<AccountsSession<Data> | null>;
8
16
  };
9
17
  //#endregion
10
- export { withNextRequestScope };
18
+ export { continueRequest, withNextRequestScope };
package/dist/next.js CHANGED
@@ -1 +1 @@
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};
1
+ import{d as e,n as t,s as n}from"./cache-kH1-Y-mz.js";import{cookies as r}from"next/headers";import{NextResponse as i,connection as a}from"next/server";function o(t,n=[]){e(t.headers,n);let r=i.next({request:{headers:t.headers}});for(let e of n)r.headers.append(`set-cookie`,e);return r}async function s(e){await a();let i=await r(),o=e=>i.get(n(e,!0))?.value||i.get(n(e,!1))?.value||null;return{session:o(e),cache:o(t(e))}}async function c(e){let i=await r(),a=t(e);for(let e of[n(a,!0),n(a,!1)])try{i.delete(e)}catch{}}function l(e){let t=e.cookieName;return{async getSession(){return e.sessionFromCookies(await s(t))},async getAccessToken(){return e.accessTokenFromCookies(await s(t))},async updateSessionData(n){let r=await e.updateSessionData(await s(t),n);return r&&await c(t),r}}}export{o as continueRequest,l as withNextRequestScope};
@@ -0,0 +1,11 @@
1
+ import { f as AccountsUser } from "./types-CEuhm7Nv.js";
2
+ //#region src/core/oidc.d.ts
3
+ type AccountsFetchOptions = {
4
+ issuer: string;
5
+ fetch?: typeof fetch;
6
+ timeoutMs?: number;
7
+ };
8
+ declare function accountsEndpoint(issuer: string, path: string): string;
9
+ declare function fetchUserInfo(options: AccountsFetchOptions, accessToken: string): Promise<AccountsUser>;
10
+ //#endregion
11
+ export { accountsEndpoint as n, fetchUserInfo as r, AccountsFetchOptions as t };
@@ -0,0 +1 @@
1
+ import{n as e,r as t,t as n}from"./types-ICH7-e-H.js";function r(e){let t=``;for(let n of e)t+=String.fromCharCode(n);return btoa(t).replace(/\+/g,`-`).replace(/\//g,`_`).replace(/=+$/g,``)}function i(e){return r(crypto.getRandomValues(new Uint8Array(e)))}async function a(e){let t=await crypto.subtle.digest(`SHA-256`,new TextEncoder().encode(e));return r(new Uint8Array(t))}function o(e,t){return new URL(t,e).toString()}function s(e){return{issuer:e.issuer,fetch:e.fetch,timeoutMs:e.lifetimes.requestTimeoutMs}}async function c(t,r,i={},a){let s=t.fetch??globalThis.fetch.bind(globalThis),c=t.timeoutMs??e.requestTimeoutMs,l=a===void 0?c:Math.min(c,a-Date.now());if(l<=0)throw new n(`Accounts request deadline passed`,0);return s(o(t.issuer,r),{...i,cache:`no-store`,signal:AbortSignal.timeout(l)})}async function l(e,t){let n=i(24),r=i(32),s=new URL(o(e.issuer,`/api/auth/oauth2/authorize`));return s.searchParams.set(`client_id`,e.clientId),s.searchParams.set(`redirect_uri`,new URL(e.paths.callback,t.origin).toString()),s.searchParams.set(`response_type`,`code`),s.searchParams.set(`scope`,e.scope),s.searchParams.set(`state`,n),s.searchParams.set(`code_challenge`,await a(r)),s.searchParams.set(`code_challenge_method`,`S256`),t.silent&&s.searchParams.set(`prompt`,`none`),{url:s.toString(),state:n,verifier:r}}function u(e,t){let n=new URL(o(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,n,r,i=!0){let a=new URLSearchParams({client_id:e.clientId,...n});e.clientSecret&&a.set(`client_secret`,e.clientSecret),e.audience&&i&&a.set(`resource`,e.audience);let o=await c(s(e),`/api/auth/oauth2/token`,{method:`POST`,headers:{"Content-Type":`application/x-www-form-urlencoded`},body:a},r);if(!o.ok){let a=await o.text().catch(()=>``),s=`unknown_error`,c=a;try{let e=JSON.parse(a);s=e.error??s,c=e.error_description??c}catch{}if(i&&e.audience&&s===`invalid_request`&&/resource/i.test(c))return d(e,n,r,!1);throw new t(o.status,s,c)}return await o.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===void 0?void 0:Date.now()+n)}async function m(e,t){let r=await c(e,`/api/auth/oauth2/userinfo`,{headers:{Authorization:`Bearer ${t}`}});if(!r.ok)throw new n(`Failed to load user info: ${r.status}`,r.status);let i=await r.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{f as a,i as c,h as d,u as i,p as l,c as n,s as o,l as r,m as s,o as t,a as u};
package/dist/testing.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { c as AccountsSessionStore } from "./types-BcXM3ZEI.js";
1
+ import { u as AccountsSessionStore } from "./types-CEuhm7Nv.js";
2
2
  //#region src/testing/index.d.ts
3
3
  /**
4
4
  * A behavioural conformance suite for the session store contract.
package/dist/testing.js CHANGED
@@ -1 +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
+ 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 with an empty patch still honours ifLeaseUntil`,run:async()=>{let n=await s();await n.claimRefreshLease(`sess_1`,170000002e4,t),e.equal(await n.update(`sess_1`,{},1700000999999),null),e.notEqual(await n.update(`sess_1`,{},170000002e4),null)}},{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)}},{name:`sweep removes the sessions created or last validated before the cutoffs`,run:async()=>{let t=await s(`sess_1`);if(!t.sweep)return;await t.create({...n(`sess_old`,i),createdAt:1699999999990}),await t.create({...n(`sess_idle`,i),lastValidatedAt:1699999999990});let r=await t.sweep({createdBefore:1699999999995,validatedBefore:1699999999995});e.equal(r,2),e.notEqual(await t.read(`sess_1`),null),e.equal(await t.read(`sess_old`),null),e.equal(await t.read(`sess_idle`),null),e.equal(await t.sweep({createdBefore:1699999999995,validatedBefore:1699999999995}),0)}}]}export{r as sessionStoreConformance};
@@ -0,0 +1,182 @@
1
+ //#region src/core/types.d.ts
2
+ type AccountsUser = {
3
+ id: string;
4
+ email: string;
5
+ name: string;
6
+ image: string | null;
7
+ };
8
+ type AccountsTokens = {
9
+ accessToken: string;
10
+ refreshToken: string | null;
11
+ idToken: string | null;
12
+ accessTokenExpiresAt: number;
13
+ };
14
+ type AccountsSession<Data> = {
15
+ id: string;
16
+ user: AccountsUser;
17
+ tokens: AccountsTokens;
18
+ data: Data;
19
+ createdAt: number;
20
+ lastValidatedAt: number;
21
+ refreshLeaseUntil: number | null;
22
+ };
23
+ type AccountsSessionPatch<Data> = Partial<Pick<AccountsSession<Data>, "user" | "tokens" | "data" | "lastValidatedAt" | "refreshLeaseUntil">>;
24
+ type SessionSweepCutoffs = {
25
+ createdBefore: number;
26
+ validatedBefore: number;
27
+ };
28
+ interface AccountsSessionStore<Data> {
29
+ create(session: AccountsSession<Data>): Promise<void>;
30
+ read(id: string): Promise<AccountsSession<Data> | null>;
31
+ update(id: string, patch: AccountsSessionPatch<Data>, ifLeaseUntil?: number): Promise<AccountsSession<Data> | null>;
32
+ claimRefreshLease(id: string, until: number, now: number): Promise<AccountsSession<Data> | null>;
33
+ /**
34
+ * Removes the session. With `ifLeaseUntil`, removes it only while that
35
+ * refresh lease is still the one on the row, and resolves false when it is
36
+ * not, so a refresher that lost its lease can tell the difference between
37
+ * "gone" and "someone else owns it".
38
+ */
39
+ delete(id: string, ifLeaseUntil?: number): Promise<boolean>;
40
+ /**
41
+ * Removes every session created before `createdBefore` or last validated
42
+ * before `validatedBefore` and resolves how many went. Expiry is otherwise
43
+ * enforced only when a session is read, so rows whose cookie never returns
44
+ * stay until something sweeps them. A store whose backend expires keys on
45
+ * its own leaves it out.
46
+ */
47
+ sweep?(cutoffs: SessionSweepCutoffs): Promise<number>;
48
+ }
49
+ type AccountsPaths = {
50
+ login: string;
51
+ callback: string;
52
+ logout: string;
53
+ logoutComplete: string;
54
+ error: string;
55
+ };
56
+ type AccountsLifetimes = {
57
+ absoluteMaxAgeMs: number;
58
+ idleMaxAgeMs: number;
59
+ revalidateIntervalMs: number;
60
+ refreshBeforeExpiryMs: number;
61
+ flowMaxAgeMs: number;
62
+ refreshLeaseMs: number;
63
+ leaseWaitMs: number;
64
+ /** Bounds every call to accounts; a refresh is also bounded by its lease. */
65
+ requestTimeoutMs: number;
66
+ /**
67
+ * How long the cache cookie serves requests after a revalidation or refresh
68
+ * failed, so a provider outage is retried on an interval rather than on
69
+ * every request.
70
+ */
71
+ retryIntervalMs: number;
72
+ };
73
+ type SignInInput<Data> = {
74
+ user: AccountsUser;
75
+ accessToken: string;
76
+ redirectPath: string;
77
+ previous: Data | null;
78
+ };
79
+ type RevalidateInput<Data> = {
80
+ session: AccountsSession<Data>;
81
+ accessToken: string;
82
+ /** The profile as accounts reports it now with `revalidateUser`, otherwise the copy taken at sign-in. */
83
+ user: AccountsUser;
84
+ };
85
+ /**
86
+ * Where a failure the flow handled rather than threw came from: the sign-in
87
+ * that became an error redirect, the store write behind `session_store_error`,
88
+ * the cache cookie that could not be written, the revalidation or the refresh
89
+ * that were kept quiet so a provider outage would not sign anyone out.
90
+ */
91
+ type AccountsErrorPhase = "sign_in" | "session_store" | "cache_cookie" | "revalidate" | "refresh";
92
+ type AccountsErrorEvent = {
93
+ phase: AccountsErrorPhase;
94
+ error: unknown;
95
+ };
96
+ type AccountsAuthConfig<Data> = {
97
+ issuer: string;
98
+ clientId: string;
99
+ clientSecret?: string;
100
+ scope?: string;
101
+ cookieName: string;
102
+ store: AccountsSessionStore<Data>;
103
+ paths?: Partial<AccountsPaths>;
104
+ lifetimes?: Partial<AccountsLifetimes>;
105
+ /**
106
+ * Returns the app data to store on the session, or null to deny the sign-in.
107
+ * Omit it when the app keeps nothing of its own on the session.
108
+ */
109
+ onSignIn?(input: SignInInput<Data>): Promise<Data | null>;
110
+ /**
111
+ * Re-derives the app data, or returns null to end the session. Throwing
112
+ * keeps it, because a failure to reach accounts says nothing about the user.
113
+ * Omit it when the app keeps nothing of its own on the session.
114
+ */
115
+ onRevalidate?(input: RevalidateInput<Data>): Promise<Data | null>;
116
+ /**
117
+ * Re-reads the account profile on every revalidation, hands it to
118
+ * `onRevalidate` and stores it, at the cost of one more call to accounts per
119
+ * user every `revalidateIntervalMs`. `session.user` is otherwise captured at
120
+ * sign-in and never changes again, so a renamed user keeps the old name
121
+ * until the next sign-in. Accounts rejecting the token (`isAccessDenied`)
122
+ * ends the session; any other failure keeps the captured copy.
123
+ */
124
+ revalidateUser?: boolean;
125
+ /**
126
+ * Runs a due revalidation after the response rather than in front of it,
127
+ * through the platform's own primitive: `after` from `next/server`, or
128
+ * `(task) => ctx.waitUntil(task())` on a Worker. The request is answered
129
+ * from the stored session with a cache cookie that lasts
130
+ * `lifetimes.retryIntervalMs`, and the first request after that reads the
131
+ * revalidated row, so no request waits on the accounts round trip and a
132
+ * session accounts has ended is honoured within one revalidation interval
133
+ * plus one retry interval. A token refresh is never deferred, and a
134
+ * scheduler that throws is answered by revalidating inline.
135
+ */
136
+ deferRevalidation?(task: () => Promise<void>): void;
137
+ /**
138
+ * Reports a failure the flow handled rather than threw. Every one of them is
139
+ * invisible otherwise, which turns `session_store_error` into a redirect
140
+ * with no cause attached, so the default writes to `console.error`; pass a
141
+ * no-op to silence it or your own logger to route it.
142
+ */
143
+ onError?(event: AccountsErrorEvent): void;
144
+ fetch?: typeof fetch;
145
+ now?: () => number;
146
+ cache?: AccountsCacheConfig;
147
+ audience?: string | false;
148
+ };
149
+ type AccountsCacheConfig = {
150
+ secret: string;
151
+ maxAgeMs?: number;
152
+ };
153
+ declare const DEFAULT_LIFETIMES: AccountsLifetimes;
154
+ declare class AccountsRequestError extends Error {
155
+ readonly status: number;
156
+ constructor(message: string, status: number);
157
+ }
158
+ /**
159
+ * A stored session that cannot be turned back into one: a token the codec
160
+ * cannot decrypt because another key wrote it, a column that does not hold
161
+ * what it should. The session manager reads it as the session being over,
162
+ * removes the row and sends the user back to sign in, so a store that decodes
163
+ * what it stored raises this rather than the failure underneath; every other
164
+ * store failure is a fault and propagates.
165
+ */
166
+ declare class SessionDecodeError extends Error {
167
+ constructor(cause: unknown);
168
+ }
169
+ declare class OAuthTokenError extends AccountsRequestError {
170
+ readonly code: string;
171
+ readonly description: string;
172
+ constructor(status: number, code: string, description: string);
173
+ }
174
+ /**
175
+ * Whether accounts rejected the token, the client, or the user, which ends
176
+ * the session, as opposed to failing to answer, which keeps it. Any 4xx but a
177
+ * timeout or a rate limit counts: the provider answers a revoked token or a
178
+ * disabled client with 400 as readily as with 401.
179
+ */
180
+ declare function isAccessDenied(error: unknown): boolean;
181
+ //#endregion
182
+ export { SessionSweepCutoffs as _, AccountsLifetimes as a, AccountsSession as c, AccountsTokens as d, AccountsUser as f, SessionDecodeError as g, RevalidateInput as h, AccountsErrorPhase as i, AccountsSessionPatch as l, OAuthTokenError as m, AccountsCacheConfig as n, AccountsPaths as o, DEFAULT_LIFETIMES as p, AccountsErrorEvent as r, AccountsRequestError as s, AccountsAuthConfig as t, AccountsSessionStore as u, SignInInput as v, isAccessDenied 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(e){let r=null,i=()=>r??=crypto.subtle.importKey(`raw`,e,{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 i(),new TextEncoder().encode(e))),a=new Uint8Array(12+r.length);return a.set(n),a.set(r,12),t(a)},async decrypt(e){let t=n(e);if(t.length<28)throw Error(`Malformed encrypted value`);let r;try{r=await crypto.subtle.decrypt({name:`AES-GCM`,iv:t.subarray(0,12)},await i(),t.subarray(12))}catch{throw Error(`Malformed encrypted value`)}return new TextDecoder().decode(r)}}}function i(t){if(!/^[0-9a-fA-F]{64}$/.test(t))throw Error(`AES-GCM codec key must be 64 hex characters (32 bytes)`);return r(e(t))}function a(e){let t=null;return()=>t??=crypto.subtle.digest(`SHA-256`,new TextEncoder().encode(e)).then(e=>r(new Uint8Array(e)))}const o={login:`/api/auth/login`,callback:`/api/auth/callback`,logout:`/api/auth/logout`,logoutComplete:`/api/auth/logout-complete`,error:`/auth/error`},s={absoluteMaxAgeMs:2592e6,idleMaxAgeMs:6048e5,revalidateIntervalMs:12e4,refreshBeforeExpiryMs:3e5,flowMaxAgeMs:6e5,refreshLeaseMs:2e4,leaseWaitMs:5e3,requestTimeoutMs:1e4,retryIntervalMs:1e4},c=Object.freeze({});function l({phase:e,error:t}){console.error(`[aui-auth] ${e}`,t)}function u(e,t,n){try{e.onError({phase:t,error:n})}catch{}}function d(e){return{...e,scope:e.scope??`openid profile email offline_access`,onSignIn:e.onSignIn??(async()=>c),onRevalidate:e.onRevalidate??(async()=>c),onError:e.onError??l,revalidateUser:e.revalidateUser??!1,paths:{...o,...e.paths},lifetimes:{...s,...e.lifetimes},fetch:e.fetch??globalThis.fetch.bind(globalThis),now:e.now??Date.now,cache:e.cache?{codec:a(e.cache.secret),maxAgeMs:e.cache.maxAgeMs??6e4}:null,audience:e.audience===!1?null:e.audience??new URL(`/api/auth`,e.issuer).toString()}}var f=class extends Error{status;constructor(e,t){super(e),this.name=`AccountsRequestError`,this.status=t}},p=class extends Error{constructor(e){super(`Stored session could not be decoded`,{cause:e}),this.name=`SessionDecodeError`}},m=class extends f{code;description;constructor(e,t,n){super(`Token request failed: ${e} ${t} ${n}`,e),this.name=`OAuthTokenError`,this.code=t,this.description=n}};const h=new Set([408,425,429]);function g(e){return e instanceof f&&e.status>=400&&e.status<500&&!h.has(e.status)}export{g as a,i as c,p as i,s as n,u as o,m as r,d as s,f as t};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aui-auth",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
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",
@@ -1 +0,0 @@
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};
@@ -1 +0,0 @@
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};
@@ -1,123 +0,0 @@
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
9
- type AccountsUser = {
10
- id: string;
11
- email: string;
12
- name: string;
13
- image: string | null;
14
- };
15
- type AccountsTokens = {
16
- accessToken: string;
17
- refreshToken: string | null;
18
- idToken: string | null;
19
- accessTokenExpiresAt: number;
20
- };
21
- type AccountsSession<Data> = {
22
- id: string;
23
- user: AccountsUser;
24
- tokens: AccountsTokens;
25
- data: Data;
26
- createdAt: number;
27
- lastValidatedAt: number;
28
- refreshLeaseUntil: number | null;
29
- };
30
- type AccountsSessionPatch<Data> = Partial<Pick<AccountsSession<Data>, "user" | "tokens" | "data" | "lastValidatedAt" | "refreshLeaseUntil">>;
31
- interface AccountsSessionStore<Data> {
32
- create(session: AccountsSession<Data>): Promise<void>;
33
- read(id: string): Promise<AccountsSession<Data> | null>;
34
- update(id: string, patch: AccountsSessionPatch<Data>, ifLeaseUntil?: number): Promise<AccountsSession<Data> | null>;
35
- claimRefreshLease(id: string, until: number, now: number): Promise<AccountsSession<Data> | null>;
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>;
43
- }
44
- type AccountsPaths = {
45
- login: string;
46
- callback: string;
47
- logout: string;
48
- logoutComplete: string;
49
- error: string;
50
- };
51
- type AccountsLifetimes = {
52
- absoluteMaxAgeMs: number;
53
- idleMaxAgeMs: number;
54
- revalidateIntervalMs: number;
55
- refreshBeforeExpiryMs: number;
56
- flowMaxAgeMs: number;
57
- refreshLeaseMs: number;
58
- leaseWaitMs: number;
59
- };
60
- type SignInInput<Data> = {
61
- user: AccountsUser;
62
- accessToken: string;
63
- redirectPath: string;
64
- previous: Data | null;
65
- };
66
- type RevalidateInput<Data> = {
67
- session: AccountsSession<Data>;
68
- accessToken: string;
69
- };
70
- type AccountsAuthConfig<Data> = {
71
- issuer: string;
72
- clientId: string;
73
- clientSecret?: string;
74
- scope?: string;
75
- cookieName: string;
76
- store: AccountsSessionStore<Data>;
77
- paths?: Partial<AccountsPaths>;
78
- lifetimes?: Partial<AccountsLifetimes>;
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>;
90
- fetch?: typeof fetch;
91
- now?: () => number;
92
- cache?: AccountsCacheConfig;
93
- audience?: string | false;
94
- };
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>;
102
- scope: string;
103
- paths: AccountsPaths;
104
- lifetimes: AccountsLifetimes;
105
- fetch: typeof fetch;
106
- now: () => number;
107
- cache: {
108
- codec: () => Promise<SessionCodec>;
109
- maxAgeMs: number;
110
- } | null;
111
- audience: string | null;
112
- };
113
- declare class AccountsRequestError extends Error {
114
- readonly status: number;
115
- constructor(message: string, status: number);
116
- }
117
- declare class OAuthTokenError extends AccountsRequestError {
118
- readonly code: string;
119
- readonly description: string;
120
- constructor(status: number, code: string, description: string);
121
- }
122
- //#endregion
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 };