@stacksjs/defaults 0.70.294 → 0.70.297
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/ai/skills/stacks-auth/SKILL.md +1 -1
- package/ai/skills/stacks-development/SKILL.md +3 -3
- package/ai/skills/stacks-stx/SKILL.md +3 -3
- package/ai/skills/stacks-ui/SKILL.md +3 -3
- package/app/Actions/Auth/LoginAction.ts +8 -2
- package/app/Actions/Auth/RegisterAction.ts +3 -2
- package/app/Actions/Auth/SocialCallbackAction.ts +75 -0
- package/app/Actions/Auth/SocialRedirectAction.ts +37 -0
- package/app/Actions/Dashboard/Analytics/request-analytics.ts +6 -1
- package/app/Actions/Dashboard/Commerce/CommercePosCheckoutAction.ts +7 -0
- package/app/Actions/Dashboard/Commerce/commerce-product-records.ts +3 -1
- package/app/Actions/Dashboard/Commerce/commerce-record.ts +13 -0
- package/app/Actions/Dashboard/Infrastructure/InsightsAction.ts +3 -3
- package/app/Actions/Dashboard/Jobs/job-records.ts +5 -1
- package/app/Actions/Dashboard/Kanban/BoardShowAction.ts +9 -7
- package/app/Actions/Dashboard/Kanban/BoardStoreAction.ts +1 -1
- package/app/Actions/Dashboard/Kanban/BoardsIndexAction.ts +6 -4
- package/app/Actions/Dashboard/Kanban/CardShowAction.ts +4 -2
- package/app/Actions/Dashboard/Library/GetAverageReleaseTime.ts +1 -1
- package/app/Actions/Dashboard/Marketing/CampaignIndexAction.ts +2 -2
- package/app/Actions/Dashboard/Marketing/ListIndexAction.ts +1 -1
- package/app/Actions/Dashboard/Queries/query-dashboard.ts +5 -3
- package/app/Actions/Password/PasswordResetAction.ts +29 -7
- package/app/Middleware/Auth.ts +4 -2
- package/app/Middleware/Can.ts +58 -5
- package/app/Models/SocialAccount.ts +70 -0
- package/app/Models/User.ts +7 -4
- package/app/Models/commerce/DeliveryRoute.ts +34 -0
- package/app/Models/commerce/DeliveryStop.ts +166 -0
- package/app/Models/commerce/Driver.ts +72 -2
- package/app/Models/commerce/DriverPing.ts +103 -0
- package/app/Models/commerce/Order.ts +44 -3
- package/app/password-policy.ts +46 -0
- package/bootstrap.ts +67 -19
- package/ide/vscode/package.json +1 -1
- package/package.json +1 -1
- package/routes/auth.ts +83 -0
- package/routes/dashboard.ts +0 -61
- package/routes/socials.ts +32 -0
- package/vcs/github/CONTRIBUTING.md +1 -1
- package/vcs/github/workflows/README.md +8 -9
- package/vcs/github/workflows/release.yml +4 -181
- package/vcs/github/workflows/export-size.yml +0 -25
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One password policy, in one place (stacksjs/stacks#2226).
|
|
3
|
+
*
|
|
4
|
+
* The framework shipped three numbers for one product concept: `RegisterAction`
|
|
5
|
+
* and `LoginAction` accepted six characters, the `User` model declared six, and
|
|
6
|
+
* `PasswordResetAction` hand-checked eight with `password.length < 8` — not in a
|
|
7
|
+
* `validations:` block at all, so it could not even be read by the machinery
|
|
8
|
+
* that reads the others.
|
|
9
|
+
*
|
|
10
|
+
* Apps then retyped the rule again in the browser, because an Action's
|
|
11
|
+
* `validations:` was unreachable from a template. That is how a real app ended
|
|
12
|
+
* up refusing a 7-character password in the browser that `POST /register` would
|
|
13
|
+
* have accepted.
|
|
14
|
+
*
|
|
15
|
+
* Change the policy here and every declaration follows. An app that wants a
|
|
16
|
+
* different one edits this file, which is scaffolded into it.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Minimum length for a NEW password.
|
|
21
|
+
*
|
|
22
|
+
* Eight, because that is what the password-reset path already enforced and it
|
|
23
|
+
* is the weaker of the two that was actually protecting anything. Raising the
|
|
24
|
+
* registration minimum only affects accounts created from now on.
|
|
25
|
+
*/
|
|
26
|
+
export const PASSWORD_MIN_LENGTH = 8
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Maximum length. Also the `varchar` width the User model's column derives
|
|
30
|
+
* from, so lowering it is a migration, not just a rule change.
|
|
31
|
+
*/
|
|
32
|
+
export const PASSWORD_MAX_LENGTH = 255
|
|
33
|
+
|
|
34
|
+
export const PASSWORD_POLICY_MESSAGE
|
|
35
|
+
= `Password must be between ${PASSWORD_MIN_LENGTH} and ${PASSWORD_MAX_LENGTH} characters.`
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* What a sign-in must require: that a password was supplied, and nothing more.
|
|
39
|
+
*
|
|
40
|
+
* Deliberately NOT the policy above. Applying a creation rule to authentication
|
|
41
|
+
* locks out every account created under a previous, shorter policy — they would
|
|
42
|
+
* get a 422 before their credentials were ever checked, with a message telling
|
|
43
|
+
* them their own password is too short. The policy belongs on the paths that
|
|
44
|
+
* SET a password.
|
|
45
|
+
*/
|
|
46
|
+
export const PASSWORD_PRESENCE_MESSAGE = 'Password is required.'
|
package/bootstrap.ts
CHANGED
|
@@ -76,26 +76,58 @@ route.use(MaintenanceMiddleware.toRouterHandler() as any)
|
|
|
76
76
|
// Overridable by registering the same path in app routes first.
|
|
77
77
|
await route.register(frameworkPath('defaults/routes/core.ts'))
|
|
78
78
|
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
79
|
+
// Which default route bundles this app mounts. The route loader resolves the
|
|
80
|
+
// selection (STACKS_DEFAULT_ROUTES, or the legacy STACKS_SKIP_DEFAULT_ROUTES)
|
|
81
|
+
// and leaves it here; see `resolveDefaultRouteBundles` in
|
|
82
|
+
// `core/router/src/route-loader.ts`. Absent when bootstrap is imported by
|
|
83
|
+
// something other than the loader, in which case every bundle is eligible and
|
|
84
|
+
// the feature gates below decide, exactly as before.
|
|
85
|
+
const selection = (globalThis as Record<string, unknown>).__stacksDefaultRouteBundles as
|
|
86
|
+
{ bundles: Set<string>, explicit: boolean } | undefined
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Whether a bundle mounts.
|
|
90
|
+
*
|
|
91
|
+
* An app that NAMED its bundles has already answered the question, so the
|
|
92
|
+
* feature flag does not get a second veto - otherwise `STACKS_DEFAULT_ROUTES=auth`
|
|
93
|
+
* would still be withheld from an app running with `dashboard` off, which is
|
|
94
|
+
* the exact case this exists for. When nothing was named, the flags gate
|
|
95
|
+
* precisely as they did before.
|
|
96
|
+
*/
|
|
97
|
+
function mounts(bundle: string, featureEnabled: boolean): boolean {
|
|
98
|
+
if (selection && !selection.bundles.has(bundle))
|
|
99
|
+
return false
|
|
100
|
+
return selection?.explicit ? true : featureEnabled
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Auth: login, registration, logout, refresh/revoke, passkeys, TOTP 2FA and
|
|
104
|
+
// password reset. Split out of dashboard.ts so it can be mounted on its own
|
|
105
|
+
// (stacksjs/stacks#2229) — previously the only way to get `/login` was to
|
|
106
|
+
// activate `dashboard` and take the storefront, reviews, AI and voice surface
|
|
107
|
+
// with it.
|
|
108
|
+
//
|
|
109
|
+
// Registered BEFORE dashboard.ts, which is where these lived, so the
|
|
110
|
+
// first-registration-wins order among framework routes is unchanged.
|
|
85
111
|
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
112
|
+
// Deliberately NOT gated on `feature('auth')`, despite the issue asking for
|
|
113
|
+
// it: `config/auth.ts` ships in every app with `enabled: true`, so that gate
|
|
114
|
+
// is true everywhere and would mount the auth surface — including
|
|
115
|
+
// `/generate-two-factor-secret`, `/logout-all` and `/auth/tokens` — in apps
|
|
116
|
+
// currently running with `dashboard` off. Widening an app's public surface on
|
|
117
|
+
// upgrade is not something a refactor gets to do silently.
|
|
118
|
+
if (mounts('auth', feature('dashboard')))
|
|
119
|
+
await route.register(frameworkPath('defaults/routes/auth.ts'))
|
|
120
|
+
|
|
121
|
+
// The rest of dashboard.ts: email subscribe, storefront cart/checkout,
|
|
122
|
+
// reviews, sitemap, AI, voice, and the admin dashboard's REST surface. Still
|
|
123
|
+
// one file and still one gate — splitting auth out was the case with a
|
|
124
|
+
// reporter behind it; marketing.ts / commerce.ts / monitoring.ts remain the
|
|
125
|
+
// obvious next cuts.
|
|
95
126
|
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
|
|
127
|
+
// Apps that need only a slice can also define the routes they want directly
|
|
128
|
+
// in `routes/api.ts` — first-registration-wins means the user version takes
|
|
129
|
+
// priority.
|
|
130
|
+
if (mounts('dashboard', feature('dashboard'))) {
|
|
99
131
|
await route.register(frameworkPath('defaults/routes/dashboard.ts'))
|
|
100
132
|
// JSON endpoints for the dev dashboard UI. Kept separate from the view
|
|
101
133
|
// routes above so the data layer is one obvious file to grep.
|
|
@@ -112,6 +144,22 @@ if (feature('dashboard')) {
|
|
|
112
144
|
// non-default mount path register their own routes in `routes/api.ts`
|
|
113
145
|
// and the framework's mount silently no-ops since user routes
|
|
114
146
|
// register first.
|
|
115
|
-
if (feature('email')) {
|
|
147
|
+
if (mounts('email', feature('email'))) {
|
|
116
148
|
await route.register(frameworkPath('defaults/routes/email.ts'))
|
|
117
149
|
}
|
|
150
|
+
|
|
151
|
+
// Social sign-in: `/auth/{provider}` + `/auth/{provider}/callback`
|
|
152
|
+
// (stacksjs/stacks#2276). An opt-in bundle, NOT part of the implicit default
|
|
153
|
+
// set or `all` — OAuth callback URLs in an app that configured no provider
|
|
154
|
+
// are surface for nothing. So `mounts()` does not apply here: an app that
|
|
155
|
+
// NAMED its bundles decides outright (`social` listed → on, absent → off),
|
|
156
|
+
// and an app that said nothing gets it exactly when a provider is actually
|
|
157
|
+
// configured in config/services.ts — configuring GitHub and finding
|
|
158
|
+
// /auth/github dead would be the puzzle, not the mount.
|
|
159
|
+
const { configuredSocialProviders } = await import('@stacksjs/socials')
|
|
160
|
+
const mountSocial = selection?.explicit
|
|
161
|
+
? selection.bundles.has('social')
|
|
162
|
+
: configuredSocialProviders().length > 0
|
|
163
|
+
if (mountSocial) {
|
|
164
|
+
await route.register(frameworkPath('defaults/routes/socials.ts'))
|
|
165
|
+
}
|
package/ide/vscode/package.json
CHANGED
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/defaults",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.70.
|
|
5
|
+
"version": "0.70.297",
|
|
6
6
|
"description": "The complete managed Stacks application scaffold, including runtime defaults, AI guidance, editor metadata, and npm-backed project support files.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"license": "MIT",
|
package/routes/auth.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework Default Routes - Auth
|
|
3
|
+
*
|
|
4
|
+
* Login, registration, logout, token refresh and revocation, passkeys, TOTP
|
|
5
|
+
* 2FA, and password reset.
|
|
6
|
+
*
|
|
7
|
+
* Split out of `dashboard.ts` for stacksjs/stacks#2229. That file bundles the
|
|
8
|
+
* auth surface together with storefront cart/checkout, reviews, sitemap, AI
|
|
9
|
+
* and voice, and the only gate over the whole thing is `feature('dashboard')`
|
|
10
|
+
* - so an app that wanted `/login` and 2FA but ships no `Product` or `Coupon`
|
|
11
|
+
* had to either mount the commerce demo surface or set
|
|
12
|
+
* `STACKS_SKIP_DEFAULT_ROUTES=1` and re-declare every auth route by hand, rate
|
|
13
|
+
* limits included. One reporting app did the latter and permanently gave up
|
|
14
|
+
* 2FA, sign-out-everywhere and API token management, because re-registering
|
|
15
|
+
* those was not worth the maintenance.
|
|
16
|
+
*
|
|
17
|
+
* Mounted by `defaults/bootstrap.ts` as the `auth` bundle; see
|
|
18
|
+
* `resolveDefaultRouteBundles` in `core/router/src/route-loader.ts` for how an
|
|
19
|
+
* app selects bundles.
|
|
20
|
+
*
|
|
21
|
+
* Users should NOT edit this file. To override any route here, define the same
|
|
22
|
+
* method + path in your `routes/api.ts`: bun-router is first-registration-wins
|
|
23
|
+
* and user routes load first, so your handler always takes priority.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { route } from '@stacksjs/router'
|
|
27
|
+
|
|
28
|
+
// Rate limits on token-issuance + password-reset endpoints
|
|
29
|
+
// (stacksjs/stacks#1921). `Auth.attempt()` already has a per-email
|
|
30
|
+
// lockout but it doesn't stop credential-stuffing across many emails
|
|
31
|
+
// from one IP, and the token endpoints have no upstream brake at all
|
|
32
|
+
// — a leaked refresh token could be hammered for unlimited access
|
|
33
|
+
// tokens until the row TTL. Userland that overrides any of these in
|
|
34
|
+
// `routes/api.ts` (user routes win) gets to pick its own limits.
|
|
35
|
+
route.post('/login', 'Actions/Auth/LoginAction').rateLimit(5, 'minute')
|
|
36
|
+
route.post('/register', 'Actions/Auth/RegisterAction').rateLimit(3, 'minute')
|
|
37
|
+
// Passkey ENROLLMENT (attaching a new credential to an account) must be
|
|
38
|
+
// auth-gated — it's not a login flow, it's a logged-in user adding a
|
|
39
|
+
// second factor to their own account. Previously unauthenticated and
|
|
40
|
+
// keyed off a client-supplied `email` field: anyone who knew a victim's
|
|
41
|
+
// email could register a passkey against that account and log in as
|
|
42
|
+
// them, no password required. GenerateRegistrationAction/
|
|
43
|
+
// VerifyRegistrationAction now derive identity from request.user().
|
|
44
|
+
route.get('/generate-registration-options', 'Actions/Auth/GenerateRegistrationAction').middleware('auth').rateLimit(10, 'minute')
|
|
45
|
+
route.post('/verify-registration', 'Actions/Auth/VerifyRegistrationAction').middleware('auth').rateLimit(5, 'minute')
|
|
46
|
+
// Passkey AUTHENTICATION (logging in) is correctly unauthenticated —
|
|
47
|
+
// the caller doesn't have a session yet, that's the point.
|
|
48
|
+
route.get('/generate-authentication-options', 'Actions/Auth/GenerateAuthenticationAction').rateLimit(10, 'minute')
|
|
49
|
+
route.get('/verify-authentication', 'Actions/Auth/VerifyAuthenticationAction').rateLimit(10, 'minute')
|
|
50
|
+
|
|
51
|
+
// TOTP 2FA. Setup/enable/disable act on the caller's own authenticated
|
|
52
|
+
// account (auth-gated, same identity rule as passkey enrollment above).
|
|
53
|
+
// verify-two-factor-login is the second step of LoginAction's flow and
|
|
54
|
+
// is correctly unauthenticated — the caller only has a short-lived
|
|
55
|
+
// challenge token at that point, not a session yet.
|
|
56
|
+
route.post('/generate-two-factor-secret', 'Actions/Auth/GenerateTwoFactorSecretAction').middleware('auth').rateLimit(10, 'minute')
|
|
57
|
+
route.post('/enable-two-factor', 'Actions/Auth/EnableTwoFactorAction').middleware('auth').rateLimit(10, 'minute')
|
|
58
|
+
route.post('/disable-two-factor', 'Actions/Auth/DisableTwoFactorAction').middleware('auth').rateLimit(10, 'minute')
|
|
59
|
+
route.post('/verify-two-factor-login', 'Actions/Auth/VerifyTwoFactorLoginAction').rateLimit(10, 'minute')
|
|
60
|
+
|
|
61
|
+
route.group({ prefix: '/auth' }, () => {
|
|
62
|
+
route.post('/refresh', 'Actions/Auth/RefreshTokenAction').rateLimit(10, 'minute')
|
|
63
|
+
route.get('/tokens', 'Actions/Auth/ListTokensAction').middleware('auth')
|
|
64
|
+
route.post('/token', 'Actions/Auth/CreateTokenAction').middleware('auth').rateLimit(10, 'minute')
|
|
65
|
+
route.delete('/tokens/{id}', 'Actions/Auth/RevokeTokenAction').middleware('auth')
|
|
66
|
+
route.get('/abilities', 'Actions/Auth/TestAbilitiesAction').middleware('auth')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
route.group({ middleware: 'auth' }, () => {
|
|
70
|
+
route.get('/me', 'Actions/Auth/AuthUserAction')
|
|
71
|
+
route.post('/logout', 'Actions/Auth/LogoutAction')
|
|
72
|
+
// Sign out everywhere: revoke every access/refresh token AND destroy
|
|
73
|
+
// every session for the authenticated user (stacksjs/stacks#1957).
|
|
74
|
+
route.post('/logout-all', 'Actions/Auth/LogoutAllAction')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
// Password Reset. `/forgot` triggers a mailer hop so it's the most
|
|
78
|
+
// abuse-prone — keep that tighter than the verification endpoints.
|
|
79
|
+
route.group({ prefix: '/password' }, () => {
|
|
80
|
+
route.post('/forgot', 'Actions/Password/SendPasswordResetEmailAction').rateLimit(3, 'minute')
|
|
81
|
+
route.post('/reset', 'Actions/Password/PasswordResetAction').rateLimit(5, 'minute')
|
|
82
|
+
route.post('/verify-token', 'Actions/Password/VerifyResetTokenAction').rateLimit(10, 'minute')
|
|
83
|
+
})
|
package/routes/dashboard.ts
CHANGED
|
@@ -18,67 +18,6 @@
|
|
|
18
18
|
import process from 'node:process'
|
|
19
19
|
import { response, route } from '@stacksjs/router'
|
|
20
20
|
|
|
21
|
-
// ============================================================================
|
|
22
|
-
// Auth Routes
|
|
23
|
-
// ============================================================================
|
|
24
|
-
|
|
25
|
-
// Rate limits on token-issuance + password-reset endpoints
|
|
26
|
-
// (stacksjs/stacks#1921). `Auth.attempt()` already has a per-email
|
|
27
|
-
// lockout but it doesn't stop credential-stuffing across many emails
|
|
28
|
-
// from one IP, and the token endpoints have no upstream brake at all
|
|
29
|
-
// — a leaked refresh token could be hammered for unlimited access
|
|
30
|
-
// tokens until the row TTL. Userland that overrides any of these in
|
|
31
|
-
// `routes/api.ts` (user routes win) gets to pick its own limits.
|
|
32
|
-
route.post('/login', 'Actions/Auth/LoginAction').rateLimit(5, 'minute')
|
|
33
|
-
route.post('/register', 'Actions/Auth/RegisterAction').rateLimit(3, 'minute')
|
|
34
|
-
// Passkey ENROLLMENT (attaching a new credential to an account) must be
|
|
35
|
-
// auth-gated — it's not a login flow, it's a logged-in user adding a
|
|
36
|
-
// second factor to their own account. Previously unauthenticated and
|
|
37
|
-
// keyed off a client-supplied `email` field: anyone who knew a victim's
|
|
38
|
-
// email could register a passkey against that account and log in as
|
|
39
|
-
// them, no password required. GenerateRegistrationAction/
|
|
40
|
-
// VerifyRegistrationAction now derive identity from request.user().
|
|
41
|
-
route.get('/generate-registration-options', 'Actions/Auth/GenerateRegistrationAction').middleware('auth').rateLimit(10, 'minute')
|
|
42
|
-
route.post('/verify-registration', 'Actions/Auth/VerifyRegistrationAction').middleware('auth').rateLimit(5, 'minute')
|
|
43
|
-
// Passkey AUTHENTICATION (logging in) is correctly unauthenticated —
|
|
44
|
-
// the caller doesn't have a session yet, that's the point.
|
|
45
|
-
route.get('/generate-authentication-options', 'Actions/Auth/GenerateAuthenticationAction').rateLimit(10, 'minute')
|
|
46
|
-
route.get('/verify-authentication', 'Actions/Auth/VerifyAuthenticationAction').rateLimit(10, 'minute')
|
|
47
|
-
|
|
48
|
-
// TOTP 2FA. Setup/enable/disable act on the caller's own authenticated
|
|
49
|
-
// account (auth-gated, same identity rule as passkey enrollment above).
|
|
50
|
-
// verify-two-factor-login is the second step of LoginAction's flow and
|
|
51
|
-
// is correctly unauthenticated — the caller only has a short-lived
|
|
52
|
-
// challenge token at that point, not a session yet.
|
|
53
|
-
route.post('/generate-two-factor-secret', 'Actions/Auth/GenerateTwoFactorSecretAction').middleware('auth').rateLimit(10, 'minute')
|
|
54
|
-
route.post('/enable-two-factor', 'Actions/Auth/EnableTwoFactorAction').middleware('auth').rateLimit(10, 'minute')
|
|
55
|
-
route.post('/disable-two-factor', 'Actions/Auth/DisableTwoFactorAction').middleware('auth').rateLimit(10, 'minute')
|
|
56
|
-
route.post('/verify-two-factor-login', 'Actions/Auth/VerifyTwoFactorLoginAction').rateLimit(10, 'minute')
|
|
57
|
-
|
|
58
|
-
route.group({ prefix: '/auth' }, () => {
|
|
59
|
-
route.post('/refresh', 'Actions/Auth/RefreshTokenAction').rateLimit(10, 'minute')
|
|
60
|
-
route.get('/tokens', 'Actions/Auth/ListTokensAction').middleware('auth')
|
|
61
|
-
route.post('/token', 'Actions/Auth/CreateTokenAction').middleware('auth').rateLimit(10, 'minute')
|
|
62
|
-
route.delete('/tokens/{id}', 'Actions/Auth/RevokeTokenAction').middleware('auth')
|
|
63
|
-
route.get('/abilities', 'Actions/Auth/TestAbilitiesAction').middleware('auth')
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
route.group({ middleware: 'auth' }, () => {
|
|
67
|
-
route.get('/me', 'Actions/Auth/AuthUserAction')
|
|
68
|
-
route.post('/logout', 'Actions/Auth/LogoutAction')
|
|
69
|
-
// Sign out everywhere: revoke every access/refresh token AND destroy
|
|
70
|
-
// every session for the authenticated user (stacksjs/stacks#1957).
|
|
71
|
-
route.post('/logout-all', 'Actions/Auth/LogoutAllAction')
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
// Password Reset. `/forgot` triggers a mailer hop so it's the most
|
|
75
|
-
// abuse-prone — keep that tighter than the verification endpoints.
|
|
76
|
-
route.group({ prefix: '/password' }, () => {
|
|
77
|
-
route.post('/forgot', 'Actions/Password/SendPasswordResetEmailAction').rateLimit(3, 'minute')
|
|
78
|
-
route.post('/reset', 'Actions/Password/PasswordResetAction').rateLimit(5, 'minute')
|
|
79
|
-
route.post('/verify-token', 'Actions/Password/VerifyResetTokenAction').rateLimit(10, 'minute')
|
|
80
|
-
})
|
|
81
|
-
|
|
82
21
|
// ============================================================================
|
|
83
22
|
// Email
|
|
84
23
|
// ============================================================================
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework Default Routes - Social sign-in (stacksjs/stacks#2276)
|
|
3
|
+
*
|
|
4
|
+
* `GET /auth/{provider}` sends the browser to the provider's consent page;
|
|
5
|
+
* `/auth/{provider}/callback` completes the exchange, resolves the local user
|
|
6
|
+
* through the find-or-create policy in `@stacksjs/auth` (with its
|
|
7
|
+
* unverified-email takeover guard), and hands the session to the browser.
|
|
8
|
+
*
|
|
9
|
+
* Mounted by `defaults/bootstrap.ts` as the `social` bundle. Unlike the other
|
|
10
|
+
* bundles it is NOT part of the implicit default set: it mounts only when the
|
|
11
|
+
* app names it in `STACKS_DEFAULT_ROUTES`, or when at least one provider is
|
|
12
|
+
* actually configured in `config/services.ts` — a callback URL for a provider
|
|
13
|
+
* nobody configured is surface for nothing. The actions themselves 404 for
|
|
14
|
+
* unconfigured provider names either way.
|
|
15
|
+
*
|
|
16
|
+
* The callback is registered for POST as well as GET because Apple mandates
|
|
17
|
+
* `response_mode=form_post` whenever scopes are requested — a GET-only
|
|
18
|
+
* callback answers Apple's redirect with a 404.
|
|
19
|
+
*
|
|
20
|
+
* Users should NOT edit this file. To override any route here, define the
|
|
21
|
+
* same method + path in your `routes/api.ts`: user routes load first and
|
|
22
|
+
* first registration wins.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { route } from '@stacksjs/router'
|
|
26
|
+
|
|
27
|
+
// Rate-limited like the other token-issuance endpoints (#1921): the callback
|
|
28
|
+
// mints a session, and the redirect endpoint is a cheap way to hammer a
|
|
29
|
+
// provider's authorize page with this app's client id.
|
|
30
|
+
route.get('/auth/{provider}', 'Actions/Auth/SocialRedirectAction').rateLimit(10, 'minute')
|
|
31
|
+
route.get('/auth/{provider}/callback', 'Actions/Auth/SocialCallbackAction').rateLimit(10, 'minute')
|
|
32
|
+
route.post('/auth/{provider}/callback', 'Actions/Auth/SocialCallbackAction').rateLimit(10, 'minute')
|
|
@@ -147,7 +147,7 @@ buddy test:types # runs typecheck
|
|
|
147
147
|
|
|
148
148
|
## 🧪 Testing
|
|
149
149
|
|
|
150
|
-
All of
|
|
150
|
+
All of this project's tests are stored within the `./tests` project folder. When adding or updating functionality, please ensure it is covered through our test suite. Ensure so by running `buddy test`.
|
|
151
151
|
|
|
152
152
|
When working on an individual Stack, tests are stored within the `./tests` project folder & it is recommended to write tests (when useful). Bu
|
|
153
153
|
|
|
@@ -2,15 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
This folder contains the following GitHub Actions:
|
|
4
4
|
|
|
5
|
-
- [CI][CI]
|
|
5
|
+
- [CI][CI] — all CI jobs for the project
|
|
6
6
|
- lints the code
|
|
7
7
|
- `typecheck`s the code
|
|
8
|
-
-
|
|
9
|
-
- runs
|
|
10
|
-
|
|
11
|
-
- [
|
|
12
|
-
- [Lock Closed Issues][Lock Closed Issues] - Locks all closed issues after 14 days of being closed
|
|
8
|
+
- runs the test suite
|
|
9
|
+
- runs on `ubuntu-latest`
|
|
10
|
+
- [Release][Release] — on a `v*` tag, generates the changelog and creates the GitHub release
|
|
11
|
+
- [Labeler][Labeler] — labels pull requests from `.github/labeler.yml`
|
|
13
12
|
|
|
14
|
-
[CI]: ./
|
|
15
|
-
[Release]: ./
|
|
16
|
-
[
|
|
13
|
+
[CI]: ./ci.yml
|
|
14
|
+
[Release]: ./release.yml
|
|
15
|
+
[Labeler]: ./labeler.yml
|
|
@@ -7,18 +7,17 @@ on:
|
|
|
7
7
|
|
|
8
8
|
permissions:
|
|
9
9
|
contents: write
|
|
10
|
-
packages: write
|
|
11
|
-
id-token: write
|
|
12
10
|
|
|
13
11
|
jobs:
|
|
14
|
-
|
|
12
|
+
release:
|
|
15
13
|
runs-on: ubuntu-latest
|
|
16
|
-
|
|
17
|
-
version: ${{ env.VERSION }}
|
|
14
|
+
|
|
18
15
|
steps:
|
|
19
16
|
- name: Checkout Code
|
|
20
17
|
uses: actions/checkout@v6
|
|
21
18
|
with:
|
|
19
|
+
# The changelog is generated from commits since the previous tag, so
|
|
20
|
+
# the full history has to be present.
|
|
22
21
|
fetch-depth: 0
|
|
23
22
|
|
|
24
23
|
- name: Setup Pantry
|
|
@@ -37,183 +36,7 @@ jobs:
|
|
|
37
36
|
- name: Install Dependencies
|
|
38
37
|
run: bun install
|
|
39
38
|
|
|
40
|
-
- name: Publish Framework
|
|
41
|
-
run: ./storage/framework/scripts/publish
|
|
42
|
-
env:
|
|
43
|
-
BUN_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
|
44
|
-
|
|
45
|
-
- name: Publish Dummy Libraries
|
|
46
|
-
run: ./storage/framework/scripts/publish-dummy-libs
|
|
47
|
-
env:
|
|
48
|
-
BUN_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
|
49
|
-
|
|
50
|
-
- name: Publish VS Code Extension
|
|
51
|
-
continue-on-error: true
|
|
52
|
-
run: |
|
|
53
|
-
cd storage/framework/defaults/ide/vscode
|
|
54
|
-
bun install
|
|
55
|
-
bunx --bun vsce publish --no-dependencies
|
|
56
|
-
cd ../../../../
|
|
57
|
-
env:
|
|
58
|
-
VSCE_PAT: ${{ secrets.VSCE_PAT }}
|
|
59
|
-
|
|
60
|
-
- name: Extract tag version
|
|
61
|
-
id: get_version
|
|
62
|
-
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
|
63
|
-
|
|
64
|
-
- name: Build Buddy Release Assets
|
|
65
|
-
run: |
|
|
66
|
-
cd storage/framework/core/buddy
|
|
67
|
-
bun run compile:all
|
|
68
|
-
bun run zip:all
|
|
69
|
-
|
|
70
39
|
- name: Create GitHub Release
|
|
71
40
|
uses: stacksjs/action-releaser@v1.2.9
|
|
72
|
-
with:
|
|
73
|
-
files: |
|
|
74
|
-
storage/framework/core/buddy/bin/buddy-linux-x64
|
|
75
|
-
storage/framework/core/buddy/bin/buddy-linux-arm64
|
|
76
|
-
storage/framework/core/buddy/bin/buddy-windows-x64.exe
|
|
77
|
-
storage/framework/core/buddy/bin/buddy-darwin-x64
|
|
78
|
-
storage/framework/core/buddy/bin/buddy-darwin-arm64
|
|
79
|
-
storage/framework/core/buddy/bin/buddy-linux-x64.zip
|
|
80
|
-
storage/framework/core/buddy/bin/buddy-linux-arm64.zip
|
|
81
|
-
storage/framework/core/buddy/bin/buddy-windows-x64.zip
|
|
82
|
-
storage/framework/core/buddy/bin/buddy-darwin-x64.zip
|
|
83
|
-
storage/framework/core/buddy/bin/buddy-darwin-arm64.zip
|
|
84
41
|
env:
|
|
85
42
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
86
|
-
|
|
87
|
-
homebrew:
|
|
88
|
-
needs: npm
|
|
89
|
-
runs-on: macos-latest
|
|
90
|
-
steps:
|
|
91
|
-
- name: Checkout Repository
|
|
92
|
-
uses: actions/checkout@v6
|
|
93
|
-
with:
|
|
94
|
-
fetch-depth: 0
|
|
95
|
-
|
|
96
|
-
- name: Debug Tag
|
|
97
|
-
run: |
|
|
98
|
-
echo "Tag: ${GITHUB_REF#refs/tags/}"
|
|
99
|
-
echo "Version from previous job: ${{ needs.release.outputs.version }}"
|
|
100
|
-
|
|
101
|
-
- name: Generate GitHub App Token
|
|
102
|
-
id: generate-token
|
|
103
|
-
uses: actions/create-github-app-token@v2
|
|
104
|
-
with:
|
|
105
|
-
app-id: ${{ secrets.GH_APP_ID }}
|
|
106
|
-
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
|
107
|
-
|
|
108
|
-
- name: Setup GitHub CLI with PAT
|
|
109
|
-
run: |
|
|
110
|
-
echo "${{ secrets.GH_PAT }}" | gh auth login --with-token
|
|
111
|
-
|
|
112
|
-
- name: Ensure Homebrew Tap Repository Exists
|
|
113
|
-
env:
|
|
114
|
-
GH_TOKEN: ${{ secrets.GH_PAT }}
|
|
115
|
-
run: |
|
|
116
|
-
if ! gh repo view stacksjs/homebrew-tap &>/dev/null; then
|
|
117
|
-
echo "Creating homebrew-tap repository..."
|
|
118
|
-
gh repo create stacksjs/homebrew-tap --public --description "Homebrew tap for Stacks packages"
|
|
119
|
-
else
|
|
120
|
-
echo "homebrew-tap repository already exists"
|
|
121
|
-
fi
|
|
122
|
-
|
|
123
|
-
- name: Authenticate GitHub CLI with App Token
|
|
124
|
-
run: echo "${{ steps.generate-token.outputs.token }}" | gh auth login --with-token
|
|
125
|
-
|
|
126
|
-
- name: Wait for Release Assets
|
|
127
|
-
run: |
|
|
128
|
-
VERSION=${GITHUB_REF#refs/tags/}
|
|
129
|
-
echo "Waiting for release assets to be available..."
|
|
130
|
-
|
|
131
|
-
# Wait for assets to be available (max 10 minutes)
|
|
132
|
-
TIMEOUT=600
|
|
133
|
-
START_TIME=$(date +%s)
|
|
134
|
-
|
|
135
|
-
while [ $(( $(date +%s) - START_TIME )) -lt $TIMEOUT ]; do
|
|
136
|
-
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" https://github.com/stacksjs/stacks/releases/download/$VERSION/buddy-darwin-arm64)
|
|
137
|
-
echo "HTTP response code: $HTTP_CODE"
|
|
138
|
-
|
|
139
|
-
if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "302" ]; then
|
|
140
|
-
echo "Assets are available!"
|
|
141
|
-
break
|
|
142
|
-
fi
|
|
143
|
-
|
|
144
|
-
echo "Assets not ready yet, waiting 10 seconds..."
|
|
145
|
-
sleep 10
|
|
146
|
-
done
|
|
147
|
-
|
|
148
|
-
if [ $(( $(date +%s) - START_TIME )) -ge $TIMEOUT ]; then
|
|
149
|
-
echo "Timed out waiting for assets, proceeding anyway..."
|
|
150
|
-
fi
|
|
151
|
-
|
|
152
|
-
# Force continue even if we couldn't detect the file
|
|
153
|
-
# Sometimes GitHub's API can be inconsistent
|
|
154
|
-
echo "Continuing with asset download..."
|
|
155
|
-
|
|
156
|
-
- name: Download Binaries Directly
|
|
157
|
-
run: |
|
|
158
|
-
VERSION=${GITHUB_REF#refs/tags/}
|
|
159
|
-
mkdir -p .github/temp
|
|
160
|
-
cd .github/temp
|
|
161
|
-
|
|
162
|
-
# Try direct download with retries
|
|
163
|
-
download_with_retry() {
|
|
164
|
-
local url=$1
|
|
165
|
-
local output=$2
|
|
166
|
-
local max_retries=5
|
|
167
|
-
local retry=0
|
|
168
|
-
|
|
169
|
-
while [ $retry -lt $max_retries ]; do
|
|
170
|
-
echo "Downloading $url (attempt $(($retry + 1))/$max_retries)"
|
|
171
|
-
if curl -L -s -f "$url" -o "$output"; then
|
|
172
|
-
echo "Downloaded $output successfully"
|
|
173
|
-
return 0
|
|
174
|
-
fi
|
|
175
|
-
echo "Download failed, retrying in 5 seconds..."
|
|
176
|
-
retry=$((retry + 1))
|
|
177
|
-
sleep 5
|
|
178
|
-
done
|
|
179
|
-
|
|
180
|
-
echo "Failed to download $url after $max_retries attempts"
|
|
181
|
-
return 1
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
download_with_retry "https://github.com/stacksjs/stacks/releases/download/$VERSION/buddy-darwin-arm64" "buddy-darwin-arm64"
|
|
185
|
-
download_with_retry "https://github.com/stacksjs/stacks/releases/download/$VERSION/buddy-darwin-x64" "buddy-darwin-x64"
|
|
186
|
-
download_with_retry "https://github.com/stacksjs/stacks/releases/download/$VERSION/buddy-linux-arm64" "buddy-linux-arm64"
|
|
187
|
-
download_with_retry "https://github.com/stacksjs/stacks/releases/download/$VERSION/buddy-linux-x64" "buddy-linux-x64"
|
|
188
|
-
|
|
189
|
-
cd ../..
|
|
190
|
-
|
|
191
|
-
- name: Update Homebrew Formula
|
|
192
|
-
run: |
|
|
193
|
-
VERSION=${GITHUB_REF#refs/tags/}
|
|
194
|
-
bash .github/scripts/update-homebrew-formula.sh $VERSION
|
|
195
|
-
|
|
196
|
-
- name: Push to Homebrew Tap Repository
|
|
197
|
-
run: |
|
|
198
|
-
# Clone the tap repo using GitHub App token
|
|
199
|
-
git clone https://x-access-token:${{ steps.generate-token.outputs.token }}@github.com/stacksjs/homebrew-tap.git
|
|
200
|
-
|
|
201
|
-
# Set up Git config
|
|
202
|
-
cd homebrew-tap
|
|
203
|
-
git config user.name "github-actions[bot]"
|
|
204
|
-
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
205
|
-
|
|
206
|
-
# Copy the formula to the tap repo
|
|
207
|
-
mkdir -p Formula
|
|
208
|
-
cp ../.github/homebrew/stacks.rb Formula/
|
|
209
|
-
|
|
210
|
-
# Check if there are changes to commit
|
|
211
|
-
if git status --porcelain | grep -q .; then
|
|
212
|
-
# Push the changes to the tap repo
|
|
213
|
-
git add Formula/stacks.rb
|
|
214
|
-
git commit -m "chore: update stacks formula to ${GITHUB_REF#refs/tags/}"
|
|
215
|
-
git push
|
|
216
|
-
echo "Successfully updated homebrew formula!"
|
|
217
|
-
else
|
|
218
|
-
echo "No changes to commit"
|
|
219
|
-
fi
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
name: Export Size
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
pull_request:
|
|
5
|
-
branches:
|
|
6
|
-
- main
|
|
7
|
-
|
|
8
|
-
jobs:
|
|
9
|
-
size:
|
|
10
|
-
strategy:
|
|
11
|
-
matrix:
|
|
12
|
-
node-version: [18.x]
|
|
13
|
-
os: [ubuntu-latest]
|
|
14
|
-
fail-fast: false
|
|
15
|
-
|
|
16
|
-
runs-on: ubuntu-latest
|
|
17
|
-
|
|
18
|
-
steps:
|
|
19
|
-
- name: Checkout Code
|
|
20
|
-
uses: actions/checkout@v3
|
|
21
|
-
|
|
22
|
-
- name: Setup Pantry
|
|
23
|
-
uses: pantry-pm/pantry/packages/action@485c7b102fe14f8af00b57441a13dc3daa1505e7 # v0.10.39
|
|
24
|
-
with:
|
|
25
|
-
install: 'false'
|