@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.
- package/dist/cookie-auth.d.ts +44 -1
- package/dist/cookie-auth.js +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1 -1
- package/dist/page-gate.d.ts +34 -0
- package/dist/page-gate.js +1 -0
- package/dist/socials.d.ts +69 -0
- package/dist/socials.js +1 -0
- package/dist/team.js +1 -1
- package/package.json +3 -3
package/dist/cookie-auth.d.ts
CHANGED
|
@@ -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
|
|
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
|
package/dist/cookie-auth.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{config}from"@stacksjs/config";import{Auth}from"./authentication";function
|
|
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
|
+
}
|
package/dist/socials.js
ADDED
|
@@ -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=
|
|
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.
|
|
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.
|
|
65
|
-
"@stacksjs/router": "0.70.
|
|
64
|
+
"@stacksjs/error-handling": "0.70.297",
|
|
65
|
+
"@stacksjs/router": "0.70.297"
|
|
66
66
|
}
|
|
67
67
|
}
|