@stacksjs/auth 0.74.58 → 0.74.60
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/browser-session.d.ts +3 -0
- package/dist/browser-session.js +1 -1
- package/dist/register.d.ts +2 -1
- package/dist/register.js +1 -1
- package/dist/two-factor.d.ts +9 -2
- package/dist/two-factor.js +1 -1
- package/package.json +13 -13
|
@@ -10,6 +10,8 @@ export declare function resolveBrowserSessionPolicy(remember: unknown, auth?: Br
|
|
|
10
10
|
* This prevents a cookie from outliving or prematurely hiding its credential.
|
|
11
11
|
*/
|
|
12
12
|
export declare function authCookieForBrowserSession(token: string, expiresIn: number | undefined): string;
|
|
13
|
+
/** Resolve a configured same-origin logout target for HTML navigation only. */
|
|
14
|
+
export declare function browserSessionLogoutRedirect(request: HeaderRequest | undefined, auth?: BrowserSessionPolicyConfig): string | undefined;
|
|
13
15
|
export declare interface ResolvedBrowserSessionPolicy {
|
|
14
16
|
remembered: boolean
|
|
15
17
|
lifetimeMs: number
|
|
@@ -17,3 +19,4 @@ export declare interface ResolvedBrowserSessionPolicy {
|
|
|
17
19
|
withRefreshToken: boolean
|
|
18
20
|
}
|
|
19
21
|
declare type BrowserSessionPolicyConfig = Pick<Partial<AuthOptions>, 'tokenExpiry' | 'browserSession'>;
|
|
22
|
+
declare type HeaderRequest = { headers?: { get: (name: string) => string | null } }
|
package/dist/browser-session.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{config}from"@stacksjs/config";import{authCookie}from"./cookie";const REMEMBERED_INPUTS=new Set(["1","true","on","yes"]);export function browserSessionRemembered(value){if(value===!0||value===1)return!0;if(typeof value!=="string")return!1;return REMEMBERED_INPUTS.has(value.trim().toLowerCase())}function validLifetime(value,setting){const milliseconds=Number(value);if(!Number.isFinite(milliseconds)||milliseconds<=0)throw TypeError(`[auth] config.auth.${setting} must be a positive lifetime in milliseconds.`);return milliseconds}export function resolveBrowserSessionPolicy(remember,auth=config.auth){const baselineLifetime=validLifetime(auth.browserSession?.baselineLifetime??auth.tokenExpiry??3600000,auth.browserSession?.baselineLifetime===void 0?"tokenExpiry":"browserSession.baselineLifetime"),rememberedLifetime=validLifetime(auth.browserSession?.rememberedLifetime??baselineLifetime,"browserSession.rememberedLifetime"),withRefreshToken=auth.browserSession?.withRefreshToken??!0;if(typeof withRefreshToken!=="boolean")throw TypeError("[auth] config.auth.browserSession.withRefreshToken must be a boolean.");const remembered=browserSessionRemembered(remember),lifetimeMs=remembered?rememberedLifetime:baselineLifetime;return{remembered,lifetimeMs,expiresInMinutes:lifetimeMs/60000,withRefreshToken}}export function authCookieForBrowserSession(token,expiresIn){const seconds=Number(expiresIn);if(!Number.isInteger(seconds)||seconds<=0)throw TypeError("[auth] The issued token lifetime must be a positive integer number of seconds.");return authCookie(token,{maxAge:seconds})}
|
|
1
|
+
import{config}from"@stacksjs/config";import{authCookie}from"./cookie";const REMEMBERED_INPUTS=new Set(["1","true","on","yes"]);export function browserSessionRemembered(value){if(value===!0||value===1)return!0;if(typeof value!=="string")return!1;return REMEMBERED_INPUTS.has(value.trim().toLowerCase())}function validLifetime(value,setting){const milliseconds=Number(value);if(!Number.isFinite(milliseconds)||milliseconds<=0)throw TypeError(`[auth] config.auth.${setting} must be a positive lifetime in milliseconds.`);return milliseconds}export function resolveBrowserSessionPolicy(remember,auth=config.auth){const baselineLifetime=validLifetime(auth.browserSession?.baselineLifetime??auth.tokenExpiry??3600000,auth.browserSession?.baselineLifetime===void 0?"tokenExpiry":"browserSession.baselineLifetime"),rememberedLifetime=validLifetime(auth.browserSession?.rememberedLifetime??baselineLifetime,"browserSession.rememberedLifetime"),withRefreshToken=auth.browserSession?.withRefreshToken??!0;if(typeof withRefreshToken!=="boolean")throw TypeError("[auth] config.auth.browserSession.withRefreshToken must be a boolean.");const remembered=browserSessionRemembered(remember),lifetimeMs=remembered?rememberedLifetime:baselineLifetime;return{remembered,lifetimeMs,expiresInMinutes:lifetimeMs/60000,withRefreshToken}}export function authCookieForBrowserSession(token,expiresIn){const seconds=Number(expiresIn);if(!Number.isInteger(seconds)||seconds<=0)throw TypeError("[auth] The issued token lifetime must be a positive integer number of seconds.");return authCookie(token,{maxAge:seconds})}function mediaPreference(accept,mediaType){const requested=mediaType.toLowerCase(),requestedType=requested.slice(0,requested.indexOf("/"));let best={order:Number.POSITIVE_INFINITY,quality:0,specificity:-1};for(const[order,raw]of accept.split(",").entries()){const[rawType="",...parameters]=raw.trim().toLowerCase().split(";"),type=rawType.trim();if(type!==requested&&type!==`${requestedType}/*`&&type!=="*/*")continue;const qualityParameter=parameters.find((parameter)=>parameter.trim().startsWith("q=")),quality=qualityParameter===void 0?1:Number(qualityParameter.slice(qualityParameter.indexOf("=")+1).trim()),normalizedQuality=Number.isFinite(quality)&&quality>=0&&quality<=1?quality:0,specificity=type===requested?2:type===`${requestedType}/*`?1:0;if(specificity>best.specificity||specificity===best.specificity&&normalizedQuality>best.quality)best={order,quality:normalizedQuality,specificity}}return best}function prefersHtml(accept){if(!accept)return!1;const html=mediaPreference(accept,"text/html"),json=mediaPreference(accept,"application/json");return html.quality>0&&(html.quality>json.quality||html.quality===json.quality&&html.order<json.order)}export function browserSessionLogoutRedirect(request,auth=config.auth){if(!prefersHtml(request?.headers?.get("accept")??null))return;const configured=auth.browserSession?.logoutRedirect;if(typeof configured!=="string")return;const candidate=configured.trim();if(!candidate.startsWith("/")||candidate.startsWith("//"))return;try{const base=new URL("https://stacks.invalid"),resolved=new URL(candidate,base);if(resolved.origin!==base.origin)return;const target=`${resolved.pathname}${resolved.search}${resolved.hash}`;if(!target.startsWith("/")||target.startsWith("//"))return;return target}catch{return}}
|
package/dist/register.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AuthToken } from './token';
|
|
2
2
|
import type { NewUser } from '@stacksjs/orm';
|
|
3
|
+
import type { TokenCreateOptions } from '@stacksjs/types';
|
|
3
4
|
/**
|
|
4
5
|
* Register a user and open a full session for them.
|
|
5
6
|
*
|
|
@@ -14,7 +15,7 @@ import type { NewUser } from '@stacksjs/orm';
|
|
|
14
15
|
*
|
|
15
16
|
* Additive, so `const { token } = await register(...)` is unaffected.
|
|
16
17
|
*/
|
|
17
|
-
export declare function register(credentials: NewUser & { referralCode?: string }): Promise<RegistrationResult>;
|
|
18
|
+
export declare function register(credentials: NewUser & { referralCode?: string }, tokenOptions?: TokenCreateOptions): Promise<RegistrationResult>;
|
|
18
19
|
/**
|
|
19
20
|
* What a successful registration hands back: a complete session, matching
|
|
20
21
|
* `Auth.loginUsingId()`.
|
package/dist/register.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{config}from"@stacksjs/config";import{db}from"@stacksjs/database/runtime";import{HttpError}from"@stacksjs/error-handling";import{User}from"@stacksjs/orm";import{makeHash}from"@stacksjs/security";import{Auth}from"./authentication";import{isUniqueViolation}from"./rbac-store-bqb";import{attributeReferral}from"./referrals";function duplicateEmailError(){if(config.auth?.registration?.preventEnumeration===!1)return new HttpError(409,"Email already exists");return new HttpError(422,"Registration could not be completed. Please check your details and try again.")}const EMAIL_RE=/^[^\s@]+@[^\s@]+\.[^\s@]+$/;export async function register(credentials){const{email,password,name}=credentials;if(typeof email!=="string"||email.length>254||!EMAIL_RE.test(email))throw new HttpError(422,"Email address is invalid");if(typeof password!=="string"||password.length<8)throw new HttpError(422,"Password must be at least 8 characters");const hashedPassword=await makeHash(password,{algorithm:"bcrypt"}),userId=await db.transaction(async(rawTrx)=>{const trx=rawTrx;if(await trx.selectFrom("users").where("email","=",email).selectAll().executeTakeFirst())throw duplicateEmailError();try{await trx.insertInto("users").values({email,password:hashedPassword,name}).execute()}catch(err){if(isUniqueViolation(err))throw duplicateEmailError();throw err}const created=await trx.selectFrom("users").where("email","=",email).selectAll().executeTakeFirst();if(!created)throw Error("Failed to retrieve created user");if(credentials.referralCode)await attributeReferral(Number(created.id),credentials.referralCode,trx);return Number(created.id)}),user=await User.find(userId);if(!user)throw Error("Failed to retrieve created user");const{plainTextToken,refreshToken,expiresIn}=await Auth.createTokenForUser(user,{name:"user-auth-token"});return{token:plainTextToken,refreshToken,expiresIn}}
|
|
1
|
+
import{config}from"@stacksjs/config";import{db}from"@stacksjs/database/runtime";import{HttpError}from"@stacksjs/error-handling";import{User}from"@stacksjs/orm";import{makeHash}from"@stacksjs/security";import{Auth}from"./authentication";import{isUniqueViolation}from"./rbac-store-bqb";import{attributeReferral}from"./referrals";function duplicateEmailError(){if(config.auth?.registration?.preventEnumeration===!1)return new HttpError(409,"Email already exists");return new HttpError(422,"Registration could not be completed. Please check your details and try again.")}const EMAIL_RE=/^[^\s@]+@[^\s@]+\.[^\s@]+$/;export async function register(credentials,tokenOptions){const{email,password,name}=credentials;if(typeof email!=="string"||email.length>254||!EMAIL_RE.test(email))throw new HttpError(422,"Email address is invalid");if(typeof password!=="string"||password.length<8)throw new HttpError(422,"Password must be at least 8 characters");const hashedPassword=await makeHash(password,{algorithm:"bcrypt"}),userId=await db.transaction(async(rawTrx)=>{const trx=rawTrx;if(await trx.selectFrom("users").where("email","=",email).selectAll().executeTakeFirst())throw duplicateEmailError();try{await trx.insertInto("users").values({email,password:hashedPassword,name}).execute()}catch(err){if(isUniqueViolation(err))throw duplicateEmailError();throw err}const created=await trx.selectFrom("users").where("email","=",email).selectAll().executeTakeFirst();if(!created)throw Error("Failed to retrieve created user");if(credentials.referralCode)await attributeReferral(Number(created.id),credentials.referralCode,trx);return Number(created.id)}),user=await User.find(userId);if(!user)throw Error("Failed to retrieve created user");const{plainTextToken,refreshToken,expiresIn}=await Auth.createTokenForUser(user,{...tokenOptions,name:"user-auth-token"});return{token:plainTextToken,refreshToken,expiresIn}}
|
package/dist/two-factor.d.ts
CHANGED
|
@@ -47,7 +47,7 @@ export declare function verifyTwoFactorLoginCode(userId: number, code: string):
|
|
|
47
47
|
* an opaque random id instead of (user_id, purpose) since a user can
|
|
48
48
|
* only have one login attempt in flight that matters here.
|
|
49
49
|
*/
|
|
50
|
-
export declare function createTwoFactorChallenge(userId: number,
|
|
50
|
+
export declare function createTwoFactorChallenge(userId: number, options?: number | TwoFactorChallengeOptions): Promise<string>;
|
|
51
51
|
/**
|
|
52
52
|
* Consume (delete-on-read) a login challenge and return the user id it
|
|
53
53
|
* was issued for, or null if missing/expired.
|
|
@@ -58,7 +58,7 @@ export declare function consumeTwoFactorChallenge(challengeToken: string): Promi
|
|
|
58
58
|
* A reset either revokes this challenge first or revokes the issued tokens
|
|
59
59
|
* afterward. It cannot commit between the second factor and token issuance.
|
|
60
60
|
*/
|
|
61
|
-
export declare function withTwoFactorChallenge<T>(challengeToken: string, complete: (userId: number) => Promise<T>): Promise<T | null>;
|
|
61
|
+
export declare function withTwoFactorChallenge<T>(challengeToken: string, complete: (userId: number, context: TwoFactorChallengeContext) => Promise<T>): Promise<T | null>;
|
|
62
62
|
/** Revoke password-stage grants in the same transaction as credential recovery. */
|
|
63
63
|
export declare function revokeTwoFactorChallenges(userId: number): Promise<void>;
|
|
64
64
|
export declare const TwoFactor: {
|
|
@@ -73,6 +73,13 @@ export declare const TwoFactor: {
|
|
|
73
73
|
createChallenge: unknown;
|
|
74
74
|
consumeChallenge: unknown
|
|
75
75
|
};
|
|
76
|
+
export declare interface TwoFactorChallengeOptions {
|
|
77
|
+
ttlSeconds?: number
|
|
78
|
+
remembered?: boolean
|
|
79
|
+
}
|
|
80
|
+
export declare interface TwoFactorChallengeContext {
|
|
81
|
+
remembered: boolean
|
|
82
|
+
}
|
|
76
83
|
export declare interface TwoFactorUser {
|
|
77
84
|
id: number
|
|
78
85
|
email?: string
|
package/dist/two-factor.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{randomBytes}from"node:crypto";import{db,getDatabaseDialect,mutationCount,parseSqlDateTime,sqlDateTime}from"@stacksjs/database/runtime";import{verifyTOTPWithCounter}from"@stacksjs/ts-auth";import{generateTwoFactorSecret,generateTwoFactorUri,twoFactorQrCode,verifyTwoFactorCode}from"./authenticator";import{RateLimiter}from"./rate-limiter";const DEFAULT_CHALLENGE_TTL_SECONDS=300;function challengeExpiry(ttlSeconds){const deadline=new Date(Date.now()+ttlSeconds*1000);if(getDatabaseDialect()==="mysql")deadline.setUTCMilliseconds(0);return sqlDateTime(deadline)}const TWO_FACTOR_RATE_LIMIT_PREFIX="2fa:";async function getLastUsedTwoFactorStep(userId){const value=(await db.primary.selectFrom("users").where("id","=",userId).select(["two_factor_last_used_step"]).executeTakeFirst())?.two_factor_last_used_step;return value==null?null:Number(value)}async function claimTwoFactorStep(userId,secret,step,lastStep){let claim=db.updateTable("users").set({two_factor_last_used_step:step}).where("id","=",userId).where("two_factor_secret","=",secret).where("two_factor_enabled","=",!0);claim=lastStep===null?claim.whereNull("two_factor_last_used_step"):claim.where("two_factor_last_used_step","=",lastStep);return mutationCount(await claim.execute())===1}export async function getTwoFactorState(userId){const row=await db.primary.selectFrom("users").where("id","=",userId).select(["two_factor_secret","two_factor_enabled"]).executeTakeFirst();return{secret:row?.two_factor_secret??null,enabled:Boolean(row?.two_factor_enabled)}}export function isTwoFactorEnabled(user){return Boolean(user.two_factor_enabled)}export function generateTwoFactorSetup(email,serviceName){const secret=generateTwoFactorSecret(),uri=generateTwoFactorUri(email,serviceName,secret);return{secret,uri,qr:twoFactorQrCode(uri)}}const PENDING_SECRET_TTL_SECONDS=600;export async function stashPendingTwoFactorSecret(userId,secret,ttlSeconds=PENDING_SECRET_TTL_SECONDS){const expiresAt=challengeExpiry(ttlSeconds);await db.transaction(async()=>{await db.upsert("two_factor_pending_secrets",[{user_id:userId,secret,expires_at:expiresAt}],["user_id"],["secret","expires_at"]);const stored=await db.primary.selectFrom("two_factor_pending_secrets").where("user_id","=",userId).select(["secret","expires_at"]).executeTakeFirst();if(stored?.secret!==secret||parseSqlDateTime(stored.expires_at)?.getTime()!==parseSqlDateTime(expiresAt)?.getTime())throw Error("[auth] Pending two-factor setup could not be stored.")})}export async function consumePendingTwoFactorSecret(userId){const row=await db.primary.selectFrom("two_factor_pending_secrets").where("user_id","=",userId).selectAll().executeTakeFirst();if(!row)return null;const claimed=await db.deleteFrom("two_factor_pending_secrets").where("user_id","=",userId).where("secret","=",row.secret).where("expires_at","=",row.expires_at).execute();if(mutationCount(claimed)!==1)return null;const expiresAt=parseSqlDateTime(row.expires_at)?.getTime()??0;if(Date.now()>=expiresAt)return null;return String(row.secret)}export async function enableTwoFactor(userId,secret,code){if(!await verifyTwoFactorCode(code,secret))return!1;return persistTwoFactorState(userId,secret,!0)}export async function disableTwoFactor(userId){await persistTwoFactorState(userId,null,!1)}async function persistTwoFactorState(userId,secret,enabled){return db.transaction(async()=>{await db.updateTable("users").set({two_factor_secret:secret,two_factor_enabled:enabled}).where("id","=",userId).execute();const stored=await db.primary.selectFrom("users").where("id","=",userId).select(["two_factor_secret","two_factor_enabled"]).executeTakeFirst();if(!stored)return!1;if(stored.two_factor_secret!==secret||Boolean(stored.two_factor_enabled)!==enabled)throw Error("[auth] Two-factor state could not be stored.");return!0})}export async function verifyTwoFactorLoginCode(userId,code){const rateKey=`${TWO_FACTOR_RATE_LIMIT_PREFIX}${userId}`;await RateLimiter.validateAttempt(rateKey);const{secret,enabled}=await getTwoFactorState(userId);if(!enabled||!secret)return!1;const{valid,counter:step}=await verifyTOTPWithCounter(code,{secret});if(!valid||step===null){await RateLimiter.recordFailedAttempt(rateKey);return!1}const lastStep=await getLastUsedTwoFactorStep(userId);if(lastStep!==null&&step<=lastStep)return!1;if(!await claimTwoFactorStep(userId,secret,step,lastStep))return!1;await RateLimiter.resetAttempts(rateKey);return!0}export async function createTwoFactorChallenge(userId,
|
|
1
|
+
import{randomBytes}from"node:crypto";import{db,getDatabaseDialect,mutationCount,parseSqlDateTime,sqlDateTime}from"@stacksjs/database/runtime";import{verifyTOTPWithCounter}from"@stacksjs/ts-auth";import{generateTwoFactorSecret,generateTwoFactorUri,twoFactorQrCode,verifyTwoFactorCode}from"./authenticator";import{RateLimiter}from"./rate-limiter";const DEFAULT_CHALLENGE_TTL_SECONDS=300,BASELINE_CHALLENGE_PREFIX="baseline.",REMEMBERED_CHALLENGE_PREFIX="remembered.";function challengeExpiry(ttlSeconds){const deadline=new Date(Date.now()+ttlSeconds*1000);if(getDatabaseDialect()==="mysql")deadline.setUTCMilliseconds(0);return sqlDateTime(deadline)}const TWO_FACTOR_RATE_LIMIT_PREFIX="2fa:";async function getLastUsedTwoFactorStep(userId){const value=(await db.primary.selectFrom("users").where("id","=",userId).select(["two_factor_last_used_step"]).executeTakeFirst())?.two_factor_last_used_step;return value==null?null:Number(value)}async function claimTwoFactorStep(userId,secret,step,lastStep){let claim=db.updateTable("users").set({two_factor_last_used_step:step}).where("id","=",userId).where("two_factor_secret","=",secret).where("two_factor_enabled","=",!0);claim=lastStep===null?claim.whereNull("two_factor_last_used_step"):claim.where("two_factor_last_used_step","=",lastStep);return mutationCount(await claim.execute())===1}export async function getTwoFactorState(userId){const row=await db.primary.selectFrom("users").where("id","=",userId).select(["two_factor_secret","two_factor_enabled"]).executeTakeFirst();return{secret:row?.two_factor_secret??null,enabled:Boolean(row?.two_factor_enabled)}}export function isTwoFactorEnabled(user){return Boolean(user.two_factor_enabled)}export function generateTwoFactorSetup(email,serviceName){const secret=generateTwoFactorSecret(),uri=generateTwoFactorUri(email,serviceName,secret);return{secret,uri,qr:twoFactorQrCode(uri)}}const PENDING_SECRET_TTL_SECONDS=600;export async function stashPendingTwoFactorSecret(userId,secret,ttlSeconds=PENDING_SECRET_TTL_SECONDS){const expiresAt=challengeExpiry(ttlSeconds);await db.transaction(async()=>{await db.upsert("two_factor_pending_secrets",[{user_id:userId,secret,expires_at:expiresAt}],["user_id"],["secret","expires_at"]);const stored=await db.primary.selectFrom("two_factor_pending_secrets").where("user_id","=",userId).select(["secret","expires_at"]).executeTakeFirst();if(stored?.secret!==secret||parseSqlDateTime(stored.expires_at)?.getTime()!==parseSqlDateTime(expiresAt)?.getTime())throw Error("[auth] Pending two-factor setup could not be stored.")})}export async function consumePendingTwoFactorSecret(userId){const row=await db.primary.selectFrom("two_factor_pending_secrets").where("user_id","=",userId).selectAll().executeTakeFirst();if(!row)return null;const claimed=await db.deleteFrom("two_factor_pending_secrets").where("user_id","=",userId).where("secret","=",row.secret).where("expires_at","=",row.expires_at).execute();if(mutationCount(claimed)!==1)return null;const expiresAt=parseSqlDateTime(row.expires_at)?.getTime()??0;if(Date.now()>=expiresAt)return null;return String(row.secret)}export async function enableTwoFactor(userId,secret,code){if(!await verifyTwoFactorCode(code,secret))return!1;return persistTwoFactorState(userId,secret,!0)}export async function disableTwoFactor(userId){await persistTwoFactorState(userId,null,!1)}async function persistTwoFactorState(userId,secret,enabled){return db.transaction(async()=>{await db.updateTable("users").set({two_factor_secret:secret,two_factor_enabled:enabled}).where("id","=",userId).execute();const stored=await db.primary.selectFrom("users").where("id","=",userId).select(["two_factor_secret","two_factor_enabled"]).executeTakeFirst();if(!stored)return!1;if(stored.two_factor_secret!==secret||Boolean(stored.two_factor_enabled)!==enabled)throw Error("[auth] Two-factor state could not be stored.");return!0})}export async function verifyTwoFactorLoginCode(userId,code){const rateKey=`${TWO_FACTOR_RATE_LIMIT_PREFIX}${userId}`;await RateLimiter.validateAttempt(rateKey);const{secret,enabled}=await getTwoFactorState(userId);if(!enabled||!secret)return!1;const{valid,counter:step}=await verifyTOTPWithCounter(code,{secret});if(!valid||step===null){await RateLimiter.recordFailedAttempt(rateKey);return!1}const lastStep=await getLastUsedTwoFactorStep(userId);if(lastStep!==null&&step<=lastStep)return!1;if(!await claimTwoFactorStep(userId,secret,step,lastStep))return!1;await RateLimiter.resetAttempts(rateKey);return!0}export async function createTwoFactorChallenge(userId,options={}){const ttlSeconds=typeof options==="number"?options:options.ttlSeconds??DEFAULT_CHALLENGE_TTL_SECONDS,id=`${typeof options==="object"&&options.remembered===!0?REMEMBERED_CHALLENGE_PREFIX:BASELINE_CHALLENGE_PREFIX}${randomBytes(32).toString("hex")}`,expiresAt=challengeExpiry(ttlSeconds);await db.transaction(async()=>{await db.deleteFrom("two_factor_challenges").where("user_id","=",userId).execute();await db.insertInto("two_factor_challenges").values({id,user_id:userId,expires_at:expiresAt}).execute();const stored=await db.primary.selectFrom("two_factor_challenges").where("user_id","=",userId).select("id").execute();if(stored.length!==1||stored[0]?.id!==id)throw Error("[auth] Two-factor login challenge could not be stored.")});return id}export async function consumeTwoFactorChallenge(challengeToken){const row=await db.primary.selectFrom("two_factor_challenges").where("id","=",challengeToken).selectAll().executeTakeFirst();if(!row)return null;const claimed=await db.deleteFrom("two_factor_challenges").where("id","=",challengeToken).execute();if(mutationCount(claimed)!==1)return null;const expiresAt=parseSqlDateTime(row.expires_at)?.getTime()??0;if(Date.now()>=expiresAt)return null;return Number(row.user_id)}export async function withTwoFactorChallenge(challengeToken,complete){const pending=await db.primary.selectFrom("two_factor_challenges").where("id","=",challengeToken).select("user_id").executeTakeFirst();if(!pending)return null;const userId=Number(pending.user_id),outcome=await db.transaction(async()=>{let owner=db.primary.selectFrom("users").where("id","=",userId).select("id");if(getDatabaseDialect()!=="sqlite")owner=owner.lockForUpdate();if(!await owner.executeTakeFirst())return{value:null};if(await consumeTwoFactorChallenge(challengeToken)!==userId)return{value:null};try{return{value:await db.transaction(()=>complete(userId,{remembered:challengeToken.startsWith(REMEMBERED_CHALLENGE_PREFIX)}))}}catch(error){return{error}}});if("error"in outcome)throw outcome.error;return outcome.value}export async function revokeTwoFactorChallenges(userId){try{await db.transaction(async()=>{await db.deleteFrom("two_factor_challenges").where("user_id","=",userId).execute();if(await db.primary.selectFrom("two_factor_challenges").where("user_id","=",userId).select("id").executeTakeFirst())throw Error("[auth] Two-factor login challenges could not be revoked.")})}catch(error){const message=error&&typeof error==="object"&&"message"in error&&typeof error.message==="string"?error.message:String(error);if(/^(?:no such table: (?:main\.)?two_factor_challenges|relation "two_factor_challenges" does not exist|Table '(?:[^'.]+\.)?two_factor_challenges' doesn't exist)$/i.test(message))return;throw error}}export const TwoFactor={isEnabled:isTwoFactorEnabled,getState:getTwoFactorState,generateSetup:generateTwoFactorSetup,stashPendingSecret:stashPendingTwoFactorSecret,consumePendingSecret:consumePendingTwoFactorSecret,enable:enableTwoFactor,disable:disableTwoFactor,verifyLoginCode:verifyTwoFactorLoginCode,createChallenge:createTwoFactorChallenge,consumeChallenge:consumeTwoFactorChallenge};
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/auth",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.74.
|
|
5
|
+
"version": "0.74.60",
|
|
6
6
|
"description": "A more simplistic way to authenticate.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -58,18 +58,18 @@
|
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"@stacksjs/bun-router": "^0.1.18",
|
|
61
|
-
"@stacksjs/cache": "0.74.
|
|
62
|
-
"@stacksjs/config": "0.74.
|
|
63
|
-
"@stacksjs/database": "0.74.
|
|
64
|
-
"@stacksjs/email": "0.74.
|
|
65
|
-
"@stacksjs/env": "0.74.
|
|
66
|
-
"@stacksjs/error-handling": "0.74.
|
|
67
|
-
"@stacksjs/logging": "0.74.
|
|
68
|
-
"@stacksjs/orm": "0.74.
|
|
69
|
-
"@stacksjs/path": "0.74.
|
|
70
|
-
"@stacksjs/router": "0.74.
|
|
71
|
-
"@stacksjs/security": "0.74.
|
|
72
|
-
"@stacksjs/storage": "0.74.
|
|
61
|
+
"@stacksjs/cache": "0.74.60",
|
|
62
|
+
"@stacksjs/config": "0.74.60",
|
|
63
|
+
"@stacksjs/database": "0.74.60",
|
|
64
|
+
"@stacksjs/email": "0.74.60",
|
|
65
|
+
"@stacksjs/env": "0.74.60",
|
|
66
|
+
"@stacksjs/error-handling": "0.74.60",
|
|
67
|
+
"@stacksjs/logging": "0.74.60",
|
|
68
|
+
"@stacksjs/orm": "0.74.60",
|
|
69
|
+
"@stacksjs/path": "0.74.60",
|
|
70
|
+
"@stacksjs/router": "0.74.60",
|
|
71
|
+
"@stacksjs/security": "0.74.60",
|
|
72
|
+
"@stacksjs/storage": "0.74.60",
|
|
73
73
|
"@stacksjs/ts-auth": "^0.4.4",
|
|
74
74
|
"ts-qr-codes": "^0.1.8"
|
|
75
75
|
},
|