@stacksjs/defaults 0.75.181 → 0.75.182

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.
@@ -337,18 +337,43 @@ const result = await actions.resetPassword(token, newPassword)
337
337
  - `logout(): Promise<void>`
338
338
  - `refresh(): Promise<void>`
339
339
 
340
- ## Passkey/WebAuthn (passkey.ts)
341
-
342
- - `getUserPasskeys(userId): Promise<PasskeyAttribute[]>`
343
- - `getUserPasskey(userId, passkeyId): Promise<PasskeyAttribute | undefined>`
344
- - `setCurrentRegistrationOptions(user, verified): Promise<void>`
345
-
346
- ### Re-exported from @stacksjs/ts-auth
347
- - `generateRegistrationOptions`, `generateAuthenticationOptions`
348
- - `verifyRegistrationResponse`, `verifyAuthenticationResponse`
349
- - `startRegistration`, `startAuthentication` (browser)
350
- - `browserSupportsWebAuthn`, `browserSupportsWebAuthnAutofill`
351
- - `platformAuthenticatorIsAvailable`
340
+ ## Passkey/WebAuthn
341
+
342
+ Opt in with `config.auth.passkeys: { enabled: true, origin?: string, name?: string }`.
343
+ The canonical origin defaults to `config.app.url`; production requires HTTPS.
344
+ `buddy migrate` / `buddy auth:setup` create `passkey_ceremonies` and `passkeys`.
345
+
346
+ - `beginPasskeyRegistration({ id, email?, name? }, { origin, name })` returns
347
+ `{ ceremony, options }` as JSON. Call only for the authenticated account.
348
+ - `finishPasskeyRegistration(userId, ceremony, credentialJSON, party)` consumes
349
+ the account-bound challenge and saves only verified resident credentials.
350
+ - `beginPasskeyAuthentication(party)` returns discoverable options without an
351
+ email lookup, usable with conditional mediation and `autocomplete="username webauthn"`.
352
+ - `finishPasskeyAuthentication(ceremony, credentialJSON, party)` returns the
353
+ verified user ID; the application then issues its usual session.
354
+ - `listPasskeys(userId)` returns display-safe metadata, never public-key material.
355
+ - `removePasskey(userId, id, hasAlternativeSignIn?)` serializes removals and
356
+ refuses to remove the last key without another login method. Supply a callback
357
+ when password hashes may be unusable placeholders or other factors exist.
358
+
359
+ Default routes under `/auth/passkeys` expose registration/options, registration,
360
+ authentication/options, authentication (POST), listing (GET) and deletion (DELETE
361
+ `/{id}`). Public authentication endpoints require the canonical Origin header;
362
+ enrollment/removal also require auth and CSRF protection. Deleting the last key
363
+ requires proving the fallback password in the request. All responses are no-store.
364
+
365
+ Browser code imports **`@stacksjs/auth/passkeys-browser`**, never the server barrel.
366
+ It exports `startRegistration`, `startAuthentication(options, { signal?, mediation? })`,
367
+ `browserSupportsWebAuthn`, `browserSupportsWebAuthnAutofill`, the JSON option/credential
368
+ codecs and types. Abort a pending conditional request before explicit sign-in or
369
+ password submission, and on unmount. Serialize results using
370
+ `registrationCredentialToJSON` / `authenticationCredentialToJSON` before POSTing.
371
+ Native JS contexts require a platform passkey bridge; web support alone does not
372
+ provide one. Keep existing native sign-in available until that bridge exists.
373
+
374
+ Legacy helpers (`getUserPasskeys`, `getUserPasskey`, `storeWebAuthnChallenge`,
375
+ `consumeWebAuthnChallenge`) remain for existing integrations. New flows should
376
+ use the ceremony API, which supports concurrent anonymous browser attempts.
352
377
 
353
378
  ## Auth Middleware (middleware.ts)
354
379
 
