@stacksjs/defaults 0.70.293 → 0.70.296

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.
Files changed (58) hide show
  1. package/ai/skills/stacks-auth/SKILL.md +1 -1
  2. package/ai/skills/stacks-development/SKILL.md +3 -3
  3. package/ai/skills/stacks-stx/SKILL.md +3 -3
  4. package/ai/skills/stacks-ui/SKILL.md +3 -3
  5. package/app/Actions/Auth/LoginAction.ts +8 -2
  6. package/app/Actions/Auth/RegisterAction.ts +3 -2
  7. package/app/Actions/Auth/SocialCallbackAction.ts +75 -0
  8. package/app/Actions/Auth/SocialRedirectAction.ts +37 -0
  9. package/app/Actions/Dashboard/Analytics/request-analytics.ts +6 -1
  10. package/app/Actions/Dashboard/Commerce/CommercePosCheckoutAction.ts +7 -0
  11. package/app/Actions/Dashboard/Commerce/commerce-product-records.ts +3 -1
  12. package/app/Actions/Dashboard/Commerce/commerce-record.ts +13 -0
  13. package/app/Actions/Dashboard/Infrastructure/InsightsAction.ts +3 -3
  14. package/app/Actions/Dashboard/Jobs/job-records.ts +5 -1
  15. package/app/Actions/Dashboard/Kanban/BoardShowAction.ts +9 -7
  16. package/app/Actions/Dashboard/Kanban/BoardStoreAction.ts +1 -1
  17. package/app/Actions/Dashboard/Kanban/BoardsIndexAction.ts +6 -4
  18. package/app/Actions/Dashboard/Kanban/CardShowAction.ts +4 -2
  19. package/app/Actions/Dashboard/Library/GetAverageReleaseTime.ts +1 -1
  20. package/app/Actions/Dashboard/Marketing/CampaignIndexAction.ts +2 -2
  21. package/app/Actions/Dashboard/Marketing/ListIndexAction.ts +1 -1
  22. package/app/Actions/Dashboard/Queries/query-dashboard.ts +5 -3
  23. package/app/Actions/Password/PasswordResetAction.ts +29 -7
  24. package/app/Middleware/Auth.ts +4 -2
  25. package/app/Middleware/Can.ts +58 -5
  26. package/app/Models/Content/Author.ts +5 -0
  27. package/app/Models/Content/Page.ts +5 -0
  28. package/app/Models/Content/Post.ts +5 -0
  29. package/app/Models/Release.ts +5 -0
  30. package/app/Models/SocialAccount.ts +70 -0
  31. package/app/Models/Tag.ts +5 -0
  32. package/app/Models/User.ts +7 -4
  33. package/app/Models/commerce/Category.ts +5 -0
  34. package/app/Models/commerce/DeliveryRoute.ts +34 -0
  35. package/app/Models/commerce/DeliveryStop.ts +166 -0
  36. package/app/Models/commerce/Driver.ts +72 -2
  37. package/app/Models/commerce/DriverPing.ts +103 -0
  38. package/app/Models/commerce/LoyaltyReward.ts +5 -0
  39. package/app/Models/commerce/Manufacturer.ts +5 -0
  40. package/app/Models/commerce/Order.ts +44 -3
  41. package/app/Models/commerce/Product.ts +5 -0
  42. package/app/Models/commerce/ProductUnit.ts +5 -0
  43. package/app/Models/commerce/ProductVariant.ts +5 -0
  44. package/app/Models/commerce/ShippingMethod.ts +5 -0
  45. package/app/Models/commerce/ShippingRate.ts +5 -0
  46. package/app/Models/commerce/ShippingZone.ts +5 -0
  47. package/app/Models/commerce/TaxRate.ts +5 -0
  48. package/app/password-policy.ts +46 -0
  49. package/bootstrap.ts +67 -19
  50. package/ide/vscode/package.json +1 -1
  51. package/package.json +1 -1
  52. package/routes/auth.ts +83 -0
  53. package/routes/dashboard.ts +0 -61
  54. package/routes/socials.ts +32 -0
  55. package/vcs/github/CONTRIBUTING.md +1 -1
  56. package/vcs/github/workflows/README.md +8 -9
  57. package/vcs/github/workflows/release.yml +4 -181
  58. package/vcs/github/workflows/export-size.yml +0 -25
