@stacksjs/defaults 0.74.56 → 0.74.58

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.
@@ -1,5 +1,5 @@
1
1
  import { Action } from '@stacksjs/actions'
2
- import { Auth, authCookie, createTwoFactorChallenge, getTwoFactorState } from '@stacksjs/auth'
2
+ import { Auth, authCookieForBrowserSession, createTwoFactorChallenge, getTwoFactorState, resolveBrowserSessionPolicy } from '@stacksjs/auth'
3
3
  import { response } from '@stacksjs/router'
4
4
  import { schema } from '@stacksjs/validation'
5
5
  import { PASSWORD_MAX_LENGTH, PASSWORD_PRESENCE_MESSAGE } from '../../password-policy'
@@ -28,6 +28,7 @@ export default new Action({
28
28
  async handle(request: RequestInstance) {
29
29
  const email = request.get('email')
30
30
  const password = request.get('password')
31
+ const remember = request.get('remember')
31
32
 
32
33
  // Verify once, then keep the observed password version locked while
33
34
  // choosing and issuing the challenge or token. A completed password reset
@@ -37,7 +38,14 @@ export default new Action({
37
38
  if (twoFactorEnabled) {
38
39
  return { kind: 'challenge' as const, token: await createTwoFactorChallenge(authedUser.id as number) }
39
40
  }
40
- return { kind: 'login' as const, result: await Auth.loginUsingId(authedUser.id as number) }
41
+ const policy = resolveBrowserSessionPolicy(remember)
42
+ return {
43
+ kind: 'login' as const,
44
+ result: await Auth.loginUsingId(authedUser.id as number, {
45
+ expiresInMinutes: policy.expiresInMinutes,
46
+ withRefreshToken: policy.withRefreshToken,
47
+ }),
48
+ }
41
49
  })
42
50
  if (!decision)
43
51
  return response.unauthorized('Incorrect email or password')
@@ -84,6 +92,6 @@ export default new Action({
84
92
  email: user?.email,
85
93
  name: user?.name,
86
94
  },
87
- }, { headers: { 'Set-Cookie': authCookie(result.token) } })
95
+ }, { headers: { 'Set-Cookie': authCookieForBrowserSession(result.token, result.expiresIn) } })
88
96
  },
89
97
  })
@@ -1,5 +1,5 @@
1
1
  import { Action } from '@stacksjs/actions'
2
- import { Auth, authCookie, consumeMagicLink } from '@stacksjs/auth'
2
+ import { Auth, authCookie, withMagicLink } from '@stacksjs/auth'
3
3
  import { config } from '@stacksjs/config'
4
4
  import { response } from '@stacksjs/router'
5
5
  import { schema } from '@stacksjs/validation'
