@stacksjs/defaults 0.75.34 → 0.75.35
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-actions/SKILL.md +1 -1
- package/ai/skills/stacks-auth/SKILL.md +11 -2
- package/ai/skills/stacks-buddy/SKILL.md +1 -1
- package/ai/skills/stacks-cms/SKILL.md +2 -0
- package/ai/skills/stacks-config/SKILL.md +1 -1
- package/ai/skills/stacks-email/SKILL.md +2 -0
- package/ai/skills/stacks-env/SKILL.md +64 -0
- package/ai/skills/stacks-server/SKILL.md +45 -0
- package/ai/skills/stacks-stx/SKILL.md +25 -0
- package/app/Actions/Auth/OAuthClientStoreAction.ts +6 -3
- package/app/Actions/Auth/OAuthClientUpdateAction.ts +6 -3
- package/app/Actions/Auth/OAuthIntrospectionAction.ts +25 -0
- package/app/Actions/Auth/OAuthTokenAction.ts +4 -2
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
- package/routes/auth.ts +2 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: stacks-actions
|
|
3
|
-
description: Use when working with Stacks server actions - creating actions in app/Actions/, auto-generated API actions from the useApi model trait, the
|
|
3
|
+
description: Use when working with Stacks server actions - creating actions in app/Actions/, auto-generated API actions from the useApi model trait, the 648 default framework actions (auth, dashboard, commerce, content, deployment, jobs), action request/response handling, or action registration. Covers @stacksjs/actions and storage/framework/defaults/app/Actions/.
|
|
4
4
|
license: MIT
|
|
5
5
|
compatibility: Bun >= 1.3.0, TypeScript
|
|
6
6
|
allowed-tools: Read Edit Write Bash Grep Glob
|
|
@@ -161,8 +161,12 @@ routes: minting and revoking both carry semantics a generic route does not.
|
|
|
161
161
|
The authorization server is opt-in through `config/auth.ts` under
|
|
162
162
|
`oauthProvider`. It is separate from social sign-in, where Stacks is the OAuth
|
|
163
163
|
client. The provider profile is Authorization Code with S256 PKCE and rotating
|
|
164
|
-
refresh tokens.
|
|
165
|
-
|
|
164
|
+
refresh tokens. Confidential client credentials are opt-in through
|
|
165
|
+
`oauthProvider.clientCredentials` and mint access-only tokens without refresh
|
|
166
|
+
tokens. Protected resource-server introspection is separately opt-in through
|
|
167
|
+
`oauthProvider.introspection`; it authenticates a confidential client and
|
|
168
|
+
requires that client to share a configured resource audience with the token.
|
|
169
|
+
It does not support implicit or password grants or OpenID Connect.
|
|
166
170
|
|
|
167
171
|
When enabled, the default auth route bundle registers:
|
|
168
172
|
|
|
@@ -178,6 +182,11 @@ Resource audiences are absolute URIs and redirect URIs use exact matching.
|
|
|
178
182
|
The consent view is `auth/oauth/consent` by default and can be overridden with
|
|
179
183
|
`oauthProvider.consent.view`. Use `oauthProvider.consent.resolveWorkspace` when
|
|
180
184
|
the application must bind consent to current server-owned workspace authority.
|
|
185
|
+
Use `oauthProvider.subjectEligibility` when account status is represented by
|
|
186
|
+
application data that is not simply the presence of a user row. The callback
|
|
187
|
+
is rechecked before consent approval, delegated code exchange and refresh, and
|
|
188
|
+
by introspection; returning false revokes the affected grant and reports the
|
|
189
|
+
token inactive.
|
|
181
190
|
|
|
182
191
|
Provider actions return 404 while `oauthProvider.enabled` is false. The token
|
|
183
192
|
and revocation endpoints use protocol credentials and intentionally skip
|
|
@@ -611,7 +611,7 @@ buddy ai:context # compact deterministic project context for coding
|
|
|
611
611
|
-o/--output [path] # write to a file instead of stdout
|
|
612
612
|
--max-chars [characters] # prompt payload character budget (default: 4000)
|
|
613
613
|
--model [model] # model family for heuristic token estimates
|
|
614
|
-
buddy doctor # health checks (Bun version, Node, package.json, .env, APP_KEY)
|
|
614
|
+
buddy doctor # health checks (Bun version, Node, package.json, .env, APP_KEY, mail DNS)
|
|
615
615
|
buddy tinker # interactive REPL with Stacks preloaded
|
|
616
616
|
-e/--eval [expr] # evaluate expression and exit
|
|
617
617
|
--print [expr] # evaluate, print, and exit
|
|
@@ -190,6 +190,8 @@ email. Moderate without a dashboard with `buddy comments:list [--status] [--type
|
|
|
190
190
|
| GET | `/blog/feed.xml` | RSS 2.0 feed (20 recent) |
|
|
191
191
|
| GET | `/blog/sitemap.xml` | XML sitemap for SEO |
|
|
192
192
|
|
|
193
|
+
The site-wide `/sitemap.xml` merges the published posts in, so crawlers find them from the root sitemap too (see `stacks-server`, "Sitemap and robots.txt").
|
|
194
|
+
|
|
193
195
|
## Post Bodies: Images, Grids, Zoom and Embeds
|
|
194
196
|
|
|
195
197
|
Blog post HTML goes through `renderPostHtml` from `@stacksjs/cms/article`, built on ts-medium-editor's article renderer. The BunPress blog (`core/actions/src/blog.ts`) does this already; an stx-native blog calls it from its post view:
|
|
@@ -65,7 +65,7 @@ they name: `defineEvents` and `defineListener` from `@stacksjs/events`,
|
|
|
65
65
|
### Core App
|
|
66
66
|
| File | Type | Key Settings |
|
|
67
67
|
|------|------|-------------|
|
|
68
|
-
| `app.ts` | AppConfig | name, env, url, debug, key, timezone, locale |
|
|
68
|
+
| `app.ts` | AppConfig | name, env, url, debug, key, timezone, locale, seo (sitemap.xml / robots.txt) |
|
|
69
69
|
| `auth.ts` | AuthConfig | guards, providers, token and browser-session lifetimes, passwordReset |
|
|
70
70
|
| `database.ts` | DatabaseConfig | default driver, connections (sqlite/mysql/postgres/dynamodb), queryLogging |
|
|
71
71
|
| `cache.ts` | CacheConfig | driver('memory'), ttl(3600), maxKeys(-1), redis config |
|
|
@@ -244,6 +244,8 @@ With `server.enabled`, `buddy deploy` reconciles the mail domain's DNS through w
|
|
|
244
244
|
|
|
245
245
|
Postmaster Tools is the only view of why Gmail files mail as spam when SPF, DKIM and DMARC all pass. Its value is issued per Google account and cannot be generated: add the domain at https://postmaster.google.com, copy the TXT value, set `server.postmaster.google`, deploy, then press Verify in Postmaster Tools. Until it is set the deploy prints where to get it; `postmaster: false` silences that.
|
|
246
246
|
|
|
247
|
+
When no configured provider can write the zone (no credentials for the provider its nameservers name, e.g. a Cloudflare zone with no `CLOUDFLARE_API_TOKEN`), nothing is published, on this deploy or any later one. The deploy warns with the exact fix (`buddy env:set CLOUDFLARE_API_TOKEN <value> -f .env.production`) and the records to add by hand. `buddy doctor` reports the same without deploying: "Mail DNS provider" (which provider the nameservers name, and whether its keys are in `.env.production`, by name only), "Mail DNS records" (MX, SPF, DKIM at `mail._domainkey`, DMARC in public DNS, each exactly once, the DMARC policy against config, SPF against the MX host's address) and "Postmaster Tools". Code: `core/buddy/src/mail-dns-health.ts`, `core/buddy/src/dns-credentials.ts`.
|
|
248
|
+
|
|
247
249
|
## Application Mail Example
|
|
248
250
|
|
|
249
251
|
```typescript
|
|
@@ -106,6 +106,70 @@ provider // 'github-actions' | 'gitlab-ci' | 'circleci' | 'travis' | 'jen
|
|
|
106
106
|
providerInfo // { name: string, ... }
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
+
## Remote integration environments
|
|
110
|
+
|
|
111
|
+
Gate anything that transmits off the box - error reporting, analytics, log
|
|
112
|
+
shipping - through `@stacksjs/env`, rather than inventing a policy per adapter.
|
|
113
|
+
Without a shared gate, every developer machine and CI run reports into the same
|
|
114
|
+
project as production, and a local startup failure reads as a live incident
|
|
115
|
+
(stacksjs/stacks#2792).
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
import { initializeIntegration } from '@stacksjs/env'
|
|
119
|
+
|
|
120
|
+
// The callback does not run when the gate is closed, so constructing a client,
|
|
121
|
+
// registering hooks or starting a flush timer inside it is safe.
|
|
122
|
+
const result = initializeIntegration(config.bughq, ({ environment }) =>
|
|
123
|
+
installBugHq({ key: config.bughq.key, environment }))
|
|
124
|
+
|
|
125
|
+
if (!result.initialized)
|
|
126
|
+
log.debug(`BugHQ not installed: ${result.gate.reason}`)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
An application declares **only** `environments`. Do not add a sibling
|
|
130
|
+
`environment` option and do not ask the app to repeat `env.APP_ENV`: the gate
|
|
131
|
+
resolves the effective label once and hands it to the adapter for event
|
|
132
|
+
metadata.
|
|
133
|
+
|
|
134
|
+
Extend `EnvironmentGatedIntegration` from the adapter's own options type so
|
|
135
|
+
the application gets `enabled` and `environments` and nothing else. Analytics
|
|
136
|
+
is the adapter shipping this today:
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
// config/analytics.ts
|
|
140
|
+
export default {
|
|
141
|
+
driver: 'analyticshq',
|
|
142
|
+
environments: ['production', 'staging'], // omit to get exactly this
|
|
143
|
+
drivers: { analyticshq: { siteId: '...' } },
|
|
144
|
+
} satisfies AnalyticsConfig
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
| Configuration | Result |
|
|
148
|
+
|---|---|
|
|
149
|
+
| `environments` omitted | `REMOTE_TELEMETRY_ENVIRONMENTS`, which is `production` and `staging` |
|
|
150
|
+
| `environments: []` | off everywhere, the explicit way to disable without removing credentials |
|
|
151
|
+
| `enabled: false` | off, always; checked before the allowlist so it is the last word |
|
|
152
|
+
| `enabled: true` | does **not** bypass the allowlist |
|
|
153
|
+
| `APP_ENV` unset or malformed | off, with `reason: 'environment-unknown'` |
|
|
154
|
+
|
|
155
|
+
The gate reads `APP_ENV` only, and reads it at call time. It does not fall back
|
|
156
|
+
to `NODE_ENV` the way `appEnv()` does, because an absent `APP_ENV` must not be
|
|
157
|
+
guessed into a label an allowlist might admit. Call time rather than import time
|
|
158
|
+
matters because a test harness or a CLI resolving `--env` routinely sets it
|
|
159
|
+
after the module graph has loaded (stacksjs/stacks#2581).
|
|
160
|
+
|
|
161
|
+
`prod`, `stage` and `dev` fold onto `production`, `staging` and `development`.
|
|
162
|
+
`local`, `development` and `test` are deliberately **not** folded together:
|
|
163
|
+
allowing your own machine must not also allow CI.
|
|
164
|
+
|
|
165
|
+
The answer is the same in a web or API server, the dashboard, a worker and a
|
|
166
|
+
Buddy command, because all of them read the same variable. Browser integrations
|
|
167
|
+
receive only the resolved public label, never server credentials.
|
|
168
|
+
|
|
169
|
+
`APP_ENV` is a configuration label, not proof of deployment identity: a local
|
|
170
|
+
command launched with `APP_ENV=production` qualifies. It prevents accidents; it
|
|
171
|
+
is not a security boundary.
|
|
172
|
+
|
|
109
173
|
## .env File Loading
|
|
110
174
|
|
|
111
175
|
```typescript
|
|
@@ -349,6 +349,51 @@ if (await isDownForMaintenance()) {
|
|
|
349
349
|
await up()
|
|
350
350
|
```
|
|
351
351
|
|
|
352
|
+
## Sitemap and robots.txt (core/actions/src/seo.ts)
|
|
353
|
+
|
|
354
|
+
The views server answers `/sitemap.xml` and `/robots.txt` for every app, generated from
|
|
355
|
+
`resources/views` at request time. `buddy dev` (`core/actions/src/dev/views.ts`) and `buddy serve`
|
|
356
|
+
(`core/buddy/src/production-server.ts`) call the same `createSiteSeoHandler()` at the same point in
|
|
357
|
+
`onRequest` (after redirects and rewrites), so dev and production answer identically. No route, no
|
|
358
|
+
build step, no app code. Do not add `route.get('/sitemap.xml')` to `routes/`: the views server does
|
|
359
|
+
not forward root GETs to the API.
|
|
360
|
+
|
|
361
|
+
- **Listed**: every static route under `resources/views`, with `lastmod` from the file, plus the
|
|
362
|
+
blog's published posts (`content/blog`, or a CMS-built `dist/blog/sitemap.xml`) merged into the
|
|
363
|
+
same file. `/blog/sitemap.xml` is still served too.
|
|
364
|
+
- **Not listed**: dynamic routes (`[slug].stx`), error and holding pages (`404`, `errors/*`,
|
|
365
|
+
`coming-soon`), `components/`, `layouts/`, `partials/`, `emails/`, `_`-prefixed files, pages behind
|
|
366
|
+
`definePageMeta({ middleware: ['auth'] })`, paths in `seo.sitemap.exclude`, paths robots.txt
|
|
367
|
+
disallows, and any page that says **noindex**. The framework's default views (`/login`, ...) are
|
|
368
|
+
never listed.
|
|
369
|
+
- **noindex**, the one signal, read from the page source: `<meta name="robots" content="noindex">`,
|
|
370
|
+
`robots: 'noindex'` in a head object, or `const noindex = true` in the server script (the flag a
|
|
371
|
+
shared head partial turns into the tag - the scan cannot follow an `@include`, so spell it that
|
|
372
|
+
way). `definePageMeta({ sitemap: false })` also keeps a page out.
|
|
373
|
+
- **robots.txt**: `User-agent: *`, `Allow: /`, `Disallow: /api/` plus `seo.robots.disallow`, extra
|
|
374
|
+
groups from `seo.robots.rules`, and `Sitemap: <origin>/sitemap.xml`. Never disallow a page just to
|
|
375
|
+
hide it: a crawler that cannot fetch it never sees its noindex.
|
|
376
|
+
- **Hand-written files win**: `public/sitemap.xml` / `public/robots.txt` are served instead, and the
|
|
377
|
+
server warns at boot when the hand-written sitemap misses a page it would list. Delete the file to
|
|
378
|
+
switch, or set `seo.handwritten: 'replace'`.
|
|
379
|
+
- **Origin**: `seo.origin`, else `app.url` (APP_URL), else the request origin (local dev only).
|
|
380
|
+
- **Config** (`config/app.ts`, type `SeoOptions` in `core/types/src/seo.ts`):
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
seo: {
|
|
384
|
+
sitemap: {
|
|
385
|
+
include: ['/products/blue-mug'], // instances of dynamic routes
|
|
386
|
+
entries: async () => (await Product.all()).map(p => `/products/${p.slug}`),
|
|
387
|
+
exclude: ['/styleguide', '/internal/'], // trailing / or * = subtree
|
|
388
|
+
},
|
|
389
|
+
robots: { disallow: ['/checkout/', '/thanks'], rules: [{ userAgent: 'GPTBot', disallow: ['/'] }] },
|
|
390
|
+
// sitemap: false / robots: false turn either off
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
Production caches each file for ten minutes (for `entries()`); development regenerates per request.
|
|
395
|
+
Static frontend builds (`buddy build views`) use stx's own SSG sitemap instead.
|
|
396
|
+
|
|
352
397
|
## Docker Build Pipeline (server/build.ts)
|
|
353
398
|
|
|
354
399
|
The build process (`bun build.ts` from `storage/framework/server/`):
|
|
@@ -240,6 +240,30 @@ function showSearch(): void {
|
|
|
240
240
|
- **Markdown** — Markdown rendering with syntax highlighting
|
|
241
241
|
- **A11y** — Accessibility checking and auto-fixing
|
|
242
242
|
|
|
243
|
+
### SEO directives
|
|
244
|
+
|
|
245
|
+
Every argument is an expression evaluated against the template context (`<script server>` variables, the render context, and, inside an `@include` partial, the partial's own `<script server>`). A quoted string is literal text.
|
|
246
|
+
|
|
247
|
+
```html
|
|
248
|
+
<script server>
|
|
249
|
+
const seo = { title: post.title, description: post.excerpt, canonical: url, openGraph: { image: post.cover } }
|
|
250
|
+
</script>
|
|
251
|
+
|
|
252
|
+
@seo(seo) <!-- or @seo({ ...seo, title }), @seo(buildSeo(post)), @seo({ title: 'Literal' }) -->
|
|
253
|
+
@seo(page.seo ?? {}) <!-- a variable only some pages define -->
|
|
254
|
+
@meta('author', post.author) <!-- unquoted value: an expression -->
|
|
255
|
+
@meta('description', 'Plain text') <!-- quoted value: literal, never looked up -->
|
|
256
|
+
@meta('keywords', tags) <!-- an array joins with ", " -->
|
|
257
|
+
@meta('og:title') <!-- one argument: reads `title`, then `openGraph.title` -->
|
|
258
|
+
@metaTag({ httpEquiv: 'refresh', content: '30' })
|
|
259
|
+
@structuredData(product) <!-- object or array of objects; @context defaults to schema.org -->
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
- `@seo` reads only `title`, `description`, `keywords`, `robots`, `canonical`, `openGraph`, `twitter`, `structuredData`. `image`, `url` and `type` are not top-level keys (use `openGraph.image`, `canonical`, `openGraph.type`); stray keys are ignored with a warning.
|
|
263
|
+
- A directive that cannot produce its tag (a variable that is not defined, a value of the wrong type, a syntax error, an unclosed call) leaves `<!-- [SEO Error ...] -->` in its place and logs `[stx] @seo(seo) in <file>: ...`. A defined variable holding `null` renders nothing, quietly. Read those warnings; they are the only sign a page shipped without its preview card.
|
|
264
|
+
- `@meta` in a page is staged into `<head>`. `@seo`, `@metaTag` and `@structuredData` write where they sit, so put them inside `<head>` (a layout or head partial). In a page body use `useSeoMeta({ title, description, ogImage })` in `<script server>`, which always reaches `<head>`.
|
|
265
|
+
- Requires `@stacksjs/stx` >= 0.2.343. Older versions only matched `@seo({ ... })` literally and sent unquoted `@meta` values such as `post.author` as text, without a warning.
|
|
266
|
+
|
|
243
267
|
### Dev Tools
|
|
244
268
|
- **Dev server** with HMR (Hot Module Replacement)
|
|
245
269
|
- **Image optimization**
|
|
@@ -291,4 +315,5 @@ await addLayout('admin', { nav: true, footer: true })
|
|
|
291
315
|
- **Script block restrictions** — only stx-compatible code (signals, composables, directives), no vanilla DOM APIs
|
|
292
316
|
- **Pre-paint state** - use `@appearanceBootstrap`, never a raw browser script in the template
|
|
293
317
|
- **Components go in `resources/`** — not in `app/` or `storage/`
|
|
318
|
+
- **SEO directives take expressions** - `@seo(seo)`, `@meta('author', post.author)` and `@structuredData(product)` are evaluated; a failure shows up as an inline `<!-- [... Error] -->` comment plus a `[stx]` console warning naming the file, never as silent emptiness. Keep `@seo` inside `<head>`
|
|
294
319
|
- **118+ modules** — STX is a comprehensive framework covering rendering, routing, forms, i18n, SEO, PWA, and more
|
|
@@ -26,10 +26,13 @@ export default new Action({
|
|
|
26
26
|
const body = await request.all() as Record<string, unknown>
|
|
27
27
|
const name = typeof body.name === 'string' ? body.name : null
|
|
28
28
|
const type = body.type === 'public' || body.type === 'confidential' ? body.type : null
|
|
29
|
-
const redirectUris = oauthStringList(body.redirect_uris)
|
|
29
|
+
const redirectUris = body.redirect_uris === undefined ? [] : oauthStringList(body.redirect_uris)
|
|
30
|
+
const grantTypes = body.grant_types === undefined
|
|
31
|
+
? ['authorization_code', 'refresh_token']
|
|
32
|
+
: oauthStringList(body.grant_types)
|
|
30
33
|
const scopes = oauthStringList(body.scopes)
|
|
31
34
|
const resources = oauthStringList(body.resources)
|
|
32
|
-
if (!name || !type || !redirectUris || !scopes || !resources)
|
|
35
|
+
if (!name || !type || !redirectUris || !grantTypes || !scopes || !resources)
|
|
33
36
|
return response.badRequest('OAuth client registration metadata is invalid')
|
|
34
37
|
|
|
35
38
|
try {
|
|
@@ -38,7 +41,7 @@ export default new Action({
|
|
|
38
41
|
type,
|
|
39
42
|
tokenEndpointAuthMethod: type === 'public' ? 'none' : 'client_secret_basic',
|
|
40
43
|
redirectUris,
|
|
41
|
-
grantTypes
|
|
44
|
+
grantTypes,
|
|
42
45
|
scopes,
|
|
43
46
|
resources,
|
|
44
47
|
})
|
|
@@ -30,17 +30,20 @@ export default new Action({
|
|
|
30
30
|
|
|
31
31
|
const body = await request.all() as Record<string, unknown>
|
|
32
32
|
const name = typeof body.name === 'string' ? body.name : null
|
|
33
|
-
const redirectUris = oauthStringList(body.redirect_uris)
|
|
33
|
+
const redirectUris = body.redirect_uris === undefined ? [] : oauthStringList(body.redirect_uris)
|
|
34
|
+
const grantTypes = body.grant_types === undefined
|
|
35
|
+
? ['authorization_code', 'refresh_token']
|
|
36
|
+
: oauthStringList(body.grant_types)
|
|
34
37
|
const scopes = oauthStringList(body.scopes)
|
|
35
38
|
const resources = oauthStringList(body.resources)
|
|
36
|
-
if (!name || !redirectUris || !scopes || !resources)
|
|
39
|
+
if (!name || !redirectUris || !grantTypes || !scopes || !resources)
|
|
37
40
|
return response.badRequest('OAuth client update metadata is invalid')
|
|
38
41
|
|
|
39
42
|
try {
|
|
40
43
|
const client = await updateOAuthClient(provider, ownerId, clientId, {
|
|
41
44
|
name,
|
|
42
45
|
redirectUris,
|
|
43
|
-
grantTypes
|
|
46
|
+
grantTypes,
|
|
44
47
|
scopes,
|
|
45
48
|
resources,
|
|
46
49
|
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Action } from '@stacksjs/actions'
|
|
2
|
+
import { handleOAuthIntrospectionRequest, resolveOAuthProviderConfig } from '@stacksjs/auth'
|
|
3
|
+
import { config } from '@stacksjs/config'
|
|
4
|
+
import { response } from '@stacksjs/router'
|
|
5
|
+
|
|
6
|
+
export default new Action({
|
|
7
|
+
name: 'OAuthIntrospectionAction',
|
|
8
|
+
description: 'Introspect a delegated OAuth token',
|
|
9
|
+
method: 'POST',
|
|
10
|
+
// Resource servers authenticate with their registered OAuth client instead
|
|
11
|
+
// of a browser session, so this protocol endpoint does not use CSRF.
|
|
12
|
+
skipCsrf: true,
|
|
13
|
+
|
|
14
|
+
async handle(request: RequestInstance) {
|
|
15
|
+
const provider = resolveOAuthProviderConfig(config.auth.oauthProvider)
|
|
16
|
+
if (!provider || !provider.introspection)
|
|
17
|
+
return response.notFound('OAuth introspection is not enabled')
|
|
18
|
+
|
|
19
|
+
return handleOAuthIntrospectionRequest(provider, {
|
|
20
|
+
body: (await request.rawBody?.()) ?? '',
|
|
21
|
+
contentType: request.headers.get('content-type'),
|
|
22
|
+
authorization: request.headers.get('authorization'),
|
|
23
|
+
})
|
|
24
|
+
},
|
|
25
|
+
})
|
|
@@ -15,7 +15,7 @@ const isSubjectEligible: OAuthSubjectEligibility = async (subject) => {
|
|
|
15
15
|
|
|
16
16
|
export default new Action({
|
|
17
17
|
name: 'OAuthTokenAction',
|
|
18
|
-
description: 'Exchange OAuth authorization codes and
|
|
18
|
+
description: 'Exchange OAuth authorization codes, refresh tokens, and machine credentials',
|
|
19
19
|
method: 'POST',
|
|
20
20
|
// OAuth clients authenticate with PKCE or HTTP Basic and cannot obtain the
|
|
21
21
|
// browser CSRF cookie. The protocol boundary validates its own credentials.
|
|
@@ -26,10 +26,12 @@ export default new Action({
|
|
|
26
26
|
if (!provider)
|
|
27
27
|
return response.notFound('OAuth provider is not enabled')
|
|
28
28
|
|
|
29
|
+
const eligible = provider.subjectEligibility ?? isSubjectEligible
|
|
30
|
+
|
|
29
31
|
return handleOAuthTokenRequest(provider, {
|
|
30
32
|
body: (await request.rawBody?.()) ?? '',
|
|
31
33
|
contentType: request.headers.get('content-type'),
|
|
32
34
|
authorization: request.headers.get('authorization'),
|
|
33
|
-
}, { isSubjectEligible })
|
|
35
|
+
}, { isSubjectEligible: eligible })
|
|
34
36
|
},
|
|
35
37
|
})
|
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.75.
|
|
5
|
+
"version": "0.75.35",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/stacksjs/stacks.git",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@iconify-json/f7": "^1.2.2",
|
|
57
57
|
"@iconify-json/hugeicons": "^1.2.27",
|
|
58
|
-
"@stacksjs/mobile": "^0.75.
|
|
58
|
+
"@stacksjs/mobile": "^0.75.35",
|
|
59
59
|
"@stacksjs/sanitizer": "^0.2.113",
|
|
60
60
|
"ts-qr-codes": "^0.1.8"
|
|
61
61
|
}
|
package/routes/auth.ts
CHANGED
|
@@ -31,6 +31,7 @@ const oauthProvider = resolveOAuthProviderConfig(config.auth.oauthProvider)
|
|
|
31
31
|
const oauthAuthorizationPath = oauthProvider ? new URL(oauthProvider.endpoints.authorization).pathname : '/oauth/authorize'
|
|
32
32
|
const oauthTokenPath = oauthProvider ? new URL(oauthProvider.endpoints.token).pathname : '/oauth/token'
|
|
33
33
|
const oauthRevocationPath = oauthProvider ? new URL(oauthProvider.endpoints.revocation).pathname : '/oauth/revoke'
|
|
34
|
+
const oauthIntrospectionPath = oauthProvider ? new URL(oauthProvider.endpoints.introspection).pathname : '/oauth/introspect'
|
|
34
35
|
const oauthMetadataPath = oauthProvider
|
|
35
36
|
? oauthAuthorizationServerMetadataPath(oauthProvider.issuer)
|
|
36
37
|
: '/.well-known/oauth-authorization-server'
|
|
@@ -49,6 +50,7 @@ route.post('/register', 'Actions/Auth/RegisterAction').rateLimit(3, 'minute')
|
|
|
49
50
|
// the endpoint's credential boundary, so the action opts out of browser CSRF.
|
|
50
51
|
route.post(oauthTokenPath, 'Actions/Auth/OAuthTokenAction').rateLimit(30, 'minute')
|
|
51
52
|
route.post(oauthRevocationPath, 'Actions/Auth/OAuthRevocationAction').rateLimit(30, 'minute')
|
|
53
|
+
route.post(oauthIntrospectionPath, 'Actions/Auth/OAuthIntrospectionAction').rateLimit(30, 'minute')
|
|
52
54
|
route.get(oauthAuthorizationPath, 'Actions/Auth/OAuthAuthorizationAction').rateLimit(60, 'minute')
|
|
53
55
|
route.post(oauthAuthorizationPath, 'Actions/Auth/OAuthConsentAction').middleware('auth').rateLimit(30, 'minute')
|
|
54
56
|
route.get(oauthMetadataPath, 'Actions/Auth/OAuthMetadataAction')
|