@stacksjs/auth 0.75.409 → 0.75.417

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/README.md CHANGED
@@ -99,3 +99,16 @@ For casual chit-chat with others using this package:
99
99
  The MIT License (MIT). Please see [LICENSE](https://github.com/stacksjs/stacks/tree/main/LICENSE.md) for more information.
100
100
 
101
101
  Made with 💙
102
+
103
+ ### Indexed recovery credentials
104
+
105
+ `mintPasswordResetToken()` returns a random credential and its SHA-256 digest. Store only
106
+ `digest` in `password_resets.token`. Token-only recovery endpoints use
107
+ `consumePasswordResetToken(token, async claim => { ... })` to change the account and
108
+ revoke its credentials in one transaction. `claim` contains `userId`, `email` and
109
+ `hasExplicitExpiry`. The result is `{ consumed: false }` or
110
+ `{ consumed: true, value }`, with the callback's value. Unknown, expired, replaced
111
+ and already consumed tokens never invoke the callback. A failed callback rolls
112
+ everything back, leaving recovery retryable. Notifications belong after commit.
113
+ This API is for high-entropy digest tokens; `passwordResets(email)` continues to
114
+ serve existing email-based bcrypt recovery credentials.
package/dist/index.d.ts CHANGED
@@ -33,6 +33,7 @@ export * from './magic-link';
33
33
  export * from './passkey';
34
34
  export * from './passkeys';
35
35
  export * from './password/reset';
36
+ export * from './password/reset-token';
36
37
  export * from './register';
37
38
  export * from './user';
38
39
  // Token management (Laravel Passport-style)
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export*from"./authentication";export*from"./impersonation";export{requestImpersonation}from"./impersonation-auth";export*from"./impersonation-store";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*from"./session-handoff";export{generateTOTP,verifyTOTP,generateTOTPSecret,totpKeyUri}from"@stacksjs/ts-auth";export*from"./two-factor";export*from"./team";export*from"./referrals";export*from"./oauth-dynamic-registration";export*from"./oauth-protected-resource";export*from"./oauth-server";
1
+ export*from"./authentication";export*from"./impersonation";export{requestImpersonation}from"./impersonation-auth";export*from"./impersonation-store";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"./password/reset-token";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*from"./session-handoff";export{generateTOTP,verifyTOTP,generateTOTPSecret,totpKeyUri}from"@stacksjs/ts-auth";export*from"./two-factor";export*from"./team";export*from"./referrals";export*from"./oauth-dynamic-registration";export*from"./oauth-protected-resource";export*from"./oauth-server";
@@ -0,0 +1,15 @@
1
+ /** High-entropy reset credentials support indexed lookup without storing a usable token. */
2
+ export declare function passwordResetTokenDigest(token: string): string;
3
+ export declare function mintPasswordResetToken(): { token: string, digest: string };
4
+ /**
5
+ * Redeem a SHA-256 reset credential and apply account changes in one transaction.
6
+ * Issuance stores mintPasswordResetToken().digest in password_resets.token.
7
+ * The callback must contain database work only; notify the person after commit.
8
+ * A callback failure restores the token, and competing redeemers cannot both win.
9
+ */
10
+ export declare function consumePasswordResetToken<T>(token: string, apply: (claim: PasswordResetClaim) => Promise<T>, options?: { ttlMs?: number }): Promise<{ consumed: false } | { consumed: true, value: T }>;
11
+ export declare interface PasswordResetClaim {
12
+ userId: number
13
+ email: string
14
+ hasExplicitExpiry: boolean
15
+ }
@@ -0,0 +1 @@
1
+ import{createHash,randomBytes}from"node:crypto";import{config}from"@stacksjs/config";import{db,lockRow,mutationCount,parseSqlDateTime}from"@stacksjs/database/runtime";export function passwordResetTokenDigest(token){return createHash("sha256").update(token).digest("hex")}export function mintPasswordResetToken(){const token=randomBytes(32).toString("hex");return{token,digest:passwordResetTokenDigest(token)}}export async function consumePasswordResetToken(token,apply,options={}){if(!token||token.length>1024)return{consumed:!1};const digest=passwordResetTokenDigest(token),observed=await db.primary.selectFrom("password_resets").select(["email"]).where("token","=",digest).executeTakeFirst();if(!observed)return{consumed:!1};const email=String(observed.email),owner=await db.primary.selectFrom("users").select("id").where("email","=",email).executeTakeFirst();if(!owner)return{consumed:!1};const ttlMs=options.ttlMs??Number(config.auth.passwordReset?.expire??60)*60000;if(!Number.isFinite(ttlMs)||ttlMs<=0)throw RangeError("Reset lifetime must be positive");return db.transaction(async(tx)=>{const user=await lockRow(tx,"users",{id:Number(owner.id)});if(!user||String(user.email)!==email)return{consumed:!1};const reset=await lockRow(tx,"password_resets",{email});if(!reset||reset.token!==digest)return{consumed:!1};if(!((reset.expires_at==null?(parseSqlDateTime(reset.created_at)?.getTime()??0)+ttlMs:parseSqlDateTime(reset.expires_at)?.getTime()??0)>Date.now())){await db.deleteFrom("password_resets").where("email","=",email).where("token","=",digest).execute();return{consumed:!1}}const value=await apply({userId:Number(owner.id),email,hasExplicitExpiry:reset.expires_at!=null}),deleted=await db.deleteFrom("password_resets").where("email","=",email).where("token","=",digest).execute();if(mutationCount(deleted)!==1)throw Error("Password reset token could not be consumed");return{consumed:!0,value}})}
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.409",
5
+ "version": "0.75.417",
6
6
  "description": "A more simplistic way to authenticate.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -59,19 +59,19 @@
59
59
  },
60
60
  "dependencies": {
61
61
  "@stacksjs/bun-router": "^0.1.24",
62
- "@stacksjs/cache": "0.75.409",
63
- "@stacksjs/config": "0.75.409",
64
- "@stacksjs/database": "0.75.409",
65
- "@stacksjs/email": "0.75.409",
66
- "@stacksjs/env": "0.75.409",
67
- "@stacksjs/error-handling": "0.75.409",
68
- "@stacksjs/logging": "0.75.409",
69
- "@stacksjs/orm": "0.75.409",
70
- "@stacksjs/path": "0.75.409",
71
- "@stacksjs/router": "0.75.409",
72
- "@stacksjs/security": "0.75.409",
73
- "@stacksjs/socials": "0.75.409",
74
- "@stacksjs/storage": "0.75.409",
62
+ "@stacksjs/cache": "0.75.417",
63
+ "@stacksjs/config": "0.75.417",
64
+ "@stacksjs/database": "0.75.417",
65
+ "@stacksjs/email": "0.75.417",
66
+ "@stacksjs/env": "0.75.417",
67
+ "@stacksjs/error-handling": "0.75.417",
68
+ "@stacksjs/logging": "0.75.417",
69
+ "@stacksjs/orm": "0.75.417",
70
+ "@stacksjs/path": "0.75.417",
71
+ "@stacksjs/router": "0.75.417",
72
+ "@stacksjs/security": "0.75.417",
73
+ "@stacksjs/socials": "0.75.417",
74
+ "@stacksjs/storage": "0.75.417",
75
75
  "@stacksjs/ts-auth": "^0.4.7",
76
76
  "ts-qr-codes": "^0.1.8"
77
77
  },