@stacksjs/auth 0.70.45 → 0.70.53
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/LICENSE.md +21 -0
- package/dist/index.js +102 -94
- package/dist/src/client.d.ts +21 -1
- package/dist/src/index.d.ts +7 -0
- package/dist/src/internal-constants.d.ts +19 -0
- package/dist/src/passkey.d.ts +51 -0
- package/dist/src/rate-limiter.d.ts +42 -4
- package/dist/src/rbac-seed.d.ts +35 -0
- package/dist/src/rbac-store-bqb.d.ts +18 -0
- package/dist/src/rbac.d.ts +17 -0
- package/dist/src/session-auth.d.ts +20 -1
- package/dist/src/team.d.ts +121 -0
- package/dist/src/tokens.d.ts +31 -0
- package/dist/src/two-factor.d.ts +67 -0
- package/package.json +4 -4
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Open Web Foundation
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.js
CHANGED
|
@@ -1,157 +1,165 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var
|
|
2
|
+
var X$=Object.defineProperty;var Y$=($)=>$;function K$($,W){this[$]=Y$.bind(null,W)}var e0=($,W)=>{for(var Z in W)X$($,Z,{get:W[Z],enumerable:!0,configurable:!0,set:K$.bind(W,Z)})};var $1=($,W)=>()=>($&&(W=$($=0)),W);var D0=import.meta.require;var K1={};e0(K1,{validateRefreshToken:()=>E$,tokens:()=>M$,tokenCant:()=>q$,tokenCanAny:()=>P$,tokenCanAll:()=>C$,tokenCan:()=>X1,tokenAbilities:()=>v$,token:()=>U$,revokeTokenById:()=>y$,revokeToken:()=>N$,revokeRefreshToken:()=>x$,revokeOtherTokens:()=>T$,revokeClient:()=>g$,revokeAllTokens:()=>j0,revokeAllRefreshTokens:()=>Y1,refreshToken:()=>S$,parseScopes:()=>f,isIssuedBeforePasswordChange:()=>d,getPasswordChangedAt:()=>s,findToken:()=>G1,findClient:()=>b$,deleteRevokedTokens:()=>w$,deleteRevokedRefreshTokens:()=>A$,deleteExpiredTokens:()=>f$,deleteExpiredRefreshTokens:()=>R$,currentAccessToken:()=>n,createToken:()=>f0,createClient:()=>k$,clients:()=>I$});import{createHash as O$,randomBytes as _$}from"crypto";import{db as B}from"@stacksjs/database";import{HttpError as X0}from"@stacksjs/error-handling";import{getCurrentRequest as z$}from"@stacksjs/router";import{env as D$}from"@stacksjs/env";import{sqlHelpers as j$}from"@stacksjs/database";function z($){return J1.param($)}function h($){return O$("sha256").update($).digest("hex")}function Q1($){let W=$.indexOf(":"),Z=W===-1?$:$.substring(0,W);return h(Z)}function Y0($=40){return _$($).toString("hex")}async function s($,W=B){if($===null||$===void 0)return null;try{let J=(await W.unsafe(`
|
|
3
|
+
SELECT password_changed_at FROM users WHERE id = ${z(1)} LIMIT 1
|
|
4
|
+
`,[$]))[0]?.password_changed_at;if(J===null||J===void 0)return null;let Q=new Date(String(J));return Number.isNaN(Q.getTime())?null:Q}catch{return null}}function d($,W){if(!W)return!1;if($===null||$===void 0)return!1;let Z=new Date(String($));if(Number.isNaN(Z.getTime()))return!1;return Z.getTime()<W.getTime()}async function M$($){return(await B.unsafe(`
|
|
3
5
|
SELECT t.*, c.provider as client_provider
|
|
4
6
|
FROM oauth_access_tokens t
|
|
5
7
|
LEFT JOIN oauth_clients c ON t.oauth_client_id = c.id
|
|
6
|
-
WHERE t.user_id = ${
|
|
7
|
-
AND t.revoked = ${
|
|
8
|
+
WHERE t.user_id = ${z(1)}
|
|
9
|
+
AND t.revoked = ${K0}
|
|
8
10
|
ORDER BY t.created_at DESC
|
|
9
|
-
`,[$])).map((
|
|
11
|
+
`,[$])).map((Z)=>({id:Z.id,userId:Z.user_id,clientId:Z.oauth_client_id,name:Z.name||"access-token",scopes:f(Z.scopes),revoked:!!Z.revoked,expiresAt:Z.expires_at?new Date(Z.expires_at):null,createdAt:new Date(Z.created_at),updatedAt:Z.updated_at?new Date(Z.updated_at):new Date}))}async function G1($){let W=Q1($),J=(await B.unsafe(`
|
|
10
12
|
SELECT * FROM oauth_access_tokens
|
|
11
|
-
WHERE token = ${
|
|
12
|
-
AND revoked = ${
|
|
13
|
-
AND (expires_at IS NULL OR expires_at > ${
|
|
13
|
+
WHERE token = ${z(1)}
|
|
14
|
+
AND revoked = ${K0}
|
|
15
|
+
AND (expires_at IS NULL OR expires_at > ${U})
|
|
14
16
|
LIMIT 1
|
|
15
|
-
`,[
|
|
17
|
+
`,[W]))[0];if(!J)return null;if(d(J.created_at,await s(J.user_id)))return null;return{id:J.id,userId:J.user_id,clientId:J.oauth_client_id,name:J.name||"access-token",scopes:f(J.scopes),revoked:!!J.revoked,expiresAt:J.expires_at?new Date(J.expires_at):null,createdAt:new Date(J.created_at),updatedAt:J.updated_at?new Date(J.updated_at):new Date}}async function n(){let $=z$();if(!$)return null;let W=$._currentAccessToken;if(W)return W;let Z=$.bearerToken?.();if(!Z)return null;let J=await G1(Z);if(J)$._currentAccessToken=J;return J}async function X1($){let W=await n();if(!W)return!1;if(W.scopes.includes("*"))return!0;return W.scopes.includes($)}async function q$($){return!await X1($)}async function C$($){let W=await n();if(!W)return!1;if(W.scopes.includes("*"))return!0;return $.every((Z)=>W.scopes.includes(Z))}async function P$($){let W=await n();if(!W)return!1;if(W.scopes.includes("*"))return!0;return $.some((Z)=>W.scopes.includes(Z))}async function v$(){return(await n())?.scopes||[]}async function f0($,W="access-token",Z=["*"],J={}){let{expiresInMinutes:Q=60,withRefreshToken:G=!0,refreshExpiresInDays:X=30}=J,Y=(await B.unsafe(`
|
|
16
18
|
SELECT id FROM oauth_clients WHERE personal_access_client = ${v} LIMIT 1
|
|
17
|
-
`))[0];if(!
|
|
19
|
+
`))[0];if(!Y)throw new X0(500,"No personal access client found. Run ./buddy auth:setup first.");let L=Y0(40),O=h(L),_=new Date;if(_.setMinutes(_.getMinutes()+Q),r)await B.unsafe(`
|
|
18
20
|
INSERT INTO oauth_access_tokens (user_id, oauth_client_id, token, name, scopes, revoked, expires_at, created_at, updated_at)
|
|
19
21
|
VALUES ($1, $2, $3, $4, $5, false, $6, NOW(), NOW())
|
|
20
|
-
`,[$,
|
|
22
|
+
`,[$,Y.id,O,W,JSON.stringify(Z),_.toISOString()]);else await B.unsafe(`
|
|
21
23
|
INSERT INTO oauth_access_tokens (user_id, oauth_client_id, token, name, scopes, revoked, expires_at, created_at, updated_at)
|
|
22
|
-
VALUES (?, ?, ?, ?, ?, 0, ?, ${
|
|
23
|
-
`,[$,
|
|
24
|
-
SELECT * FROM oauth_access_tokens WHERE token = ${
|
|
25
|
-
`,[
|
|
24
|
+
VALUES (?, ?, ?, ?, ?, 0, ?, ${U}, ${U})
|
|
25
|
+
`,[$,Y.id,O,W,JSON.stringify(Z),_.toISOString()]);let F=(await B.unsafe(`
|
|
26
|
+
SELECT * FROM oauth_access_tokens WHERE token = ${z(1)} LIMIT 1
|
|
27
|
+
`,[O]))[0];if(!F)throw new X0(500,"Failed to create access token \u2014 inserted row not found");let H={id:F.id,userId:F.user_id,clientId:F.oauth_client_id,name:F.name,scopes:f(F.scopes),revoked:!1,expiresAt:_,createdAt:new Date(F.created_at),updatedAt:new Date(F.updated_at)},P;if(G){P=Y0(40);let T=h(P),A=new Date;if(A.setDate(A.getDate()+X),r)await B.unsafe(`
|
|
26
28
|
INSERT INTO oauth_refresh_tokens (access_token_id, token, revoked, expires_at, created_at)
|
|
27
29
|
VALUES ($1, $2, false, $3, NOW())
|
|
28
|
-
`,[
|
|
30
|
+
`,[H.id,T,A.toISOString()]);else await B.unsafe(`
|
|
29
31
|
INSERT INTO oauth_refresh_tokens (access_token_id, token, revoked, expires_at, created_at)
|
|
30
|
-
VALUES (?, ?, 0, ?, ${
|
|
31
|
-
`,[
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
32
|
+
VALUES (?, ?, 0, ?, ${U})
|
|
33
|
+
`,[H.id,T,A.toISOString()])}return{accessToken:H,plainTextToken:L,refreshToken:P,expiresIn:Q*60}}async function S$($,W={}){let{expiresInMinutes:Z=60,refreshExpiresInDays:J=30}=W,Q=h($);return await B.transaction(async(G)=>{let X=G,Y=(await X.unsafe(`
|
|
34
|
+
SELECT r.*, t.user_id, t.oauth_client_id, t.name, t.scopes
|
|
35
|
+
FROM oauth_refresh_tokens r
|
|
36
|
+
JOIN oauth_access_tokens t ON r.access_token_id = t.id
|
|
37
|
+
WHERE r.token = ${z(1)}
|
|
38
|
+
AND r.revoked = ${K0}
|
|
39
|
+
AND (r.expires_at IS NULL OR r.expires_at > ${U})
|
|
40
|
+
LIMIT 1
|
|
41
|
+
`,[Q]))[0];if(!Y)throw new X0(401,"Invalid or expired refresh token");if(d(Y.created_at,await s(Y.user_id,X)))throw new X0(401,"Invalid or expired refresh token");await X.unsafe(`
|
|
42
|
+
UPDATE oauth_refresh_tokens
|
|
43
|
+
SET revoked = ${v}
|
|
44
|
+
WHERE id = ${z(1)}
|
|
45
|
+
`,[Y.id]),await X.unsafe(`
|
|
46
|
+
UPDATE oauth_access_tokens
|
|
47
|
+
SET revoked = ${v}
|
|
48
|
+
WHERE id = ${z(1)}
|
|
49
|
+
`,[Y.access_token_id]);let L=Y0(40),O=h(L),_=new Date;if(_.setMinutes(_.getMinutes()+Z),r)await X.unsafe(`
|
|
50
|
+
INSERT INTO oauth_access_tokens (user_id, oauth_client_id, token, name, scopes, revoked, expires_at, created_at, updated_at)
|
|
51
|
+
VALUES ($1, $2, $3, $4, $5, false, $6, NOW(), NOW())
|
|
52
|
+
`,[Y.user_id,Y.oauth_client_id,O,Y.name,Y.scopes,_.toISOString()]);else await X.unsafe(`
|
|
53
|
+
INSERT INTO oauth_access_tokens (user_id, oauth_client_id, token, name, scopes, revoked, expires_at, created_at, updated_at)
|
|
54
|
+
VALUES (?, ?, ?, ?, ?, 0, ?, ${U}, ${U})
|
|
55
|
+
`,[Y.user_id,Y.oauth_client_id,O,Y.name,Y.scopes,_.toISOString()]);let F=(await X.unsafe(`
|
|
56
|
+
SELECT * FROM oauth_access_tokens WHERE token = ${z(1)} LIMIT 1
|
|
57
|
+
`,[O]))[0],H={id:F.id,userId:F.user_id,clientId:F.oauth_client_id,name:F.name,scopes:f(F.scopes),revoked:!1,expiresAt:_,createdAt:new Date(F.created_at),updatedAt:new Date(F.updated_at)};if(d(F.created_at,await s(Y.user_id,X)))throw new X0(401,"Invalid or expired refresh token");let P=Y0(40),T=h(P),A=new Date;if(A.setDate(A.getDate()+J),r)await X.unsafe(`
|
|
58
|
+
INSERT INTO oauth_refresh_tokens (access_token_id, token, revoked, expires_at, created_at)
|
|
59
|
+
VALUES ($1, $2, false, $3, NOW())
|
|
60
|
+
`,[H.id,T,A.toISOString()]);else await X.unsafe(`
|
|
61
|
+
INSERT INTO oauth_refresh_tokens (access_token_id, token, revoked, expires_at, created_at)
|
|
62
|
+
VALUES (?, ?, 0, ?, ${U})
|
|
63
|
+
`,[H.id,T,A.toISOString()]);return{accessToken:H,plainTextToken:L,refreshToken:P,expiresIn:Z*60}})}async function E$($){let W=h($);return(await B.unsafe(`
|
|
58
64
|
SELECT id FROM oauth_refresh_tokens
|
|
59
|
-
WHERE token = ${
|
|
60
|
-
AND revoked = ${
|
|
61
|
-
AND (expires_at IS NULL OR expires_at > ${
|
|
65
|
+
WHERE token = ${z(1)}
|
|
66
|
+
AND revoked = ${K0}
|
|
67
|
+
AND (expires_at IS NULL OR expires_at > ${U})
|
|
62
68
|
LIMIT 1
|
|
63
|
-
`,[
|
|
69
|
+
`,[W])).length>0}async function x$($){let W=h($);await B.unsafe(`
|
|
64
70
|
UPDATE oauth_refresh_tokens
|
|
65
71
|
SET revoked = ${v}
|
|
66
|
-
WHERE token = ${
|
|
67
|
-
`,[
|
|
72
|
+
WHERE token = ${z(1)}
|
|
73
|
+
`,[W])}async function Y1($){await B.unsafe(`
|
|
68
74
|
UPDATE oauth_refresh_tokens
|
|
69
75
|
SET revoked = ${v}
|
|
70
76
|
WHERE access_token_id IN (
|
|
71
|
-
SELECT id FROM oauth_access_tokens WHERE user_id = ${
|
|
77
|
+
SELECT id FROM oauth_access_tokens WHERE user_id = ${z(1)}
|
|
72
78
|
)
|
|
73
|
-
`,[$])}async function
|
|
79
|
+
`,[$])}async function R$(){let $=await B.unsafe(`
|
|
74
80
|
DELETE FROM oauth_refresh_tokens
|
|
75
|
-
WHERE expires_at < ${
|
|
76
|
-
`);return $?.changes||$?.rowCount||0}async function
|
|
81
|
+
WHERE expires_at < ${U}
|
|
82
|
+
`);return $?.changes||$?.rowCount||0}async function A$($=7){let W=new Date;W.setDate(W.getDate()-$);let Z=await B.unsafe(`
|
|
77
83
|
DELETE FROM oauth_refresh_tokens
|
|
78
|
-
WHERE revoked = ${v} AND created_at < ${
|
|
79
|
-
`,[
|
|
84
|
+
WHERE revoked = ${v} AND created_at < ${z(1)}
|
|
85
|
+
`,[W.toISOString()]);return Z?.changes||Z?.rowCount||0}async function N$($){let W=Q1($);await B.unsafe(`
|
|
80
86
|
UPDATE oauth_refresh_tokens
|
|
81
87
|
SET revoked = ${v}
|
|
82
88
|
WHERE access_token_id IN (
|
|
83
|
-
SELECT id FROM oauth_access_tokens WHERE token = ${
|
|
89
|
+
SELECT id FROM oauth_access_tokens WHERE token = ${z(1)}
|
|
84
90
|
)
|
|
85
|
-
`,[
|
|
91
|
+
`,[W]),await B.unsafe(`
|
|
86
92
|
UPDATE oauth_access_tokens
|
|
87
|
-
SET revoked = ${v}, updated_at = ${
|
|
88
|
-
WHERE token = ${
|
|
89
|
-
`,[
|
|
93
|
+
SET revoked = ${v}, updated_at = ${U}
|
|
94
|
+
WHERE token = ${z(1)}
|
|
95
|
+
`,[W])}async function y$($){await B.unsafe(`
|
|
90
96
|
UPDATE oauth_refresh_tokens
|
|
91
97
|
SET revoked = ${v}
|
|
92
|
-
WHERE access_token_id = ${
|
|
93
|
-
`,[$]),await
|
|
98
|
+
WHERE access_token_id = ${z(1)}
|
|
99
|
+
`,[$]),await B.unsafe(`
|
|
94
100
|
UPDATE oauth_access_tokens
|
|
95
|
-
SET revoked = ${v}, updated_at = ${
|
|
96
|
-
WHERE id = ${
|
|
97
|
-
`,[$])}async function
|
|
101
|
+
SET revoked = ${v}, updated_at = ${U}
|
|
102
|
+
WHERE id = ${z(1)}
|
|
103
|
+
`,[$])}async function j0($){await Y1($),await B.unsafe(`
|
|
98
104
|
UPDATE oauth_access_tokens
|
|
99
|
-
SET revoked = ${v}, updated_at = ${
|
|
100
|
-
WHERE user_id = ${
|
|
101
|
-
`,[$])}async function
|
|
105
|
+
SET revoked = ${v}, updated_at = ${U}
|
|
106
|
+
WHERE user_id = ${z(1)}
|
|
107
|
+
`,[$])}async function T$($){let W=await n();if(!W)return j0($);if(r)await B.unsafe(`
|
|
102
108
|
UPDATE oauth_refresh_tokens
|
|
103
109
|
SET revoked = true
|
|
104
110
|
WHERE access_token_id IN (
|
|
105
111
|
SELECT id FROM oauth_access_tokens WHERE user_id = $1 AND id != $2
|
|
106
112
|
)
|
|
107
|
-
`,[$,
|
|
113
|
+
`,[$,W.id]),await B.unsafe(`
|
|
108
114
|
UPDATE oauth_access_tokens
|
|
109
115
|
SET revoked = true, updated_at = NOW()
|
|
110
116
|
WHERE user_id = $1 AND id != $2
|
|
111
|
-
`,[$,
|
|
117
|
+
`,[$,W.id]);else await B.unsafe(`
|
|
112
118
|
UPDATE oauth_refresh_tokens
|
|
113
119
|
SET revoked = 1
|
|
114
120
|
WHERE access_token_id IN (
|
|
115
121
|
SELECT id FROM oauth_access_tokens WHERE user_id = ? AND id != ?
|
|
116
122
|
)
|
|
117
|
-
`,[$,
|
|
123
|
+
`,[$,W.id]),await B.unsafe(`
|
|
118
124
|
UPDATE oauth_access_tokens
|
|
119
|
-
SET revoked = 1, updated_at = ${
|
|
125
|
+
SET revoked = 1, updated_at = ${U}
|
|
120
126
|
WHERE user_id = ? AND id != ?
|
|
121
|
-
`,[$,
|
|
127
|
+
`,[$,W.id])}async function f$(){await B.unsafe(`
|
|
122
128
|
DELETE FROM oauth_refresh_tokens
|
|
123
129
|
WHERE access_token_id IN (
|
|
124
|
-
SELECT id FROM oauth_access_tokens WHERE expires_at < ${
|
|
130
|
+
SELECT id FROM oauth_access_tokens WHERE expires_at < ${U}
|
|
125
131
|
)
|
|
126
|
-
`);let $=await
|
|
132
|
+
`);let $=await B.unsafe(`
|
|
127
133
|
DELETE FROM oauth_access_tokens
|
|
128
|
-
WHERE expires_at < ${
|
|
129
|
-
`);return $?.changes||$?.rowCount||0}async function
|
|
134
|
+
WHERE expires_at < ${U}
|
|
135
|
+
`);return $?.changes||$?.rowCount||0}async function w$($=7){let W=new Date;W.setDate(W.getDate()-$),await B.unsafe(`
|
|
130
136
|
DELETE FROM oauth_refresh_tokens
|
|
131
137
|
WHERE access_token_id IN (
|
|
132
|
-
SELECT id FROM oauth_access_tokens WHERE revoked = ${v} AND updated_at < ${
|
|
138
|
+
SELECT id FROM oauth_access_tokens WHERE revoked = ${v} AND updated_at < ${z(1)}
|
|
133
139
|
)
|
|
134
|
-
`,[
|
|
140
|
+
`,[W.toISOString()]);let Z=await B.unsafe(`
|
|
135
141
|
DELETE FROM oauth_access_tokens
|
|
136
|
-
WHERE revoked = ${v} AND updated_at < ${
|
|
137
|
-
`,[
|
|
142
|
+
WHERE revoked = ${v} AND updated_at < ${z(1)}
|
|
143
|
+
`,[W.toISOString()]);return Z?.changes||Z?.rowCount||0}async function I$($){return(await B.unsafe(`
|
|
138
144
|
SELECT * FROM oauth_clients
|
|
139
|
-
WHERE user_id = ${
|
|
145
|
+
WHERE user_id = ${z(1)} AND revoked = ${K0}
|
|
140
146
|
ORDER BY created_at DESC
|
|
141
|
-
`,[$])).map(
|
|
142
|
-
SELECT * FROM oauth_clients WHERE id = ${
|
|
143
|
-
`,[$]))[0];return
|
|
147
|
+
`,[$])).map(w0)}async function b$($){let Z=(await B.unsafe(`
|
|
148
|
+
SELECT * FROM oauth_clients WHERE id = ${z(1)} LIMIT 1
|
|
149
|
+
`,[$]))[0];return Z?w0(Z):null}async function k$($){let W=Y0(40);if(r)await B.unsafe(`
|
|
144
150
|
INSERT INTO oauth_clients (name, secret, provider, redirect, personal_access_client, password_client, revoked, created_at)
|
|
145
151
|
VALUES ($1, $2, 'local', $3, $4, $5, false, NOW())
|
|
146
|
-
`,[$.name,
|
|
152
|
+
`,[$.name,W,$.redirect,$.personalAccessClient||!1,$.passwordClient||!1]);else await B.unsafe(`
|
|
147
153
|
INSERT INTO oauth_clients (name, secret, provider, redirect, personal_access_client, password_client, revoked, created_at)
|
|
148
|
-
VALUES (?, ?, 'local', ?, ?, ?, 0, ${
|
|
149
|
-
`,[$.name,
|
|
150
|
-
SELECT * FROM oauth_clients WHERE secret = ${
|
|
151
|
-
`,[
|
|
154
|
+
VALUES (?, ?, 'local', ?, ?, ?, 0, ${U})
|
|
155
|
+
`,[$.name,W,$.redirect,$.personalAccessClient?1:0,$.passwordClient?1:0]);let Z=await B.unsafe(`
|
|
156
|
+
SELECT * FROM oauth_clients WHERE secret = ${z(1)} LIMIT 1
|
|
157
|
+
`,[W]);return{client:w0(Z[0]),plainTextSecret:W}}async function g$($){await B.unsafe(`
|
|
152
158
|
UPDATE oauth_clients
|
|
153
|
-
SET revoked = ${v}, updated_at = ${
|
|
154
|
-
WHERE id = ${
|
|
155
|
-
`,[$])}function y($){if(!$)return[];if(Array.isArray($))return $;try{let Z=JSON.parse($);return Array.isArray(Z)?Z:[]}catch{return[]}}function j0($){return{id:$.id,name:$.name,secret:$.secret,provider:$.provider,redirect:$.redirect,personalAccessClient:Boolean($.personal_access_client),passwordClient:Boolean($.password_client),revoked:Boolean($.revoked),createdAt:$.created_at?new Date($.created_at):new Date,updatedAt:$.updated_at?new Date($.updated_at):null}}function l($){return u4("sha256").update($).digest("hex")}class R{static authUser=void 0;static clientSecret=void 0;static currentToken=void 0;static getBearerToken(){let $=M0.bearerToken?.();if(!$){let Z=M0.headers?.get?.("authorization")||M0.headers?.get?.("Authorization");if(Z&&Z.startsWith("Bearer "))$=Z.substring(7)}return $||null}static parseToken($){let Z=$.indexOf(":");if(Z===-1)return null;let Q=$.substring(0,Z),X=$.substring(Z+1);if(!Q||!X)return null;return{plainToken:Q,encryptedId:X}}static async getClientSecret(){if(this.clientSecret)return this.clientSecret;let $=await this.getPersonalAccessClient();return this.clientSecret=$.secret,$.secret}static async getPersonalAccessClient(){try{let $=await O.selectFrom("oauth_clients").where("personal_access_client","=",!0).selectAll().executeTakeFirst();if(!$)throw new J0(500,"No personal access client found. Please run `./buddy auth:setup` first.");return $}catch($){if($ instanceof Error&&$.message.includes("does not exist"))throw new J0(500,"OAuth tables not found. Please run `./buddy auth:setup` first.");throw $}}static async validateClient($,Z){let Q=await O.selectFrom("oauth_clients").where("id","=",$).selectAll().executeTakeFirst();if(!Q?.secret)return!1;let X=A.from(String(Q.secret)),Y=A.from(Z);if(X.length!==Y.length)return!1;return z0(X,Y)}static async getTokenFromId($){let Z=await O.selectFrom("oauth_access_tokens").where("id","=",$).selectAll().executeTakeFirst();if(!Z)return null;let Q=Z;return{id:Q.id,userId:Q.user_id,clientId:Q.oauth_client_id,name:Q.name||"auth-token",scopes:y(Q.scopes),abilities:y(Q.scopes),expiresAt:Q.expires_at?new Date(String(Q.expires_at)):null,createdAt:Q.created_at?new Date(String(Q.created_at)):new Date,updatedAt:Q.updated_at?new Date(String(Q.updated_at)):new Date,revoked:!!Q.revoked}}static async attempt($){let Z=U.auth.username||"email",Q=U.auth.password||"password",X=$[Z];if(!X)return!1;if(r.isRateLimited(X))return!1;let Y=await $0.where("email","=",X).first(),_=$[Q]||"",W=Y?.password||"$2b$12$000000000000000000000uGByljkdFkOJRCRiYZGFOAstyLlSgTSW";if(await H0(_,W)&&Y)return r.resetAttempts(X),this.authUser=Y,!0;return r.recordFailedAttempt(X),!1}static async validate($){let Z=U.auth.username||"email",Q=U.auth.password||"password",X=$[Z];if(!X)return!1;let Y=await $0.where("email","=",X).first(),_=$[Q]||"",W=Y?.password||"$2b$12$000000000000000000000uGByljkdFkOJRCRiYZGFOAstyLlSgTSW";return await H0(_,W)&&!!Y}static async login($,Z){if(!await this.attempt($)||!this.authUser)return null;let{plainTextToken:X,refreshToken:Y,expiresIn:_}=await this.createTokenForUser(this.authUser,Z);return{user:this.authUser,token:X,refreshToken:Y,expiresIn:_}}static async loginUsingId($,Z){let Q=await $0.find($);if(!Q)return null;this.authUser=Q;let{plainTextToken:X,refreshToken:Y,expiresIn:_}=await this.createTokenForUser(Q,Z);return{user:Q,token:X,refreshToken:Y,expiresIn:_}}static async logout(){let $=this.getBearerToken();if($){let Z=this.parseToken($);if(Z){let Q=await this.getClientSecret(),X=await c(Z.encryptedId,Q).catch(()=>null);if(X)await O.updateTable("oauth_refresh_tokens").set({revoked:!0}).where("access_token_id","=",Number(X)).execute()}await this.revokeToken($)}this.authUser=void 0,this.currentToken=void 0}static async user(){if(this.authUser)return this.authUser;let $=this.getBearerToken();if(!$)return;let Z=await this.getUserFromToken($);if(Z)this.authUser=Z;return Z}static async check(){return await this.user()!==void 0}static async guest(){return!await this.check()}static async id(){return(await this.user())?.id}static setUser($){this.authUser=$}static async createTokenForUser($,Z){let Q=await this.getPersonalAccessClient(),X=await this.getClientSecret(),Y=Z?.name??U.auth.defaultTokenName??"auth-token",_=Z?.abilities??Z?.scopes??U.auth.defaultAbilities??["*"],W=Z?.expiresInMinutes!==void 0?Z.expiresInMinutes*60*1000:U.auth.tokenExpiry??3600000,J=Z?.expiresAt??new Date(Date.now()+W),G=Math.max(1,Math.floor((J.getTime()-Date.now())/1000)),K=_0.generateJWT($.id,G),B=l(K);y0.debug(`[auth] Creating token for user#${$.id}: ${Y}`),await O.insertInto("oauth_access_tokens").values({user_id:$.id,oauth_client_id:Q.id,name:Y,token:B,scopes:JSON.stringify(_),revoked:!1,expires_at:P(J)}).execute();let V=await O.selectFrom("oauth_access_tokens").where("token","=",B).selectAll().executeTakeFirst(),q=Number(V?.id);if(!q)throw new J0(500,"Failed to create token");let j=await N0(q.toString(),X),x=`${K}:${j}`,E={id:q,userId:$.id,clientId:Q.id,name:Y,scopes:_,abilities:_,expiresAt:J,createdAt:new Date,updatedAt:new Date,revoked:!1,plainTextToken:x},T;if(Z?.withRefreshToken!==!1){let k=Z?.refreshExpiresInDays??Math.max(1,Math.round((U.auth.refreshTokenExpiry??2592000000)/86400000));T=c4(40).toString("hex");let P0=l(T),q0=new Date(Date.now()+k*24*60*60*1000);await O.insertInto("oauth_refresh_tokens").values({access_token_id:q,token:P0,revoked:!1,expires_at:P(q0)}).execute()}return{accessToken:E,plainTextToken:x,refreshToken:T,expiresIn:G}}static async createToken($,Z=U.auth.defaultTokenName||"auth-token",Q=U.auth.defaultAbilities||["*"]){let{plainTextToken:X}=await this.createTokenForUser($,{name:Z,abilities:Q});return X}static async requestToken($,Z,Q){if(!await this.validateClient(Z,Q))throw new J0(401,"Invalid client credentials");if(!await this.attempt($)||!this.authUser)return null;return{token:await this.createToken(this.authUser,"user-auth-token")}}static async validateToken($){let Z=this.parseToken($);if(!Z)return!1;let{plainToken:Q,encryptedId:X}=Z,Y=await this.getClientSecret(),_=await c(X,Y);if(!_)return!1;let W=await O.selectFrom("oauth_access_tokens").where("id","=",Number(_)).selectAll().executeTakeFirst();if(!W)return!1;let J=l(Q),G=String(W.token);if(J.length!==G.length)return!1;if(!z0(A.from(J,"utf-8"),A.from(G,"utf-8")))return!1;if(y0.debug(`[auth] Token validated for token#${W.id}`),W.expires_at&&new Date(String(W.expires_at))<new Date)return await O.deleteFrom("oauth_access_tokens").where("id","=",W.id).execute(),!1;if(W.revoked)return!1;let B=U.auth.tokenRotation??24,V=W.updated_at?new Date(String(W.updated_at)):new Date,q=new Date;if((q.getTime()-V.getTime())/3600000>=B)await this.rotateToken($);else await O.updateTable("oauth_access_tokens").set({updated_at:P(q)}).where("id","=",W.id).execute();return!0}static async getUserFromToken($){let Z=this.parseToken($);if(!Z)return;let{plainToken:Q,encryptedId:X}=Z,Y=await this.getClientSecret(),_=await c(X,Y);if(!_)return;let W=await O.selectFrom("oauth_access_tokens").where("id","=",Number(_)).selectAll().executeTakeFirst();if(!W||W.token!==l(Q))return;if(W.expires_at&&new Date(String(W.expires_at))<new Date){await O.deleteFrom("oauth_access_tokens").where("id","=",W.id).execute();return}if(W.revoked)return;if(this.currentToken=await this.getTokenFromId(W.id)??void 0,await O.updateTable("oauth_access_tokens").set({updated_at:P(new Date)}).where("id","=",W.id).execute(),!W?.user_id)return;return await $0.find(W.user_id)}static async currentAccessToken(){if(this.currentToken)return this.currentToken;let $=this.getBearerToken();if(!$)return;let Z=this.parseToken($);if(!Z)return;let Q=await this.getClientSecret(),X=await c(Z.encryptedId,Q);if(!X)return;let Y=await this.getTokenFromId(Number(X));if(Y)this.currentToken=Y;return Y??void 0}static async tokenCan($){let Z=await this.currentAccessToken();if(!Z)return!1;if(Z.abilities.includes("*"))return!0;return Z.abilities.includes($)}static async tokenCant($){return!await this.tokenCan($)}static async tokenAbilities(){return(await this.currentAccessToken())?.abilities??[]}static async tokenCanAll($){for(let Z of $)if(!await this.tokenCan(Z))return!1;return!0}static async tokenCanAny($){for(let Z of $)if(await this.tokenCan(Z))return!0;return!1}static async tokens($){let Z=$??await this.id();if(!Z)return[];return(await O.selectFrom("oauth_access_tokens").where("user_id","=",Z).where("revoked","=",!1).selectAll().execute()).map((X)=>({id:Number(X.id),userId:Number(X.user_id),clientId:Number(X.oauth_client_id),name:String(X.name||"auth-token"),scopes:y(String(X.scopes??"")),abilities:y(String(X.scopes??"")),expiresAt:X.expires_at?new Date(String(X.expires_at)):null,createdAt:X.created_at?new Date(String(X.created_at)):new Date,updatedAt:X.updated_at?new Date(String(X.updated_at)):new Date,revoked:!!X.revoked}))}static async revokeToken($){let Z=this.parseToken($);if(!Z)return;let Q=await this.getClientSecret(),X=await c(Z.encryptedId,Q);if(!X)return;await O.updateTable("oauth_access_tokens").set({revoked:!0,updated_at:P(new Date)}).where("id","=",Number(X)).execute()}static async revokeTokenById($){await O.updateTable("oauth_access_tokens").set({revoked:!0,updated_at:P(new Date)}).where("id","=",$).execute()}static async revokeAllTokens($){let Z=$??await this.id();if(!Z)return;await O.updateTable("oauth_access_tokens").set({revoked:!0,updated_at:P(new Date)}).where("user_id","=",Z).execute()}static async revokeOtherTokens($){let Z=$??await this.id();if(!Z)return;let Q=await this.currentAccessToken();if(!Q)return;await O.updateTable("oauth_access_tokens").set({revoked:!0,updated_at:P(new Date)}).where("user_id","=",Z).where("id","!=",Q.id).execute()}static async pruneExpiredTokens(){let $=await O.deleteFrom("oauth_access_tokens").where("expires_at","<",P(new Date)).executeTakeFirst();return Number($?.numDeletedRows)||0}static async pruneRevokedTokens(){let $=await O.deleteFrom("oauth_access_tokens").where("revoked","=",!0).executeTakeFirst();return Number($?.numDeletedRows)||0}static async rotateToken($){let Z=this.parseToken($);if(!Z)return null;let{plainToken:Q,encryptedId:X}=Z,Y=await this.getClientSecret(),_=await c(X,Y);if(!_)return null;let W=await O.selectFrom("oauth_access_tokens").where("id","=",Number(_)).selectAll().executeTakeFirst();if(!W)return null;let J=l(Q),G=String(W.token);if(J.length!==G.length)return null;if(!z0(A.from(J,"utf-8"),A.from(G,"utf-8")))return null;let B=W.expires_at?new Date(String(W.expires_at)).getTime():Date.now()+(U.auth.tokenExpiry??3600000),V=Math.max(1,Math.floor((B-Date.now())/1000)),q=_0.generateJWT(W.user_id,V);await O.updateTable("oauth_access_tokens").set({token:l(q),updated_at:P(new Date)}).where("id","=",W.id).execute();let j=await N0(W.id.toString(),Y);return`${q}:${j}`}static async findToken($){return this.getTokenFromId($)}static async once($){let Z=U.auth.username||"email",Q=U.auth.password||"password",X=$[Z];if(!X)return!1;let Y=await $0.where("email","=",X).first(),_=$[Q]||"",W=Y?.password||"$2b$12$000000000000000000000uGByljkdFkOJRCRiYZGFOAstyLlSgTSW";if(await H0(_,W)&&Y)return this.authUser=Y,!0;return!1}static guard($){return this}static viaRemember(){return!1}static clearState(){this.authUser=void 0,this.currentToken=void 0,this.clientSecret=void 0}}import{generateTOTP as l4,generateTOTPSecret as d4,totpKeyUri as n4,verifyTOTP as i4}from"@stacksjs/ts-auth";function t4(){return d4()}async function c6($){return l4({secret:$})}async function l6($,Z){return i4($,{secret:Z})}function d6($,Z,Q){let X=$||"johndoe@example.com",Y=Z||"StacksJS 2fa",_=Q||t4();return n4(X,Y,_)}import{randomBytes as o4}from"crypto";import{db as U0}from"@stacksjs/database";import{formatDate as a4}from"@stacksjs/orm";import{HttpError as r4,ok as s4}from"@stacksjs/error-handling";async function r6(){let $=o4(40).toString("hex");if(await U0.insertInto("oauth_clients").values({name:"Personal Access Client",secret:$,provider:"local",redirect:"http://localhost",personal_access_client:!0,password_client:!1,revoked:!1,created_at:a4(new Date)}).execute(),!(await U0.selectFrom("oauth_clients").where("secret","=",$).select(["id"]).executeTakeFirst())?.id)throw new r4(500,"Failed to create personal access client");return s4($)}async function e4($){let Z=$.bearerToken?.();if(!Z){let Y=$.headers?.get?.("authorization")||$.headers?.get?.("Authorization");if(Y&&Y.startsWith("Bearer "))Z=Y.substring(7)}if(!Z){let Y=Error("No authentication token provided.");throw Y.statusCode=401,Y}let Q=await R.getUserFromToken(Z);if(!Q){let Y=Error("Invalid or expired authentication token.");throw Y.statusCode=401,Y}R.setUser(Q),$._authenticatedUser=Q;let X=await R.currentAccessToken();$._currentAccessToken=X}var $7={name:"auth",handle:e4};import{db as v0}from"@stacksjs/database";import{generateRegistrationOptions as G7,generateAuthenticationOptions as L7,verifyRegistrationResponse as F7,verifyAuthenticationResponse as K7,startRegistration as V7,startAuthentication as B7,browserSupportsWebAuthn as O7,browserSupportsWebAuthnAutofill as D7,platformAuthenticatorIsAvailable as q7}from"@stacksjs/ts-auth";async function X7($){return await v0.selectFrom("passkeys").selectAll().where("user_id","=",$).execute()}async function Y7($,Z){return await v0.selectFrom("passkeys").selectAll().where("id","=",Z).where("user_id","=",$).executeTakeFirst()}async function W7($,Z){let Q=Z.registrationInfo?.credential.id,X=Z.registrationInfo?.credential.publicKey;if(!Q)throw Error("[auth/passkey] WebAuthn registration response is missing credential.id");if(!X)throw Error("[auth/passkey] WebAuthn registration response is missing credential.publicKey");let Y={id:Q,cred_public_key:JSON.stringify(X),user_id:$.id,webauthn_user_id:$.email||"",counter:Z.registrationInfo?.credential.counter||0,credential_type:Z.registrationInfo?.credentialType||"",device_type:Z.registrationInfo?.credentialDeviceType||"",backup_eligible:!1,backup_status:Z.registrationInfo?.credentialBackedUp||!1,transports:JSON.stringify(["internal"]),last_used_at:$5()};await v0.insertInto("passkeys").values(Y).executeTakeFirstOrThrow()}function $5(){let $=new Date,Z=(G)=>String(G).padStart(2,"0"),Q=$.getFullYear(),X=Z($.getMonth()+1),Y=Z($.getDate()),_=Z($.getHours()),W=Z($.getMinutes()),J=Z($.getSeconds());return`${Q}-${X}-${Y} ${_}:${W}:${J}`}import{randomBytes as Z5}from"crypto";import{config as h}from"@stacksjs/config";import{db as G0}from"@stacksjs/database";import{mail as f0,template as b0}from"@stacksjs/email";import{makeHash as I0,verifyHash as T0}from"@stacksjs/security";function E0(){return h.auth.passwordReset?.expire??60}async function Q5($){let Z=h.app.name||"Stacks",Q=h.app.supportEmail||h.email?.from?.address||"",X=new Date().toLocaleString("en-US",{dateStyle:"full",timeStyle:"short"});try{let{html:Y,text:_}=await b0("password-changed",{subject:`Your ${Z} password has been changed`,variables:{changedAt:X,supportEmail:Q}});await f0.send({to:$,subject:`Your ${Z} password has been changed`,text:_,html:Y})}catch(Y){console.error("[PasswordReset] Failed to send password changed notification:",Y)}}function U7($){function Z(){return Z5(32).toString("hex")}async function Q(){let W=Z(),J=await I0(W,{algorithm:"bcrypt"});return await G0.insertInto("password_resets").values({email:$,token:J}).executeTakeFirst(),W}async function X(){let W=await Q(),G=`${h.app.url?`https://${h.app.url}`:`http://localhost:${process.env.PORT||"3000"}`}/password/reset/${W}?email=${encodeURIComponent($)}`,K=E0(),B=h.app.name||"Stacks",{html:V,text:q}=await b0("password-reset",{subject:`Reset Your ${B} Password`,variables:{resetUrl:G,expireMinutes:K}});await f0.send({to:$,subject:`Reset Your ${B} Password`,text:q,html:V})}async function Y(W){let J=await G0.selectFrom("password_resets").where("email","=",$).selectAll().executeTakeFirst();if(!J)return!1;let G=E0(),K=new Date(J.created_at);if((new Date().getTime()-K.getTime())/60000>G)return await G0.deleteFrom("password_resets").where("email","=",$).execute(),!1;let q=J.token;return await T0(W,q)}async function _(W,J){let G=await G0.transaction(async(K)=>{let B=K,V=await B.selectFrom("password_resets").where("email","=",$).selectAll().executeTakeFirst();if(!V)return{success:!1,message:"Invalid or expired reset token"};let q=E0(),j=new Date(V.created_at);if((new Date().getTime()-j.getTime())/60000>q)return await B.deleteFrom("password_resets").where("email","=",$).execute(),{success:!1,message:"This password reset link has expired. Please request a new one."};let T=V.token;if(!await T0(W,T))return{success:!1,message:"Invalid or expired reset token"};if(!await B.selectFrom("users").where("email","=",$).selectAll().executeTakeFirst())return{success:!1,message:"Invalid or expired reset token"};let q0=await I0(J,{algorithm:"bcrypt"});return await B.updateTable("users").set({password:q0}).where("email","=",$).executeTakeFirst(),await B.deleteFrom("password_resets").where("email","=",$).execute(),{success:!0}});if(G.success)Q5($).catch((K)=>{console.error("[PasswordReset] Failed to send notification:",K)});return G}return{sendEmail:X,verifyToken:Y,resetPassword:_}}import{db as X5}from"@stacksjs/database";import{HttpError as S0}from"@stacksjs/error-handling";import{User as g0}from"@stacksjs/orm";import{makeHash as Y5}from"@stacksjs/security";var W5=/^[^\s@]+@[^\s@]+\.[^\s@]+$/;async function R7($){let{email:Z,password:Q,name:X}=$;if(typeof Z!=="string"||Z.length>254||!W5.test(Z))throw new S0(422,"Email address is invalid");if(typeof Q!=="string"||Q.length<8)throw new S0(422,"Password must be at least 8 characters");if(await g0.where("email","=",Z).first())throw new S0(409,"Email already exists");let _=await Y5(Q,{algorithm:"bcrypt"});await X5.insertInto("users").values({email:Z,password:_,name:X}).execute();let W=await g0.where("email","=",Z).first();if(!W)throw Error("Failed to retrieve created user");return{token:await R.createToken(W,"user-auth-token")}}import{request as d}from"@stacksjs/router";async function Z0(){let $=d?._authenticatedUser;if($)return $;let Z=d.bearerToken?.();if(!Z){let Q=d.headers?.get?.("authorization")||d.headers?.get?.("Authorization");if(Q&&Q.startsWith("Bearer "))Z=Q.substring(7)}if(!Z)return;return await R.getUserFromToken(Z)}async function I7(){return Z0()}async function _5(){return!!await Z0()}async function T7(){return(await Z0())?.id}async function f7(){return(await Z0())?.email}async function b7(){return(await Z0())?.name}async function g7(){return await _5()}async function k7(){await R.logout()}async function h7(){if(d?._authenticatedUser)d._authenticatedUser=void 0}t();t();t();import{log as f}from"@stacksjs/logging";import*as B0 from"@stacksjs/path";class G5{allow($){return z.allow($)}deny($,Z){return z.deny($,Z)}denyIf($,Z){if($)return this.deny(Z);return!0}denyUnless($,Z){if(!$)return this.deny(Z);return!0}allowIf($,Z){if($)return this.allow(Z);return!1}}async function L5(){let{fs:$}=await import("@stacksjs/storage"),Z=B0.appPath("Policies");if(!$.existsSync(Z)){f.debug("No Policies directory found");return}try{let _=(await import(B0.appPath("Gates.ts"))).policies||{};for(let[W,J]of Object.entries(_)){let G=typeof J==="string"?J:J.policy,K=`${Z}/${G}.ts`;if($.existsSync(K)){let B=await import(K),V=B.default||B[G];if(V)X0(W,V),f.debug(`Registered policy: ${G} for ${W}`)}}}catch{f.debug("No Gates.ts found, using convention-based discovery")}let Q=$.readdirSync(Z).filter((X)=>X.endsWith("Policy.ts"));for(let X of Q){let Y=X.replace(".ts",""),_=Y.replace("Policy",""),W=`${Z}/${X}`;try{let J=await import(W),G=J.default||J[Y];if(G)X0(_,G),f.debug(`Auto-discovered policy: ${Y} for ${_}`)}catch(J){f.error(`Failed to load policy ${Y}:`,J)}}}async function F5(){let{define:$,before:Z,after:Q}=await Promise.resolve().then(() => (t(),r0));try{let Y=await import(B0.appPath("Gates.ts")),_=Y.gates||Y.default?.gates||{};for(let[G,K]of Object.entries(_))if(typeof K==="function")$(G,K),f.debug(`Registered gate: ${G}`);let W=Y.before||Y.default?.before||[];for(let G of W)if(typeof G==="function")Z(G);let J=Y.after||Y.default?.after||[];for(let G of J)if(typeof G==="function")Q(G);f.debug("Gates registered successfully")}catch{f.debug("No Gates.ts found or failed to load")}}async function l7(){await F5(),await L5()}t();async function K5($,Z,...Q){return L0(Z,$,...Q)}async function V5($,Z,...Q){return F0(Z,$,...Q)}async function B5($,Z,...Q){return Y0(Z,$,...Q)}async function O5($,Z,...Q){return K0(Z,$,...Q)}async function D5($,Z,...Q){return V0(Z,$,...Q)}async function i7($,Z,...Q){return n(Z,$,...Q)}function t7($){return Object.assign($,{can:(Q,...X)=>K5($,Q,...X),cannot:(Q,...X)=>V5($,Q,...X),canAny:(Q,...X)=>B5($,Q,...X),canAll:(Q,...X)=>O5($,Q,...X),authorize:(Q,...X)=>D5($,Q,...X)})}var L={userRoles:new Map,userPermissions:new Map,rolePermissions:new Map,roles:new Map,permissions:new Map},x0=null;function q5($){x0=$,O0()}function D(){if(!x0)throw Error("RBAC store not configured. Call setRbacStore() first.");return x0}function O0(){L.userRoles.clear(),L.userPermissions.clear(),L.rolePermissions.clear(),L.roles.clear(),L.permissions.clear()}function S($){if(typeof $==="number")return $;return $.id}async function C5($,Z="web",Q){let X=await D().createRole($,Z,Q);return L.roles.set(`${$}:${Z}`,X),X}async function b($,Z="web"){let Q=`${$}:${Z}`;if(L.roles.has(Q))return L.roles.get(Q);let X=await D().findRoleByName($,Z);if(X)L.roles.set(Q,X);return X}async function j5($,Z="web"){let Q=await b($,Z);if(Q)await D().deleteRole(Q.id),L.roles.delete(`${$}:${Z}`),O0()}async function M5($){return D().getAllRoles($)}async function z5($,Z="web",Q){let X=await D().createPermission($,Z,Q);return L.permissions.set(`${$}:${Z}`,X),X}async function g($,Z="web"){let Q=`${$}:${Z}`;if(L.permissions.has(Q))return L.permissions.get(Q);let X=await D().findPermissionByName($,Z);if(X)L.permissions.set(Q,X);return X}async function H5($,Z="web"){let Q=await g($,Z);if(Q)await D().deletePermission(Q.id),L.permissions.delete(`${$}:${Z}`),O0()}async function U5($){return D().getAllPermissions($)}async function o($){let Z=S($);if(L.userRoles.has(Z))return L.userRoles.get(Z);let Q=await D().getUserRoles(Z);return L.userRoles.set(Z,Q),Q}async function s0($,Z,Q="web"){let X=S($),Y=await b(Z,Q);if(!Y)throw Error(`Role '${Z}' not found.`);await D().assignRoleToUser(X,Y.id),L.userRoles.delete(X),L.userPermissions.delete(X)}async function e0($,Z,Q="web"){let X=S($),Y=await b(Z,Q);if(!Y)return;await D().removeRoleFromUser(X,Y.id),L.userRoles.delete(X),L.userPermissions.delete(X)}async function v5($){let Z=S($);await D().removeAllRolesFromUser(Z),L.userRoles.delete(Z),L.userPermissions.delete(Z)}async function $4($,Z,Q="web"){let X=S($),Y=[];for(let _ of Z){let W=await b(_,Q);if(!W)throw Error(`Role '${_}' not found.`);Y.push(W.id)}await D().syncUserRoles(X,Y),L.userRoles.delete(X),L.userPermissions.delete(X)}async function Z4($,Z,Q="web"){return(await o($)).some((Y)=>Y.name===Z&&Y.guard_name===Q)}async function Q4($,Z,Q="web"){let X=await o($);return Z.some((Y)=>X.some((_)=>_.name===Y&&_.guard_name===Q))}async function X4($,Z,Q="web"){let X=await o($);return Z.every((Y)=>X.some((_)=>_.name===Y&&_.guard_name===Q))}async function W0($){let Z=S($);if(L.userPermissions.has(Z))return L.userPermissions.get(Z);let Q=await D().getUserDirectPermissions(Z),X=await o($),Y=[];for(let J of X){let G=await F4(J.id);Y.push(...G)}let _=new Set,W=[];for(let J of[...Q,...Y])if(!_.has(J.id))_.add(J.id),W.push(J);return L.userPermissions.set(Z,W),W}async function Y4($,Z,Q="web"){let X=S($),Y=await g(Z,Q);if(!Y)throw Error(`Permission '${Z}' not found.`);await D().assignPermissionToUser(X,Y.id),L.userPermissions.delete(X)}async function W4($,Z,Q="web"){let X=S($),Y=await g(Z,Q);if(!Y)return;await D().removePermissionFromUser(X,Y.id),L.userPermissions.delete(X)}async function E5($){let Z=S($);await D().removeAllPermissionsFromUser(Z),L.userPermissions.delete(Z)}async function _4($,Z,Q="web"){let X=S($),Y=[];for(let _ of Z){let W=await g(_,Q);if(!W)throw Error(`Permission '${_}' not found.`);Y.push(W.id)}await D().syncUserPermissions(X,Y),L.userPermissions.delete(X)}async function J4($,Z,Q="web"){return(await W0($)).some((Y)=>Y.name===Z&&Y.guard_name===Q)}async function G4($,Z,Q="web"){let X=await W0($);return Z.some((Y)=>X.some((_)=>_.name===Y&&_.guard_name===Q))}async function L4($,Z,Q="web"){let X=await W0($);return Z.every((Y)=>X.some((_)=>_.name===Y&&_.guard_name===Q))}async function F4($){if(L.rolePermissions.has($))return L.rolePermissions.get($);let Z=await D().getRolePermissions($);return L.rolePermissions.set($,Z),Z}async function S5($,Z,Q="web"){let X=await b($,Q);if(!X)throw Error(`Role '${$}' not found.`);let Y=await g(Z,Q);if(!Y)throw Error(`Permission '${Z}' not found.`);await D().assignPermissionToRole(X.id,Y.id),L.rolePermissions.delete(X.id),L.userPermissions.clear()}async function x5($,Z,Q="web"){let X=await b($,Q);if(!X)return;let Y=await g(Z,Q);if(!Y)return;await D().removePermissionFromRole(X.id,Y.id),L.rolePermissions.delete(X.id),L.userPermissions.clear()}async function P5($,Z,Q="web"){let X=await b($,Q);if(!X)throw Error(`Role '${$}' not found.`);let Y=[];for(let _ of Z){let W=await g(_,Q);if(!W)throw Error(`Permission '${_}' not found.`);Y.push(W.id)}await D().syncRolePermissions(X.id,Y),L.rolePermissions.delete(X.id),L.userPermissions.clear()}function A5($){let Z=S($);return Object.assign($,{hasRole:(X,Y)=>Z4(Z,X,Y),hasAnyRole:(X,Y)=>Q4(Z,X,Y),hasAllRoles:(X,Y)=>X4(Z,X,Y),hasPermission:(X,Y)=>J4(Z,X,Y),hasAnyPermission:(X,Y)=>G4(Z,X,Y),hasAllPermissions:(X,Y)=>L4(Z,X,Y),getRoles:()=>o(Z),getPermissions:()=>W0(Z),assignRole:(X,Y)=>s0(Z,X,Y),removeRole:(X,Y)=>e0(Z,X,Y),syncRoles:(X,Y)=>$4(Z,X,Y),givePermission:(X,Y)=>Y4(Z,X,Y),revokePermission:(X,Y)=>W4(Z,X,Y),syncPermissions:(X,Y)=>_4(Z,X,Y)})}var a7={setStore:q5,flushCache:O0,createRole:C5,findRole:b,deleteRole:j5,getAllRoles:M5,createPermission:z5,findPermission:g,deletePermission:H5,getAllPermissions:U5,getUserRoles:o,assignRole:s0,removeRole:e0,removeAllRoles:v5,syncRoles:$4,hasRole:Z4,hasAnyRole:Q4,hasAllRoles:X4,getUserPermissions:W0,givePermission:Y4,revokePermission:W4,revokeAllPermissions:E5,syncPermissions:_4,hasPermission:J4,hasAnyPermission:G4,hasAllPermissions:L4,getRolePermissions:F4,givePermissionToRole:S5,revokePermissionFromRole:x5,syncRolePermissions:P5,withRbac:A5};import{Buffer as K4}from"buffer";import{createHmac as B4,randomBytes as R5,timingSafeEqual as w5}from"crypto";import{config as a}from"@stacksjs/config";import{db as p}from"@stacksjs/database";import{mail as V4,template as N5}from"@stacksjs/email";import{log as y5}from"@stacksjs/logging";function I5($){let Z=R5(32).toString("hex"),Q=a.app.key||"stacks-default-key",X=`${$}:${Z}`,Y=B4("sha256",Q).update(X).digest("hex");return{token:Z,hash:Y}}function T5($,Z,Q){let X=a.app.key||"stacks-default-key",Y=`${$}:${Z}`,_=B4("sha256",X).update(Y).digest("hex"),W=K4.from(_),J=K4.from(Q);if(W.length!==J.length)return!1;return w5(W,J)}function f5(){let Z=(a.auth??{}).emailVerification;if(Z!=null&&typeof Z==="object"){let Q=Z;if(typeof Q.expire==="number")return Q.expire}return 60}function O4($){return $.email_verified_at!=null}async function D4($){let{token:Z,hash:Q}=I5($.id),X=f5(),Y=new Date(Date.now()+X*60*1000);await p.deleteFrom("email_verifications").where("user_id","=",$.id).execute(),await p.insertInto("email_verifications").values({user_id:$.id,token:Q,expires_at:Y.toISOString()}).executeTakeFirst();let W=`${a.app.url?`https://${a.app.url}`:`http://localhost:${process.env.PORT||"3000"}`}/verify-email/${$.id}/${Z}`,J=a.app.name||"Stacks";try{let{html:G,text:K}=await N5("email-verification",{subject:`Verify Your ${J} Email Address`,variables:{verificationUrl:W,expiryMinutes:X,userName:$.name||$.email}});await V4.send({to:$.email,subject:`Verify Your ${J} Email Address`,text:K,html:G})}catch(G){let K=G instanceof Error?G.message:String(G);y5.warn(`[email] Email verification template failed, using plain text fallback: ${K}`),await V4.send({to:$.email,subject:`Verify Your ${J} Email Address`,text:`Please verify your email address by visiting: ${W}
|
|
159
|
+
SET revoked = ${v}, updated_at = ${U}
|
|
160
|
+
WHERE id = ${z(1)}
|
|
161
|
+
`,[$])}function f($){if(!$)return[];if(Array.isArray($))return $;try{let W=JSON.parse($);return Array.isArray(W)?W:[]}catch{return[]}}function w0($){return{id:$.id,name:$.name,secret:$.secret,provider:$.provider,redirect:$.redirect,personalAccessClient:Boolean($.personal_access_client),passwordClient:Boolean($.password_client),revoked:Boolean($.revoked),createdAt:$.created_at?new Date($.created_at):new Date,updatedAt:$.updated_at?new Date($.updated_at):null}}var H$,J1,r,qZ,U,v,K0,U$;var L0=$1(()=>{H$=D$.DB_CONNECTION||"sqlite",J1=j$(H$),{isPostgres:r,isMysql:qZ,now:U,boolTrue:v,boolFalse:K0}=J1;U$=n});var w1={};e0(w1,{policy:()=>O0,none:()=>x1,inspect:()=>W0,hasPolicy:()=>N1,has:()=>A1,getPolicyFor:()=>R1,flush:()=>T1,denies:()=>E1,define:()=>C1,default:()=>MW,cannot:()=>S0,can:()=>v0,before:()=>P1,authorize:()=>x0,any:()=>_0,allows:()=>S1,all:()=>E0,after:()=>v1,abilities:()=>y1,Gate:()=>f1,AuthorizationResponse:()=>q,AuthorizationException:()=>B0});class q{isAllowed;message;code;constructor($,W,Z){this.isAllowed=$,this.message=W,this.code=Z}static allow($){return new q(!0,$)}static deny($,W){return new q(!1,$||"This action is unauthorized.",W)}allowed(){return this.isAllowed}denied(){return!this.isAllowed}authorize(){if(!this.isAllowed)throw new B0(this.message||"This action is unauthorized.",this.code)}}function C1($,W){C.gates.set($,W)}function O0($,W){let Z=typeof $==="string"?$:$.name;C.policies.set(Z,W)}function P1($){C.beforeCallbacks.push($)}function v1($){C.afterCallbacks.push($)}async function S1($,W,...Z){return Z0($,W,...Z)}async function E1($,W,...Z){return!await Z0($,W,...Z)}async function v0($,W,...Z){return Z0($,W,...Z)}async function S0($,W,...Z){return!await Z0($,W,...Z)}async function _0($,W,...Z){for(let J of $)if(await Z0(J,W,...Z))return!0;return!1}async function E0($,W,...Z){for(let J of $)if(!await Z0(J,W,...Z))return!1;return!0}async function x1($,W,...Z){return!await _0($,W,...Z)}async function x0($,W,...Z){let J=await W0($,W,...Z);if(!J.isAllowed)throw new B0(J.message,J.code);return J}async function W0($,W,...Z){for(let G of C.beforeCallbacks){let X=await G(W,$,Z);if(X===!0)return q.allow();if(X===!1)return q.deny()}let J=Z[0];if(J&&typeof J==="object"){let G=J.constructor?.name,X=C.policies.get(G);if(X){let K=new X;if(K.before){let L=await K.before(W,$);if(L===!0)return q.allow();if(L===!1)return q.deny()}let Y=K[$];if(Y){let L=await Y.call(K,W,...Z);return q1(L??!1)}}}let Q=C.gates.get($);if(Q){let G=await Q(W,...Z),X=q1(G);for(let K of C.afterCallbacks){let Y=await K(W,$,X.isAllowed,Z);if(typeof Y==="boolean")return Y?q.allow():q.deny()}return X}return q.deny(`No gate or policy defined for ability: ${$}`)}async function Z0($,W,...Z){return(await W0($,W,...Z)).isAllowed}function q1($){if($ instanceof q)return $;if(typeof $!=="boolean")throw TypeError(`[gate] Policy must return boolean or AuthorizationResponse; got ${typeof $}. If you returned a model/value by mistake, return \`true\`/\`false\` instead.`);return $?q.allow():q.deny()}function R1($){if(!$||typeof $!=="object")return null;let W=$.constructor?.name,Z=C.policies.get(W);if(Z)return new Z;return null}function A1($){return C.gates.has($)}function N1($){let W=typeof $==="string"?$:$.name;return C.policies.has(W)}function y1(){return Array.from(C.gates.keys())}function T1(){C.gates.clear(),C.policies.clear(),C.beforeCallbacks=[],C.afterCallbacks=[]}var B0,C,f1,MW;var J0=$1(()=>{B0=class B0 extends Error{code;status;constructor($="This action is unauthorized.",W,Z=403){super($);this.code=W;this.status=Z;this.name="AuthorizationException"}};C={gates:new Map,policies:new Map,beforeCallbacks:[],afterCallbacks:[]};f1={define:C1,policy:O0,before:P1,after:v1,allows:S1,denies:E1,can:v0,cannot:S0,any:_0,all:E0,none:x1,authorize:x0,inspect:W0,has:A1,hasPolicy:N1,abilities:y1,getPolicyFor:R1,flush:T1,AuthorizationResponse:q,AuthorizationException:B0},MW=f1});import{config as S}from"@stacksjs/config";import{db as M}from"@stacksjs/database";import{HttpError as I0}from"@stacksjs/error-handling";import{formatDate as i,User as e}from"@stacksjs/orm";import{getCurrentRequest as h$,request as b0}from"@stacksjs/router";import{Buffer as H0}from"buffer";import{createHash as p$,timingSafeEqual as k0}from"crypto";import{decrypt as L1,encrypt as m$,verifyHash as M0}from"@stacksjs/security";import{log as V1}from"@stacksjs/logging";var a="$2b$12$000000000000000000000uGByljkdFkOJRCRiYZGFOAstyLlSgTSW";import{HttpError as L$}from"@stacksjs/error-handling";var V$=5,W1=900000,F$=1e4,B$=300000;class T0{store=new Map;lastEviction=Date.now();evict(){let $=Date.now(),W=$-this.lastEviction>=B$,Z=this.store.size>=F$;if(!W&&!Z)return;this.lastEviction=$;for(let[J,Q]of this.store)if(Q.lockedUntil>0&&Q.lockedUntil<=$)this.store.delete(J);else if(Q.lockedUntil===0&&Q.attempts===0)this.store.delete(J)}get($){return this.evict(),this.store.get($)}set($,W){this.store.set($,W)}delete($){this.store.delete($)}}class Z1{prefix="auth:ratelimit:";async get($){let{cache:W}=await import("@stacksjs/cache"),Z=await W.get(`${this.prefix}${$}`);if(Z==null)return;try{return typeof Z==="string"?JSON.parse(Z):Z}catch{return}}async set($,W,Z){let{cache:J}=await import("@stacksjs/cache");await J.set(`${this.prefix}${$}`,JSON.stringify(W),Math.ceil(Z/1000))}async delete($){let{cache:W}=await import("@stacksjs/cache");await W.remove(`${this.prefix}${$}`)}}var g=new T0;class k{static useStore($){g=$}static useSharedStore(){g=new Z1}static useMemoryStore(){g=new T0}static async isRateLimited($){$=$.toLowerCase();let W=Date.now(),Z=await g.get($);if(!Z)return!1;if(Z.lockedUntil>0&&Z.lockedUntil<=W)return await g.delete($),!1;return Z.lockedUntil>0}static async recordFailedAttempt($){$=$.toLowerCase();let W=Date.now(),Z=await g.get($)||{attempts:0,lockedUntil:0};if(Z.attempts++,Z.attempts>=V$)Z.lockedUntil=W+W1,Z.attempts=0;await g.set($,Z,W1)}static async resetAttempts($){await g.delete($.toLowerCase())}static async validateAttempt($){if(await this.isRateLimited($))throw new L$(429,"Too many login attempts. Please try again later.")}}L0();var F1=Symbol.for("stacks.requestAuthState");function x(){let $=h$();if(!$)return null;let W=$[F1];if(!W)W={},$[F1]=W;return W}function V0($){return p$("sha256").update($).digest("hex")}class R{static getBearerToken(){let $=b0.bearerToken?.();if(!$){let W=b0.headers?.get?.("authorization")||b0.headers?.get?.("Authorization");if(W&&W.startsWith("Bearer "))$=W.substring(7)}return $||null}static parseToken($){let W=$.indexOf(":");if(W===-1)return null;let Z=$.substring(0,W),J=$.substring(W+1);if(!Z||!J)return null;return{plainToken:Z,encryptedId:J}}static async getClientSecret(){let $=x();if($?.clientSecret)return $.clientSecret;let W=await this.getPersonalAccessClient();if($)$.clientSecret=W.secret;return W.secret}static async encryptTokenId($){return await m$(String($))}static async decryptTokenId($){try{return await L1($)}catch{try{let W=await this.getClientSecret();return await L1($,W)}catch{return null}}}static async getPersonalAccessClient(){try{let $=await M.selectFrom("oauth_clients").where("personal_access_client","=",!0).where("revoked","=",!1).selectAll().executeTakeFirst();if(!$)throw new I0(500,"No personal access client found. Please run `./buddy auth:setup` first.");return $}catch($){if($ instanceof Error&&$.message.includes("does not exist"))throw new I0(500,"OAuth tables not found. Please run `./buddy auth:setup` first.");throw $}}static async validateClient($,W){let Z=await M.selectFrom("oauth_clients").where("id","=",$).where("revoked","=",!1).selectAll().executeTakeFirst(),J=H0.from(W);if(!Z?.secret){let X=H0.alloc(Math.max(J.length,1)),K=J.length>0?J:H0.alloc(1);return k0(X,K),!1}let Q=String(Z.secret);if(Q.startsWith("$2"))return await M0(W,Q);let G=H0.from(Q);if(G.length!==J.length)return k0(G,G),!1;return k0(G,J)}static async getTokenFromId($){let W=await M.selectFrom("oauth_access_tokens").where("id","=",$).selectAll().executeTakeFirst();if(!W)return null;let Z=W;return{id:Z.id,userId:Z.user_id,clientId:Z.oauth_client_id,name:Z.name||"auth-token",scopes:f(Z.scopes),abilities:f(Z.scopes),expiresAt:Z.expires_at?new Date(String(Z.expires_at)):null,createdAt:Z.created_at?new Date(String(Z.created_at)):new Date,updatedAt:Z.updated_at?new Date(String(Z.updated_at)):new Date,revoked:!!Z.revoked}}static async attempt($){let W=S.auth.username||"email",Z=S.auth.password||"password",J=$[W];if(!J)return!1;let Q=await k.isRateLimited(J),G=await e.where("email","=",J).first(),X=$[Z]||"",K=G?.password||a,Y=await M0(X,K);if(Q)return!1;if(Y&&G){await k.resetAttempts(J);let L=x();if(L)L.authUser=G;return!0}return await k.recordFailedAttempt(J),!1}static async validate($){let W=S.auth.username||"email",Z=S.auth.password||"password",J=$[W];if(!J)return!1;let Q=await e.where("email","=",J).first(),G=$[Z]||"",X=Q?.password||a;return await M0(G,X)&&!!Q}static async login($,W){let Z=await this.attempt($),J=x()?.authUser;if(!Z||!J)return null;let{plainTextToken:Q,refreshToken:G,expiresIn:X}=await this.createTokenForUser(J,W);return{user:J,token:Q,refreshToken:G,expiresIn:X}}static async loginUsingId($,W){let Z=await e.find($);if(!Z)return null;let J=x();if(J)J.authUser=Z;let{plainTextToken:Q,refreshToken:G,expiresIn:X}=await this.createTokenForUser(Z,W);return{user:Z,token:Q,refreshToken:G,expiresIn:X}}static async logout(){let $=this.getBearerToken();if($){let Z=await M.selectFrom("oauth_access_tokens").where("token","=",V0($)).select(["id"]).executeTakeFirst();if(Z)await M.updateTable("oauth_refresh_tokens").set({revoked:!0}).where("access_token_id","=",Number(Z.id)).execute();await this.revokeToken($)}let W=x();if(W)W.authUser=void 0,W.currentToken=void 0}static async user(){let $=x();if($?.authUser)return $.authUser;let W=this.getBearerToken();if(!W)return;let Z=await this.getUserFromToken(W);if(Z&&$)$.authUser=Z;return Z}static async check(){return await this.user()!==void 0}static async guest(){return!await this.check()}static async id(){return(await this.user())?.id}static setUser($){let W=x();if(W)W.authUser=$}static async createTokenForUser($,W){let Z=W?.name??S.auth.defaultTokenName??"auth-token",J=W?.abilities??W?.scopes??S.auth.defaultAbilities??["*"],Q=W?.expiresInMinutes!==void 0?W.expiresInMinutes*60*1000:S.auth.tokenExpiry??3600000,X=W?.expiresAt??new Date(Date.now()+Q),K=Math.max(1,Math.floor((X.getTime()-Date.now())/60000)),Y=W?.refreshExpiresInDays??Math.max(1,Math.round((S.auth.refreshTokenExpiry??2592000000)/86400000));V1.debug(`[auth] Creating token for user#${$.id}: ${Z}`);let L=await f0($.id,Z,J,{expiresInMinutes:K,withRefreshToken:W?.withRefreshToken!==!1,refreshExpiresInDays:Y}),O=L.plainTextToken;return{accessToken:{id:L.accessToken.id,userId:L.accessToken.userId,clientId:L.accessToken.clientId,name:L.accessToken.name,scopes:L.accessToken.scopes,abilities:J,expiresAt:L.accessToken.expiresAt??X,createdAt:L.accessToken.createdAt,updatedAt:L.accessToken.updatedAt,revoked:L.accessToken.revoked,plainTextToken:O},plainTextToken:O,refreshToken:L.refreshToken,expiresIn:L.expiresIn}}static async createToken($,W=S.auth.defaultTokenName||"auth-token",Z=S.auth.defaultAbilities||["*"]){let{plainTextToken:J}=await this.createTokenForUser($,{name:W,abilities:Z});return J}static async requestToken($,W,Z){if(!await this.validateClient(W,Z))throw new I0(401,"Invalid client credentials");let Q=await this.attempt($),G=x()?.authUser;if(!Q||!G)return null;return{token:await this.createToken(G,"user-auth-token")}}static async validateToken($){let W=V0($),Z=await M.selectFrom("oauth_access_tokens").where("token","=",W).selectAll().executeTakeFirst();if(!Z)return!1;if(V1.debug(`[auth] Token validated for token#${Z.id}`),Z.expires_at&&new Date(String(Z.expires_at))<new Date)return await M.deleteFrom("oauth_access_tokens").where("id","=",Z.id).execute(),!1;if(Z.revoked)return!1;if(d(Z.created_at,await s(Z.user_id)))return!1;return await M.updateTable("oauth_access_tokens").set({updated_at:i(new Date)}).where("id","=",Z.id).execute(),!0}static async getUserFromToken($){let W=V0($),Z=await M.selectFrom("oauth_access_tokens").where("token","=",W).selectAll().executeTakeFirst();if(!Z)return;if(Z.expires_at&&new Date(String(Z.expires_at))<new Date){await M.deleteFrom("oauth_access_tokens").where("id","=",Z.id).execute();return}if(Z.revoked)return;let J=x();if(J)J.currentToken=await this.getTokenFromId(Z.id)??void 0;if(await M.updateTable("oauth_access_tokens").set({updated_at:i(new Date)}).where("id","=",Z.id).execute(),!Z?.user_id)return;let Q=await e.find(Z.user_id),G=Q?.password_changed_at,X=G?new Date(String(G)):null;if(d(Z.created_at,X))return;return Q}static async currentAccessToken(){let $=x();if($?.currentToken)return $.currentToken;let W=this.getBearerToken();if(!W)return;let Z=await M.selectFrom("oauth_access_tokens").where("token","=",V0(W)).select(["id"]).executeTakeFirst();if(!Z)return;let J=await this.getTokenFromId(Number(Z.id));if(J&&$)$.currentToken=J;return J??void 0}static async tokenCan($){let W=await this.currentAccessToken();if(!W)return!1;if(W.abilities.includes("*"))return!0;return W.abilities.includes($)}static async tokenCant($){return!await this.tokenCan($)}static async tokenAbilities(){return(await this.currentAccessToken())?.abilities??[]}static async tokenCanAll($){let W=await this.currentAccessToken();if(!W)return!1;if(W.abilities.includes("*"))return!0;return $.every((Z)=>W.abilities.includes(Z))}static async tokenCanAny($){let W=await this.currentAccessToken();if(!W)return!1;if(W.abilities.includes("*"))return!0;return $.some((Z)=>W.abilities.includes(Z))}static async tokens($){let W=$??await this.id();if(!W)return[];return(await M.selectFrom("oauth_access_tokens").where("user_id","=",W).where("revoked","=",!1).selectAll().execute()).map((J)=>({id:Number(J.id),userId:Number(J.user_id),clientId:Number(J.oauth_client_id),name:String(J.name||"auth-token"),scopes:f(String(J.scopes??"")),abilities:f(String(J.scopes??"")),expiresAt:J.expires_at?new Date(String(J.expires_at)):null,createdAt:J.created_at?new Date(String(J.created_at)):new Date,updatedAt:J.updated_at?new Date(String(J.updated_at)):new Date,revoked:!!J.revoked}))}static async revokeToken($){await M.updateTable("oauth_access_tokens").set({revoked:!0,updated_at:i(new Date)}).where("token","=",V0($)).execute()}static async revokeTokenById($){await M.updateTable("oauth_access_tokens").set({revoked:!0,updated_at:i(new Date)}).where("id","=",$).execute()}static async revokeAllTokens($){let W=$??await this.id();if(!W)return;await M.updateTable("oauth_access_tokens").set({revoked:!0,updated_at:i(new Date)}).where("user_id","=",W).execute()}static async revokeOtherTokens($){let W=$??await this.id();if(!W)return;let Z=await this.currentAccessToken();if(!Z)return;await M.updateTable("oauth_access_tokens").set({revoked:!0,updated_at:i(new Date)}).where("user_id","=",W).where("id","!=",Z.id).execute()}static async pruneExpiredTokens(){let $=await M.deleteFrom("oauth_access_tokens").where("expires_at","<",i(new Date)).executeTakeFirst();return Number($?.numDeletedRows)||0}static async pruneRevokedTokens(){let $=await M.deleteFrom("oauth_access_tokens").where("revoked","=",!0).executeTakeFirst();return Number($?.numDeletedRows)||0}static async rotateToken($){let{findToken:W,revokeToken:Z}=await Promise.resolve().then(() => (L0(),K1)),J=await W($);if(!J)return null;let Q=J.expiresAt?J.expiresAt.getTime()-Date.now():S.auth.tokenExpiry??3600000,G=Math.max(1,Math.floor(Q/60000));await Z($);let X=await e.find(J.userId);if(!X)return null;return(await this.createTokenForUser(X,{name:J.name,abilities:J.scopes??["*"],expiresInMinutes:G,withRefreshToken:!1})).plainTextToken}static async findToken($){return this.getTokenFromId($)}static async once($){let W=S.auth.username||"email",Z=S.auth.password||"password",J=$[W];if(!J)return!1;let Q=await e.where("email","=",J).first(),G=$[Z]||"",X=Q?.password||a;if(await M0(G,X)&&Q){let Y=x();if(Y)Y.authUser=Q;return!0}return!1}static guard($){return this}static viaRemember(){return!1}static clearState(){let $=x();if($)$.authUser=void 0,$.currentToken=void 0,$.clientSecret=void 0}}import{generateTOTP as c$,generateTOTPSecret as u$,totpKeyUri as l$,verifyTOTP as d$}from"@stacksjs/ts-auth";function g0(){return u$()}async function kZ($){return c$({secret:$})}async function h0($,W){return d$($,{secret:W})}function B1($,W,Z){let J=$||"johndoe@example.com",Q=W||"StacksJS 2fa",G=Z||g0();return l$(J,Q,G)}import{randomBytes as n$}from"crypto";import{db as U0}from"@stacksjs/database";import{formatDate as i$}from"@stacksjs/orm";import{err as t$,HttpError as o$,ok as a$}from"@stacksjs/error-handling";import{makeHash as r$}from"@stacksjs/security";async function lZ(){if((await U0.selectFrom("oauth_clients").where("personal_access_client","=",!0).where("revoked","=",!1).select(["id"]).executeTakeFirst())?.id)return t$({code:"already-exists",message:"A personal access client already exists. Revoke the existing client (`./buddy auth:revoke-client`) before creating a new one."});let W=n$(40).toString("hex"),Z=await r$(W,{algorithm:"bcrypt"});if(await U0.insertInto("oauth_clients").values({name:"Personal Access Client",secret:Z,provider:"local",redirect:"http://localhost",personal_access_client:!0,password_client:!1,revoked:!1,created_at:i$(new Date)}).execute(),!(await U0.selectFrom("oauth_clients").where("secret","=",Z).select(["id"]).executeTakeFirst())?.id)throw new o$(500,"Failed to create personal access client");return a$(W)}async function s$($){let W=$.bearerToken?.();if(!W){let Q=$.headers?.get?.("authorization")||$.headers?.get?.("Authorization");if(Q&&Q.startsWith("Bearer "))W=Q.substring(7)}if(!W){let Q=Error("No authentication token provided.");throw Q.statusCode=401,Q}let Z=await R.getUserFromToken(W);if(!Z){let Q=Error("Invalid or expired authentication token.");throw Q.statusCode=401,Q}R.setUser(Z),$._authenticatedUser=Z;let J=await R.currentAccessToken();$._currentAccessToken=J}var iZ={name:"auth",handle:s$};import{db as p}from"@stacksjs/database";import{generateRegistrationOptions as J2,generateAuthenticationOptions as Q2,verifyRegistrationResponse as G2,verifyAuthenticationResponse as X2,startRegistration as Y2,startAuthentication as K2,browserSupportsWebAuthn as L2,browserSupportsWebAuthnAutofill as V2,platformAuthenticatorIsAvailable as F2}from"@stacksjs/ts-auth";async function aZ($){return await p.selectFrom("passkeys").selectAll().where("user_id","=",$).execute()}async function e$($,W){return await p.selectFrom("passkeys").selectAll().where("id","=",W).where("user_id","=",$).executeTakeFirst()}async function rZ($,W,Z){let J=await e$($,W);if(!J)return!1;let Q=Number(J.counter??0);if(Z!==0&&Z<=Q)return!1;return await p.updateTable("passkeys").set({counter:Z,last_used_at:O1()}).where("id","=",W).where("user_id","=",$).execute(),!0}async function sZ($,W){let Z=W.registrationInfo?.credential.id,J=W.registrationInfo?.credential.publicKey;if(!Z)throw Error("[auth/passkey] WebAuthn registration response is missing credential.id");if(!J)throw Error("[auth/passkey] WebAuthn registration response is missing credential.publicKey");let Q={id:Z,cred_public_key:JSON.stringify(J),user_id:$.id,webauthn_user_id:$.email||"",counter:W.registrationInfo?.credential.counter||0,credential_type:W.registrationInfo?.credentialType||"",device_type:W.registrationInfo?.credentialDeviceType||"",backup_eligible:!1,backup_status:W.registrationInfo?.credentialBackedUp||!1,transports:JSON.stringify(["internal"]),last_used_at:O1()};await p.insertInto("passkeys").values(Q).executeTakeFirstOrThrow()}function O1(){let $=new Date,W=(Y)=>String(Y).padStart(2,"0"),Z=$.getFullYear(),J=W($.getMonth()+1),Q=W($.getDate()),G=W($.getHours()),X=W($.getMinutes()),K=W($.getSeconds());return`${Z}-${J}-${Q} ${G}:${X}:${K}`}var $W=300;async function eZ($,W,Z,J=$W){let Q=new Date(Date.now()+J*1000).toISOString();await p.deleteFrom("webauthn_challenges").where("user_id","=",$).where("purpose","=",Z).execute(),await p.insertInto("webauthn_challenges").values({user_id:$,challenge:W,purpose:Z,expires_at:Q}).execute()}async function $2($,W){let Z=await p.selectFrom("webauthn_challenges").where("user_id","=",$).where("purpose","=",W).selectAll().executeTakeFirst();if(!Z)return null;await p.deleteFrom("webauthn_challenges").where("user_id","=",$).where("purpose","=",W).execute();let J=Z.expires_at?new Date(String(Z.expires_at)).getTime():0;if(Date.now()>J)return null;return String(Z.challenge)}import{randomBytes as LW}from"crypto";import{config as m}from"@stacksjs/config";import{db as t}from"@stacksjs/database";import{mail as C0,template as H1}from"@stacksjs/email";import{log as VW}from"@stacksjs/logging";import{formatDate as FW}from"@stacksjs/orm";import{makeHash as z1,verifyHash as D1}from"@stacksjs/security";import{HttpError as p0}from"@stacksjs/error-handling";import{log as q0}from"@stacksjs/logging";import{User as _1}from"@stacksjs/orm";import{verifyHash as WW}from"@stacksjs/security";import{db as w}from"@stacksjs/database";import{getCurrentRequest as ZW}from"@stacksjs/router";function JW(){let $=crypto.getRandomValues(new Uint8Array(32));return Array.from($,(W)=>W.toString(16).padStart(2,"0")).join("")}function QW($){if($)return{ip:$.ip??null,userAgent:$.userAgent??null};let W=ZW();if(!W)return{ip:null,userAgent:null};let Z=W.headers,J=Z?.get?.("x-forwarded-for")||Z?.get?.("X-Forwarded-For")||"",Q=Z?.get?.("x-real-ip")||Z?.get?.("X-Real-IP")||"",G=(J?J.split(",")[0].trim():"")||Q||null,X=Z?.get?.("user-agent")||Z?.get?.("User-Agent")||null;return{ip:G,userAgent:X}}async function GW($,W,Z){let J=($||"").toLowerCase(),Q=await k.isRateLimited(J),G=await _1.where("email",$).first(),X=G?.password||a,K=await WW(W,X);if(Q)throw new p0(429,"Too many login attempts. Please try again later.");if(!K||!G)throw await k.recordFailedAttempt(J),new p0(401,"Invalid credentials");await k.resetAttempts(J);let Y=JW(),L=new Date(Date.now()+86400000),{ip:O,userAgent:_}=QW(Z);q0.debug(`[auth] Session created for user#${G.id}`);try{await w.insertInto("sessions").values({id:Y,user_id:G.id,ip_address:O,user_agent:_,payload:"{}",last_activity:Math.floor(Date.now()/1000),expires_at:L.toISOString()}).execute()}catch(E){throw q0.error(`[auth] Session persistence failed for user#${G.id}: ${E.message}`),new p0(500,"Session could not be created. Ensure the `sessions` table exists (run `./buddy migrate`).")}return{user:G,sessionId:Y}}async function XW($){q0.debug("[auth] Session destroyed");try{await w.deleteFrom("sessions").where("id","=",$).execute()}catch(W){q0.debug(`[auth] Session destroy failed: ${W.message}`)}}async function m0($){try{await w.deleteFrom("sessions").where("user_id","=",$).execute()}catch(W){let Z=W instanceof Error?W.message:String(W);if(Z.includes("sessions")&&/no such table|does not exist|doesn't exist/i.test(Z))return;throw W}}async function c0($){try{let W=await w.selectFrom("sessions").where("id","=",$).selectAll().executeTakeFirst();if(!W)return;let Z=W.expires_at?new Date(String(W.expires_at)).getTime():0;if(Date.now()>Z){await w.deleteFrom("sessions").where("id","=",$).execute();return}return await _1.find(W.user_id)}catch{return}}async function YW($){try{let W=await w.selectFrom("sessions").where("id","=",$).selectAll().executeTakeFirst();if(!W)return!1;let Z=W.expires_at?new Date(String(W.expires_at)).getTime():0;if(Date.now()>Z)return await w.deleteFrom("sessions").where("id","=",$).execute(),!1;return!0}catch{return!1}}async function KW($,W=86400000){try{let Z=await w.selectFrom("sessions").where("id","=",$).selectAll().executeTakeFirst();if(!Z)return!1;let J=Z.expires_at?new Date(String(Z.expires_at)).getTime():0;if(Date.now()>J)return await w.deleteFrom("sessions").where("id","=",$).execute(),!1;let Q=new Date(Date.now()+W);return await w.updateTable("sessions").set({expires_at:Q.toISOString(),last_activity:Math.floor(Date.now()/1000)}).where("id","=",$).execute(),!0}catch{return!1}}var U2={login:GW,logout:XW,destroyAll:m0,user:c0,check:YW,refresh:KW};L0();function u0(){return m.auth.passwordReset?.expire??60}function j1($){let W=$.expires_at;if(typeof W==="string"||W instanceof Date)return new Date(W).getTime()>Date.now();let Z=$.created_at;if(typeof Z==="string"||Z instanceof Date){let J=u0();return new Date(Z).getTime()+J*60000>Date.now()}return!1}async function BW($){let W=m.app.name||"Stacks",Z=m.app.supportEmail||m.email?.from?.address||"",J=new Date().toLocaleString("en-US",{dateStyle:"full",timeStyle:"short"});try{let{html:Q,text:G}=await H1("password-changed",{subject:`Your ${W} password has been changed`,variables:{changedAt:J,supportEmail:Z}});if(!Q&&!G){await C0.send({to:$,subject:`Your ${W} password has been changed`,text:`Your ${W} password was changed on ${J}.${Z?` If this wasn't you, contact ${Z}.`:""}`});return}await C0.send({to:$,subject:`Your ${W} password has been changed`,text:G,html:Q})}catch(Q){console.error("[PasswordReset] Failed to send password changed notification:",Q)}}function y2($){function W(){return LW(32).toString("hex")}async function Z(){let X=W(),K=await z1(X,{algorithm:"bcrypt"}),Y=u0(),L=new Date(Date.now()+Y*60000).toISOString();return await t.deleteFrom("password_resets").where("email","=",$).execute(),await t.insertInto("password_resets").values({email:$,token:K,expires_at:L}).executeTakeFirst(),X}async function J(){if(!await t.selectFrom("users").where("email","=",$).selectAll().executeTakeFirst())return;let K=await Z(),Y=u0(),L=m.app.name||"Stacks",O=m.app.url?`https://${m.app.url}`:`http://localhost:${process.env.PORT||"3000"}`,E=(m.auth.passwordReset?.url??"/password/reset/{token}?email={email}").replace("{token}",K).replace("{email}",encodeURIComponent($)),F=/^https?:\/\//.test(E)?E:`${O}${E.startsWith("/")?"":"/"}${E}`;try{let{html:H,text:P}=await H1("password-reset",{subject:`Reset Your ${L} Password`,variables:{resetUrl:F,expireMinutes:Y}});if(!H&&!P)throw Error("password-reset template missing or rendered empty");await C0.send({to:$,subject:`Reset Your ${L} Password`,text:P,html:H})}catch(H){let P=H instanceof Error?H.message:String(H);console.warn(`[PasswordReset] template render failed, sending plain-text fallback: ${P}`),await C0.send({to:$,subject:`Reset Your ${L} Password`,text:`Reset your password by visiting: ${F}
|
|
162
|
+
|
|
163
|
+
This link expires in ${Y} minutes. If you didn't request this, you can safely ignore this email.`})}}async function Q(X){let K=await t.selectFrom("password_resets").where("email","=",$).selectAll().executeTakeFirst();if(!K)return!1;if(!j1(K))return await t.deleteFrom("password_resets").where("email","=",$).execute(),!1;let Y=K.token;return await D1(X,Y)}async function G(X,K){let Y=await t.transaction(async(L)=>{let O=L,_=await O.selectFrom("password_resets").where("email","=",$).selectAll().executeTakeFirst();if(!_)return{success:!1,message:"Invalid or expired reset token"};if(!j1(_))return await O.deleteFrom("password_resets").where("email","=",$).execute(),{success:!1,message:"This password reset link has expired. Please request a new one."};let E=_.token;if(!await D1(X,E))return{success:!1,message:"Invalid or expired reset token"};let H=await O.selectFrom("users").where("email","=",$).selectAll().executeTakeFirst();if(!H)return{success:!1,message:"Invalid or expired reset token"};let P=await z1(K,{algorithm:"bcrypt"});try{await O.updateTable("users").set({password:P,password_changed_at:FW(new Date)}).where("email","=",$).executeTakeFirst()}catch(T){let A=T instanceof Error?T.message:String(T);if(/password_changed_at|no such column|unknown column/i.test(A))VW.warn("[PasswordReset] password_changed_at column missing \u2014 run `buddy migrate`; resetting without the credential-version stamp"),await O.updateTable("users").set({password:P}).where("email","=",$).executeTakeFirst();else throw T}return await O.deleteFrom("password_resets").where("email","=",$).execute(),{success:!0,userId:Number(H.id)}});if(Y.success)return await j0(Y.userId),await m0(Y.userId),BW($).catch((L)=>{console.error("[PasswordReset] Failed to send notification:",L)}),{success:!0};return Y}return{sendEmail:J,verifyToken:Q,resetPassword:G}}import{db as _W}from"@stacksjs/database";import{HttpError as P0}from"@stacksjs/error-handling";import{User as zW}from"@stacksjs/orm";import{makeHash as DW}from"@stacksjs/security";import{db as D}from"@stacksjs/database";import{isUniqueViolation as t0}from"@stacksjs/orm";function i0($){if(!t0($))throw $}function I($){if(!$)return null;return{id:Number($.id),name:String($.name),guard_name:String($.guard_name),description:$.description==null?void 0:String($.description),created_at:$.created_at==null?void 0:String($.created_at),updated_at:$.updated_at==null?void 0:String($.updated_at)}}async function M1($,W,Z,J){await D.insertInto($).values({name:W,guard_name:Z,description:J??null}).execute();let Q=await D.selectFrom($).selectAll().where("name","=",W).where("guard_name","=",Z).orderBy("id","desc").limit(1).executeTakeFirst(),G=I(Q);if(!G)throw Error(`[rbac] insertAndFetch(${$}, ${W}, ${Z}) succeeded but follow-up SELECT returned nothing`);return G}async function l0($,W){let Z=D.selectFrom($).select("user_id"in W?"user_id":"role_id");for(let[Q,G]of Object.entries(W))Z=Z.where(Q,"=",G);if(await Z.limit(1).executeTakeFirst())return;try{await D.insertInto($).values(W).execute()}catch(Q){i0(Q)}}async function d0($,W){let Z=D.deleteFrom($);for(let[J,Q]of Object.entries(W))Z=Z.where(J,"=",Q);await Z.execute()}async function U1($,W,Z){await D.deleteFrom($).where(W,"=",Z).execute()}async function n0($,W,Z,J,Q){let G=Array.from(new Set(Q));await D.transaction(async(X)=>{let K=X;if(await K.deleteFrom($).where(W,"=",Z).execute(),G.length===0)return;let Y=G.map((L)=>({[W]:Z,[J]:L}));try{await K.insertInto($).values(Y).execute()}catch(L){i0(L);for(let O of G)try{await K.insertInto($).values({[W]:Z,[J]:O}).execute()}catch(_){i0(_)}}})}function OW(){return{async findRoleByName($,W="web"){let Z=await D.selectFrom("roles").selectAll().where("name","=",$).where("guard_name","=",W).limit(1).executeTakeFirst();return I(Z)},async findRoleById($){let W=await D.selectFrom("roles").selectAll().where("id","=",$).limit(1).executeTakeFirst();return I(W)},async createRole($,W="web",Z){return await M1("roles",$,W,Z)},async deleteRole($){await D.deleteFrom("user_roles").where("role_id","=",$).execute(),await D.deleteFrom("role_permissions").where("role_id","=",$).execute(),await D.deleteFrom("roles").where("id","=",$).execute()},async getAllRoles($){let W=D.selectFrom("roles").selectAll();if($)W=W.where("guard_name","=",$);return(await W.orderBy("id","asc").execute()).map((J)=>I(J)).filter(Boolean)},async findPermissionByName($,W="web"){let Z=await D.selectFrom("permissions").selectAll().where("name","=",$).where("guard_name","=",W).limit(1).executeTakeFirst();return I(Z)},async findPermissionById($){let W=await D.selectFrom("permissions").selectAll().where("id","=",$).limit(1).executeTakeFirst();return I(W)},async createPermission($,W="web",Z){return await M1("permissions",$,W,Z)},async deletePermission($){await D.deleteFrom("user_permissions").where("permission_id","=",$).execute(),await D.deleteFrom("role_permissions").where("permission_id","=",$).execute(),await D.deleteFrom("permissions").where("id","=",$).execute()},async getAllPermissions($){let W=D.selectFrom("permissions").selectAll();if($)W=W.where("guard_name","=",$);return(await W.orderBy("id","asc").execute()).map((J)=>I(J)).filter(Boolean)},async getUserRoles($){return(await D.selectFrom("roles").innerJoin("user_roles","user_roles.role_id","=","roles.id").select(["roles.id as id","roles.name as name","roles.guard_name as guard_name","roles.description as description","roles.created_at as created_at","roles.updated_at as updated_at"]).where("user_roles.user_id","=",$).orderBy("roles.id","asc").execute()).map((Z)=>I(Z)).filter(Boolean)},assignRoleToUser($,W){return l0("user_roles",{user_id:$,role_id:W})},removeRoleFromUser($,W){return d0("user_roles",{user_id:$,role_id:W})},removeAllRolesFromUser($){return U1("user_roles","user_id",$)},syncUserRoles($,W){return n0("user_roles","user_id",$,"role_id",W)},async getUserDirectPermissions($){return(await D.selectFrom("permissions").innerJoin("user_permissions","user_permissions.permission_id","=","permissions.id").select(["permissions.id as id","permissions.name as name","permissions.guard_name as guard_name","permissions.description as description","permissions.created_at as created_at","permissions.updated_at as updated_at"]).where("user_permissions.user_id","=",$).orderBy("permissions.id","asc").execute()).map((Z)=>I(Z)).filter(Boolean)},assignPermissionToUser($,W){return l0("user_permissions",{user_id:$,permission_id:W})},removePermissionFromUser($,W){return d0("user_permissions",{user_id:$,permission_id:W})},removeAllPermissionsFromUser($){return U1("user_permissions","user_id",$)},syncUserPermissions($,W){return n0("user_permissions","user_id",$,"permission_id",W)},async getRolePermissions($){return(await D.selectFrom("permissions").innerJoin("role_permissions","role_permissions.permission_id","=","permissions.id").select(["permissions.id as id","permissions.name as name","permissions.guard_name as guard_name","permissions.description as description","permissions.created_at as created_at","permissions.updated_at as updated_at"]).where("role_permissions.role_id","=",$).orderBy("permissions.id","asc").execute()).map((Z)=>I(Z)).filter(Boolean)},assignPermissionToRole($,W){return l0("role_permissions",{role_id:$,permission_id:W})},removePermissionFromRole($,W){return d0("role_permissions",{role_id:$,permission_id:W})},syncRolePermissions($,W){return n0("role_permissions","role_id",$,"permission_id",W)}}}var jW=/^[^\s@]+@[^\s@]+\.[^\s@]+$/;async function c2($){let{email:W,password:Z,name:J}=$;if(typeof W!=="string"||W.length>254||!jW.test(W))throw new P0(422,"Email address is invalid");if(typeof Z!=="string"||Z.length<8)throw new P0(422,"Password must be at least 8 characters");let Q=await DW(Z,{algorithm:"bcrypt"}),G=await _W.transaction(async(K)=>{let Y=K;if(await Y.selectFrom("users").where("email","=",W).selectAll().executeTakeFirst())throw new P0(409,"Email already exists");try{await Y.insertInto("users").values({email:W,password:Q,name:J}).execute()}catch(_){if(t0(_))throw new P0(409,"Email already exists");throw _}let O=await Y.selectFrom("users").where("email","=",W).selectAll().executeTakeFirst();if(!O)throw Error("Failed to retrieve created user");return Number(O.id)}),X=await zW.find(G);if(!X)throw Error("Failed to retrieve created user");return{token:await R.createToken(X,"user-auth-token")}}import{request as $0}from"@stacksjs/router";async function F0(){let $=$0?._authenticatedUser;if($)return $;let W=$0.bearerToken?.();if(!W){let Z=$0.headers?.get?.("authorization")||$0.headers?.get?.("Authorization");if(Z&&Z.startsWith("Bearer "))W=Z.substring(7)}if(!W)return;return await R.getUserFromToken(W)}async function n2(){return F0()}async function HW(){return!!await F0()}async function i2(){return(await F0())?.id}async function t2(){return(await F0())?.email}async function o2(){return(await F0())?.name}async function a2(){return await HW()}async function r2(){await R.logout()}async function s2(){if($0?._authenticatedUser)$0._authenticatedUser=void 0}L0();J0();J0();J0();import{log as c}from"@stacksjs/logging";import*as R0 from"@stacksjs/path";class UW{allow($){return q.allow($)}deny($,W){return q.deny($,W)}denyIf($,W){if($)return this.deny(W);return!0}denyUnless($,W){if(!$)return this.deny(W);return!0}allowIf($,W){if($)return this.allow(W);return!1}}async function qW(){let{fs:$}=await import("@stacksjs/storage"),W=R0.appPath("Policies");if(!$.existsSync(W)){c.debug("No Policies directory found");return}try{let G=(await import(R0.appPath("Gates.ts"))).policies||{};for(let[X,K]of Object.entries(G)){let Y=typeof K==="string"?K:K.policy,L=`${W}/${Y}.ts`;if($.existsSync(L)){let O=await import(L),_=O.default||O[Y];if(_)O0(X,_),c.debug(`Registered policy: ${Y} for ${X}`)}}}catch{c.debug("No Gates.ts found, using convention-based discovery")}let Z=$.readdirSync(W).filter((J)=>J.endsWith("Policy.ts"));for(let J of Z){let Q=J.replace(".ts",""),G=Q.replace("Policy",""),X=`${W}/${J}`;try{let K=await import(X),Y=K.default||K[Q];if(Y)O0(G,Y),c.debug(`Auto-discovered policy: ${Q} for ${G}`)}catch(K){c.error(`Failed to load policy ${Q}:`,K)}}}async function CW(){let{define:$,before:W,after:Z}=await Promise.resolve().then(() => (J0(),w1));try{let Q=await import(R0.appPath("Gates.ts")),G=Q.gates||Q.default?.gates||{};for(let[Y,L]of Object.entries(G))if(typeof L==="function")$(Y,L),c.debug(`Registered gate: ${Y}`);let X=Q.before||Q.default?.before||[];for(let Y of X)if(typeof Y==="function")W(Y);let K=Q.after||Q.default?.after||[];for(let Y of K)if(typeof Y==="function")Z(Y);c.debug("Gates registered successfully")}catch{c.debug("No Gates.ts found or failed to load")}}async function J3(){await CW(),await qW()}J0();async function PW($,W,...Z){return v0(W,$,...Z)}async function vW($,W,...Z){return S0(W,$,...Z)}async function SW($,W,...Z){return _0(W,$,...Z)}async function EW($,W,...Z){return E0(W,$,...Z)}async function xW($,W,...Z){return x0(W,$,...Z)}async function X3($,W,...Z){return W0(W,$,...Z)}function Y3($){return Object.assign($,{can:(Z,...J)=>PW($,Z,...J),cannot:(Z,...J)=>vW($,Z,...J),canAny:(Z,...J)=>SW($,Z,...J),canAll:(Z,...J)=>EW($,Z,...J),authorize:(Z,...J)=>xW($,Z,...J)})}class A0{max;map=new Map;constructor($){this.max=$}get($){return this.map.get($)}has($){return this.map.has($)}set($,W){if(this.map.has($))this.map.delete($);if(this.map.set($,W),this.map.size>this.max){let Z=this.map.keys().next().value;if(Z!==void 0)this.map.delete(Z)}return this}delete($){return this.map.delete($)}clear(){this.map.clear()}}var o0=1e4,V={userRoles:new A0(o0),userPermissions:new A0(o0),rolePermissions:new A0(o0),roles:new Map,permissions:new Map},a0=null;function RW($){a0=$,N0()}function j(){if(!a0)throw Error("RBAC store not configured. Call setRbacStore() first.");return a0}function N0(){V.userRoles.clear(),V.userPermissions.clear(),V.rolePermissions.clear(),V.roles.clear(),V.permissions.clear()}function N($){if(typeof $==="number"){if(!Number.isFinite($)||$<=0)throw TypeError("RBAC user id must be a positive number");return $}let W=$.id;if(typeof W!=="number"||!Number.isFinite(W)||W<=0)throw TypeError(`RBAC user id must be a positive number, got ${typeof W} (${String(W)})`);return W}async function r0($,W="web",Z){let J=await j().createRole($,W,Z);return V.roles.set(`${$}:${W}`,J),J}async function b($,W="web"){let Z=`${$}:${W}`;if(V.roles.has(Z))return V.roles.get(Z);let J=await j().findRoleByName($,W);if(J)V.roles.set(Z,J);return J}async function AW($,W="web"){let Z=await b($,W);if(Z)await j().deleteRole(Z.id),V.roles.delete(`${$}:${W}`),N0()}async function NW($){return j().getAllRoles($)}async function yW($,W="web",Z){let J=await j().createPermission($,W,Z);return V.permissions.set(`${$}:${W}`,J),J}async function u($,W="web"){let Z=`${$}:${W}`;if(V.permissions.has(Z))return V.permissions.get(Z);let J=await j().findPermissionByName($,W);if(J)V.permissions.set(Z,J);return J}async function TW($,W="web"){let Z=await u($,W);if(Z)await j().deletePermission(Z.id),V.permissions.delete(`${$}:${W}`),N0()}async function fW($){return j().getAllPermissions($)}async function Q0($){let W=N($);if(V.userRoles.has(W))return V.userRoles.get(W);let Z=await j().getUserRoles(W);return V.userRoles.set(W,Z),Z}async function I1($,W,Z="web"){let J=N($),Q=await b(W,Z);if(!Q)throw Error(`Role '${W}' not found.`);await j().assignRoleToUser(J,Q.id),V.userRoles.delete(J),V.userPermissions.delete(J)}async function b1($,W,Z="web"){let J=N($),Q=await b(W,Z);if(!Q)return;await j().removeRoleFromUser(J,Q.id),V.userRoles.delete(J),V.userPermissions.delete(J)}async function wW($){let W=N($);await j().removeAllRolesFromUser(W),V.userRoles.delete(W),V.userPermissions.delete(W)}async function k1($,W,Z="web"){let J=N($),Q=[];for(let G of W){let X=await b(G,Z);if(!X)throw Error(`Role '${G}' not found.`);Q.push(X.id)}await j().syncUserRoles(J,Q),V.userRoles.delete(J),V.userPermissions.delete(J)}async function g1($,W,Z="web"){return(await Q0($)).some((Q)=>Q.name===W&&Q.guard_name===Z)}async function h1($,W,Z="web"){let J=await Q0($);return W.some((Q)=>J.some((G)=>G.name===Q&&G.guard_name===Z))}async function p1($,W,Z="web"){let J=await Q0($);return W.every((Q)=>J.some((G)=>G.name===Q&&G.guard_name===Z))}async function z0($){let W=N($);if(V.userPermissions.has(W))return V.userPermissions.get(W);let Z=await j().getUserDirectPermissions(W),J=await Q0($),Q=[];for(let K of J){let Y=await i1(K.id);Q.push(...Y)}let G=new Set,X=[];for(let K of[...Z,...Q])if(!G.has(K.id))G.add(K.id),X.push(K);return V.userPermissions.set(W,X),X}async function m1($,W,Z="web"){let J=N($),Q=await u(W,Z);if(!Q)throw Error(`Permission '${W}' not found.`);await j().assignPermissionToUser(J,Q.id),V.userPermissions.delete(J)}async function c1($,W,Z="web"){let J=N($),Q=await u(W,Z);if(!Q)return;await j().removePermissionFromUser(J,Q.id),V.userPermissions.delete(J)}async function IW($){let W=N($);await j().removeAllPermissionsFromUser(W),V.userPermissions.delete(W)}async function u1($,W,Z="web"){let J=N($),Q=[];for(let G of W){let X=await u(G,Z);if(!X)throw Error(`Permission '${G}' not found.`);Q.push(X.id)}await j().syncUserPermissions(J,Q),V.userPermissions.delete(J)}async function l1($,W,Z="web"){return(await z0($)).some((Q)=>Q.name===W&&Q.guard_name===Z)}async function d1($,W,Z="web"){let J=await z0($);return W.some((Q)=>J.some((G)=>G.name===Q&&G.guard_name===Z))}async function n1($,W,Z="web"){let J=await z0($);return W.every((Q)=>J.some((G)=>G.name===Q&&G.guard_name===Z))}async function i1($){if(V.rolePermissions.has($))return V.rolePermissions.get($);let W=await j().getRolePermissions($);return V.rolePermissions.set($,W),W}async function bW($,W,Z="web"){let J=await b($,Z);if(!J)throw Error(`Role '${$}' not found.`);let Q=await u(W,Z);if(!Q)throw Error(`Permission '${W}' not found.`);await j().assignPermissionToRole(J.id,Q.id),V.rolePermissions.delete(J.id),V.userPermissions.clear()}async function kW($,W,Z="web"){let J=await b($,Z);if(!J)return;let Q=await u(W,Z);if(!Q)return;await j().removePermissionFromRole(J.id,Q.id),V.rolePermissions.delete(J.id),V.userPermissions.clear()}async function gW($,W,Z="web"){let J=await b($,Z);if(!J)throw Error(`Role '${$}' not found.`);let Q=[];for(let G of W){let X=await u(G,Z);if(!X)throw Error(`Permission '${G}' not found.`);Q.push(X.id)}await j().syncRolePermissions(J.id,Q),V.rolePermissions.delete(J.id),V.userPermissions.clear()}function hW($){let W=N($);return Object.assign($,{hasRole:(J,Q)=>g1(W,J,Q),hasAnyRole:(J,Q)=>h1(W,J,Q),hasAllRoles:(J,Q)=>p1(W,J,Q),hasPermission:(J,Q)=>l1(W,J,Q),hasAnyPermission:(J,Q)=>d1(W,J,Q),hasAllPermissions:(J,Q)=>n1(W,J,Q),getRoles:()=>Q0(W),getPermissions:()=>z0(W),assignRole:(J,Q)=>I1(W,J,Q),removeRole:(J,Q)=>b1(W,J,Q),syncRoles:(J,Q)=>k1(W,J,Q),givePermission:(J,Q)=>m1(W,J,Q),revokePermission:(J,Q)=>c1(W,J,Q),syncPermissions:(J,Q)=>u1(W,J,Q)})}var L3={setStore:RW,flushCache:N0,createRole:r0,findRole:b,deleteRole:AW,getAllRoles:NW,createPermission:yW,findPermission:u,deletePermission:TW,getAllPermissions:fW,getUserRoles:Q0,assignRole:I1,removeRole:b1,removeAllRoles:wW,syncRoles:k1,hasRole:g1,hasAnyRole:h1,hasAllRoles:p1,getUserPermissions:z0,givePermission:m1,revokePermission:c1,revokeAllPermissions:IW,syncPermissions:u1,hasPermission:l1,hasAnyPermission:d1,hasAllPermissions:n1,getRolePermissions:i1,givePermissionToRole:bW,revokePermissionFromRole:kW,syncRolePermissions:gW,withRbac:hW};var t1=[{name:"admin",guard_name:"web",description:"Full access. Sees every dashboard surface, every model, every infra control."},{name:"dev",guard_name:"web",description:"Developer / infra. Sees dev-mode surfaces (CI, query inspector, runner alerts) but not billing/admin-only management."},{name:"client",guard_name:"web",description:"End user / client. Sees content, orders, profile, billing \u2014 no dev tools, no infra surfaces."}];async function pW(){let $=[],W=[];for(let Z of t1){if(await b(Z.name,Z.guard_name)){W.push({name:Z.name,guard_name:Z.guard_name,reason:"already_exists"});continue}let Q=await r0(Z.name,Z.guard_name,Z.description);$.push(Q)}return{created:$,skipped:W}}import{Buffer as o1}from"buffer";import{createHmac as r1,randomBytes as mW,timingSafeEqual as cW}from"crypto";import{config as G0}from"@stacksjs/config";import{db as o}from"@stacksjs/database";import{mail as a1,template as uW}from"@stacksjs/email";import{log as lW}from"@stacksjs/logging";function s1(){let $=G0.app.key;if(typeof $!=="string"||$.length===0)throw Error("[auth] config.app.key is not set \u2014 email-verification HMAC requires a real APP_KEY. "+"Run `./buddy key:generate` to provision one, or set the APP_KEY env var before booting the app.");return $}function dW($){let W=mW(32).toString("hex"),Z=`${$}:${W}`,J=r1("sha256",s1()).update(Z).digest("hex");return{token:W,hash:J}}function nW($,W,Z){let J=`${$}:${W}`,Q=r1("sha256",s1()).update(J).digest("hex"),G=o1.from(Q),X=o1.from(Z);if(G.length!==X.length)return!1;return cW(G,X)}function iW(){let W=(G0.auth??{}).emailVerification;if(W!=null&&typeof W==="object"){let Z=W;if(typeof Z.expire==="number")return Z.expire}return 60}function tW($,W){let Z=G0.app.url?`https://${G0.app.url}`:`http://localhost:${process.env.PORT||"3000"}`,Q=(G0.auth.emailVerification?.url??"/verify-email/{id}/{token}").replace("{id}",String($)).replace("{token}",W);return/^https?:\/\//.test(Q)?Q:`${Z}${Q.startsWith("/")?"":"/"}${Q}`}function e1($){return $.email_verified_at!=null}async function $$($){let{token:W,hash:Z}=dW($.id),J=iW(),Q=new Date(Date.now()+J*60*1000);await o.deleteFrom("email_verifications").where("user_id","=",$.id).execute(),await o.insertInto("email_verifications").values({user_id:$.id,token:Z,expires_at:Q.toISOString()}).executeTakeFirst();let G=tW($.id,W),X=G0.app.name||"Stacks";try{let{html:K,text:Y}=await uW("email-verification",{subject:`Verify Your ${X} Email Address`,variables:{verificationUrl:G,expiryMinutes:J,userName:$.name||$.email}});if(!K&&!Y)throw Error("email-verification template missing or rendered empty");await a1.send({to:$.email,subject:`Verify Your ${X} Email Address`,text:Y,html:K})}catch(K){let Y=K instanceof Error?K.message:String(K);lW.warn(`[email] Email verification template failed, using plain text fallback: ${Y}`),await a1.send({to:$.email,subject:`Verify Your ${X} Email Address`,text:`Please verify your email address by visiting: ${G}
|
|
156
164
|
|
|
157
|
-
This link expires in ${X} minutes.`})}}async function b5($,Z){let Q=await p.selectFrom("email_verifications").where("user_id","=",$).selectAll().executeTakeFirst();if(!Q)return{success:!1,message:"No verification request found. Please request a new verification email."};let X=new Date(Q.expires_at);if(new Date>X)return await p.deleteFrom("email_verifications").where("user_id","=",$).execute(),{success:!1,message:"Verification link has expired. Please request a new one."};if(!T5($,Z,Q.token))return{success:!1,message:"Invalid verification link."};return await p.updateTable("users").set({email_verified_at:new Date().toISOString()}).where("id","=",$).executeTakeFirst(),await p.deleteFrom("email_verifications").where("user_id","=",$).execute(),{success:!0,message:"Email verified successfully."}}async function g5($){if(O4($))return{success:!1,message:"Email is already verified."};let Z=await p.selectFrom("email_verifications").where("user_id","=",$.id).selectAll().executeTakeFirst();if(Z){let Q=new Date(Z.created_at),X=(Date.now()-Q.getTime())/1000;if(X<60)return{success:!1,message:`Please wait ${Math.ceil(60-X)} seconds before requesting another verification email.`}}return await D4($),{success:!0,message:"Verification email sent."}}var Y8={isVerified:O4,send:D4,verify:b5,resend:g5};import{HttpError as k5}from"@stacksjs/error-handling";import{log as D0}from"@stacksjs/logging";import{User as q4}from"@stacksjs/orm";import{verifyHash as h5}from"@stacksjs/security";import{db as I}from"@stacksjs/database";var p5="$2b$12$000000000000000000000uGByljkdFkOJRCRiYZGFOAstyLlSgTSW";function m5(){let $=crypto.getRandomValues(new Uint8Array(32));return Array.from($,(Z)=>Z.toString(16).padStart(2,"0")).join("")}async function u5($,Z){let Q=await q4.where("email",$).first(),X=Q?.password||p5;if(!await h5(Z,X)||!Q)throw new k5(401,"Invalid credentials");let _=m5(),W=new Date(Date.now()+86400000);D0.debug(`[auth] Session created for user#${Q.id}`);try{await I.insertInto("sessions").values({id:_,user_id:Q.id,ip_address:null,user_agent:null,payload:"{}",last_activity:Math.floor(Date.now()/1000),expires_at:W.toISOString()}).execute()}catch(J){D0.debug(`[auth] Sessions table not available, session is memory-only. err=${J.message}`)}return{user:Q,sessionId:_}}async function c5($){D0.debug("[auth] Session destroyed");try{await I.deleteFrom("sessions").where("id","=",$).execute()}catch(Z){D0.debug(`[auth] Session destroy failed: ${Z.message}`)}}async function l5($){try{let Z=await I.selectFrom("sessions").where("id","=",$).selectAll().executeTakeFirst();if(!Z)return;let Q=Z.expires_at?new Date(String(Z.expires_at)).getTime():0;if(Date.now()>Q){await I.deleteFrom("sessions").where("id","=",$).execute();return}return await q4.find(Z.user_id)}catch{return}}async function d5($){try{let Z=await I.selectFrom("sessions").where("id","=",$).selectAll().executeTakeFirst();if(!Z)return!1;let Q=Z.expires_at?new Date(String(Z.expires_at)).getTime():0;if(Date.now()>Q)return await I.deleteFrom("sessions").where("id","=",$).execute(),!1;return!0}catch{return!1}}async function n5($,Z=86400000){try{let Q=await I.selectFrom("sessions").where("id","=",$).selectAll().executeTakeFirst();if(!Q)return!1;let X=Q.expires_at?new Date(String(Q.expires_at)).getTime():0;if(Date.now()>X)return await I.deleteFrom("sessions").where("id","=",$).execute(),!1;let Y=new Date(Date.now()+Z);return await I.updateTable("sessions").set({expires_at:Y.toISOString(),last_activity:Math.floor(Date.now()/1000)}).where("id","=",$).execute(),!0}catch{return!1}}var K8={login:u5,logout:c5,user:l5,check:d5,refresh:n5};import{generateTOTP as N8,verifyTOTP as y8,generateTOTPSecret as I8,totpKeyUri as T8}from"@stacksjs/ts-auth";export{A5 as withRbac,t7 as withAuthorization,l6 as verifyTwoFactorCode,y8 as verifyTOTP,F7 as verifyRegistrationResponse,b5 as verifyEmail,K7 as verifyAuthenticationResponse,D6 as validateRefreshToken,V5 as userCannot,B5 as userCanAny,O5 as userCanAll,K5 as userCan,T8 as totpKeyUri,J6 as tokens,L6 as tokenCant,K6 as tokenCanAny,F6 as tokenCanAll,h4 as tokenCan,V6 as tokenAbilities,G6 as token,$4 as syncRoles,P5 as syncRolePermissions,_4 as syncPermissions,V7 as startRegistration,B7 as startAuthentication,q5 as setRbacStore,W7 as setCurrentRegistrationOptions,l5 as sessionUser,n5 as sessionRefresh,c5 as sessionLogout,u5 as sessionLogin,d5 as sessionCheck,D4 as sendVerificationEmail,z6 as revokeTokenById,M6 as revokeToken,q6 as revokeRefreshToken,x5 as revokePermissionFromRole,W4 as revokePermission,H6 as revokeOtherTokens,P6 as revokeClient,m4 as revokeAllTokens,p4 as revokeAllRefreshTokens,E5 as revokeAllPermissions,g5 as resendVerificationEmail,e0 as removeRole,v5 as removeAllRoles,F5 as registerGates,R7 as register,O6 as refreshToken,h7 as refresh,X0 as policy,q7 as platformAuthenticatorIsAvailable,U7 as passwordResets,y as parseScopes,l0 as none,b7 as name,k7 as logout,O4 as isEmailVerified,g7 as isAuthenticated,i7 as inspectUser,n as inspect,l7 as initializeAuthorization,T7 as id,Z4 as hasRole,i0 as hasPolicy,J4 as hasPermission,Q4 as hasAnyRole,G4 as hasAnyPermission,X4 as hasAllRoles,L4 as hasAllPermissions,n0 as has,S5 as givePermissionToRole,Y4 as givePermission,o as getUserRoles,W0 as getUserPermissions,X7 as getUserPasskeys,Y7 as getUserPasskey,F4 as getRolePermissions,d0 as getPolicyFor,I7 as getCurrentUser,M5 as getAllRoles,U5 as getAllPermissions,d6 as generateTwoFactorUri,c6 as generateTwoFactorToken,t4 as generateTwoFactorSecret,I8 as generateTOTPSecret,N8 as generateTOTP,G7 as generateRegistrationOptions,L7 as generateAuthenticationOptions,O0 as flushRbacCache,o0 as flush,k4 as findToken,b as findRole,g as findPermission,S6 as findClient,f7 as email,L5 as discoverPolicies,c0 as denies,j5 as deleteRole,v6 as deleteRevokedTokens,j6 as deleteRevokedRefreshTokens,H5 as deletePermission,U6 as deleteExpiredTokens,C6 as deleteExpiredRefreshTokens,h0 as define,u as currentAccessToken,B6 as createToken,C5 as createRole,r6 as createPersonalAccessClient,z5 as createPermission,x6 as createClient,E6 as clients,_5 as check,F0 as cannot,L0 as can,D7 as browserSupportsWebAuthnAutofill,O7 as browserSupportsWebAuthn,p0 as before,D5 as authorizeUser,V0 as authorize,Z0 as authUser,$7 as authMiddlewareHandler,e4 as authMiddleware,s0 as assignRole,Y0 as any,u0 as allows,K0 as all,m0 as after,t0 as abilities,K8 as SessionAuth,a7 as Rbac,r as RateLimiter,a0 as Gate,Y8 as EmailVerification,G5 as BasePolicy,z as AuthorizationResponse,Q0 as AuthorizationException,R as Auth};
|
|
165
|
+
This link expires in ${J} minutes.`})}}async function oW($,W){let Z=await o.selectFrom("email_verifications").where("user_id","=",$).selectAll().executeTakeFirst();if(!Z)return{success:!1,message:"No verification request found. Please request a new verification email."};let J=new Date(Z.expires_at);if(new Date>J)return await o.deleteFrom("email_verifications").where("user_id","=",$).execute(),{success:!1,message:"Verification link has expired. Please request a new one."};if(!nW($,W,Z.token))return{success:!1,message:"Invalid verification link."};return await o.updateTable("users").set({email_verified_at:new Date().toISOString()}).where("id","=",$).executeTakeFirst(),await o.deleteFrom("email_verifications").where("user_id","=",$).execute(),{success:!0,message:"Email verified successfully."}}async function aW($){if(e1($))return{success:!1,message:"Email is already verified."};let W=await o.selectFrom("email_verifications").where("user_id","=",$.id).selectAll().executeTakeFirst();if(W){let Z=new Date(W.created_at),J=(Date.now()-Z.getTime())/1000;if(J<60)return{success:!1,message:`Please wait ${Math.ceil(60-J)} seconds before requesting another verification email.`}}return await $$($),{success:!0,message:"Verification email sent."}}var M3={isVerified:e1,send:$$,verify:oW,resend:aW};import{generateTOTP as W4,verifyTOTP as Z4,generateTOTPSecret as J4,totpKeyUri as Q4}from"@stacksjs/ts-auth";import{randomBytes as rW}from"crypto";import{db as y}from"@stacksjs/database";var sW=300;async function W$($){let W=await y.selectFrom("users").where("id","=",$).select(["two_factor_secret","two_factor_enabled"]).executeTakeFirst();return{secret:W?.two_factor_secret??null,enabled:Boolean(W?.two_factor_enabled)}}function eW($){return Boolean($.two_factor_enabled)}function $Z($,W){let Z=g0(),J=B1($,W,Z);return{secret:Z,uri:J}}var WZ=600;async function ZZ($,W,Z=WZ){let J=new Date(Date.now()+Z*1000).toISOString();await y.deleteFrom("two_factor_pending_secrets").where("user_id","=",$).execute(),await y.insertInto("two_factor_pending_secrets").values({user_id:$,secret:W,expires_at:J}).execute()}async function JZ($){let W=await y.selectFrom("two_factor_pending_secrets").where("user_id","=",$).selectAll().executeTakeFirst();if(!W)return null;await y.deleteFrom("two_factor_pending_secrets").where("user_id","=",$).execute();let Z=W.expires_at?new Date(String(W.expires_at)).getTime():0;if(Date.now()>Z)return null;return String(W.secret)}async function QZ($,W,Z){if(!await h0(Z,W))return!1;return await y.updateTable("users").set({two_factor_secret:W,two_factor_enabled:!0}).where("id","=",$).executeTakeFirst(),!0}async function GZ($){await y.updateTable("users").set({two_factor_secret:null,two_factor_enabled:!1}).where("id","=",$).executeTakeFirst()}async function XZ($,W){let{secret:Z,enabled:J}=await W$($);if(!J||!Z)return!1;return h0(W,Z)}async function YZ($,W=sW){let Z=rW(32).toString("hex"),J=new Date(Date.now()+W*1000).toISOString();return await y.deleteFrom("two_factor_challenges").where("user_id","=",$).execute(),await y.insertInto("two_factor_challenges").values({id:Z,user_id:$,expires_at:J}).execute(),Z}async function KZ($){let W=await y.selectFrom("two_factor_challenges").where("id","=",$).selectAll().executeTakeFirst();if(!W)return null;await y.deleteFrom("two_factor_challenges").where("id","=",$).execute();let Z=W.expires_at?new Date(String(W.expires_at)).getTime():0;if(Date.now()>Z)return null;return Number(W.user_id)}var v3={isEnabled:eW,getState:W$,generateSetup:$Z,stashPendingSecret:ZZ,consumePendingSecret:JZ,enable:QZ,disable:GZ,verifyLoginCode:XZ,createChallenge:YZ,consumeChallenge:KZ};import{config as l}from"@stacksjs/config";import{db as y0}from"@stacksjs/database";var s0="active_team";function Z$($){let W=$.rolePriority??{owner:0,admin:1},Z=[...$.memberships??[]].sort((G,X)=>(W[G.role??""]??2)-(W[X.role??""]??2)),J=$.activeTeamId==null?null:Number($.activeTeamId);if(J!=null&&Number.isFinite(J)){let G=Z.find((X)=>Number(X.team_id)===J);if(G)return{teamId:J,role:G.role??null};if($.allowAnyTeam)return{teamId:J,role:"admin"}}let Q=Z[0];return Q?{teamId:Number(Q.team_id),role:Q.role??null}:{teamId:null,role:null}}function J$($){let W=$.cookies?.get(s0);if(!W)return null;let Z=Number(W);return Number.isFinite(Z)&&Z>0?Z:null}function N3($,W={}){let Z=Math.max(0,Math.floor(W.maxAgeSeconds??31536000)),J=[`${s0}=${encodeURIComponent(String($))}`,"Path=/","HttpOnly","SameSite=Lax",`Max-Age=${Z}`];if(W.secure)J.push("Secure");return J.join("; ")}function y3(){return`${s0}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`}async function LZ($){let W=await VZ($);if(!W?.id)return null;let Z=await y0.selectFrom("team_members").where("user_id","=",W.id).where("status","=","active").select(["team_id","role"]).execute();if(Z.length===0)return null;let J=Z$({memberships:Z.map((Q)=>({team_id:Number(Q.team_id),role:String(Q.role)})),activeTeamId:J$($)});return J.teamId!=null?{teamId:J.teamId,role:J.role??String(Z[0].role)}:null}async function T3($,W={}){let Z={user:null,teamId:null,role:null,teams:[],activeTeamId:null},J=l.auth?.default||"api",G=(l.auth?.guards?.[J]?.driver||"token")==="session"?await Q$($):await G$($);if(!G?.id)return Z;let X=await y0.selectFrom("team_members").where("user_id","=",G.id).where("status","=","active").select(["team_id","role"]).execute(),K=W.allowAnyTeam?!!W.allowAnyTeam(G):!1,Y=J$($),L=Z$({memberships:X.map((F)=>({team_id:Number(F.team_id),role:String(F.role)})),activeTeamId:Y,allowAnyTeam:K}),O=new Map(X.map((F)=>[Number(F.team_id),String(F.role)])),_=[];if(K)_=await y0.selectFrom("teams").select(["id","name"]).execute();else{let F=[...O.keys()];_=F.length?await y0.selectFrom("teams").whereIn("id",F).select(["id","name"]).execute():[]}let E=_.map((F)=>({id:Number(F.id),name:String(F.name),role:O.get(Number(F.id))||"viewer"})).sort((F,H)=>F.name.localeCompare(H.name));return{user:G,teamId:L.teamId,role:L.role,teams:E,activeTeamId:Y}}async function f3($){let W=await LZ($);return W?W.teamId:null}async function VZ($){let W=l.auth?.default||"api",Q=((l.auth?.guards?.[W]||{driver:"token"}).driver||"token")==="session"?await Q$($):await G$($);return Q?.id?Q:void 0}async function Q$($){let W=$.cookies?.get("session_id");if(!W)return;return c0(W)}async function G$($){let W=l.auth?.defaultTokenName||"auth-token",Z=(typeof $.bearerToken==="function"?$.bearerToken():void 0)??$.cookies?.get(W);if(!Z)return;return R.getUserFromToken(Z)}export{hW as withRbac,Y3 as withAuthorization,XZ as verifyTwoFactorLoginCode,h0 as verifyTwoFactorCode,Z4 as verifyTOTP,G2 as verifyRegistrationResponse,oW as verifyEmail,X2 as verifyAuthenticationResponse,E$ as validateRefreshToken,vW as userCannot,SW as userCanAny,EW as userCanAll,PW as userCan,rZ as updatePasskeyCounter,Q4 as totpKeyUri,M$ as tokens,q$ as tokenCant,P$ as tokenCanAny,C$ as tokenCanAll,X1 as tokenCan,v$ as tokenAbilities,U$ as token,k1 as syncRoles,gW as syncRolePermissions,u1 as syncPermissions,eZ as storeWebAuthnChallenge,ZZ as stashPendingTwoFactorSecret,Y2 as startRegistration,K2 as startAuthentication,RW as setRbacStore,sZ as setCurrentRegistrationOptions,c0 as sessionUser,KW as sessionRefresh,XW as sessionLogout,GW as sessionLogin,m0 as sessionDestroyAll,YW as sessionCheck,$$ as sendVerificationEmail,Z$ as selectActiveTeam,pW as seedDefaultRoles,y$ as revokeTokenById,N$ as revokeToken,x$ as revokeRefreshToken,kW as revokePermissionFromRole,c1 as revokePermission,T$ as revokeOtherTokens,g$ as revokeClient,j0 as revokeAllTokens,Y1 as revokeAllRefreshTokens,IW as revokeAllPermissions,T3 as resolveTeamContext,VZ as resolveAuthenticatedUser,f3 as resolveAuthenticatedTeamId,LZ as resolveAuthenticatedMembership,aW as resendVerificationEmail,b1 as removeRole,wW as removeAllRoles,CW as registerGates,c2 as register,S$ as refreshToken,s2 as refresh,O0 as policy,F2 as platformAuthenticatorIsAvailable,y2 as passwordResets,f as parseScopes,x1 as none,o2 as name,r2 as logout,eW as isTwoFactorEnabled,d as isIssuedBeforePasswordChange,e1 as isEmailVerified,a2 as isAuthenticated,X3 as inspectUser,W0 as inspect,J3 as initializeAuthorization,i2 as id,g1 as hasRole,N1 as hasPolicy,l1 as hasPermission,h1 as hasAnyRole,d1 as hasAnyPermission,p1 as hasAllRoles,n1 as hasAllPermissions,A1 as has,bW as givePermissionToRole,m1 as givePermission,Q0 as getUserRoles,z0 as getUserPermissions,aZ as getUserPasskeys,e$ as getUserPasskey,W$ as getTwoFactorState,i1 as getRolePermissions,R1 as getPolicyFor,s as getPasswordChangedAt,n2 as getCurrentUser,NW as getAllRoles,fW as getAllPermissions,J$ as getActiveTeamPreference,B1 as generateTwoFactorUri,kZ as generateTwoFactorToken,$Z as generateTwoFactorSetup,g0 as generateTwoFactorSecret,J4 as generateTOTPSecret,W4 as generateTOTP,J2 as generateRegistrationOptions,Q2 as generateAuthenticationOptions,N0 as flushRbacCache,T1 as flush,G1 as findToken,b as findRole,u as findPermission,b$ as findClient,QZ as enableTwoFactor,t2 as email,qW as discoverPolicies,GZ as disableTwoFactor,E1 as denies,AW as deleteRole,w$ as deleteRevokedTokens,A$ as deleteRevokedRefreshTokens,TW as deletePermission,f$ as deleteExpiredTokens,R$ as deleteExpiredRefreshTokens,C1 as define,n as currentAccessToken,YZ as createTwoFactorChallenge,f0 as createToken,r0 as createRole,lZ as createPersonalAccessClient,yW as createPermission,k$ as createClient,OW as createBqbRbacStore,$2 as consumeWebAuthnChallenge,KZ as consumeTwoFactorChallenge,JZ as consumePendingTwoFactorSecret,I$ as clients,y3 as clearActiveTeamCookie,HW as check,S0 as cannot,v0 as can,N3 as buildActiveTeamCookie,V2 as browserSupportsWebAuthnAutofill,L2 as browserSupportsWebAuthn,P1 as before,xW as authorizeUser,x0 as authorize,F0 as authUser,iZ as authMiddlewareHandler,s$ as authMiddleware,I1 as assignRole,_0 as any,S1 as allows,E0 as all,v1 as after,y1 as abilities,v3 as TwoFactor,U2 as SessionAuth,L3 as Rbac,k as RateLimiter,f1 as Gate,M3 as EmailVerification,t1 as DEFAULT_ROLE_PACKS,UW as BasePolicy,q as AuthorizationResponse,B0 as AuthorizationException,R as Auth,s0 as ACTIVE_TEAM_COOKIE};
|
package/dist/src/client.d.ts
CHANGED
|
@@ -1,2 +1,22 @@
|
|
|
1
1
|
import type { Result } from '@stacksjs/error-handling';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Create a personal access OAuth client.
|
|
4
|
+
*
|
|
5
|
+
* Idempotent: returns an `err({ code: 'already-exists' })` when a
|
|
6
|
+
* non-revoked personal access client already exists in the
|
|
7
|
+
* `oauth_clients` table, rather than inserting a duplicate. The
|
|
8
|
+
* previous behaviour silently created a second row, which made
|
|
9
|
+
* `getPersonalAccessClient()` (which `LIMIT 1`s with no `ORDER BY`)
|
|
10
|
+
* return whichever the DB happened to surface first — racy with
|
|
11
|
+
* subsequent token mints. See stacksjs/stacks#1860 M-7.
|
|
12
|
+
*
|
|
13
|
+
* The plaintext secret is what callers (e.g., `./buddy auth:token`)
|
|
14
|
+
* surface to the operator — they store it themselves. The DB holds
|
|
15
|
+
* only the bcrypt hash so a DB compromise doesn't leak usable client
|
|
16
|
+
* credentials (stacksjs/stacks#1861 M-1).
|
|
17
|
+
*/
|
|
18
|
+
export declare function createPersonalAccessClient(): Promise<Result<string, CreatePersonalAccessClientError>>;
|
|
19
|
+
export declare interface CreatePersonalAccessClientError {
|
|
20
|
+
code: 'already-exists'
|
|
21
|
+
message: string
|
|
22
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export type { SeedDefaultRolesResult } from './rbac-seed';
|
|
1
2
|
export * from './authentication';
|
|
2
3
|
export * from './authenticator';
|
|
3
4
|
export * from './client';
|
|
@@ -16,6 +17,8 @@ export * from './policy';
|
|
|
16
17
|
export * from './authorizable';
|
|
17
18
|
// Role-Based Access Control (RBAC)
|
|
18
19
|
export * from './rbac';
|
|
20
|
+
export { createBqbRbacStore } from './rbac-store-bqb';
|
|
21
|
+
export { DEFAULT_ROLE_PACKS, seedDefaultRoles } from './rbac-seed';
|
|
19
22
|
// Email Verification
|
|
20
23
|
export * from './email-verification';
|
|
21
24
|
// Session-based Authentication (SPA Cookie Auth)
|
|
@@ -27,3 +30,7 @@ export {
|
|
|
27
30
|
generateTOTPSecret,
|
|
28
31
|
totpKeyUri,
|
|
29
32
|
} from '@stacksjs/ts-auth';
|
|
33
|
+
// TOTP setup/enable/disable + login-challenge persistence
|
|
34
|
+
export * from './two-factor';
|
|
35
|
+
// Team resolution from auth credentials (dashboard-form scoping)
|
|
36
|
+
export * from './team';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal constants shared across the auth package.
|
|
3
|
+
*
|
|
4
|
+
* Lives in its own file so consumers like `authentication.ts` and
|
|
5
|
+
* `session-auth.ts` can pull the same literal without re-declaring it
|
|
6
|
+
* (stacksjs/stacks#1861 L-1).
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Pre-computed bcrypt hash of nothing — used to keep timing constant
|
|
10
|
+
* when the lookup-by-email half of a login fails. Without it, the
|
|
11
|
+
* "user not found" branch would short-circuit fast and the "wrong
|
|
12
|
+
* password" branch would always pay the bcrypt cost, leaking
|
|
13
|
+
* "does this email exist?" via response time.
|
|
14
|
+
*
|
|
15
|
+
* The hash itself is intentionally non-verifiable: nothing the
|
|
16
|
+
* attacker types will ever match it, so the dummy compare always
|
|
17
|
+
* returns false but spends the same CPU as a real one would.
|
|
18
|
+
*/
|
|
19
|
+
export declare const DUMMY_BCRYPT_HASH: '$2b$12$000000000000000000000uGByljkdFkOJRCRiYZGFOAstyLlSgTSW';
|
package/dist/src/passkey.d.ts
CHANGED
|
@@ -14,7 +14,43 @@ export type {
|
|
|
14
14
|
} from '@stacksjs/ts-auth';
|
|
15
15
|
export declare function getUserPasskeys(userId: number): Promise<PasskeyAttribute[]>;
|
|
16
16
|
export declare function getUserPasskey(userId: number, passkeyId: string): Promise<PasskeyAttribute | undefined>;
|
|
17
|
+
/**
|
|
18
|
+
* Persist the post-verification authenticator counter and refresh the
|
|
19
|
+
* passkey's last-used timestamp. WebAuthn's anti-cloning guarantee
|
|
20
|
+
* depends on the relying party rejecting any authentication whose
|
|
21
|
+
* `newCounter` is **not strictly greater** than the stored value —
|
|
22
|
+
* authenticators monotonically increment their counter on every use,
|
|
23
|
+
* so a counter that doesn't advance (or goes backwards) signals a
|
|
24
|
+
* cloned or replayed credential.
|
|
25
|
+
*
|
|
26
|
+
* Returns `true` when the counter was updated successfully; `false`
|
|
27
|
+
* when the new counter is not greater than the stored one (the
|
|
28
|
+
* authentication MUST be rejected by the caller in that case).
|
|
29
|
+
* stacksjs/stacks#1861 A-4.
|
|
30
|
+
*/
|
|
31
|
+
export declare function updatePasskeyCounter(userId: number, passkeyId: string, newCounter: number): Promise<boolean>;
|
|
17
32
|
export declare function setCurrentRegistrationOptions(user: UserModel, verified: VerifiedRegistrationResponse): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Persist a server-issued WebAuthn challenge for the given user +
|
|
35
|
+
* purpose. Deletes any prior challenge for the same (user, purpose)
|
|
36
|
+
* pair so a fresh `generateOptions` invalidates the previous one.
|
|
37
|
+
*
|
|
38
|
+
* The unique index on `(user_id, purpose)` enforces single-outstanding
|
|
39
|
+
* at the DB layer; this delete makes the upsert safe even on installs
|
|
40
|
+
* that ran an earlier auth:setup before the unique index existed.
|
|
41
|
+
*/
|
|
42
|
+
export declare function storeWebAuthnChallenge(userId: number, challenge: string, purpose: WebAuthnChallengePurpose, ttlSeconds?: number): Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* Read + delete (single-use) a WebAuthn challenge for the given user
|
|
45
|
+
* + purpose. Returns `null` when no outstanding challenge exists or
|
|
46
|
+
* when the stored challenge has expired.
|
|
47
|
+
*
|
|
48
|
+
* Single-use semantics matter: a successful verify must invalidate
|
|
49
|
+
* the challenge so a captured assertion can't be replayed even within
|
|
50
|
+
* the TTL window. Callers MUST treat a `null` return as a verification
|
|
51
|
+
* failure.
|
|
52
|
+
*/
|
|
53
|
+
export declare function consumeWebAuthnChallenge(userId: number, purpose: WebAuthnChallengePurpose): Promise<string | null>;
|
|
18
54
|
export declare interface PasskeyAttribute {
|
|
19
55
|
id: string
|
|
20
56
|
cred_public_key: string
|
|
@@ -31,6 +67,21 @@ export declare interface PasskeyAttribute {
|
|
|
31
67
|
}
|
|
32
68
|
declare type UserModel = InstanceType<typeof User>;
|
|
33
69
|
declare type PasskeyInsertable = Insertable<PasskeyAttribute>;
|
|
70
|
+
// =============================================================================
|
|
71
|
+
// WebAuthn challenge persistence (stacksjs/stacks#1866)
|
|
72
|
+
// =============================================================================
|
|
73
|
+
//
|
|
74
|
+
// WebAuthn relying parties MUST verify the assertion's challenge
|
|
75
|
+
// against a server-issued nonce. Previously Stacks returned the
|
|
76
|
+
// challenge in `generateOptions` and trusted the client to echo it
|
|
77
|
+
// back on verify — so any attacker who captured the assertion AND the
|
|
78
|
+
// challenge could replay the response. Persisting the challenge
|
|
79
|
+
// server-side and consuming it on verify closes that gap.
|
|
80
|
+
//
|
|
81
|
+
// The default TTL is 5 minutes — long enough for slow biometric
|
|
82
|
+
// flows, short enough to bound the replay window. Override per-call
|
|
83
|
+
// via the `ttlSeconds` parameter.
|
|
84
|
+
export type WebAuthnChallengePurpose = 'registration' | 'authentication';
|
|
34
85
|
// Re-export WebAuthn functions from ts-auth
|
|
35
86
|
export {
|
|
36
87
|
generateRegistrationOptions,
|
|
@@ -1,6 +1,44 @@
|
|
|
1
|
+
export declare interface RateLimitEntry {
|
|
2
|
+
attempts: number
|
|
3
|
+
lockedUntil: number
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Pluggable backing store for the auth rate limiter.
|
|
7
|
+
*
|
|
8
|
+
* Methods may be sync or async — the limiter awaits them either way. The
|
|
9
|
+
* default {@link MemoryStore} is process-local (fine for single-instance and
|
|
10
|
+
* dev). On a horizontally-scaled deployment the in-memory store is trivially
|
|
11
|
+
* bypassed by spreading attempts across instances, so production should swap
|
|
12
|
+
* in a shared store via `RateLimiter.useSharedStore()` (cache-backed; becomes
|
|
13
|
+
* cluster-wide when the cache driver is Redis) or a custom `useStore()`.
|
|
14
|
+
*/
|
|
15
|
+
export declare interface RateLimiterStore {
|
|
16
|
+
get: (key: string) => Promise<RateLimitEntry | undefined> | RateLimitEntry | undefined
|
|
17
|
+
set: (key: string, entry: RateLimitEntry, ttlMs: number) => Promise<void> | void
|
|
18
|
+
delete: (key: string) => Promise<void> | void
|
|
19
|
+
}
|
|
20
|
+
/** Process-local store — the default. */
|
|
21
|
+
declare class MemoryStore implements RateLimiterStore {
|
|
22
|
+
get(key: string): RateLimitEntry | undefined;
|
|
23
|
+
set(key: string, entry: RateLimitEntry): void;
|
|
24
|
+
delete(key: string): void;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Cache-backed store. Cross-instance when the configured cache driver is
|
|
28
|
+
* Redis; otherwise behaves like an in-memory store with TTL eviction. Entries
|
|
29
|
+
* carry a TTL so attempts decay automatically — no separate eviction pass.
|
|
30
|
+
*/
|
|
31
|
+
declare class CacheStore implements RateLimiterStore {
|
|
32
|
+
get(key: string): Promise<RateLimitEntry | undefined>;
|
|
33
|
+
set(key: string, entry: RateLimitEntry, ttlMs: number): Promise<void>;
|
|
34
|
+
delete(key: string): Promise<void>;
|
|
35
|
+
}
|
|
1
36
|
export declare class RateLimiter {
|
|
2
|
-
static
|
|
3
|
-
static
|
|
4
|
-
static
|
|
5
|
-
static
|
|
37
|
+
static useStore(custom: RateLimiterStore): void;
|
|
38
|
+
static useSharedStore(): void;
|
|
39
|
+
static useMemoryStore(): void;
|
|
40
|
+
static isRateLimited(email: string): Promise<boolean>;
|
|
41
|
+
static recordFailedAttempt(email: string): Promise<void>;
|
|
42
|
+
static resetAttempts(email: string): Promise<void>;
|
|
43
|
+
static validateAttempt(email: string): Promise<void>;
|
|
6
44
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { RoleRecord } from './rbac';
|
|
2
|
+
/**
|
|
3
|
+
* Idempotently seed the three default role packs. Existing role records
|
|
4
|
+
* with the same `(name, guard_name)` are left untouched.
|
|
5
|
+
*
|
|
6
|
+
* Throws only if the underlying `createRole` throws for a reason other
|
|
7
|
+
* than a unique-constraint race (the BqbRbacStore's `swallowDuplicate`
|
|
8
|
+
* already handles that case). Caller should typically log the error and
|
|
9
|
+
* surface it — a missing roles table means the migrations haven't run
|
|
10
|
+
* yet, which is a different bug than a seeder failure.
|
|
11
|
+
*/
|
|
12
|
+
export declare function seedDefaultRoles(): Promise<SeedDefaultRolesResult>;
|
|
13
|
+
/**
|
|
14
|
+
* The role packs every Stacks install starts with. `useRole()`'s built-in
|
|
15
|
+
* predicates (`isAdmin`, `isDev`, `isClient`) check these names verbatim,
|
|
16
|
+
* so renaming them in a project effectively unbinds the composable from
|
|
17
|
+
* its defaults — that's fine, but obvious to readers.
|
|
18
|
+
*/
|
|
19
|
+
export declare const DEFAULT_ROLE_PACKS: readonly [{
|
|
20
|
+
name: 'admin';
|
|
21
|
+
guard_name: 'web';
|
|
22
|
+
description: 'Full access. Sees every dashboard surface, every model, every infra control.'
|
|
23
|
+
}, {
|
|
24
|
+
name: 'dev';
|
|
25
|
+
guard_name: 'web';
|
|
26
|
+
description: 'Developer / infra. Sees dev-mode surfaces (CI, query inspector, runner alerts) but not billing/admin-only management.'
|
|
27
|
+
}, {
|
|
28
|
+
name: 'client';
|
|
29
|
+
guard_name: 'web';
|
|
30
|
+
description: 'End user / client. Sees content, orders, profile, billing — no dev tools, no infra surfaces.'
|
|
31
|
+
}];
|
|
32
|
+
export declare interface SeedDefaultRolesResult {
|
|
33
|
+
created: RoleRecord[]
|
|
34
|
+
skipped: Array<{ name: string, guard_name: string, reason: 'already_exists' }>
|
|
35
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { isUniqueViolation } from '@stacksjs/orm';
|
|
2
|
+
import type { PermissionRecord, RbacStore, RoleRecord } from './rbac';
|
|
3
|
+
/**
|
|
4
|
+
* Swallow unique-constraint violations. Anything else is re-thrown so a
|
|
5
|
+
* "connection lost" mid-INSERT doesn't get silently treated as
|
|
6
|
+
* "already assigned".
|
|
7
|
+
*
|
|
8
|
+
* Exported for direct unit testing — the live pivot helpers below also use it.
|
|
9
|
+
*/
|
|
10
|
+
export declare function swallowDuplicate(err: unknown): void;
|
|
11
|
+
/**
|
|
12
|
+
* Map a raw DB row to a typed record. Both `roles` and `permissions` share
|
|
13
|
+
* the same shape, so one mapper covers both — the call site provides the
|
|
14
|
+
* generic. Exported for unit testing.
|
|
15
|
+
*/
|
|
16
|
+
export declare function toRecord<T extends RoleRecord | PermissionRecord>(row: Record<string, unknown> | undefined): T | null;
|
|
17
|
+
export declare function createBqbRbacStore(): RbacStore;
|
|
18
|
+
export { isUniqueViolation };
|
package/dist/src/rbac.d.ts
CHANGED
|
@@ -216,4 +216,21 @@ export declare interface RbacMethods {
|
|
|
216
216
|
// Use the row/instance shape from orm so role helpers operate on the
|
|
217
217
|
// authenticated user object, not the User class constructor.
|
|
218
218
|
declare type UserModel = OrmUserModel;
|
|
219
|
+
/**
|
|
220
|
+
* FIFO-bounded Map. Wraps Map with a hard size cap; on overflow, the
|
|
221
|
+
* oldest entry (Map insertion order) is evicted. Used for the RBAC
|
|
222
|
+
* per-user caches because the previous unbounded `Map<number, ...>`
|
|
223
|
+
* grew without limit across the process lifetime — a long-running
|
|
224
|
+
* server serving many distinct users would OOM eventually
|
|
225
|
+
* (stacksjs/stacks#1860 M-5). Same shape as `BoundedMap` in
|
|
226
|
+
* `@stacksjs/router/stacks-router.ts`.
|
|
227
|
+
*/
|
|
228
|
+
declare class BoundedMap<K, V> {
|
|
229
|
+
constructor(max: number);
|
|
230
|
+
get(key: K): V | undefined;
|
|
231
|
+
has(key: K): boolean;
|
|
232
|
+
set(key: K, value: V): this;
|
|
233
|
+
delete(key: K): boolean;
|
|
234
|
+
clear(): void;
|
|
235
|
+
}
|
|
219
236
|
export default Rbac;
|
|
@@ -2,12 +2,30 @@ import { User } from '@stacksjs/orm';
|
|
|
2
2
|
/**
|
|
3
3
|
* Authenticate a user via email and password, creating a session.
|
|
4
4
|
* Sessions are persisted to the database so they survive server restarts.
|
|
5
|
+
*
|
|
6
|
+
* The `fingerprint` override is mainly for tests; in normal HTTP
|
|
7
|
+
* handling the active request's IP + UA are captured automatically.
|
|
5
8
|
*/
|
|
6
|
-
export declare function sessionLogin(email: string, password: string): Promise<{ user: UserModel, sessionId: string }>;
|
|
9
|
+
export declare function sessionLogin(email: string, password: string, fingerprint?: { ip?: string | null, userAgent?: string | null }): Promise<{ user: UserModel, sessionId: string }>;
|
|
7
10
|
/**
|
|
8
11
|
* Destroy the session for the given session ID.
|
|
9
12
|
*/
|
|
10
13
|
export declare function sessionLogout(sessionId: string): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Destroy every session for a user — the credential-change sweep.
|
|
16
|
+
* Sessions are validated purely on row existence + `expires_at`, never
|
|
17
|
+
* re-checked against the password hash, so without this a stolen
|
|
18
|
+
* session cookie survives a password reset for up to 24h
|
|
19
|
+
* (stacksjs/stacks#1947).
|
|
20
|
+
*
|
|
21
|
+
* Unlike `sessionLogout`, real failures propagate (fail loud): a reset
|
|
22
|
+
* that reports success while the attacker's session lives would be a
|
|
23
|
+
* lie. A missing `sessions` table alone is a benign no-op — no
|
|
24
|
+
* framework migration creates it (only userland adopting session-auth
|
|
25
|
+
* does), and without the table `sessionCheck` can never validate a
|
|
26
|
+
* session, so there is no credential left to revoke.
|
|
27
|
+
*/
|
|
28
|
+
export declare function sessionDestroyAll(userId: number): Promise<void>;
|
|
11
29
|
/**
|
|
12
30
|
* Get the authenticated user from a session ID.
|
|
13
31
|
*/
|
|
@@ -23,6 +41,7 @@ export declare function sessionRefresh(sessionId: string, ttlMs?: unknown): Prom
|
|
|
23
41
|
export declare const SessionAuth: {
|
|
24
42
|
login: unknown;
|
|
25
43
|
logout: unknown;
|
|
44
|
+
destroyAll: unknown;
|
|
26
45
|
user: unknown;
|
|
27
46
|
check: unknown;
|
|
28
47
|
refresh: unknown
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure resolver — no I/O — that decides which team is "active" for a user
|
|
3
|
+
* given their memberships and an optional switch preference. Reusable across
|
|
4
|
+
* apps: it encodes the whole workspace-switching precedence in one place.
|
|
5
|
+
*
|
|
6
|
+
* - Honors `activeTeamId` only when the user may access it: a member always
|
|
7
|
+
* may; a privileged operator may access any team when `allowAnyTeam` is set
|
|
8
|
+
* (the app decides who is privileged — e.g. a super-admin flag).
|
|
9
|
+
* - Otherwise falls back to the highest-priority membership (owner > admin >
|
|
10
|
+
* anything else), matching what dashboard pages have always shown, so the
|
|
11
|
+
* "current team" never differs between a rendered form and the action it
|
|
12
|
+
* posts to.
|
|
13
|
+
*/
|
|
14
|
+
export declare function selectActiveTeam(opts: {
|
|
15
|
+
memberships: Array<{ team_id: number | string, role?: string | null }>
|
|
16
|
+
activeTeamId?: number | null
|
|
17
|
+
allowAnyTeam?: boolean
|
|
18
|
+
rolePriority?: Record<string, number>
|
|
19
|
+
}): { teamId: number | null, role: string | null };
|
|
20
|
+
/** Read the active-team switch preference from a request's cookie. */
|
|
21
|
+
export declare function getActiveTeamPreference(request: TeamAuthRequest): number | null;
|
|
22
|
+
/**
|
|
23
|
+
* Build the `Set-Cookie` that pins the active team (workspace switcher). A
|
|
24
|
+
* year-long HttpOnly cookie: only the server needs it (SSR reads it, the
|
|
25
|
+
* switch action writes it), and it carries no privilege of its own.
|
|
26
|
+
*/
|
|
27
|
+
export declare function buildActiveTeamCookie(teamId: number, opts?: { maxAgeSeconds?: number, secure?: boolean }): string;
|
|
28
|
+
/** Clear the active-team cookie {@link buildActiveTeamCookie} sets. */
|
|
29
|
+
export declare function clearActiveTeamCookie(): string;
|
|
30
|
+
/**
|
|
31
|
+
* Resolve the requesting user's active team membership (team id + role)
|
|
32
|
+
* from their auth credential. Driver-aware: reads config/auth.ts's
|
|
33
|
+
* configured guard driver rather than hardcoding a validation scheme.
|
|
34
|
+
*
|
|
35
|
+
* Dashboard forms are plain HTML POSTs (no client JS, no Authorization
|
|
36
|
+
* header) — the browser only ever sends the auth cookie set on login,
|
|
37
|
+
* so that's checked for the 'token' driver alongside a bearer-header
|
|
38
|
+
* fallback for JS/API callers.
|
|
39
|
+
*
|
|
40
|
+
* Owner membership wins over admin, which wins over any other active
|
|
41
|
+
* membership, when a user belongs to more than one team — the same
|
|
42
|
+
* precedence server-rendered dashboard pages use, so a user never sees
|
|
43
|
+
* a different "current team" between the page that rendered a form and
|
|
44
|
+
* the action that form posts to.
|
|
45
|
+
*
|
|
46
|
+
* Returns `null` when unauthenticated or without an active team
|
|
47
|
+
* membership — callers must treat that as "reject the request," not
|
|
48
|
+
* "fall back to a default team."
|
|
49
|
+
*/
|
|
50
|
+
export declare function resolveAuthenticatedMembership(request: TeamAuthRequest): Promise<TeamMembershipResult | null>;
|
|
51
|
+
/**
|
|
52
|
+
* Full team context for a server-rendered dashboard: the authenticated user,
|
|
53
|
+
* the resolved active team (honoring the workspace switcher), and the list of
|
|
54
|
+
* teams the user can switch between. One call replaces the per-page auth+team
|
|
55
|
+
* boilerplate every dashboard view used to copy, and centralizes the
|
|
56
|
+
* switch-aware scoping so pages and the actions they post to never disagree.
|
|
57
|
+
*
|
|
58
|
+
* `opts.allowAnyTeam(user)` lets an app grant an operator (e.g. a super-admin)
|
|
59
|
+
* access to every team — they can switch to, and see, teams they aren't a
|
|
60
|
+
* member of. The full user row is returned so the app can read its own
|
|
61
|
+
* columns (like a super-admin flag) without a second query.
|
|
62
|
+
*/
|
|
63
|
+
export declare function resolveTeamContext(request: TeamAuthRequest, opts?: { allowAnyTeam?: (user: any) => boolean }): Promise<TeamContext>;
|
|
64
|
+
/**
|
|
65
|
+
* Team id only — the common case for actions that just need to scope a
|
|
66
|
+
* write to the requester's team. See {@link resolveAuthenticatedMembership}
|
|
67
|
+
* when the caller also needs the role (e.g. owner/admin-only settings).
|
|
68
|
+
*/
|
|
69
|
+
export declare function resolveAuthenticatedTeamId(request: TeamAuthRequest): Promise<number | null>;
|
|
70
|
+
/**
|
|
71
|
+
* The authenticated USER (not team) from a request's real auth
|
|
72
|
+
* credential — bearer header first, then the login cookie, driver-aware
|
|
73
|
+
* like {@link resolveAuthenticatedMembership} (which builds on this).
|
|
74
|
+
* For dashboard form actions that operate on the requester themselves
|
|
75
|
+
* (security settings, profile) rather than on team-scoped rows: plain
|
|
76
|
+
* HTML POSTs carry no Authorization header, so `request.user()` (stamped
|
|
77
|
+
* by the auth middleware from a bearer/session) is undefined there and
|
|
78
|
+
* the login cookie is the only credential available.
|
|
79
|
+
*/
|
|
80
|
+
export declare function resolveAuthenticatedUser(request: TeamAuthRequest): Promise<{ id: number, email?: string } | undefined>;
|
|
81
|
+
/**
|
|
82
|
+
* Name of the cookie that remembers which team the user last switched the
|
|
83
|
+
* dashboard to (a workspace switcher). It is NOT a security credential: the
|
|
84
|
+
* server re-validates membership against `selectActiveTeam` on every request,
|
|
85
|
+
* so a tampered value can only ever resolve to a team the user already
|
|
86
|
+
* belongs to (or, for operators, any team when `allowAnyTeam` is set).
|
|
87
|
+
*/
|
|
88
|
+
export declare const ACTIVE_TEAM_COOKIE: 'active_team';
|
|
89
|
+
/**
|
|
90
|
+
* Team resolution from a request's real auth credential (bearer token or
|
|
91
|
+
* session cookie) — never from a client-supplied form field. Extracted
|
|
92
|
+
* from app-land (stacksjs/status config/auth-team.ts) because every app
|
|
93
|
+
* with team-scoped dashboard forms needs exactly this, and `config/` is
|
|
94
|
+
* for autoloaded config files, not shared helpers.
|
|
95
|
+
*
|
|
96
|
+
* Structural request type on purpose: callers pass whatever request
|
|
97
|
+
* object their action received. Partial objects (tests, non-HTTP
|
|
98
|
+
* callers) resolve to "unauthenticated" rather than crashing.
|
|
99
|
+
*/
|
|
100
|
+
export declare interface TeamAuthRequest {
|
|
101
|
+
bearerToken?: () => string | null | undefined
|
|
102
|
+
cookies?: { get: (name: string) => string | null | undefined }
|
|
103
|
+
}
|
|
104
|
+
export declare interface TeamMembershipResult {
|
|
105
|
+
teamId: number
|
|
106
|
+
role: string
|
|
107
|
+
}
|
|
108
|
+
/** A team the authenticated user may switch the dashboard to. */
|
|
109
|
+
export declare interface SwitchableTeam {
|
|
110
|
+
id: number
|
|
111
|
+
name: string
|
|
112
|
+
role: string
|
|
113
|
+
}
|
|
114
|
+
/** Full dashboard team context — see {@link resolveTeamContext}. */
|
|
115
|
+
export declare interface TeamContext {
|
|
116
|
+
user: any | null
|
|
117
|
+
teamId: number | null
|
|
118
|
+
role: string | null
|
|
119
|
+
teams: SwitchableTeam[]
|
|
120
|
+
activeTeamId: number | null
|
|
121
|
+
}
|
package/dist/src/tokens.d.ts
CHANGED
|
@@ -1,4 +1,33 @@
|
|
|
1
1
|
import type { AccessToken, CreateClientOptions, CreateClientResult, OAuthClient, PersonalAccessTokenResult, RefreshTokenResult, TokenScopes } from '@stacksjs/types';
|
|
2
|
+
/**
|
|
3
|
+
* Read `users.password_changed_at` for a user.
|
|
4
|
+
*
|
|
5
|
+
* Binds a token's validity to the account's credential state: a token
|
|
6
|
+
* issued before the user last changed their password is no longer
|
|
7
|
+
* trusted, regardless of its own `revoked`/`expires_at` flags. This is
|
|
8
|
+
* the durable, use-time backstop behind the post-reset revocation sweep
|
|
9
|
+
* (#1947) — even a freshly minted pair that the sweep never saw is
|
|
10
|
+
* rejected on first use.
|
|
11
|
+
*
|
|
12
|
+
* Returns `null` on ANY error (missing column / missing table) so a
|
|
13
|
+
* not-yet-migrated database degrades to legacy-allow rather than locking
|
|
14
|
+
* everyone out. Accepts an optional query runner so the refresh exchange
|
|
15
|
+
* can read the stamp inside its own transaction.
|
|
16
|
+
*/
|
|
17
|
+
export declare function getPasswordChangedAt(userId: unknown, q?: { unsafe: (sql: string, params?: any[]) => any }): Promise<Date | null>;
|
|
18
|
+
/**
|
|
19
|
+
* True when a credential issued at `createdAt` predates the user's last
|
|
20
|
+
* password change (`changedAt`) and must therefore be rejected.
|
|
21
|
+
*
|
|
22
|
+
* Legacy-allow semantics:
|
|
23
|
+
* - `changedAt` null (no stamp / un-migrated) => never reject.
|
|
24
|
+
* - `createdAt` missing/unparseable => never reject.
|
|
25
|
+
*
|
|
26
|
+
* Strict `<` so a token minted in the SAME second as (or after) the
|
|
27
|
+
* reset — e.g. the victim's immediate post-reset login — is NOT bricked
|
|
28
|
+
* by CURRENT_TIMESTAMP's one-second granularity.
|
|
29
|
+
*/
|
|
30
|
+
export declare function isIssuedBeforePasswordChange(createdAt: unknown, changedAt: Date | null): boolean;
|
|
2
31
|
/**
|
|
3
32
|
* Get all access tokens for a user
|
|
4
33
|
*
|
|
@@ -241,6 +270,8 @@ export declare function revokeClient(clientId: number): Promise<void>;
|
|
|
241
270
|
// HELPER FUNCTIONS
|
|
242
271
|
// ============================================================================
|
|
243
272
|
export declare function parseScopes(scopes: string | string[] | null | undefined): TokenScopes;
|
|
273
|
+
/** Cross-database SQL helpers */
|
|
274
|
+
declare const sql: unknown;
|
|
244
275
|
/**
|
|
245
276
|
* Alias for currentAccessToken
|
|
246
277
|
*
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads `two_factor_secret`/`two_factor_enabled` directly — these are
|
|
3
|
+
* guarantee-ALTER columns (see ensureUsersAuthColumns), not part of
|
|
4
|
+
* the User model's typed `attributes`, so callers query them the same
|
|
5
|
+
* way email-verification.ts reads email_verified_at: raw db access,
|
|
6
|
+
* not the ORM model.
|
|
7
|
+
*/
|
|
8
|
+
export declare function getTwoFactorState(userId: number): Promise<{ secret: string | null, enabled: boolean }>;
|
|
9
|
+
export declare function isTwoFactorEnabled(user: TwoFactorUser): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Generate a new (unpersisted) secret + otpauth:// URI for setup.
|
|
12
|
+
*/
|
|
13
|
+
export declare function generateTwoFactorSetup(email: string, serviceName?: string): { secret: string, uri: string };
|
|
14
|
+
/**
|
|
15
|
+
* Stash a freshly generated secret server-side while the user goes
|
|
16
|
+
* scan/enter it into their authenticator app. Single pending secret
|
|
17
|
+
* per user — generating a new one invalidates any prior unconfirmed
|
|
18
|
+
* attempt, same delete-then-insert shape as storeWebAuthnChallenge.
|
|
19
|
+
*/
|
|
20
|
+
export declare function stashPendingTwoFactorSecret(userId: number, secret: string, ttlSeconds?: number): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Consume (delete-on-read) the pending secret stashed for a user, or
|
|
23
|
+
* null if none exists / it expired.
|
|
24
|
+
*/
|
|
25
|
+
export declare function consumePendingTwoFactorSecret(userId: number): Promise<string | null>;
|
|
26
|
+
/**
|
|
27
|
+
* Verify the setup code against the not-yet-persisted secret and, if
|
|
28
|
+
* valid, persist it + flip `two_factor_enabled` on.
|
|
29
|
+
*/
|
|
30
|
+
export declare function enableTwoFactor(userId: number, secret: string, code: string): Promise<boolean>;
|
|
31
|
+
export declare function disableTwoFactor(userId: number): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Verify a live login/dashboard-reauth code against the user's
|
|
34
|
+
* already-persisted secret.
|
|
35
|
+
*/
|
|
36
|
+
export declare function verifyTwoFactorLoginCode(userId: number, code: string): Promise<boolean>;
|
|
37
|
+
/**
|
|
38
|
+
* Create a single-use, short-lived login challenge for a user whose
|
|
39
|
+
* password just verified but who still needs to supply a TOTP code.
|
|
40
|
+
* Mirrors storeWebAuthnChallenge's delete-then-insert shape, keyed by
|
|
41
|
+
* an opaque random id instead of (user_id, purpose) since a user can
|
|
42
|
+
* only have one login attempt in flight that matters here.
|
|
43
|
+
*/
|
|
44
|
+
export declare function createTwoFactorChallenge(userId: number, ttlSeconds?: number): Promise<string>;
|
|
45
|
+
/**
|
|
46
|
+
* Consume (delete-on-read) a login challenge and return the user id it
|
|
47
|
+
* was issued for, or null if missing/expired.
|
|
48
|
+
*/
|
|
49
|
+
export declare function consumeTwoFactorChallenge(challengeToken: string): Promise<number | null>;
|
|
50
|
+
export declare const TwoFactor: {
|
|
51
|
+
isEnabled: unknown;
|
|
52
|
+
getState: unknown;
|
|
53
|
+
generateSetup: unknown;
|
|
54
|
+
stashPendingSecret: unknown;
|
|
55
|
+
consumePendingSecret: unknown;
|
|
56
|
+
enable: unknown;
|
|
57
|
+
disable: unknown;
|
|
58
|
+
verifyLoginCode: unknown;
|
|
59
|
+
createChallenge: unknown;
|
|
60
|
+
consumeChallenge: unknown
|
|
61
|
+
};
|
|
62
|
+
export declare interface TwoFactorUser {
|
|
63
|
+
id: number
|
|
64
|
+
email?: string
|
|
65
|
+
two_factor_secret?: string | null
|
|
66
|
+
two_factor_enabled?: boolean | number | null
|
|
67
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/auth",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.70.
|
|
4
|
+
"version": "0.70.53",
|
|
5
5
|
"description": "A more simplistic way to authenticate.",
|
|
6
6
|
"author": "Chris Breuer",
|
|
7
7
|
"contributors": [
|
|
@@ -46,11 +46,11 @@
|
|
|
46
46
|
"prepublishOnly": "bun run build"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@stacksjs/ts-auth": "^0.4.
|
|
49
|
+
"@stacksjs/ts-auth": "^0.4.3"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"better-dx": "^0.2.12",
|
|
53
|
-
"@stacksjs/error-handling": "
|
|
54
|
-
"@stacksjs/router": "
|
|
53
|
+
"@stacksjs/error-handling": "0.70.53",
|
|
54
|
+
"@stacksjs/router": "0.70.53"
|
|
55
55
|
}
|
|
56
56
|
}
|