@stacksjs/auth 0.72.5 → 0.72.7

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.
@@ -10,6 +10,26 @@
10
10
  * protected route (#2306).
11
11
  */
12
12
  export declare function authMiddleware(request: any): Promise<void>;
13
+ /**
14
+ * The authenticated user, for a middleware or action that needs to reason
15
+ * about one.
16
+ *
17
+ * Every authorization middleware in the scaffold used to open with the same
18
+ * line:
19
+ *
20
+ * const user = request.user || request._user || request._authenticatedUser
21
+ *
22
+ * Two things were wrong with it. `_user` is assigned nowhere in the framework,
23
+ * so it was a dead term. And `user` is a lazily-resolving MACRO - a function -
24
+ * so on any request carrying it, `user` WAS the function: truthy enough to
25
+ * pass the "is anyone signed in" check, then missing every field the caller
26
+ * went on to read, which surfaces as a confusing 403 rather than an honest
27
+ * 401.
28
+ *
29
+ * Resolving it here means every caller agrees on what "the user" is, and the
30
+ * answer is a user rather than a callable.
31
+ */
32
+ export declare function authenticatedUser(request: any): Promise<any | undefined>;
13
33
  /**
14
34
  * Auth middleware object with handle method (for compatibility with middleware loader)
15
35
  * @defaultValue `{ name: 'auth' }`
@@ -1 +1 @@
1
- import{Auth}from"./authentication";import{requestToken}from"./request-token";export async function authMiddleware(request){const bearerToken=requestToken(request);if(!bearerToken){const error=Error("No authentication token provided.");error.statusCode=401;throw error}const user=await Auth.getUserFromToken(bearerToken);if(!user){const error=Error("Invalid or expired authentication token.");error.statusCode=401;throw error}Auth.setUser(user);request._authenticatedUser=user;const accessToken=await Auth.currentAccessToken();request._currentAccessToken=accessToken}export const authMiddlewareHandler={name:"auth",handle:authMiddleware};
1
+ import{Auth}from"./authentication";import{requestToken}from"./request-token";export async function authMiddleware(request){const bearerToken=requestToken(request);if(!bearerToken){const error=Error("No authentication token provided.");error.statusCode=401;throw error}const user=await Auth.getUserFromToken(bearerToken);if(!user){const error=Error("Invalid or expired authentication token.");error.statusCode=401;throw error}Auth.setUser(user);request._authenticatedUser=user;const accessToken=await Auth.currentAccessToken();request._currentAccessToken=accessToken}export const authMiddlewareHandler={name:"auth",handle:authMiddleware};export async function authenticatedUser(request){const cached=request?._authenticatedUser;if(cached&&typeof cached==="object")return cached;const macro=request?.user;if(typeof macro==="function"){const resolved=await macro();return resolved&&typeof resolved==="object"?resolved:void 0}if(macro&&typeof macro==="object")return macro;return}
package/dist/passkey.d.ts CHANGED
@@ -12,6 +12,17 @@ export type {
12
12
  RegistrationOptions,
13
13
  AuthenticationOptions,
14
14
  } from '@stacksjs/ts-auth';
15
+ /**
16
+ * Turn a user's stored passkeys into credential descriptors for the options a
17
+ * server hands the browser.
18
+ *
19
+ * Both `GenerateRegistrationAction` (as `excludeCredentials`) and
20
+ * `GenerateAuthenticationAction` (as `allowCredentials`) need the same three
21
+ * facts: the stored base64url id, the `type: 'public-key'` the spec requires,
22
+ * and the transports. Building it in one place is also where the
23
+ * ArrayBuffer-vs-JSON boundary gets explained once rather than at each call.
24
+ */
25
+ export declare function passkeyDescriptors(passkeys: readonly PasskeyAttribute[]): PublicKeyCredentialDescriptorJSON[];
15
26
  export declare function getUserPasskeys(userId: number): Promise<PasskeyAttribute[]>;
16
27
  export declare function getUserPasskey(userId: number, passkeyId: string): Promise<PasskeyAttribute | undefined>;
17
28
  /**
@@ -51,6 +62,45 @@ export declare function storeWebAuthnChallenge(userId: number, challenge: string
51
62
  * failure.
52
63
  */
53
64
  export declare function consumeWebAuthnChallenge(userId: number, purpose: WebAuthnChallengePurpose): Promise<Uint8Array | null>;
65
+ /**
66
+ * A credential descriptor as it crosses the wire.
67
+ *
68
+ * `ts-auth`'s `PublicKeyCredentialRequestOptions` describes the shape the
69
+ * BROWSER wants: `id` as an ArrayBuffer. A server handing those options to a
70
+ * client has to send JSON, and an ArrayBuffer serializes to `{}` - so ids
71
+ * travel as the base64url strings the passkey rows already store, and the
72
+ * client turns them back into buffers before calling `navigator.credentials`.
73
+ *
74
+ * The passkey actions were already written against these names; they simply
75
+ * had nowhere to import them from, so the whole file typechecked as `any`.
76
+ */
77
+ export declare interface PublicKeyCredentialDescriptorJSON {
78
+ id: string
79
+ type: 'public-key'
80
+ transports?: Array<'ble' | 'internal' | 'nfc' | 'usb' | 'hybrid'>
81
+ }
82
+ export declare interface PublicKeyCredentialRequestOptionsJSON {
83
+ challenge: Uint8Array
84
+ rpId?: string
85
+ allowCredentials?: PublicKeyCredentialDescriptorJSON[]
86
+ userVerification?: 'required' | 'preferred' | 'discouraged'
87
+ timeout?: number
88
+ }
89
+ export declare interface PublicKeyCredentialCreationOptionsJSON {
90
+ challenge: Uint8Array
91
+ rp: { name: string, id?: string }
92
+ user: { id: Uint8Array | string, name: string, displayName: string }
93
+ pubKeyCredParams: Array<{ alg: number, type: 'public-key' }>
94
+ timeout?: number
95
+ attestation?: string
96
+ authenticatorSelection?: {
97
+ authenticatorAttachment?: 'platform' | 'cross-platform'
98
+ requireResidentKey?: boolean
99
+ residentKey?: 'discouraged' | 'preferred' | 'required'
100
+ userVerification?: 'required' | 'preferred' | 'discouraged'
101
+ }
102
+ excludeCredentials?: PublicKeyCredentialDescriptorJSON[]
103
+ }
54
104
  export declare interface PasskeyAttribute {
55
105
  id: string
56
106
  cred_public_key: string
package/dist/passkey.js CHANGED
@@ -1 +1 @@
1
- import{Buffer}from"node:buffer";import{db,sqlDateTime,parseSqlDateTime}from"@stacksjs/database";import{User}from"@stacksjs/orm";export{generateRegistrationOptions,generateAuthenticationOptions,verifyRegistrationResponse,verifyAuthenticationResponse,startRegistration,startAuthentication,browserSupportsWebAuthn,browserSupportsWebAuthnAutofill,platformAuthenticatorIsAvailable}from"@stacksjs/ts-auth";export async function getUserPasskeys(userId){return await db.selectFrom("passkeys").selectAll().where("user_id","=",userId).execute()}export async function getUserPasskey(userId,passkeyId){return await db.selectFrom("passkeys").selectAll().where("id","=",passkeyId).where("user_id","=",userId).executeTakeFirst()}export async function updatePasskeyCounter(userId,passkeyId,newCounter){const passkey=await getUserPasskey(userId,passkeyId);if(!passkey)return!1;const stored=Number(passkey.counter??0);if(newCounter<=stored&&!(newCounter===0&&stored===0))return!1;await db.updateTable("passkeys").set({counter:newCounter,last_used_at:formatDateTime()}).where("id","=",passkeyId).where("user_id","=",userId).execute();return!0}export async function setCurrentRegistrationOptions(user,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(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:formatDateTime()};await db.insertInto("passkeys").values(passkeyData).executeTakeFirstOrThrow()}function formatDateTime(){const date=new Date,pad=(num)=>String(num).padStart(2,"0"),year=date.getFullYear(),month=pad(date.getMonth()+1),day=pad(date.getDate()),hours=pad(date.getHours()),minutes=pad(date.getMinutes()),seconds=pad(date.getSeconds());return`${year}-${month}-${day} ${hours}:${minutes}:${seconds}`}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.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()}export async function consumeWebAuthnChallenge(userId,purpose){const row=await db.selectFrom("webauthn_challenges").where("user_id","=",userId).where("purpose","=",purpose).selectAll().executeTakeFirst();if(!row)return null;await db.deleteFrom("webauthn_challenges").where("user_id","=",userId).where("purpose","=",purpose).execute();const expiresAt=parseSqlDateTime(row.expires_at)?.getTime()??0;if(Date.now()>expiresAt)return null;return Buffer.from(String(row.challenge),"base64url")}
1
+ import{Buffer}from"node:buffer";import{db,sqlDateTime,parseSqlDateTime}from"@stacksjs/database";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["internal"];try{const parsed=JSON.parse(stored);return Array.isArray(parsed)&&parsed.length?parsed:["internal"]}catch{return["internal"]}}export async function getUserPasskeys(userId){return await db.selectFrom("passkeys").selectAll().where("user_id","=",userId).execute()}export async function getUserPasskey(userId,passkeyId){return await db.selectFrom("passkeys").selectAll().where("id","=",passkeyId).where("user_id","=",userId).executeTakeFirst()}export async function updatePasskeyCounter(userId,passkeyId,newCounter){const passkey=await getUserPasskey(userId,passkeyId);if(!passkey)return!1;const stored=Number(passkey.counter??0);if(newCounter<=stored&&!(newCounter===0&&stored===0))return!1;await db.updateTable("passkeys").set({counter:newCounter,last_used_at:formatDateTime()}).where("id","=",passkeyId).where("user_id","=",userId).execute();return!0}export async function setCurrentRegistrationOptions(user,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(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:formatDateTime()};await db.insertInto("passkeys").values(passkeyData).executeTakeFirstOrThrow()}function formatDateTime(){const date=new Date,pad=(num)=>String(num).padStart(2,"0"),year=date.getFullYear(),month=pad(date.getMonth()+1),day=pad(date.getDate()),hours=pad(date.getHours()),minutes=pad(date.getMinutes()),seconds=pad(date.getSeconds());return`${year}-${month}-${day} ${hours}:${minutes}:${seconds}`}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.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()}export async function consumeWebAuthnChallenge(userId,purpose){const row=await db.selectFrom("webauthn_challenges").where("user_id","=",userId).where("purpose","=",purpose).selectAll().executeTakeFirst();if(!row)return null;await db.deleteFrom("webauthn_challenges").where("user_id","=",userId).where("purpose","=",purpose).execute();const expiresAt=parseSqlDateTime(row.expires_at)?.getTime()??0;if(Date.now()>expiresAt)return null;return Buffer.from(String(row.challenge),"base64url")}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/auth",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.72.5",
5
+ "version": "0.72.7",
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.23",
64
- "@stacksjs/error-handling": "0.72.5",
65
- "@stacksjs/router": "0.72.5"
64
+ "@stacksjs/error-handling": "0.72.7",
65
+ "@stacksjs/router": "0.72.7"
66
66
  }
67
67
  }