@@ -22,6 +22,11 @@ export default defineModel({
22
22
  },
23
23
 
24
24
  useApi: {
25
+ // Public catalog: anyone may browse, only authenticated callers may
26
+ // write. Declared explicitly because the trait now defaults BOTH sides to
27
+ // `auth` — an undeclared read route is how a customer list leaks
28
+ // (stacksjs/stacks#2224). Behaviour here is unchanged.
29
+ middleware: { read: [], write: ['auth'] },
25
30
  uri: 'product-manufacturers',
26
31
  },
27
32
 
@@ -29,7 +29,7 @@ export default defineModel({
29
29
  observe: true,
30
30
  },
31
31
 
32
- hasMany: ['OrderItem', 'Payment', 'LicenseKey'],
32
+ hasMany: ['OrderItem', 'Payment', 'LicenseKey', 'DeliveryStop'],
33
33
  belongsTo: ['Customer', 'Coupon'],
34
34
 
35
35
  attributes: {
@@ -39,7 +39,18 @@ export default defineModel({
39
39
  validation: {
40
40
  rule: schema.string().required(),
41
41
  },
42
- factory: faker => faker.helpers.arrayElement(['PENDING', 'PREPARING', 'READY', 'DELIVERED', 'CANCELED']),
42
+ /*
43
+ * The canonical vocabulary is `OrderStatus` in
44
+ * `commerce/src/orders/events.ts`, which is what `canTransition` and
45
+ * `emitForStatus` are keyed on. The factory used to generate
46
+ * PREPARING / READY / CANCELED, none of which are in that union, so
47
+ * seeded orders could not legally transition anywhere.
48
+ *
49
+ * The column stays a free string rather than an enum: existing
50
+ * databases hold the old spellings, and adding a CHECK constraint here
51
+ * would fail their next migration rather than fix their data.
52
+ */
53
+ factory: faker => faker.helpers.arrayElement(['PENDING', 'PROCESSING', 'SHIPPED', 'OUT_FOR_DELIVERY', 'DELIVERED']),
43
54
  },
44
55
 
45
56
  totalAmount: {
@@ -141,8 +152,38 @@ export default defineModel({
141
152
  },
142
153
  },
143
154
 
144
- appliedCouponId: {
155
+ /**
156
+ * Unguessable handle for the customer-facing tracking page.
157
+ *
158
+ * A tracking URL is opened from an SMS, on a phone, by someone who is not
159
+ * signed in, so it authorises on possession of this token. Sequential
160
+ * order ids would let anyone walk the table.
161
+ */
162
+ trackingToken: {
145
163
  order: 13,
164
+ unique: true,
165
+ fillable: true,
166
+ validation: { rule: schema.string().max(64) },
167
+ factory: faker => faker.string.alphanumeric({ length: 32 }),
168
+ },
169
+
170
+ /** Geocoded delivery destination, so the map has somewhere to point. */
171
+ deliveryLatitude: {
172
+ order: 14,
173
+ fillable: true,
174
+ validation: { rule: schema.number().min(-90).max(90) },
175
+ factory: faker => faker.location.latitude(),
176
+ },
177
+
178
+ deliveryLongitude: {
179
+ order: 15,
180
+ fillable: true,
181
+ validation: { rule: schema.number().min(-180).max(180) },
182
+ factory: faker => faker.location.longitude(),
183
+ },
184
+
185
+ appliedCouponId: {
186
+ order: 16,
146
187
  fillable: true,
147
188
  validation: {
148
189
  rule: schema.string(),
@@ -22,6 +22,11 @@ export default defineModel({
22
22
  },
23
23
 
24
24
  useApi: {
25
+ // Public catalog: anyone may browse, only authenticated callers may
26
+ // write. Declared explicitly because the trait now defaults BOTH sides to
27
+ // `auth` — an undeclared read route is how a customer list leaks
28
+ // (stacksjs/stacks#2224). Behaviour here is unchanged.
29
+ middleware: { read: [], write: ['auth'] },
25
30
  uri: 'products',
26
31
  },
27
32
 
@@ -22,6 +22,11 @@ export default defineModel({
22
22
  },
23
23
 
24
24
  useApi: {
25
+ // Public catalog: anyone may browse, only authenticated callers may
26
+ // write. Declared explicitly because the trait now defaults BOTH sides to
27
+ // `auth` — an undeclared read route is how a customer list leaks
28
+ // (stacksjs/stacks#2224). Behaviour here is unchanged.
29
+ middleware: { read: [], write: ['auth'] },
25
30
  uri: 'product-units',
26
31
  },
27
32
 
@@ -22,6 +22,11 @@ export default defineModel({
22
22
  },
23
23
 
24
24
  useApi: {
25
+ // Public catalog: anyone may browse, only authenticated callers may
26
+ // write. Declared explicitly because the trait now defaults BOTH sides to
27
+ // `auth` — an undeclared read route is how a customer list leaks
28
+ // (stacksjs/stacks#2224). Behaviour here is unchanged.
29
+ middleware: { read: [], write: ['auth'] },
25
30
  uri: 'product-variants',
26
31
  },
27
32
 
@@ -22,6 +22,11 @@ export default defineModel({
22
22
  },
23
23
 
24
24
  useApi: {
25
+ // Public catalog: anyone may browse, only authenticated callers may
26
+ // write. Declared explicitly because the trait now defaults BOTH sides to
27
+ // `auth` — an undeclared read route is how a customer list leaks
28
+ // (stacksjs/stacks#2224). Behaviour here is unchanged.
29
+ middleware: { read: [], write: ['auth'] },
25
30
  uri: 'shipping-methods',
26
31
  },
27
32
 
@@ -22,6 +22,11 @@ export default defineModel({
22
22
  },
23
23
 
24
24
  useApi: {
25
+ // Public catalog: anyone may browse, only authenticated callers may
26
+ // write. Declared explicitly because the trait now defaults BOTH sides to
27
+ // `auth` — an undeclared read route is how a customer list leaks
28
+ // (stacksjs/stacks#2224). Behaviour here is unchanged.
29
+ middleware: { read: [], write: ['auth'] },
25
30
  uri: 'shipping-rates',
26
31
  },
27
32
 
@@ -22,6 +22,11 @@ export default defineModel({
22
22
  },
23
23
 
24
24
  useApi: {
25
+ // Public catalog: anyone may browse, only authenticated callers may
26
+ // write. Declared explicitly because the trait now defaults BOTH sides to
27
+ // `auth` — an undeclared read route is how a customer list leaks
28
+ // (stacksjs/stacks#2224). Behaviour here is unchanged.
29
+ middleware: { read: [], write: ['auth'] },
25
30
  uri: 'shipping-zones',
26
31
  },
27
32
 
@@ -22,6 +22,11 @@ export default defineModel({
22
22
  },
23
23
 
24
24
  useApi: {
25
+ // Public catalog: anyone may browse, only authenticated callers may
26
+ // write. Declared explicitly because the trait now defaults BOTH sides to
27
+ // `auth` — an undeclared read route is how a customer list leaks
28
+ // (stacksjs/stacks#2224). Behaviour here is unchanged.
29
+ middleware: { read: [], write: ['auth'] },
25
30
  uri: 'tax-rates',
26
31
  },
27
32
 
@@ -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
- // Feature-gated route registration. The dashboard.ts file currently bundles
80
- // ~687 lines covering auth, password reset, email subscribe, storefront
81
- // cart/checkout, reviews, sitemap, AI, voice, and the admin dashboard's
82
- // REST surface. Until that file is split per-feature (auth.ts, marketing.ts,
83
- // commerce.ts, monitoring.ts), the whole thing loads when `dashboard` is
84
- // activated and stays inert otherwise.
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
- // Apps that need only a slice e.g. a marketing site that wants
87
- // `/api/email/subscribe` and `/api/contact` but not the rest can either
88
- // 1. Activate `dashboard` and live with the over-broad register; the
89
- // action handlers for routes you don't hit never fire, and their
90
- // models stay un-loaded as long as the corresponding feature flag
91
- // (`commerce`, `cms`, `monitoring`) is off, so there's no hidden
92
- // cost beyond the bun-router route-table entries.
93
- // 2. Define the routes they want directly in `routes/api.ts` —
94
- // first-registration-wins means the user version takes priority.
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
- // Once the per-feature route split lands, each `if (feature('X'))` block
97
- // below registers just the X-specific routes file.
98
- if (feature('dashboard')) {
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
+ }
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.70.293",
5
+ "version": "0.70.296",
6
6
  "description": "A modern Stacks development environment.",
7
7
  "license": "MIT",
8
8
  "funding": "https://github.com/sponsors/chrisbbreuer",
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.293",
5
+ "version": "0.70.296",
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
+ })
@@ -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 the framework tests are stored within within the `./storage/framework/tests` project folder. When adding or updating functionality, please ensure it is covered through our test suite. Ensure so by running `buddy test`.
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] - all CI jobs for the project
5
+ - [CI][CI] all CI jobs for the project
6
6
  - lints the code
7
7
  - `typecheck`s the code
8
- - automatically fixes & applies code style updates
9
- - runs tests (unit, end-to-end)
10
- - runs on `ubuntu-latest` with `bun-versions` set to `[x]`
11
- - [Release][Release] - automates the release process & changelog generation
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]: ./workflows/ci.yml
15
- [Release]: ./workflows/release.yml
16
- [Lock Closed Issues]: ./workflows/lock-closed-issues.yml
13
+ [CI]: ./ci.yml
14
+ [Release]: ./release.yml
15
+ [Labeler]: ./labeler.yml