@@ -18,7 +18,7 @@ Browser runtime bindings are documented separately in `stacks-auto-imports`.
18
18
  | `@stacksjs/api` | [`stacks-api`](../stacks-api/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/api/src/index.ts) | 7 |
19
19
  | `@stacksjs/arrays` | [`stacks-arrays`](../stacks-arrays/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/arrays/src/index.ts) | 6 |
20
20
  | `@stacksjs/audio` | [`stacks-audio`](../stacks-audio/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/audio/src/index.ts) | 2 |
21
- | `@stacksjs/auth` | [`stacks-auth`](../stacks-auth/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/index.ts) | 112 |
21
+ | `@stacksjs/auth` | [`stacks-auth`](../stacks-auth/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/index.ts) | 113 |
22
22
  | `@stacksjs/browser` | [`stacks-browser`](../stacks-browser/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/browser/src/index.ts) | 8 |
23
23
  | `@stacksjs/browser-extension` | [`stacks-browser-extension`](../stacks-browser-extension/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/browser-extension/src/index.ts) | 7 |
24
24
  | `@stacksjs/buddy` | [`stacks-buddy`](../stacks-buddy/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/src/index.ts) | 145 |
@@ -183,9 +183,9 @@ Guidance: [`stacks-auth`](../stacks-auth/SKILL.md). Source: [storage/framework/c
183
183
 
184
184
  Declared runtime exports: `DEFAULT_ROLE_PACKS`, `createBqbRbacStore`, `generateTOTP`, `generateTOTPSecret`, `seedDefaultRoles`, `totpKeyUri`, `verifyTOTP`.
185
185
 
186
- Local source modules: [src/authentication.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/authentication.ts), [src/authenticator.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/authenticator.ts), [src/authorizable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/authorizable.ts), [src/browser-session.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/browser-session.ts), [src/client.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/client.ts), [src/cookie-auth.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/cookie-auth.ts), [src/credential-user.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/credential-user.ts), [src/email-verification.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/email-verification.ts), [src/gate.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/gate.ts), [src/magic-link.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/magic-link.ts), [src/middleware.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/middleware.ts), [src/oauth-authorization-codes.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-authorization-codes.ts), [src/oauth-authorization-endpoint.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-authorization-endpoint.ts), [src/oauth-authorization-requests.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-authorization-requests.ts), [src/oauth-authorization-response.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-authorization-response.ts), [src/oauth-authorization.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-authorization.ts), [src/oauth-browser-session.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-browser-session.ts), [src/oauth-client-registration.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-client-registration.ts), [src/oauth-consent.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-consent.ts), [src/oauth-delegated-access.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-delegated-access.ts), [src/oauth-grants.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-grants.ts), [src/oauth-introspection.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-introspection.ts), [src/oauth-metadata.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-metadata.ts), [src/oauth-pkce.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-pkce.ts), [src/oauth-provider.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-provider.ts), [src/oauth-pruning.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-pruning.ts), [src/oauth-revocation.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-revocation.ts), [src/oauth-token-endpoint.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-token-endpoint.ts), [src/oauth-token-exchange.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-token-exchange.ts), [src/oauth-token-request.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-token-request.ts), [src/oauth-token-response.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-token-response.ts), [src/page-gate.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/page-gate.ts), [src/passkey.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/passkey.ts), [src/password/reset.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/password/reset.ts), [src/permissions.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/permissions.ts), [src/policy.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/policy.ts), [src/rate-limiter.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/rate-limiter.ts), [src/rbac-seed.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/rbac-seed.ts), [src/rbac-store-bqb.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/rbac-store-bqb.ts), [src/rbac.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/rbac.ts), [src/referrals.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/referrals.ts), [src/register.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/register.ts), [src/request-token.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/request-token.ts), [src/session-auth.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/session-auth.ts), [src/socials.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/socials.ts), [src/team.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/team.ts), [src/tokens.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/tokens.ts), [src/two-factor.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/two-factor.ts), [src/user.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/user.ts).
186
+ Local source modules: [src/authentication.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/authentication.ts), [src/authenticator.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/authenticator.ts), [src/authorizable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/authorizable.ts), [src/browser-session.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/browser-session.ts), [src/client.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/client.ts), [src/cookie-auth.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/cookie-auth.ts), [src/credential-user.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/credential-user.ts), [src/email-verification.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/email-verification.ts), [src/gate.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/gate.ts), [src/magic-link.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/magic-link.ts), [src/middleware.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/middleware.ts), [src/oauth-authorization-codes.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-authorization-codes.ts), [src/oauth-authorization-endpoint.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-authorization-endpoint.ts), [src/oauth-authorization-requests.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-authorization-requests.ts), [src/oauth-authorization-response.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-authorization-response.ts), [src/oauth-authorization.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-authorization.ts), [src/oauth-browser-session.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-browser-session.ts), [src/oauth-client-registration.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-client-registration.ts), [src/oauth-consent.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-consent.ts), [src/oauth-delegated-access.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-delegated-access.ts), [src/oauth-grants.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-grants.ts), [src/oauth-introspection.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-introspection.ts), [src/oauth-metadata.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-metadata.ts), [src/oauth-pkce.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-pkce.ts), [src/oauth-provider.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-provider.ts), [src/oauth-pruning.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-pruning.ts), [src/oauth-revocation.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-revocation.ts), [src/oauth-token-endpoint.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-token-endpoint.ts), [src/oauth-token-exchange.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-token-exchange.ts), [src/oauth-token-request.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-token-request.ts), [src/oauth-token-response.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-token-response.ts), [src/page-gate.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/page-gate.ts), [src/passkey.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/passkey.ts), [src/passkeys.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/passkeys.ts), [src/password/reset.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/password/reset.ts), [src/permissions.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/permissions.ts), [src/policy.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/policy.ts), [src/rate-limiter.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/rate-limiter.ts), [src/rbac-seed.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/rbac-seed.ts), [src/rbac-store-bqb.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/rbac-store-bqb.ts), [src/rbac.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/rbac.ts), [src/referrals.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/referrals.ts), [src/register.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/register.ts), [src/request-token.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/request-token.ts), [src/session-auth.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/session-auth.ts), [src/socials.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/socials.ts), [src/team.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/team.ts), [src/tokens.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/tokens.ts), [src/two-factor.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/two-factor.ts), [src/user.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/user.ts).
187
187
 
188
- Evidence directory: [112 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
188
+ Evidence directory: [113 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
189
189
 
190
190
  ### @stacksjs/browser
191
191
 
@@ -0,0 +1,23 @@
1
+ import { Action } from '@stacksjs/actions/runtime'
2
+ import { finishPasskeyAuthentication, Auth, resolveBrowserSessionPolicy, authCookieForBrowserSession } from '@stacksjs/auth'
3
+ import { passkeyParty, passkeyFailure } from './passkey-config'
4
+
5
+ export default new Action({
6
+ name: 'PasskeyAuthentication',
7
+ method: 'POST',
8
+ async handle(request) {
9
+ const party = passkeyParty()
10
+ if (!party) return new Response(null, { status: 404 })
11
+ if (request.headers.get('origin') !== new URL(party.origin).origin) return new Response(null, { status: 403 })
12
+ try {
13
+ const body = request.all()
14
+ const id = await finishPasskeyAuthentication(body.ceremony, body.credential, party)
15
+ const policy = resolveBrowserSessionPolicy(body.remember)
16
+ const session = await Auth.loginUsingId(id, { expiresInMinutes: policy.expiresInMinutes, withRefreshToken: policy.withRefreshToken })
17
+ if (!session) return new Response(null, { status: 401 })
18
+ return Response.json({ token: session.token, user: { id: session.user?.id, email: session.user?.email, name: session.user?.name }, expires_in: session.expiresIn, remembered: policy.remembered },
19
+ { headers: { 'Cache-Control': 'no-store', 'Set-Cookie': authCookieForBrowserSession(session.token, session.expiresIn) } })
20
+ }
21
+ catch (error) { return passkeyFailure(error) }
22
+ },
23
+ })
@@ -0,0 +1,17 @@
1
+ import { Action } from '@stacksjs/actions/runtime'
2
+ import { beginPasskeyAuthentication } from '@stacksjs/auth'
3
+ import { passkeyParty, passkeyFailure, passkeyJSON } from './passkey-config'
4
+
5
+ export default new Action({
6
+ name: 'PasskeyAuthenticationOptions',
7
+ method: 'POST',
8
+ async handle(request) {
9
+ const party = passkeyParty()
10
+ if (!party) return new Response(null, { status: 404 })
11
+ if (request.headers.get('origin') !== new URL(party.origin).origin) return new Response(null, { status: 403 })
12
+ try {
13
+ return passkeyJSON(await beginPasskeyAuthentication(party))
14
+ }
15
+ catch (error) { return passkeyFailure(error) }
16
+ },
17
+ })
@@ -0,0 +1,24 @@
1
+ import { Action } from '@stacksjs/actions/runtime'
2
+ import { verifyHash } from '@stacksjs/security'
3
+ import { removePasskey } from '@stacksjs/auth'
4
+ import { passkeyParty, passkeyFailure, passkeyJSON } from './passkey-config'
5
+
6
+ export default new Action({
7
+ name: 'PasskeyDestroy',
8
+ method: 'DELETE',
9
+ async handle(request) {
10
+ const party = passkeyParty()
11
+ if (!party) return new Response(null, { status: 404 })
12
+ try {
13
+ const user = await request.user()
14
+ if (!user) return new Response(null, { status: 401 })
15
+ const id = request.getParam('id')
16
+ if (typeof id !== 'string' || !/^[A-Za-z0-9_-]{1,1366}$/.test(id)) return new Response(null, { status: 422 })
17
+ return await removePasskey(Number(user.id), id, async row => {
18
+ const password = request.get('password')
19
+ return typeof password === 'string' && !!password && typeof row.password === 'string' && await verifyHash(password, row.password)
20
+ }) ? passkeyJSON({ ok: true }) : new Response(null, { status: 404 })
21
+ }
22
+ catch (error) { return passkeyFailure(error) }
23
+ },
24
+ })
@@ -0,0 +1,18 @@
1
+ import { Action } from '@stacksjs/actions/runtime'
2
+ import { listPasskeys } from '@stacksjs/auth'
3
+ import { passkeyParty, passkeyFailure, passkeyJSON } from './passkey-config'
4
+
5
+ export default new Action({
6
+ name: 'PasskeyIndex',
7
+ method: 'GET',
8
+ async handle(request) {
9
+ const party = passkeyParty()
10
+ if (!party) return new Response(null, { status: 404 })
11
+ try {
12
+ const user = await request.user()
13
+ if (!user) return new Response(null, { status: 401 })
14
+ return passkeyJSON({ passkeys: await listPasskeys(Number(user.id)) })
15
+ }
16
+ catch (error) { return passkeyFailure(error) }
17
+ },
18
+ })
@@ -0,0 +1,20 @@
1
+ import { Action } from '@stacksjs/actions/runtime'
2
+ import { finishPasskeyRegistration } from '@stacksjs/auth'
3
+ import { passkeyParty, passkeyFailure, passkeyJSON } from './passkey-config'
4
+
5
+ export default new Action({
6
+ name: 'PasskeyRegistration',
7
+ method: 'POST',
8
+ async handle(request) {
9
+ const party = passkeyParty()
10
+ if (!party) return new Response(null, { status: 404 })
11
+ try {
12
+ const user = await request.user()
13
+ if (!user) return new Response(null, { status: 401 })
14
+ const body = request.all()
15
+ await finishPasskeyRegistration(Number(user.id), body.ceremony, body.credential, party)
16
+ return passkeyJSON({ ok: true })
17
+ }
18
+ catch (error) { return passkeyFailure(error) }
19
+ },
20
+ })
@@ -0,0 +1,18 @@
1
+ import { Action } from '@stacksjs/actions/runtime'
2
+ import { beginPasskeyRegistration } from '@stacksjs/auth'
3
+ import { passkeyParty, passkeyFailure, passkeyJSON } from './passkey-config'
4
+
5
+ export default new Action({
6
+ name: 'PasskeyRegistrationOptions',
7
+ method: 'POST',
8
+ async handle(request) {
9
+ const party = passkeyParty()
10
+ if (!party) return new Response(null, { status: 404 })
11
+ try {
12
+ const user = await request.user()
13
+ if (!user) return new Response(null, { status: 401 })
14
+ return passkeyJSON(await beginPasskeyRegistration({ id: Number(user.id), email: user.email, name: user.name }, party))
15
+ }
16
+ catch (error) { return passkeyFailure(error) }
17
+ },
18
+ })
@@ -0,0 +1,13 @@
1
+ import { config } from '@stacksjs/config'
2
+ import { PasskeyError } from '@stacksjs/auth'
3
+ export function passkeyParty() {
4
+ const options = config.auth.passkeys
5
+ return options?.enabled ? { origin: options.origin || config.app.url || 'http://localhost:3333', name: options.name || config.app.name || 'Stacks' } : null
6
+ }
7
+ export function passkeyFailure(error: unknown): Response {
8
+ if (!(error instanceof PasskeyError)) throw error
9
+ return Response.json({ error: error.message }, { status: 400, headers: { 'Cache-Control': 'no-store' } })
10
+ }
11
+ export function passkeyJSON(value: unknown): Response {
12
+ return Response.json(value, { headers: { 'Cache-Control': 'no-store' } })
13
+ }
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.75.181",
5
+ "version": "0.75.182",
6
6
  "description": "Stacks, built into VS Code: stx templates, pickier linting and formatting, encrypted env files, the dev server preview and every buddy command. No other extensions required.",
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.75.181",
5
+ "version": "0.75.182",
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.35",
58
- "@stacksjs/mobile": "0.75.181",
58
+ "@stacksjs/mobile": "0.75.182",
59
59
  "@stacksjs/sanitizer": "^0.2.483",
60
60
  "@xterm/addon-fit": "^0.11.0",
61
61
  "@xterm/xterm": "^6.0.0",
package/routes/auth.ts CHANGED
@@ -121,3 +121,12 @@ route.group({ prefix: '/password' }, () => {
121
121
  route.post('/reset', 'Actions/Password/PasswordResetAction').rateLimit(5, 'minute')
122
122
  route.post('/verify-token', 'Actions/Password/VerifyResetTokenAction').rateLimit(10, 'minute')
123
123
  })
124
+
125
+ // Discoverable passkeys: canonical JSON ceremonies, independent concurrent browser attempts.
126
+ // Public flows carry a one-use ceremony capability; enrollment and removal also require auth/CSRF.
127
+ route.post('/auth/passkeys/authentication/options', 'Actions/Auth/PasskeyAuthenticationOptionsAction').skipCsrf().rateLimit(10, 'minute')
128
+ route.post('/auth/passkeys/authentication', 'Actions/Auth/PasskeyAuthenticationAction').skipCsrf().rateLimit(10, 'minute')
129
+ route.post('/auth/passkeys/registration/options', 'Actions/Auth/PasskeyRegistrationOptionsAction').middleware('auth').rateLimit(5, 'minute')
130
+ route.post('/auth/passkeys/registration', 'Actions/Auth/PasskeyRegistrationAction').middleware('auth').rateLimit(5, 'minute')
131
+ route.get('/auth/passkeys', 'Actions/Auth/PasskeyIndexAction').middleware('auth')
132
+ route.delete('/auth/passkeys/{id}', 'Actions/Auth/PasskeyDestroyAction').middleware('auth').rateLimit(10, 'minute')