@@ -20,7 +20,10 @@ export default new Action({
20
20
  if (!config.auth.magicLink?.enabled)
21
21
  return response.notFound('Magic-link sign-in is not enabled')
22
22
 
23
- const consumed = await consumeMagicLink(String(request.get('token')))
23
+ const consumed = await withMagicLink(String(request.get('token')), async grant => ({
24
+ grant,
25
+ result: await Auth.loginUsingId(grant.userId),
26
+ }))
24
27
  if (!consumed.ok) {
25
28
  const messages: Record<string, string> = {
26
29
  invalid: 'That sign-in link is not valid.',
@@ -33,7 +36,7 @@ export default new Action({
33
36
 
34
37
  // The same token pack + httpOnly cookie a password login issues, so
35
38
  // stxPageAuthMiddleware-gated pages treat passwordless users identically.
36
- const result = await Auth.loginUsingId(consumed.userId)
39
+ const { grant, result } = consumed.value
37
40
  if (!result)
38
41
  return response.unauthorized('That sign-in link is not valid.')
39
42
 
@@ -42,7 +45,7 @@ export default new Action({
42
45
  refresh_token: result.refreshToken,
43
46
  token_type: 'Bearer',
44
47
  expires_in: result.expiresIn,
45
- redirect_to: consumed.redirectTo,
48
+ redirect_to: grant.redirectTo,
46
49
  user: {
47
50
  id: result.user?.id,
48
51
  email: result.user?.email,
@@ -1,5 +1,5 @@
1
1
  import { Action } from '@stacksjs/actions'
2
- import { Auth, authCookie, consumeTwoFactorChallenge, verifyTwoFactorLoginCode } from '@stacksjs/auth'
2
+ import { Auth, authCookie, verifyTwoFactorLoginCode, withTwoFactorChallenge } from '@stacksjs/auth'
3
3
  import { response } from '@stacksjs/router'
4
4
  import { schema } from '@stacksjs/validation'
5
5
 
@@ -27,17 +27,12 @@ export default new Action({
27
27
  // (whether the code was right or wrong) must start over from
28
28
  // LoginAction, not retry — mirrors the WebAuthn challenge
29
29
  // delete-on-read semantics in passkey.ts (stacksjs/stacks#1866).
30
- const userId = await consumeTwoFactorChallenge(challengeToken)
31
- if (!userId)
32
- return response.unauthorized('This login attempt has expired — please sign in again.')
33
-
34
- const valid = await verifyTwoFactorLoginCode(userId, code)
35
- if (!valid)
36
- return response.unauthorized('Invalid code — please sign in again.')
37
-
38
- const result = await Auth.loginUsingId(userId)
30
+ const result = await withTwoFactorChallenge(challengeToken, async (userId) => {
31
+ if (!await verifyTwoFactorLoginCode(userId, code)) return null
32
+ return Auth.loginUsingId(userId)
33
+ })
39
34
  if (!result)
40
- return response.unauthorized('Invalid code — please sign in again.')
35
+ return response.unauthorized('Invalid or expired login attempt. Please sign in again.')
41
36
 
42
37
  const user = result.user
43
38
 
@@ -159,7 +159,7 @@ export function createCsrfCookie(req: Request, minted?: string): string {
159
159
  * behaviour: it says nothing about what the browser is looking at.
160
160
  */
161
161
  export function responseMayUseCsrfToken(response: Response): boolean {
162
- const type = (response.headers.get('content-type') || '').split(';')[0].trim().toLowerCase()
162
+ const type = ((response.headers.get('content-type') || '').split(';')[0] ?? '').trim().toLowerCase()
163
163
  if (!type)
164
164
  return true
165
165
  return type === 'text/html'
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.74.56",
5
+ "version": "0.74.58",
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.74.56",
5
+ "version": "0.74.58",
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.74.56",
58
+ "@stacksjs/mobile": "^0.74.58",
59
59
  "@stacksjs/sanitizer": "^0.2.113",
60
60
  "ts-qr-codes": "^0.1.8"
61
61
  }
@@ -28,10 +28,53 @@ concurrency:
28
28
  cancel-in-progress: false
29
29
 
30
30
  jobs:
31
+ # Is the commit CI passed still the tip of main?
32
+ #
33
+ # Deploys queue one at a time, and each checks out the commit its CI run
34
+ # was for. CI runs are not ordered: when two pushes land close together the
35
+ # older one can finish CI last, and its deploy then replaces the newer
36
+ # release with both deploys green. Checked here, when the deploy's turn
37
+ # comes: if main has moved on, the newer commit's own CI run deploys it.
38
+ current:
39
+ if: github.event_name == 'workflow_run'
40
+ runs-on: ubuntu-latest
41
+ permissions:
42
+ contents: read
43
+ outputs:
44
+ latest: ${{ steps.tip.outputs.latest }}
45
+ steps:
46
+ - id: tip
47
+ env:
48
+ GH_TOKEN: ${{ github.token }}
49
+ SHA: ${{ github.event.workflow_run.head_sha }}
50
+ run: |
51
+ tip=$(gh api "repos/${{ github.repository }}/commits/main" --jq .sha)
52
+ if [ "$tip" = "$SHA" ]; then
53
+ echo "latest=true" >> "$GITHUB_OUTPUT"
54
+ else
55
+ echo "latest=false" >> "$GITHUB_OUTPUT"
56
+ echo "::notice::Skipping ${SHA:0:8}: main is at ${tip:0:8}, whose CI run deploys it."
57
+ fi
58
+
31
59
  deploy:
60
+ needs: current
32
61
  # `workflow_run` fires for failed runs too — that is the point of the
33
62
  # gate, so check the conclusion.
34
- if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success'
63
+ #
64
+ # And check what CI ran on. CI also runs for pull requests, and
65
+ # `branches: [main]` matches the head branch name, so a fork's PR from a
66
+ # branch called `main` that passed CI would deploy the fork's commit with
67
+ # the production secrets. Only a push to this repository ships.
68
+ #
69
+ # `always()` because `current` is skipped for a manual dispatch, which
70
+ # ships whatever it was pointed at.
71
+ if: >-
72
+ always() && (
73
+ github.event_name == 'workflow_dispatch' ||
74
+ (github.event.workflow_run.conclusion == 'success' &&
75
+ github.event.workflow_run.event == 'push' &&
76
+ github.event.workflow_run.head_repository.full_name == github.repository &&
77
+ needs.current.outputs.latest == 'true'))
35
78
  runs-on: ubuntu-latest
36
79
  # Attach a GitHub Environment to get required reviewers and to scope these
37
80
  # secrets to it.