@stacksjs/auth 0.75.181 → 0.75.182
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/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/passkey.d.ts +2 -2
- package/dist/passkey.js +1 -1
- package/dist/passkeys-browser.d.ts +2 -0
- package/dist/passkeys-browser.js +1 -0
- package/dist/passkeys.d.ts +13 -0
- package/dist/passkeys.js +1 -0
- package/package.json +13 -13
package/dist/index.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export * from './rate-limiter';
|
|
|
28
28
|
// WebAuthn/Passkey support (now using ts-auth - no external dependencies)
|
|
29
29
|
export * from './magic-link';
|
|
30
30
|
export * from './passkey';
|
|
31
|
+
export * from './passkeys';
|
|
31
32
|
export * from './password/reset';
|
|
32
33
|
export * from './register';
|
|
33
34
|
export * from './user';
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export*from"./authentication";export*from"./authenticator";export*from"./client";export*from"./credential-user";export*from"./middleware";export*from"./oauth-authorization";export*from"./oauth-authorization-endpoint";export*from"./oauth-authorization-requests";export*from"./oauth-authorization-response";export*from"./oauth-authorization-codes";export*from"./oauth-browser-session";export*from"./oauth-client-registration";export*from"./oauth-consent";export*from"./oauth-delegated-access";export*from"./oauth-grants";export*from"./oauth-introspection";export*from"./oauth-metadata";export*from"./oauth-provider";export*from"./oauth-pruning";export*from"./oauth-pkce";export*from"./oauth-revocation";export*from"./oauth-token-exchange";export*from"./oauth-token-endpoint";export*from"./oauth-token-request";export*from"./oauth-token-response";export*from"./rate-limiter";export*from"./magic-link";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"./browser-session";export*from"./request-token";export*from"./page-gate";export*from"./socials";export{generateTOTP,verifyTOTP,generateTOTPSecret,totpKeyUri}from"@stacksjs/ts-auth";export*from"./two-factor";export*from"./team";export*from"./referrals";
|
|
1
|
+
export*from"./authentication";export*from"./authenticator";export*from"./client";export*from"./credential-user";export*from"./middleware";export*from"./oauth-authorization";export*from"./oauth-authorization-endpoint";export*from"./oauth-authorization-requests";export*from"./oauth-authorization-response";export*from"./oauth-authorization-codes";export*from"./oauth-browser-session";export*from"./oauth-client-registration";export*from"./oauth-consent";export*from"./oauth-delegated-access";export*from"./oauth-grants";export*from"./oauth-introspection";export*from"./oauth-metadata";export*from"./oauth-provider";export*from"./oauth-pruning";export*from"./oauth-pkce";export*from"./oauth-revocation";export*from"./oauth-token-exchange";export*from"./oauth-token-endpoint";export*from"./oauth-token-request";export*from"./oauth-token-response";export*from"./rate-limiter";export*from"./magic-link";export*from"./passkey";export*from"./passkeys";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"./browser-session";export*from"./request-token";export*from"./page-gate";export*from"./socials";export{generateTOTP,verifyTOTP,generateTOTPSecret,totpKeyUri}from"@stacksjs/ts-auth";export*from"./two-factor";export*from"./team";export*from"./referrals";
|
package/dist/passkey.d.ts
CHANGED
|
@@ -39,7 +39,7 @@ export declare function getUserPasskey(userId: number, passkeyId: string): Promi
|
|
|
39
39
|
* authentication MUST be rejected by the caller in that case).
|
|
40
40
|
* stacksjs/stacks#1861 A-4.
|
|
41
41
|
*/
|
|
42
|
-
export declare function updatePasskeyCounter(userId: number, passkeyId: string, newCounter: number): Promise<boolean>;
|
|
42
|
+
export declare function updatePasskeyCounter(userId: number, passkeyId: string, newCounter: number, verifiedCredential?: Pick<PasskeyAttribute, 'counter' | 'cred_public_key'>): Promise<boolean>;
|
|
43
43
|
export declare function setCurrentRegistrationOptions(user: { id: number, email?: string }, verified: VerifiedRegistrationResponse): Promise<void>;
|
|
44
44
|
/**
|
|
45
45
|
* Persist a server-issued WebAuthn challenge for the given user +
|
|
@@ -113,7 +113,7 @@ export declare interface PasskeyAttribute {
|
|
|
113
113
|
backup_status: boolean
|
|
114
114
|
transports?: string
|
|
115
115
|
created_at?: Date
|
|
116
|
-
last_used_at: string
|
|
116
|
+
last_used_at: string | null
|
|
117
117
|
}
|
|
118
118
|
declare type UserModel = NonNullable<Awaited<ReturnType<typeof User.find>>>;
|
|
119
119
|
declare type PasskeyInsertable = Insertable<PasskeyAttribute>;
|
package/dist/passkey.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Buffer}from"node:buffer";import{db,getDatabaseDialect,mutationCount,parseSqlDateTime,sqlDateTime}from"@stacksjs/database/runtime";import{User}from"@stacksjs/orm";export{generateRegistrationOptions,generateAuthenticationOptions,verifyRegistrationResponse,verifyAuthenticationResponse,startRegistration,startAuthentication,browserSupportsWebAuthn,browserSupportsWebAuthnAutofill,platformAuthenticatorIsAvailable}from"@stacksjs/ts-auth";export function passkeyDescriptors(passkeys){return passkeys.map((passkey)=>({id:passkey.id,type:"public-key",transports:parseTransports(passkey.transports)}))}function parseTransports(stored){if(!stored)return
|
|
1
|
+
import{Buffer}from"node:buffer";import{db,getDatabaseDialect,mutationCount,parseSqlDateTime,sqlDateTime}from"@stacksjs/database/runtime";import{User}from"@stacksjs/orm";export{generateRegistrationOptions,generateAuthenticationOptions,verifyRegistrationResponse,verifyAuthenticationResponse,startRegistration,startAuthentication,browserSupportsWebAuthn,browserSupportsWebAuthnAutofill,platformAuthenticatorIsAvailable}from"@stacksjs/ts-auth";export function passkeyDescriptors(passkeys){return passkeys.map((passkey)=>({id:passkey.id,type:"public-key",transports:parseTransports(passkey.transports)}))}function parseTransports(stored){if(!stored)return;try{const parsed=JSON.parse(stored);return Array.isArray(parsed)&&parsed.length?parsed.filter((value)=>["ble","internal","nfc","usb","hybrid"].includes(value)):void 0}catch{return}}export async function getUserPasskeys(userId){return await db.primary.selectFrom("passkeys").selectAll().where("user_id","=",userId).execute()}export async function getUserPasskey(userId,passkeyId){return await db.primary.selectFrom("passkeys").selectAll().where("id","=",passkeyId).where("user_id","=",userId).executeTakeFirst()}export async function updatePasskeyCounter(userId,passkeyId,newCounter,verifiedCredential){if(!Number.isSafeInteger(newCounter)||newCounter<0)return!1;const passkey=verifiedCredential??await getUserPasskey(userId,passkeyId);if(!passkey)return!1;const stored=Number(passkey.counter??0);if(newCounter<=stored&&!(newCounter===0&&stored===0))return!1;if(newCounter===0)return db.transaction(async()=>{let query=db.primary.selectFrom("passkeys").selectAll().where("id","=",passkeyId).where("user_id","=",userId).where("counter","=",0).where("cred_public_key","=",passkey.cred_public_key);if(getDatabaseDialect()!=="sqlite")query=query.lockForUpdate();if(!await query.executeTakeFirst())return!1;await db.updateTable("passkeys").set({last_used_at:sqlDateTime()}).where("id","=",passkeyId).where("user_id","=",userId).execute();return!0});const claimed=await db.updateTable("passkeys").set({counter:newCounter,last_used_at:sqlDateTime()}).where("id","=",passkeyId).where("user_id","=",userId).where("counter","=",stored).where("cred_public_key","=",passkey.cred_public_key).execute();return mutationCount(claimed)===1}export async function setCurrentRegistrationOptions(user,verified){if(!verified.verified)throw Error("[auth/passkey] Registration is not verified");const credentialId=verified.registrationInfo?.credential.id,credentialPublicKey=verified.registrationInfo?.credential.publicKey;if(!credentialId)throw Error("[auth/passkey] WebAuthn registration response is missing credential.id");if(!credentialPublicKey)throw Error("[auth/passkey] WebAuthn registration response is missing credential.publicKey");const passkeyData={id:credentialId,cred_public_key:JSON.stringify(Array.from(new Uint8Array(credentialPublicKey))),user_id:user.id,webauthn_user_id:user.email||"",counter:verified.registrationInfo?.credential.counter||0,credential_type:verified.registrationInfo?.credentialType||"",device_type:verified.registrationInfo?.credentialDeviceType||"",backup_eligible:!1,backup_status:verified.registrationInfo?.credentialBackedUp||!1,transports:JSON.stringify(["internal"]),last_used_at:sqlDateTime()};await db.insertInto("passkeys").values(passkeyData).executeTakeFirstOrThrow()}const DEFAULT_CHALLENGE_TTL_SECONDS=300;export async function storeWebAuthnChallenge(userId,challenge,purpose,ttlSeconds=DEFAULT_CHALLENGE_TTL_SECONDS){const expiresAt=sqlDateTime(new Date(Date.now()+ttlSeconds*1000)),encodedChallenge=typeof challenge==="string"?challenge:Buffer.from(challenge).toString("base64url");await db.transaction(async()=>{await db.deleteFrom("webauthn_challenges").where("user_id","=",userId).where("purpose","=",purpose).execute();await db.insertInto("webauthn_challenges").values({user_id:userId,challenge:encodedChallenge,purpose,expires_at:expiresAt}).execute();const stored=await db.primary.selectFrom("webauthn_challenges").where("user_id","=",userId).where("purpose","=",purpose).select("challenge").execute();if(stored.length!==1||stored[0]?.challenge!==encodedChallenge)throw Error("[auth] WebAuthn challenge could not be stored.")})}export async function consumeWebAuthnChallenge(userId,purpose){const row=await db.primary.selectFrom("webauthn_challenges").where("user_id","=",userId).where("purpose","=",purpose).select(["challenge","expires_at"]).executeTakeFirst();if(!row)return null;const claimed=await db.deleteFrom("webauthn_challenges").where("user_id","=",userId).where("purpose","=",purpose).where("challenge","=",row.challenge).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 Buffer.from(String(row.challenge),"base64url")}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export*from"@stacksjs/ts-auth/passkeys-browser";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { RegistrationOptionsJSON, AuthenticationOptionsJSON, RegistrationCredentialJSON, AuthenticationCredentialJSON } from '@stacksjs/ts-auth';
|
|
2
|
+
export declare function beginPasskeyRegistration(user: { id: number, email?: string, name?: string }, party: PasskeyRelyingParty): Promise<{ ceremony: string, options: RegistrationOptionsJSON }>;
|
|
3
|
+
export declare function finishPasskeyRegistration(userId: number, token: unknown, credential: RegistrationCredentialJSON, party: PasskeyRelyingParty): Promise<void>;
|
|
4
|
+
export declare function beginPasskeyAuthentication(party: PasskeyRelyingParty): Promise<{ ceremony: string, options: AuthenticationOptionsJSON }>;
|
|
5
|
+
export declare function finishPasskeyAuthentication(token: unknown, credential: AuthenticationCredentialJSON, party: PasskeyRelyingParty): Promise<number>;
|
|
6
|
+
/** Only display-safe metadata leaves the server. */
|
|
7
|
+
export declare function listPasskeys(userId: number): Promise<{ id: string, device: string, created_at: unknown, last_used_at: unknown }[]>;
|
|
8
|
+
/** Serialize removals per account so two simultaneous deletes cannot remove the last way in. */
|
|
9
|
+
export declare function removePasskey(userId: number, credentialId: string, hasAlternativeSignIn?: (user: Record<string, unknown>) => boolean | Promise<boolean>): Promise<boolean>;
|
|
10
|
+
export declare interface PasskeyRelyingParty { origin: string, name: string }
|
|
11
|
+
export declare class PasskeyError extends Error {
|
|
12
|
+
|
|
13
|
+
}
|
package/dist/passkeys.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createHash,randomBytes}from"node:crypto";import{Buffer}from"node:buffer";import{db,getDatabaseDialect,mutationCount,parseSqlDateTime,sqlDateTime}from"@stacksjs/database/runtime";import{generateRegistrationOptions,generateAuthenticationOptions,verifyRegistrationResponse,verifyAuthenticationResponse,registrationOptionsToJSON,authenticationOptionsToJSON,registrationCredentialFromJSON,authenticationCredentialFromJSON}from"@stacksjs/ts-auth";import{getUserPasskeys,passkeyDescriptors,updatePasskeyCounter}from"./passkey";export class PasskeyError extends Error{}const TTL=300000,digest=(token)=>createHash("sha256").update(token).digest("hex");function relyingParty(party){const url=new URL(party.origin);if(url.protocol!=="https:"&&!(url.protocol==="http:"&&["localhost","127.0.0.1","[::1]"].includes(url.hostname)))throw Error("Passkeys require HTTPS or localhost");return{origin:url.origin,rpID:url.hostname}}async function issue(purpose,challenge,userId,origin){const token=randomBytes(32).toString("base64url");await db.deleteFrom("passkey_ceremonies").where("expires_at","<",sqlDateTime()).execute();await db.insertInto("passkey_ceremonies").values({id:digest(token),user_id:userId,purpose,challenge:Buffer.from(challenge).toString("base64url"),origin,expires_at:sqlDateTime(new Date(Date.now()+TTL))}).execute();return token}async function consume(token,purpose,origin,userId){if(typeof token!=="string"||!/^[A-Za-z0-9_-]{43}$/.test(token))throw new PasskeyError("Passkey request expired. Please try again.");const row=await db.primary.selectFrom("passkey_ceremonies").selectAll().where("id","=",digest(token)).where("purpose","=",purpose).where("origin","=",origin).executeTakeFirst();if(!row||userId!==void 0&&Number(row.user_id)!==userId)throw new PasskeyError("Passkey request expired. Please try again.");const claimed=await db.deleteFrom("passkey_ceremonies").where("id","=",row.id).execute();if(mutationCount(claimed)!==1||(parseSqlDateTime(row.expires_at)?.getTime()??0)<=Date.now())throw new PasskeyError("Passkey request expired. Please try again.");return row}export async function beginPasskeyRegistration(user,party){const{origin,rpID}=relyingParty(party),existing=await getUserPasskeys(user.id),handle=createHash("sha256").update(`stacks:passkey:${rpID}:${user.id}`).digest("base64url"),options=generateRegistrationOptions({rpID,rpName:party.name,userID:handle,userName:user.email||String(user.id),userDisplayName:user.name,attestationType:"none",excludeCredentials:passkeyDescriptors(existing),authenticatorSelection:{residentKey:"required",requireResidentKey:!0,userVerification:"required"}});return{ceremony:await issue("registration",options.challenge,user.id,origin),options:registrationOptionsToJSON(options)}}export async function finishPasskeyRegistration(userId,token,credential,party){const{origin,rpID}=relyingParty(party),ceremony=await consume(token,"registration",origin,userId);let verified;try{verified=await verifyRegistrationResponse(registrationCredentialFromJSON(credential),Buffer.from(ceremony.challenge,"base64url"),origin,rpID,{requireUserVerification:!0})}catch{throw new PasskeyError("Passkey could not be verified.")}const info=verified.registrationInfo;if(!verified.verified||!info||info.credential.id.length>255)throw new PasskeyError("Passkey could not be verified.");const transports=Array.isArray(credential.response.transports)?credential.response.transports.filter((value)=>["internal","hybrid","usb","nfc","ble"].includes(value)):[];await db.insertInto("passkeys").values({id:info.credential.id,user_id:userId,cred_public_key:Buffer.from(info.credential.publicKey).toString("base64url"),webauthn_user_id:createHash("sha256").update(`stacks:passkey:${rpID}:${userId}`).digest("base64url"),counter:info.credential.counter,credential_type:info.credentialType,device_type:info.credentialDeviceType,backup_eligible:info.credentialDeviceType==="multiDevice",backup_status:info.credentialBackedUp,transports:JSON.stringify(transports||[]),created_at:sqlDateTime(),last_used_at:null}).execute()}export async function beginPasskeyAuthentication(party){const{origin,rpID}=relyingParty(party),options=generateAuthenticationOptions({rpID,userVerification:"required"});return{ceremony:await issue("authentication",options.challenge,null,origin),options:authenticationOptionsToJSON(options)}}export async function finishPasskeyAuthentication(token,credential,party){const{origin,rpID}=relyingParty(party),ceremony=await consume(token,"authentication",origin);try{const decoded=authenticationCredentialFromJSON(credential),passkey=await db.primary.selectFrom("passkeys").selectAll().where("id","=",decoded.id).executeTakeFirst();if(!passkey)throw new PasskeyError("Passkey could not be verified.");if(decoded.response.userHandle&&Buffer.from(decoded.response.userHandle).toString()!==passkey.webauthn_user_id)throw new PasskeyError("Passkey could not be verified.");const stored=String(passkey.cred_public_key),key=stored.startsWith("{")||stored.startsWith("[")?Uint8Array.from(Object.values(JSON.parse(stored))).buffer:new Uint8Array(Buffer.from(stored,"base64url")).buffer,verified=await verifyAuthenticationResponse(decoded,Buffer.from(ceremony.challenge,"base64url"),origin,rpID,key,Number(passkey.counter),{requireUserVerification:!0}),counter=verified.authenticationInfo?.newCounter;if(!verified.verified||counter===void 0||!await updatePasskeyCounter(Number(passkey.user_id),decoded.id,counter,{counter:Number(passkey.counter),cred_public_key:String(passkey.cred_public_key)}))throw new PasskeyError("Passkey could not be verified.");return Number(passkey.user_id)}catch{throw new PasskeyError("Passkey could not be verified.")}}export async function listPasskeys(userId){return(await getUserPasskeys(userId)).map((key)=>({id:key.id,device:key.device_type,created_at:key.created_at,last_used_at:key.last_used_at}))}export async function removePasskey(userId,credentialId,hasAlternativeSignIn){return db.transaction(async()=>{let query=db.primary.selectFrom("users").selectAll().where("id","=",userId);if(getDatabaseDialect()!=="sqlite")query=query.lockForUpdate();const user=await query.executeTakeFirst();if(!user)return!1;const keys=await getUserPasskeys(userId);if(!keys.some((key)=>key.id===credentialId))return!1;const alternative=hasAlternativeSignIn?await hasAlternativeSignIn(user):typeof user.password==="string"&&user.password.length>0;if(keys.length===1&&!alternative)throw new PasskeyError("Add a password or another passkey before removing your last passkey.");return mutationCount(await db.deleteFrom("passkeys").where("user_id","=",userId).where("id","=",credentialId).execute())===1})}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/auth",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.75.
|
|
5
|
+
"version": "0.75.182",
|
|
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.24",
|
|
61
|
-
"@stacksjs/cache": "0.75.
|
|
62
|
-
"@stacksjs/config": "0.75.
|
|
63
|
-
"@stacksjs/database": "0.75.
|
|
64
|
-
"@stacksjs/email": "0.75.
|
|
65
|
-
"@stacksjs/env": "0.75.
|
|
66
|
-
"@stacksjs/error-handling": "0.75.
|
|
67
|
-
"@stacksjs/logging": "0.75.
|
|
68
|
-
"@stacksjs/orm": "0.75.
|
|
69
|
-
"@stacksjs/path": "0.75.
|
|
70
|
-
"@stacksjs/router": "0.75.
|
|
71
|
-
"@stacksjs/security": "0.75.
|
|
72
|
-
"@stacksjs/storage": "0.75.
|
|
61
|
+
"@stacksjs/cache": "0.75.182",
|
|
62
|
+
"@stacksjs/config": "0.75.182",
|
|
63
|
+
"@stacksjs/database": "0.75.182",
|
|
64
|
+
"@stacksjs/email": "0.75.182",
|
|
65
|
+
"@stacksjs/env": "0.75.182",
|
|
66
|
+
"@stacksjs/error-handling": "0.75.182",
|
|
67
|
+
"@stacksjs/logging": "0.75.182",
|
|
68
|
+
"@stacksjs/orm": "0.75.182",
|
|
69
|
+
"@stacksjs/path": "0.75.182",
|
|
70
|
+
"@stacksjs/router": "0.75.182",
|
|
71
|
+
"@stacksjs/security": "0.75.182",
|
|
72
|
+
"@stacksjs/storage": "0.75.182",
|
|
73
73
|
"@stacksjs/ts-auth": "^0.4.5",
|
|
74
74
|
"ts-qr-codes": "^0.1.8"
|
|
75
75
|
},
|