@stacksjs/auth 0.70.294 → 0.70.297

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.
@@ -1,4 +1,46 @@
1
1
  import { Auth } from './authentication';
2
+ /**
3
+ * The one name the auth cookie has.
4
+ *
5
+ * There used to be two. `authCookie()` wrote `stacks_auth` (via
6
+ * `config.auth.cookie.name`, a key that did not exist on `AuthOptions`, so no
7
+ * app using `satisfies AuthConfig` could even set it), while the Auth
8
+ * middleware, `team.ts` and the stx page gate all read
9
+ * `config.auth.defaultTokenName` — `auth-token`. A cookie the framework wrote
10
+ * was never one the framework read, which is why apps ended up hand-writing a
11
+ * token pack into `localStorage` from an inline script instead
12
+ * (stacksjs/stacks#2236).
13
+ *
14
+ * Resolution order:
15
+ * 1. an explicit `options.name`
16
+ * 2. `config.auth.cookie.name` — the supported key
17
+ * 3. `config.auth.defaultTokenName` — DEPRECATED, honoured so an app that
18
+ * had renamed it (and thereby renamed the cookie those readers wanted)
19
+ * keeps working. Ignored with a warning when it is not a legal cookie
20
+ * name, which it very often is not: it is a human-readable token label
21
+ * like `Web Session`.
22
+ * 4. `auth-token`
23
+ */
24
+ export declare function authCookieName(options?: AuthCookieOptions): string;
25
+ /**
26
+ * Whether the auth cookie should carry `Secure`, decided from what the app
27
+ * demonstrably is rather than what its environment is called.
28
+ *
29
+ * The old rule was "Secure unless APP_ENV looks development-ish", and it
30
+ * failed open: `.env.example` ships `APP_ENV=development`, so an HTTPS
31
+ * deployment that never changed the env name served its session token
32
+ * without `Secure` (stacksjs/stacks#2275). Now the URL decides:
33
+ *
34
+ * - an `https://` app URL is always Secure
35
+ * - a plain-HTTP or scheme-less URL drops Secure only on a loopback host
36
+ * (localhost, `*.localhost`, 127.0.0.1) — the one place plain HTTP is a
37
+ * development reality rather than a misconfiguration
38
+ * - with no URL configured at all, only the unambiguous `local` / `dev`
39
+ * environment names opt out; `development` no longer does
40
+ *
41
+ * Exported for tests; `authCookie()` feeds it the live config.
42
+ */
43
+ export declare function shouldSecureAuthCookie(app?: { url?: unknown, env?: unknown }): boolean;
2
44
  /**
3
45
  * The `Set-Cookie` value that signs a browser in.
4
46
  *
@@ -56,7 +98,8 @@ export declare function logoutCookie(request: Request | { headers: Headers }, op
56
98
  *
57
99
  * The cookie is httpOnly (a page script never needs it), SameSite=Lax (so a
58
100
  * link from an email still arrives signed in, while a cross-site POST does
59
- * not), and Secure everywhere except local HTTP development.
101
+ * not), and Secure everywhere except a plain-HTTP loopback app URL — see
102
+ * `shouldSecureAuthCookie` for exactly how that is decided.
60
103
  */
61
104
  export declare interface AuthCookieOptions {
62
105
  name?: string
@@ -1 +1 @@
1
- import{config}from"@stacksjs/config";import{Auth}from"./authentication";function cookieName(options){return options?.name??config.auth?.cookie?.name??"stacks_auth"}function defaultMaxAge(){const milliseconds=Number(config.auth?.tokenExpiry??3600000);if(!Number.isFinite(milliseconds)||milliseconds<=0)return 3600;return Math.max(60,Math.round(milliseconds/1000))}function isLocal(){const environment=String(config.app?.env??process.env.APP_ENV??"");return environment==="local"||environment==="development"||environment==="dev"}export function authCookie(token,options={}){const parts=[`${cookieName(options)}=${encodeURIComponent(token)}`,`Path=${options.path??"/"}`,`Max-Age=${options.maxAge??defaultMaxAge()}`,"HttpOnly",`SameSite=${options.sameSite??"Lax"}`];if(options.domain)parts.push(`Domain=${options.domain}`);if(options.secure??!isLocal())parts.push("Secure");return parts.join("; ")}export function clearAuthCookie(options={}){return authCookie("",{...options,maxAge:0})}export function authCookieToken(request,options={}){const header=request.headers.get("cookie");if(!header)return;const wanted=cookieName(options);for(const pair of header.split(";")){const index=pair.indexOf("=");if(index===-1)continue;if(pair.slice(0,index).trim()!==wanted)continue;const value=decodeURIComponent(pair.slice(index+1).trim());return value.length>0?value:void 0}return}export async function userFromCookie(request,options={}){const token=authCookieToken(request,options);if(!token)return;return Auth.getUserFromToken(token)}export async function cookieCheck(request,options={}){return Boolean(await userFromCookie(request,options))}export async function logoutCookie(request,options={}){const token=authCookieToken(request,options);if(token)try{await Auth.revokeToken(token)}catch{}return clearAuthCookie(options)}
1
+ import{config}from"@stacksjs/config";import{log}from"@stacksjs/logging";import{Auth}from"./authentication";const COOKIE_NAME_RE=/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;export function authCookieName(options){if(options?.name)return options.name;const configured=config.auth?.cookie?.name;if(typeof configured==="string"&&configured.length>0)return configured;const legacy=config.auth?.defaultTokenName;if(typeof legacy==="string"&&legacy.length>0&&legacy!=="auth-token"){if(COOKIE_NAME_RE.test(legacy))return legacy;console.warn(`[auth] config.auth.defaultTokenName ("${legacy}") is not a valid cookie name and is being ignored for cookie naming; using "auth-token". defaultTokenName is a personal access token label, not a `+"cookie name \u2014 set config.auth.cookie.name instead (stacksjs/stacks#2236).")}return"auth-token"}function cookieName(options){return authCookieName(options)}function defaultMaxAge(){const milliseconds=Number(config.auth?.tokenExpiry??3600000);if(!Number.isFinite(milliseconds)||milliseconds<=0)return 3600;return Math.max(60,Math.round(milliseconds/1000))}function isLoopbackHost(hostname){const host=hostname.toLowerCase();return host==="localhost"||host.endsWith(".localhost")||host==="127.0.0.1"||host==="0.0.0.0"||host==="::1"||host==="[::1]"}export function shouldSecureAuthCookie(app=config.app??{}){const rawUrl=String(app?.url??process.env.APP_URL??"").trim();if(rawUrl)try{const url=new URL(rawUrl.includes("://")?rawUrl:`http://${rawUrl}`);if(url.protocol==="https:")return!0;return!isLoopbackHost(url.hostname)}catch{}const environment=String(app?.env??process.env.APP_ENV??"");return!(environment==="local"||environment==="dev")}let warnedInsecureOverride=!1;export function authCookie(token,options={}){const parts=[`${cookieName(options)}=${encodeURIComponent(token)}`,`Path=${options.path??"/"}`,`Max-Age=${options.maxAge??defaultMaxAge()}`,"HttpOnly",`SameSite=${options.sameSite??"Lax"}`];if(options.domain)parts.push(`Domain=${options.domain}`);if(options.secure??shouldSecureAuthCookie())parts.push("Secure");else if(options.secure===!1&&shouldSecureAuthCookie()&&!warnedInsecureOverride){warnedInsecureOverride=!0;log.warn("[auth] authCookie() was asked for secure: false while the app URL is HTTPS \u2014 the session cookie will also travel over plain HTTP.")}return parts.join("; ")}export function clearAuthCookie(options={}){return authCookie("",{...options,maxAge:0})}export function authCookieToken(request,options={}){const header=request.headers.get("cookie");if(!header)return;const wanted=cookieName(options);for(const pair of header.split(";")){const index=pair.indexOf("=");if(index===-1)continue;if(pair.slice(0,index).trim()!==wanted)continue;const value=decodeURIComponent(pair.slice(index+1).trim());return value.length>0?value:void 0}return}export async function userFromCookie(request,options={}){const token=authCookieToken(request,options);if(!token)return;return Auth.getUserFromToken(token)}export async function cookieCheck(request,options={}){return Boolean(await userFromCookie(request,options))}export async function logoutCookie(request,options={}){const token=authCookieToken(request,options);if(token)try{await Auth.revokeToken(token)}catch{}return clearAuthCookie(options)}
package/dist/index.d.ts CHANGED
@@ -26,6 +26,10 @@ export * from './email-verification';
26
26
  export * from './session-auth';
27
27
  // Cookie-carried access tokens, for server-rendered pages.
28
28
  export * from './cookie-auth';
29
+ // The stx page gate (`middleware: ['auth' | 'guest']`), token-validating.
30
+ export * from './page-gate';
31
+ // Social sign-in: which local user a provider identity resolves to.
32
+ export * from './socials';
29
33
  // TOTP (Two-Factor Authentication) - re-export from ts-auth
30
34
  export {
31
35
  generateTOTP,
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export*from"./authentication";export*from"./authenticator";export*from"./client";export*from"./middleware";export*from"./rate-limiter";export*from"./passkey";export*from"./password/reset";export*from"./register";export*from"./user";export*from"./tokens";export*from"./gate";export*from"./policy";export*from"./authorizable";export*from"./permissions";export*from"./rbac";export{createBqbRbacStore}from"./rbac-store-bqb";export{DEFAULT_ROLE_PACKS,seedDefaultRoles}from"./rbac-seed";export*from"./email-verification";export*from"./session-auth";export*from"./cookie-auth";export{generateTOTP,verifyTOTP,generateTOTPSecret,totpKeyUri}from"@stacksjs/ts-auth";export*from"./two-factor";export*from"./team";
1
+ export*from"./authentication";export*from"./authenticator";export*from"./client";export*from"./middleware";export*from"./rate-limiter";export*from"./passkey";export*from"./password/reset";export*from"./register";export*from"./user";export*from"./tokens";export*from"./gate";export*from"./policy";export*from"./authorizable";export*from"./permissions";export*from"./rbac";export{createBqbRbacStore}from"./rbac-store-bqb";export{DEFAULT_ROLE_PACKS,seedDefaultRoles}from"./rbac-seed";export*from"./email-verification";export*from"./session-auth";export*from"./cookie-auth";export*from"./page-gate";export*from"./socials";export{generateTOTP,verifyTOTP,generateTOTPSecret,totpKeyUri}from"@stacksjs/ts-auth";export*from"./two-factor";export*from"./team";
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The `middleware` map to pass to stx-serve, overriding its existence-only
3
+ * built-ins with token-validating equivalents.
4
+ */
5
+ export declare function stxPageAuthMiddleware(options?: PageGateOptions): Record<'auth' | 'guest', StxPageMiddleware>;
6
+ /**
7
+ * The stx page gate, with the token actually validated.
8
+ *
9
+ * bun-plugin-stx's built-in `auth` / `guest` middleware — the pair behind
10
+ * `definePageMeta({ middleware: ['auth'] })` — checks only that the auth
11
+ * cookie EXISTS. `document.cookie = 'auth-token=x'` in a console satisfied it
12
+ * on every protected page, and a stale cookie trapped a signed-out visitor on
13
+ * `guest` pages (stacksjs/stacks#2274).
14
+ *
15
+ * stx-serve builds its registry as `{ ...builtInMiddleware, ...options.middleware }`,
16
+ * so entries supplied here win over the built-ins by construction. These
17
+ * validate the cookie's token exactly as a bearer token would be — through
18
+ * `Auth.getUserFromToken()`, so a forged, revoked or expired token redirects
19
+ * the same as no token at all.
20
+ *
21
+ * This gates SSR page rendering; API requests validate their own credentials.
22
+ * Both the dev views server and `buddy serve` register it.
23
+ */
24
+ declare interface StxPageContext {
25
+ cookies: Record<string, string>
26
+ redirect: (to: string, status?: number) => Response
27
+ }
28
+ export declare interface PageGateOptions {
29
+ cookieName?: string
30
+ redirectTo?: string
31
+ home?: string
32
+ validate?: (token: string) => Promise<unknown>
33
+ }
34
+ declare type StxPageMiddleware = (req: Request, ctx: StxPageContext) => Promise<Response | null>;
@@ -0,0 +1 @@
1
+ import{Auth}from"./authentication";import{authCookieName}from"./cookie-auth";export function stxPageAuthMiddleware(options={}){const redirectTo=options.redirectTo??"/login",home=options.home??"/",validate=options.validate??((token)=>Auth.getUserFromToken(token));async function signedInUser(ctx){const token=ctx.cookies[authCookieName({name:options.cookieName})];if(!token)return;try{return await validate(token)}catch{return}}return{auth:async(_req,ctx)=>{if(await signedInUser(ctx))return null;return ctx.redirect(redirectTo)},guest:async(_req,ctx)=>{if(await signedInUser(ctx))return Response.redirect(home,302);return null}}}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Resolve a provider identity to a local user id, creating the user and/or
3
+ * the link row according to the configured matching policy.
4
+ *
5
+ * Throws {@link SocialSignInRefusedError} when the policy says no; every
6
+ * other failure mode (db down, etc.) throws its own error untouched.
7
+ */
8
+ export declare function resolveSocialSignIn(provider: string, identity: SocialIdentity, store?: SocialSignInStore): Promise<SocialSignInResult>;
9
+ /**
10
+ * The social sign-in policy: which local user a provider identity resolves to
11
+ * (stacksjs/stacks#2276).
12
+ *
13
+ * `@stacksjs/socials` covers the OAuth exchange and stops at a normalized
14
+ * provider user; `Auth.loginUsingId()` covers session issuance. This is the
15
+ * piece between them — the find-or-create decision every app previously
16
+ * hand-rolled, and the one they usually got wrong in the same way: linking an
17
+ * unverified provider email onto an existing local account, which lets an
18
+ * attacker register a victim's address at any provider that skips email
19
+ * verification and inherit the local account.
20
+ *
21
+ * That takeover guard is therefore NOT configurable: a provider identity only
22
+ * links to an existing user by email when the provider explicitly vouches for
23
+ * the address (`emailVerified === true`). What IS configurable is what happens
24
+ * when no link row exists — `config.auth.socials.matching`:
25
+ *
26
+ * - `'link'` (default): a verified-email match links to the existing user;
27
+ * no match creates a new user; an unverified match refuses.
28
+ * - `'create'`: never match by email — a first-time provider identity always
29
+ * becomes a new user. No linking means no takeover surface at all, at the
30
+ * cost of duplicate accounts for people who registered with a password
31
+ * first.
32
+ * - `'refuse'`: only identities linked beforehand (from a signed-in session)
33
+ * may sign in. The strictest posture; social becomes a second factor for
34
+ * existing accounts rather than an acquisition channel.
35
+ */
36
+ /** What the policy needs from a provider profile — @stacksjs/socials' `SocialUser` satisfies this. */
37
+ export declare interface SocialIdentity {
38
+ id: string
39
+ name?: string | null
40
+ nickname?: string | null
41
+ email?: string | null
42
+ emailVerified?: boolean | null
43
+ avatar?: string | null
44
+ }
45
+ export declare interface SocialSignInResult {
46
+ userId: number
47
+ createdUser: boolean
48
+ linked: boolean
49
+ }
50
+ /**
51
+ * Storage the policy runs against. The default store reads and writes the
52
+ * `social_accounts` and `users` tables; tests inject their own.
53
+ */
54
+ export declare interface SocialSignInStore {
55
+ findLink: (provider: string, providerUserId: string) => Promise<{ userId: number } | undefined>
56
+ createLink: (link: { userId: number, provider: string, providerUserId: string, providerEmail: string | null }) => Promise<void>
57
+ findUserIdByEmail: (email: string) => Promise<number | undefined>
58
+ createUser: (attrs: { name: string, email: string }) => Promise<number>
59
+ }
60
+ export type SocialMatchingPolicy = 'link' | 'create' | 'refuse';
61
+ export type SocialRefusalReason = | 'no-linked-account'
62
+ | 'unverified-provider-email'
63
+ | 'no-email-to-create-with'
64
+ | 'email-already-registered';
65
+ /** A sign-in the policy declines. `reason` is safe to show a visitor. */
66
+ export declare class SocialSignInRefusedError extends Error {
67
+ readonly reason: SocialRefusalReason;
68
+ constructor(reason: SocialRefusalReason, message: string);
69
+ }
@@ -0,0 +1 @@
1
+ import{config}from"@stacksjs/config";import{db}from"@stacksjs/database";export class SocialSignInRefusedError extends Error{reason;constructor(reason,message){super(message);this.name="SocialSignInRefusedError";this.reason=reason}}function defaultStore(){return{async findLink(provider,providerUserId){const row=await db.selectFrom("social_accounts").select(["user_id"]).where("provider","=",provider).where("provider_user_id","=",providerUserId).executeTakeFirst();return row?{userId:Number(row.user_id)}:void 0},async createLink(link){await db.insertInto("social_accounts").values({user_id:link.userId,provider:link.provider,provider_user_id:link.providerUserId,provider_email:link.providerEmail,created_at:new Date().toISOString().slice(0,19).replace("T"," ")}).execute()},async findUserIdByEmail(email){const row=await db.selectFrom("users").select(["id"]).where("email","=",email).executeTakeFirst();return row?Number(row.id):void 0},async createUser(attrs){const{makeHash}=await import("@stacksjs/security"),password=await makeHash(crypto.randomUUID(),{algorithm:"bcrypt"}),created=await globalThis.User?.create?.({name:attrs.name,email:attrs.email,password});if(created?.id)return Number(created.id);const row=await db.insertInto("users").values({name:attrs.name,email:attrs.email,password,created_at:new Date().toISOString().slice(0,19).replace("T"," ")}).executeTakeFirst();return Number(row?.insertId??0)}}}function configuredMatching(){const raw=config.auth?.socials?.matching;return raw==="create"||raw==="refuse"?raw:"link"}export async function resolveSocialSignIn(provider,identity,store=defaultStore()){const providerUserId=String(identity.id??"").trim();if(!provider||!providerUserId)throw TypeError("[auth/socials] a provider name and provider user id are required.");const existing=await store.findLink(provider,providerUserId);if(existing)return{userId:existing.userId,createdUser:!1,linked:!1};const matching=configuredMatching();if(matching==="refuse")throw new SocialSignInRefusedError("no-linked-account",`No ${provider} account is linked here. Sign in with your password, then connect ${provider} from your account settings.`);const email=identity.email?.trim().toLowerCase()||null,displayName=identity.name?.trim()||identity.nickname?.trim()||(email??`${provider} user`);if(matching==="link"&&email){const matchedUserId=await store.findUserIdByEmail(email);if(matchedUserId!==void 0){if(identity.emailVerified!==!0)throw new SocialSignInRefusedError("unverified-provider-email",`${provider} did not verify ${email}, and an account with that address already exists. Sign in with your password, then connect ${provider} from your account settings.`);await store.createLink({userId:matchedUserId,provider,providerUserId,providerEmail:email});return{userId:matchedUserId,createdUser:!1,linked:!0}}}if(!email)throw new SocialSignInRefusedError("no-email-to-create-with",`${provider} shared no email address, so an account cannot be created. Grant the email permission at ${provider}, or register with an email first.`);if(matching==="create"&&await store.findUserIdByEmail(email)!==void 0)throw new SocialSignInRefusedError("email-already-registered",`An account with ${email} already exists. Sign in with your password, then connect ${provider} from your account settings.`);const userId=await store.createUser({name:displayName,email});await store.createLink({userId,provider,providerUserId,providerEmail:email});return{userId,createdUser:!0,linked:!0}}
package/dist/team.js CHANGED
@@ -1 +1 @@
1
- import{config}from"@stacksjs/config";import{db}from"@stacksjs/database";import{Auth}from"./authentication";import{sessionUser}from"./session-auth";export const ACTIVE_TEAM_COOKIE="active_team";export function selectActiveTeam(opts){const priority=opts.rolePriority??{owner:0,admin:1},sorted=[...opts.memberships??[]].sort((a,b)=>(priority[a.role??""]??2)-(priority[b.role??""]??2)),active=opts.activeTeamId==null?null:Number(opts.activeTeamId);if(active!=null&&Number.isFinite(active)){const member=sorted.find((x)=>Number(x.team_id)===active);if(member)return{teamId:active,role:member.role??null};if(opts.allowAnyTeam)return{teamId:active,role:"admin"}}const first=sorted[0];return first?{teamId:Number(first.team_id),role:first.role??null}:{teamId:null,role:null}}export function getActiveTeamPreference(request){const raw=request.cookies?.get(ACTIVE_TEAM_COOKIE);if(!raw)return null;const n=Number(raw);return Number.isFinite(n)&&n>0?n:null}export function buildActiveTeamCookie(teamId,opts={}){const maxAge=Math.max(0,Math.floor(opts.maxAgeSeconds??31536000)),parts=[`${ACTIVE_TEAM_COOKIE}=${encodeURIComponent(String(teamId))}`,"Path=/","HttpOnly","SameSite=Lax",`Max-Age=${maxAge}`];if(opts.secure)parts.push("Secure");return parts.join("; ")}export function clearActiveTeamCookie(){return`${ACTIVE_TEAM_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`}export async function resolveAuthenticatedMembership(request){const user=await resolveAuthenticatedUser(request);if(!user?.id)return null;const memberships=await db.selectFrom("team_members").where("user_id","=",user.id).where("status","=","active").select(["team_id","role"]).execute();if(memberships.length===0)return null;const picked=selectActiveTeam({memberships:memberships.map((m)=>({team_id:Number(m.team_id),role:m.role==null?null:String(m.role)})),activeTeamId:getActiveTeamPreference(request)});return picked.teamId!=null?{teamId:picked.teamId,role:picked.role??""}:null}export async function resolveTeamContext(request,opts={}){const empty={user:null,teamId:null,role:null,teams:[],activeTeamId:null},guardName=config.auth?.default||"api",user=(config.auth?.guards?.[guardName]?.driver||"token")==="session"?await resolveSessionUser(request):await resolveTokenUser(request);if(!user?.id)return empty;const memberships=await db.selectFrom("team_members").where("user_id","=",user.id).where("status","=","active").select(["team_id","role"]).execute(),allowAny=opts.allowAnyTeam?!!opts.allowAnyTeam(user):!1,activeTeamId=getActiveTeamPreference(request),picked=selectActiveTeam({memberships:memberships.map((m)=>({team_id:Number(m.team_id),role:String(m.role)})),activeTeamId,allowAnyTeam:allowAny}),roleByTeam=new Map(memberships.map((m)=>[Number(m.team_id),String(m.role)]));let teamRows=[];if(allowAny)teamRows=await db.selectFrom("teams").select(["id","name"]).execute();else{const ids=[...roleByTeam.keys()];teamRows=ids.length?await db.selectFrom("teams").whereIn("id",ids).select(["id","name"]).execute():[]}const teams=teamRows.map((t)=>({id:Number(t.id),name:String(t.name),role:roleByTeam.get(Number(t.id))||"viewer"})).sort((a,b)=>a.name.localeCompare(b.name));return{user,teamId:picked.teamId,role:picked.role,teams,activeTeamId}}export async function resolveAuthenticatedTeamId(request){const membership=await resolveAuthenticatedMembership(request);return membership?membership.teamId:null}export async function resolveAuthenticatedUser(request){const guardName=config.auth?.default||"api",user=((config.auth?.guards?.[guardName]||{driver:"token"}).driver||"token")==="session"?await resolveSessionUser(request):await resolveTokenUser(request);return user?.id?user:void 0}async function resolveSessionUser(request){const sessionId=request.cookies?.get("session_id");if(!sessionId)return;return sessionUser(sessionId)}async function resolveTokenUser(request){const cookieName=config.auth?.defaultTokenName||"auth-token",bearer=(typeof request.bearerToken==="function"?request.bearerToken():void 0)??request.cookies?.get(cookieName);if(!bearer)return;return Auth.getUserFromToken(bearer)}
1
+ import{config}from"@stacksjs/config";import{db}from"@stacksjs/database";import{Auth}from"./authentication";import{sessionUser}from"./session-auth";import{authCookieName}from"./cookie-auth";export const ACTIVE_TEAM_COOKIE="active_team";export function selectActiveTeam(opts){const priority=opts.rolePriority??{owner:0,admin:1},sorted=[...opts.memberships??[]].sort((a,b)=>(priority[a.role??""]??2)-(priority[b.role??""]??2)),active=opts.activeTeamId==null?null:Number(opts.activeTeamId);if(active!=null&&Number.isFinite(active)){const member=sorted.find((x)=>Number(x.team_id)===active);if(member)return{teamId:active,role:member.role??null};if(opts.allowAnyTeam)return{teamId:active,role:"admin"}}const first=sorted[0];return first?{teamId:Number(first.team_id),role:first.role??null}:{teamId:null,role:null}}export function getActiveTeamPreference(request){const raw=request.cookies?.get(ACTIVE_TEAM_COOKIE);if(!raw)return null;const n=Number(raw);return Number.isFinite(n)&&n>0?n:null}export function buildActiveTeamCookie(teamId,opts={}){const maxAge=Math.max(0,Math.floor(opts.maxAgeSeconds??31536000)),parts=[`${ACTIVE_TEAM_COOKIE}=${encodeURIComponent(String(teamId))}`,"Path=/","HttpOnly","SameSite=Lax",`Max-Age=${maxAge}`];if(opts.secure)parts.push("Secure");return parts.join("; ")}export function clearActiveTeamCookie(){return`${ACTIVE_TEAM_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`}export async function resolveAuthenticatedMembership(request){const user=await resolveAuthenticatedUser(request);if(!user?.id)return null;const memberships=await db.selectFrom("team_members").where("user_id","=",user.id).where("status","=","active").select(["team_id","role"]).execute();if(memberships.length===0)return null;const picked=selectActiveTeam({memberships:memberships.map((m)=>({team_id:Number(m.team_id),role:m.role==null?null:String(m.role)})),activeTeamId:getActiveTeamPreference(request)});return picked.teamId!=null?{teamId:picked.teamId,role:picked.role??""}:null}export async function resolveTeamContext(request,opts={}){const empty={user:null,teamId:null,role:null,teams:[],activeTeamId:null},guardName=config.auth?.default||"api",user=(config.auth?.guards?.[guardName]?.driver||"token")==="session"?await resolveSessionUser(request):await resolveTokenUser(request);if(!user?.id)return empty;const memberships=await db.selectFrom("team_members").where("user_id","=",user.id).where("status","=","active").select(["team_id","role"]).execute(),allowAny=opts.allowAnyTeam?!!opts.allowAnyTeam(user):!1,activeTeamId=getActiveTeamPreference(request),picked=selectActiveTeam({memberships:memberships.map((m)=>({team_id:Number(m.team_id),role:String(m.role)})),activeTeamId,allowAnyTeam:allowAny}),roleByTeam=new Map(memberships.map((m)=>[Number(m.team_id),String(m.role)]));let teamRows=[];if(allowAny)teamRows=await db.selectFrom("teams").select(["id","name"]).execute();else{const ids=[...roleByTeam.keys()];teamRows=ids.length?await db.selectFrom("teams").whereIn("id",ids).select(["id","name"]).execute():[]}const teams=teamRows.map((t)=>({id:Number(t.id),name:String(t.name),role:roleByTeam.get(Number(t.id))||"viewer"})).sort((a,b)=>a.name.localeCompare(b.name));return{user,teamId:picked.teamId,role:picked.role,teams,activeTeamId}}export async function resolveAuthenticatedTeamId(request){const membership=await resolveAuthenticatedMembership(request);return membership?membership.teamId:null}export async function resolveAuthenticatedUser(request){const guardName=config.auth?.default||"api",user=((config.auth?.guards?.[guardName]||{driver:"token"}).driver||"token")==="session"?await resolveSessionUser(request):await resolveTokenUser(request);return user?.id?user:void 0}async function resolveSessionUser(request){const sessionId=request.cookies?.get("session_id");if(!sessionId)return;return sessionUser(sessionId)}async function resolveTokenUser(request){const cookieName=authCookieName(),bearer=(typeof request.bearerToken==="function"?request.bearerToken():void 0)??request.cookies?.get(cookieName);if(!bearer)return;return Auth.getUserFromToken(bearer)}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/auth",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.70.294",
5
+ "version": "0.70.297",
6
6
  "description": "A more simplistic way to authenticate.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -61,7 +61,7 @@
61
61
  },
62
62
  "devDependencies": {
63
63
  "better-dx": "^0.2.17",
64
- "@stacksjs/error-handling": "0.70.294",
65
- "@stacksjs/router": "0.70.294"
64
+ "@stacksjs/error-handling": "0.70.297",
65
+ "@stacksjs/router": "0.70.297"
66
66
  }
67
67
  }