@stacksjs/defaults 0.70.365 → 0.70.367
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-browse/SKILL.md +9 -1
- package/ai/skills/stacks-dashboard/SKILL.md +71 -14
- package/ai/skills/stacks-deploy/SKILL.md +18 -0
- package/ai/skills/stacks-desktop/SKILL.md +11 -2
- package/ai/skills/stacks-stx/SKILL.md +25 -0
- package/app/Actions/Auth/LoginAction.ts +11 -2
- package/app/Actions/Auth/RefreshTokenAction.ts +6 -2
- package/app/Actions/Auth/RegisterAction.ts +6 -2
- package/app/Actions/Auth/VerifyTwoFactorLoginAction.ts +6 -2
- package/app/Actions/Dashboard/Deployments/CreateDeployment.ts +64 -44
- package/app/Actions/Dashboard/Deployments/CreateDeploymentRollback.ts +26 -0
- package/app/Actions/Dashboard/Deployments/PreviewDeployment.ts +39 -0
- package/app/Actions/Dashboard/Deployments/PreviewDeploymentRollback.ts +22 -0
- package/app/Actions/Dashboard/Deployments/deployment-input.test.ts +12 -2
- package/app/Actions/Dashboard/Deployments/deployment-input.ts +5 -3
- package/app/Actions/Dashboard/Deployments/deployment-preview.test.ts +29 -0
- package/app/Actions/Dashboard/Deployments/deployment-preview.ts +41 -0
- package/app/Actions/Dashboard/Deployments/deployment-rollback.test.ts +16 -0
- package/app/Actions/Dashboard/Deployments/deployment-rollback.ts +89 -0
- package/app/Actions/Dashboard/Operations/AuditIndexAction.ts +25 -0
- package/app/Actions/Dashboard/Operations/ChangeApprovalAction.ts +28 -0
- package/app/Actions/Dashboard/Operations/ChangeIndexAction.ts +32 -0
- package/app/Actions/Dashboard/Operations/IncidentIndexAction.ts +32 -0
- package/app/Actions/Dashboard/Operations/IncidentUpdateAction.ts +34 -0
- package/app/Actions/Dashboard/Operations/MigrationApplyAction.ts +30 -0
- package/app/Actions/Dashboard/Operations/MigrationIndexAction.ts +24 -0
- package/app/Actions/Dashboard/Operations/MigrationReconcileAction.ts +23 -0
- package/app/Actions/Dashboard/Operations/RecoveryDestinationStoreAction.ts +91 -0
- package/app/Actions/Dashboard/Operations/RecoveryDestinationTestAction.ts +34 -0
- package/app/Actions/Dashboard/Operations/RecoveryIndexAction.ts +58 -0
- package/app/Actions/Dashboard/Operations/RecoveryPolicyStoreAction.ts +90 -0
- package/app/Actions/Dashboard/Operations/RecoveryProtectAction.ts +32 -0
- package/app/Actions/Dashboard/Operations/RecoveryRestoreAction.ts +72 -0
- package/app/Actions/Dashboard/Operations/RecoveryRetentionAction.ts +17 -0
- package/app/Actions/Dashboard/Operations/RecoveryRunAction.ts +25 -0
- package/app/Actions/Dashboard/Operations/RecoveryVerifyAction.ts +24 -0
- package/app/Actions/Dashboard/Operations/SchedulerIndexAction.ts +34 -0
- package/app/Actions/Dashboard/Operations/SchedulerRunAction.ts +33 -0
- package/app/Actions/Dashboard/Operations/SchedulerToggleAction.ts +36 -0
- package/app/Actions/Dashboard/Operations/control-plane.ts +150 -0
- package/app/Actions/Dashboard/Operations/migration-operations.ts +80 -0
- package/app/Actions/Dashboard/Operations/operations-runtime.ts +11 -0
- package/app/Actions/Dashboard/Operations/recovery-input.test.ts +22 -0
- package/app/Actions/Dashboard/Operations/recovery-input.ts +33 -0
- package/app/Actions/Dashboard/Operations/recovery-runtime.ts +99 -0
- package/app/Actions/Dashboard/Operations/scheduler-operations.test.ts +23 -0
- package/app/Actions/Dashboard/Operations/scheduler-operations.ts +138 -0
- package/functions/deployments.ts +18 -0
- package/functions/operations.ts +361 -0
- package/ide/vscode/package.json +1 -1
- package/package.json +1 -1
- package/resources/components/Dashboard/Auth/ForgotPasswordDashboard.stx +0 -2
- package/resources/components/Dashboard/Auth/LoginDashboard.stx +1 -1
- package/resources/components/Dashboard/Auth/RegisterDashboard.stx +1 -3
- package/resources/components/Dashboard/Ci/CiDashboard.stx +4 -4
- package/resources/components/Dashboard/Deployments/DeploymentList.stx +147 -14
- package/resources/components/Dashboard/Deployments/DeploymentPreviewDialog.stx +118 -0
- package/resources/components/Dashboard/GlobalSearch.stx +87 -91
- package/resources/components/Dashboard/Kanban/KanbanBoardDashboard.stx +9 -435
- package/resources/components/Dashboard/Kanban/KanbanCardDialog.stx +416 -0
- package/resources/components/Dashboard/Modals/BaseModal.stx +13 -36
- package/resources/components/Dashboard/Modals/Popups/Alert.stx +18 -44
- package/resources/components/Dashboard/Operations/AuditDashboard.stx +16 -0
- package/resources/components/Dashboard/Operations/ChangeDashboard.stx +66 -0
- package/resources/components/Dashboard/Operations/IncidentDashboard.stx +39 -0
- package/resources/components/Dashboard/Operations/MigrationDashboard.stx +146 -0
- package/resources/components/Dashboard/Operations/OperationHistory.stx +61 -0
- package/resources/components/Dashboard/Operations/OperationsNavigation.stx +32 -0
- package/resources/components/Dashboard/Operations/RecoveryDashboard.stx +229 -0
- package/resources/components/Dashboard/Operations/RecoveryDestinationDialog.stx +126 -0
- package/resources/components/Dashboard/Operations/RecoveryJobList.stx +32 -0
- package/resources/components/Dashboard/Operations/RecoveryPointTable.stx +82 -0
- package/resources/components/Dashboard/Operations/RecoveryPolicyDialog.stx +155 -0
- package/resources/components/Dashboard/Operations/RecoveryRestoreDialog.stx +90 -0
- package/resources/components/Dashboard/Operations/SchedulerDashboard.stx +144 -0
- package/resources/components/Dashboard/Operations/SchedulerTaskTable.stx +81 -0
- package/resources/components/Dashboard/Settings/SettingsDashboard.stx +45 -3
- package/resources/components/Dashboard/UI/Modal.stx +43 -14
- package/resources/components/Dashboard/UI/Table.stx +30 -25
- package/resources/functions/dashboard/sidebar.ts +1 -0
- package/routes/dashboard-api.ts +23 -0
- package/routes/dashboard.ts +1 -0
- package/views/dashboard/AUDIT.md +21 -16
- package/views/dashboard/operations/audit.stx +5 -0
- package/views/dashboard/operations/changes.stx +5 -0
- package/views/dashboard/operations/incidents.stx +5 -0
- package/views/dashboard/operations/migrations.stx +8 -0
- package/views/dashboard/operations/recovery.stx +8 -0
- package/views/dashboard/operations/scheduler.stx +8 -0
- package/views/dashboard/stores/ci.ts +2 -2
|
@@ -45,6 +45,11 @@ you to `brew install --cask chromium` or set `BROWSE_BROWSER`.
|
|
|
45
45
|
- Docs: `http://localhost:3005`
|
|
46
46
|
- API: `http://localhost:3008`
|
|
47
47
|
|
|
48
|
+
For headless dashboard QA, start the server with
|
|
49
|
+
`STACKS_NO_NATIVE=1 ./buddy dev --dashboard`. Never point `CRAFT_BIN` at a
|
|
50
|
+
missing file to suppress the native window because invalid binary overrides are
|
|
51
|
+
configuration errors.
|
|
52
|
+
|
|
48
53
|
> Pages with an open HMR/SSE connection never reach "network idle"; the driver waits for
|
|
49
54
|
> the load event (with a timeout) plus a short settle, so it won't hang on the dev server.
|
|
50
55
|
|
|
@@ -133,7 +138,10 @@ diagnostics without printing every visited path or overflowing element.
|
|
|
133
138
|
|
|
134
139
|
When testing a Stacks app, check:
|
|
135
140
|
- **Dashboard routes** — admin pages rendering? (`localhost:3002`)
|
|
136
|
-
- **API health** — `GET localhost:3008/health` returns ok?
|
|
141
|
+
- **API health** — `GET localhost:3008/api/health` returns ok? (`/api/health`, not
|
|
142
|
+
`/health`: the probe is mounted under `/api` so it cannot collide with a page of
|
|
143
|
+
the same name, and the API port answers `/api` only - every other path is a 404
|
|
144
|
+
there by design, so point page checks at the frontend instead.)
|
|
137
145
|
- **Auth flow** — `/login`, `/register`
|
|
138
146
|
- **CMS/blog** — `/blog`, post detail pages, `/blog/feed.xml`, `/blog/sitemap.xml`
|
|
139
147
|
- **STX components** — do custom components render server-side?
|
|
@@ -111,12 +111,60 @@ inject message HTML into the dashboard document.
|
|
|
111
111
|
- `/deployments/{id}` - one persisted Deployment model record
|
|
112
112
|
|
|
113
113
|
The deployment page composes `DeploymentList`, `DeploymentTable`,
|
|
114
|
-
`DeployScript`, and `LiveTerminalOutput`.
|
|
114
|
+
`DeploymentPreviewDialog`, `DeployScript`, and `LiveTerminalOutput`. The
|
|
115
|
+
Preview action first collects the environment and optional domain, then calls
|
|
116
|
+
the guarded `POST /api/dashboard/deployments/preview` Action. That Action runs
|
|
117
|
+
the native `buddy deploy --dry-run --json` planner and returns its versioned,
|
|
118
|
+
non-mutating plan. The dialog renders the ordered operations and resolved sites
|
|
119
|
+
before the user may continue to the separate real deployment confirmation.
|
|
120
|
+
Script reads and atomic writes use
|
|
115
121
|
`GET|PUT /api/dashboard/deployments/script`. The terminal uses
|
|
116
122
|
`GET /api/dashboard/deployments/terminal` and pauses polling while the document
|
|
117
123
|
is hidden. Do not create separate `/deployments/scripts` or
|
|
118
124
|
`/deployments/live-terminal` pages.
|
|
119
125
|
|
|
126
|
+
Deployment recovery uses `POST /api/dashboard/deployments/rollback/preview`
|
|
127
|
+
followed by `POST /api/dashboard/deployments/rollback`. The preview must run
|
|
128
|
+
the native `buddy deploy:rollback --dry-run` path successfully, and execution
|
|
129
|
+
must re-run that preview, compare its revision, and require the typed target
|
|
130
|
+
environment confirmation. Never implement rollback by editing release links,
|
|
131
|
+
restarting services directly, or guessing a prior release from Deployment
|
|
132
|
+
model rows. Deployment rows are application history. Preserved releases and
|
|
133
|
+
activation are owned by ts-cloud.
|
|
134
|
+
|
|
135
|
+
### Operations control plane
|
|
136
|
+
|
|
137
|
+
The Operations sidebar entry deliberately remains one item. Section navigation
|
|
138
|
+
lives in `Dashboard/Operations/OperationsNavigation.stx`:
|
|
139
|
+
|
|
140
|
+
- `/operations/changes` - unified change review, active work, release approvals
|
|
141
|
+
- `/operations/scheduler` - registered task runs and persisted pause state
|
|
142
|
+
- `/operations/recovery` - destinations, policies, recovery points, restore drills
|
|
143
|
+
- `/operations/migrations` - model diff, schema effects, ledger reconciliation
|
|
144
|
+
- `/operations/incidents` - native alerts, health rules, ownership, silence state
|
|
145
|
+
- `/operations/audit` - append-only operator events and correlations
|
|
146
|
+
|
|
147
|
+
Operational state belongs in the ts-cloud control plane initialized by
|
|
148
|
+
`Operations/control-plane.ts`. Use its stores for durable operations, events,
|
|
149
|
+
releases, approvals, alerts, backups, actors, and environments. Do not create a
|
|
150
|
+
parallel dashboard-only JSON file or duplicate those entities in application
|
|
151
|
+
models. Application domain records still follow the normal `app/Models` and
|
|
152
|
+
`useApi` convention. Use dashboard Actions for aggregate operational views and
|
|
153
|
+
guard every route in `dashboard-api.ts`.
|
|
154
|
+
|
|
155
|
+
Every mutating operator action must resolve the authenticated actor and append
|
|
156
|
+
or correlate a control-plane event. Use `trackOperatorOperation()` for bounded
|
|
157
|
+
synchronous work and the native durable queue for long-running backup, restore,
|
|
158
|
+
or provider work. Empty states must reflect real absence of configuration or
|
|
159
|
+
events. Never seed operational pages with sample incidents, releases, backups,
|
|
160
|
+
or health data.
|
|
161
|
+
|
|
162
|
+
Migration execution must start from `previewPendingMigrations()`, audit the
|
|
163
|
+
ledger against live schema effects, hash the reviewed plan, and recheck it
|
|
164
|
+
immediately before `buddy migrate`. Only `reconcileMigrationLedger()` may
|
|
165
|
+
repair provable ledger drift. Partial and unverifiable migrations require human
|
|
166
|
+
review and must not be silently recorded.
|
|
167
|
+
|
|
120
168
|
### Utilities
|
|
121
169
|
- `/health`, `/insights`, `/logs` - operational health and logs
|
|
122
170
|
- `/servers`, `/serverless`, `/realtime` - runtime infrastructure
|
|
@@ -354,20 +402,19 @@ drawers must use the shared `dashboard-modal-layer` class so their interactive
|
|
|
354
402
|
surface starts beside that sidebar and returns to `left: 0` on mobile and in
|
|
355
403
|
the Craft native-sidebar shell.
|
|
356
404
|
|
|
357
|
-
Use
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
</div>
|
|
365
|
-
```
|
|
405
|
+
Use `Dashboard/UI/Modal`, `Dashboard/UI/Drawer`, and
|
|
406
|
+
`Dashboard/UI/ConfirmDialog` for page dialogs, inspectors, forms, and
|
|
407
|
+
confirmations. They own native `<dialog>` behavior, scroll locking, focus
|
|
408
|
+
restoration, Escape and backdrop handling, accessibility labels, and the
|
|
409
|
+
sidebar-aware boundary. `Dashboard/Modals/BaseModal` and
|
|
410
|
+
`Dashboard/Modals/Popups/Alert` are compatibility wrappers over that same
|
|
411
|
+
primitive, not alternate overlay implementations.
|
|
366
412
|
|
|
367
|
-
Do not
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
413
|
+
Do not add a page-owned `fixed inset-0` overlay. A purpose-built application
|
|
414
|
+
surface such as the global command palette or mobile navigation may own a
|
|
415
|
+
custom layer only when the shared dialog or drawer semantics do not fit. It
|
|
416
|
+
must still use `dashboard-modal-layer` and provide complete keyboard, focus,
|
|
417
|
+
and ARIA behavior. Do not solve sidebar overlap by increasing z-index alone.
|
|
371
418
|
|
|
372
419
|
### Live dashboard audit
|
|
373
420
|
|
|
@@ -378,6 +425,9 @@ the project root:
|
|
|
378
425
|
bun storage/framework/defaults/ai/skills/stacks-dashboard/scripts/audit.ts
|
|
379
426
|
# Or target a non-default origin:
|
|
380
427
|
bun storage/framework/defaults/ai/skills/stacks-dashboard/scripts/audit.ts --base-url http://127.0.0.1:3002
|
|
428
|
+
|
|
429
|
+
# Exercise hydrated navigation, console errors, failed requests, and layout:
|
|
430
|
+
bun storage/framework/defaults/ai/skills/stacks-browse/scripts/browse.ts crawl http://localhost:3002/ --max 500 --settle 350 --summary
|
|
381
431
|
```
|
|
382
432
|
|
|
383
433
|
Pass a base URL as the first argument when the dashboard is not on
|
|
@@ -388,6 +438,13 @@ dashboard API. It fails on missing page renders, invalid fragment contracts,
|
|
|
388
438
|
empty or non-HTML pages, unresolved component tags, 5xx or method-mismatch
|
|
389
439
|
APIs, HTML API fallbacks, invalid JSON, and HTTP-200 error payloads.
|
|
390
440
|
|
|
441
|
+
The dependency-free browser crawl follows the rendered link graph in a real
|
|
442
|
+
browser and fails on non-200 pages, console errors, failed subrequests, or
|
|
443
|
+
horizontal overflow. Seed source-only routes with repeated `--path` flags,
|
|
444
|
+
including optional or parameterized pages that the current data set does not
|
|
445
|
+
link. The HTTP audit and browser crawl cover different boundaries, so run both
|
|
446
|
+
for exhaustive dashboard work.
|
|
447
|
+
|
|
391
448
|
Run this after dashboard route, Action, STX, model, migration, or dev-server
|
|
392
449
|
changes. Record provider-backed or destructive success paths as explicit
|
|
393
450
|
environment boundaries. Never replace a failed live contract with sample data
|
|
@@ -16,6 +16,23 @@ The deployment workflow for Stacks applications.
|
|
|
16
16
|
buddy deploy
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
Preview the complete environment-aware plan before changing anything:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
buddy deploy --dry-run
|
|
23
|
+
buddy deploy --dry-run --env staging
|
|
24
|
+
buddy deploy --dry-run --site docs
|
|
25
|
+
buddy deploy --dry-run --json
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The preview uses the same environment transformation and ts-cloud site
|
|
29
|
+
classification as a real deploy. It reports the ordered validation,
|
|
30
|
+
infrastructure, build, package, release, runtime, gateway, DNS, TLS, and
|
|
31
|
+
optional container operations. It exits before prerequisite setup, builds,
|
|
32
|
+
packaging, hooks, provider calls, persistence, DNS, TLS, or service restarts.
|
|
33
|
+
The JSON form emits a versioned `STACKS_DEPLOY_PREVIEW_JSON=` line for API and
|
|
34
|
+
tool consumers.
|
|
35
|
+
|
|
19
36
|
## Deployment Prerequisites
|
|
20
37
|
|
|
21
38
|
1. **AWS credentials configured**: `buddy configure:aws`
|
|
@@ -87,6 +104,7 @@ buddy deploy
|
|
|
87
104
|
|
|
88
105
|
```bash
|
|
89
106
|
buddy deploy # deploy to cloud
|
|
107
|
+
buddy deploy --dry-run # preview without changing anything
|
|
90
108
|
buddy cloud --diff # preview infrastructure changes before deploying
|
|
91
109
|
buddy cloud --ssh # SSH into deployed server
|
|
92
110
|
buddy cloud:remove # tear down infrastructure
|
|
@@ -23,8 +23,17 @@ Craft window. `buddy build:desktop` compiles a small Stacks launcher and places
|
|
|
23
23
|
the launcher, pinned Craft runtime, manifest, provenance, and checksums in
|
|
24
24
|
`storage/framework/desktop-dist`.
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
Craft is normally installed through pantry and resolved from PATH. Set
|
|
27
|
+
`CRAFT_BIN` to an explicit compiled native binary in CI or local Craft
|
|
28
|
+
development. When testing local SDK source as well, set `CRAFT_SDK_SRC` to its
|
|
29
|
+
`src/index.ts` entry. Set `STACKS_NO_NATIVE=1` only when a deliberately web-only
|
|
30
|
+
dashboard process is required, such as headless browser QA. A missing
|
|
31
|
+
`CRAFT_BIN` is an error and is never treated as a headless flag.
|
|
32
|
+
|
|
33
|
+
Native development windows load the dashboard through its loopback HTTP
|
|
34
|
+
origin. Keep pretty HTTPS domains for browser access. WKWebView does not assume
|
|
35
|
+
that a developer's local certificate authority is trusted, and a failed TLS
|
|
36
|
+
navigation otherwise appears as a blank native window.
|
|
28
37
|
|
|
29
38
|
## API
|
|
30
39
|
|
|
@@ -91,6 +91,30 @@ STX signals are callable. Read with `count()`, replace with
|
|
|
91
91
|
`count.update(value => value + 1)`. Do not use Vue-style `.value` access in
|
|
92
92
|
STX templates or `resources/functions`.
|
|
93
93
|
|
|
94
|
+
### Client loop conditionals
|
|
95
|
+
|
|
96
|
+
Attribute loops create a client-owned scope. Their item and index aliases may
|
|
97
|
+
be used with either reactive attribute conditionals or STX block conditionals.
|
|
98
|
+
The compiler promotes a block chain that reads those aliases into one reactive
|
|
99
|
+
runtime chain:
|
|
100
|
+
|
|
101
|
+
```html
|
|
102
|
+
<template :for="(field, index) in fields()">
|
|
103
|
+
@if(field.editable)
|
|
104
|
+
<input x-model="drafts[field.key]">
|
|
105
|
+
@elseif(index === 0)
|
|
106
|
+
<span>First read-only field</span>
|
|
107
|
+
@else
|
|
108
|
+
<span>{{ field.value }}</span>
|
|
109
|
+
@endif
|
|
110
|
+
</template>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Loop aliases are scoped to the owning element and inherited by nested client
|
|
114
|
+
loops. A condition outside that element remains server-owned. Do not mix a
|
|
115
|
+
server context value into a client loop chain. Serialize the value into client
|
|
116
|
+
state when the browser must reevaluate it.
|
|
117
|
+
|
|
94
118
|
### Native form binding
|
|
95
119
|
|
|
96
120
|
Use `x-model` for two-way form state instead of pairing `:value` with a
|
|
@@ -260,6 +284,7 @@ await addLayout('admin', { nav: true, footer: true })
|
|
|
260
284
|
- **Server-to-client values are explicit** - a JSON-serializable top-level value exported by `<script server>` can be referenced by name in `<script client>`. STX serializes only referenced values and never overwrites a client-owned declaration
|
|
261
285
|
- **`storage/framework/stx/`** — stx build cache and the generated route manifest. `config/ui.ts` sets stx's `stateDir` here, so nothing lands in the project root. Gitignored; safe to delete
|
|
262
286
|
- **Reactivity is signal-based** - use callable `state()` and `derived()` signals, not Vue-style `ref()` or `.value`
|
|
287
|
+
- **Client loop conditions** - `@if` chains that read `:for` or `@for` aliases compile into scoped runtime chains; aliases do not leak outside the loop element
|
|
263
288
|
- **Component tag case is semantic** - `<Input v-model:value="query">` is a paired component, while lowercase `<input v-model="query">` is a native void element. Keep component tags PascalCase, including names that collide with native elements
|
|
264
289
|
- **Structural DOM timing** - use `nextTick()` with `useRef()` after opening reactive markup
|
|
265
290
|
- **Crosswind for styling** — use utility classes, not inline styles
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Action } from '@stacksjs/actions'
|
|
2
|
-
import { Auth, createTwoFactorChallenge, getTwoFactorState } from '@stacksjs/auth'
|
|
2
|
+
import { Auth, authCookie, createTwoFactorChallenge, getTwoFactorState } from '@stacksjs/auth'
|
|
3
3
|
import { User } from '@stacksjs/orm'
|
|
4
4
|
import { response } from '@stacksjs/router'
|
|
5
5
|
import { schema } from '@stacksjs/validation'
|
|
@@ -66,6 +66,15 @@ export default new Action({
|
|
|
66
66
|
// The legacy `token` field is kept for backward compatibility
|
|
67
67
|
// with clients that haven't been updated yet — it shadows
|
|
68
68
|
// `access_token` and will be removed in a future major.
|
|
69
|
+
//
|
|
70
|
+
// The same token also goes out as an httpOnly cookie, matching
|
|
71
|
+
// SocialCallbackAction. Without it, how a browser ends up signed in
|
|
72
|
+
// depended on which way it signed in: OAuth left a cookie, email+password
|
|
73
|
+
// left only JSON, and a server-rendered page cannot read JSON — it posts a
|
|
74
|
+
// form, follows a redirect, and comes back carrying nothing but cookies.
|
|
75
|
+
// So the first authenticated document render had no way to identify the
|
|
76
|
+
// user (#2306). The body is unchanged, so an API client that ignores the
|
|
77
|
+
// cookie behaves exactly as before.
|
|
69
78
|
return response.json({
|
|
70
79
|
access_token: result.token,
|
|
71
80
|
refresh_token: result.refreshToken,
|
|
@@ -77,6 +86,6 @@ export default new Action({
|
|
|
77
86
|
email: user?.email,
|
|
78
87
|
name: user?.name,
|
|
79
88
|
},
|
|
80
|
-
})
|
|
89
|
+
}, { headers: { 'Set-Cookie': authCookie(result.token) } })
|
|
81
90
|
},
|
|
82
91
|
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Action } from '@stacksjs/actions'
|
|
2
|
-
import { refreshToken } from '@stacksjs/auth'
|
|
2
|
+
import { authCookie, refreshToken } from '@stacksjs/auth'
|
|
3
3
|
import { response } from '@stacksjs/router'
|
|
4
4
|
import { schema } from '@stacksjs/validation'
|
|
5
5
|
|
|
@@ -25,12 +25,16 @@ export default new Action({
|
|
|
25
25
|
refreshExpiresInDays: 30, // 30 day refresh token
|
|
26
26
|
})
|
|
27
27
|
|
|
28
|
+
// Rotation invalidates the token the cookie was carrying, so a cookie
|
|
29
|
+
// left untouched here would go stale at the exact moment the session was
|
|
30
|
+
// meant to be extended: the browser keeps presenting a revoked token
|
|
31
|
+
// while the JSON body holds a live one it cannot read (#2306).
|
|
28
32
|
return response.json({
|
|
29
33
|
access_token: result.plainTextToken,
|
|
30
34
|
refresh_token: result.refreshToken,
|
|
31
35
|
token_type: 'Bearer',
|
|
32
36
|
expires_in: result.expiresIn,
|
|
33
|
-
})
|
|
37
|
+
}, { headers: { 'Set-Cookie': authCookie(result.plainTextToken) } })
|
|
34
38
|
}
|
|
35
39
|
catch (error: any) {
|
|
36
40
|
return response.unauthorized(error.message || 'Invalid or expired refresh token')
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Action } from '@stacksjs/actions'
|
|
2
|
-
import { Auth, register } from '@stacksjs/auth'
|
|
2
|
+
import { Auth, authCookie, register } from '@stacksjs/auth'
|
|
3
3
|
import { dispatch } from '@stacksjs/events'
|
|
4
4
|
import { response } from '@stacksjs/router'
|
|
5
5
|
import { schema } from '@stacksjs/validation'
|
|
@@ -53,6 +53,10 @@ export default new Action({
|
|
|
53
53
|
// were signed out an hour into their first session while every other
|
|
54
54
|
// user refreshed normally (stacksjs/stacks#2212). The legacy `token`
|
|
55
55
|
// alias stays for backward compatibility.
|
|
56
|
+
// Registering signs the account in, so it is a session-issuing path like
|
|
57
|
+
// the other three and carries the cookie for the same reason: the very
|
|
58
|
+
// next thing a server-rendered app does is redirect to an authenticated
|
|
59
|
+
// page (#2306).
|
|
56
60
|
return response.json({
|
|
57
61
|
access_token: result.token,
|
|
58
62
|
refresh_token: result.refreshToken,
|
|
@@ -64,7 +68,7 @@ export default new Action({
|
|
|
64
68
|
email: user?.email,
|
|
65
69
|
name: user?.name,
|
|
66
70
|
},
|
|
67
|
-
})
|
|
71
|
+
}, { headers: { 'Set-Cookie': authCookie(result.token) } })
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
return response.error('Registration failed')
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Action } from '@stacksjs/actions'
|
|
2
|
-
import { Auth, consumeTwoFactorChallenge, verifyTwoFactorLoginCode } from '@stacksjs/auth'
|
|
2
|
+
import { Auth, authCookie, consumeTwoFactorChallenge, verifyTwoFactorLoginCode } from '@stacksjs/auth'
|
|
3
3
|
import { response } from '@stacksjs/router'
|
|
4
4
|
import { schema } from '@stacksjs/validation'
|
|
5
5
|
|
|
@@ -41,6 +41,10 @@ export default new Action({
|
|
|
41
41
|
|
|
42
42
|
const user = result.user
|
|
43
43
|
|
|
44
|
+
// This is where a 2FA account's session actually begins — LoginAction
|
|
45
|
+
// deliberately mints nothing for these users until the code is verified —
|
|
46
|
+
// so the cookie belongs here too. Setting it only on LoginAction would
|
|
47
|
+
// have left every 2FA account exactly where it started (#2306).
|
|
44
48
|
return response.json({
|
|
45
49
|
access_token: result.token,
|
|
46
50
|
refresh_token: result.refreshToken,
|
|
@@ -52,6 +56,6 @@ export default new Action({
|
|
|
52
56
|
email: user?.email,
|
|
53
57
|
name: user?.name,
|
|
54
58
|
},
|
|
55
|
-
})
|
|
59
|
+
}, { headers: { 'Set-Cookie': authCookie(result.token) } })
|
|
56
60
|
},
|
|
57
61
|
})
|
|
@@ -5,7 +5,8 @@ import process from 'node:process'
|
|
|
5
5
|
import { Deployment } from '@stacksjs/orm'
|
|
6
6
|
import { response } from '@stacksjs/router'
|
|
7
7
|
import { dashboardOperationalError, dashboardOperationalIssue } from '../dashboard-response'
|
|
8
|
-
import { booleanValue, deploymentCommandArgs } from './deployment-input'
|
|
8
|
+
import { booleanValue, deploymentCommandArgs, deploymentPreviewCommandArgs } from './deployment-input'
|
|
9
|
+
import { runDeploymentPreview } from './deployment-preview'
|
|
9
10
|
|
|
10
11
|
function gitValue(args: string[]): string {
|
|
11
12
|
const result = Bun.spawnSync(['git', ...args], {
|
|
@@ -26,8 +27,35 @@ export default new Action({
|
|
|
26
27
|
|
|
27
28
|
async handle(request: RequestInstance) {
|
|
28
29
|
const dryRun = booleanValue(request.get('dryRun') || request.get('dry_run'))
|
|
30
|
+
if (dryRun) {
|
|
31
|
+
let previewArgs: string[]
|
|
32
|
+
try {
|
|
33
|
+
previewArgs = deploymentPreviewCommandArgs({
|
|
34
|
+
environment: request.get('environment') || request.get('env'),
|
|
35
|
+
domain: request.get('domain'),
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
return response.json({
|
|
40
|
+
success: false,
|
|
41
|
+
message: error instanceof Error ? error.message : 'Deployment preview input is invalid.',
|
|
42
|
+
}, { status: 422 })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
return {
|
|
47
|
+
success: true,
|
|
48
|
+
plan: await runDeploymentPreview(previewArgs),
|
|
49
|
+
message: 'Deployment preview generated.',
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
return dashboardOperationalError(error, 'Deployment preview could not be generated.', 'CreateDeployment.preview', 500)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
29
57
|
const confirmed = booleanValue(request.get('confirmed'))
|
|
30
|
-
if (!
|
|
58
|
+
if (!confirmed) {
|
|
31
59
|
return response.json({
|
|
32
60
|
success: false,
|
|
33
61
|
confirmationRequired: true,
|
|
@@ -40,7 +68,6 @@ export default new Action({
|
|
|
40
68
|
args = deploymentCommandArgs({
|
|
41
69
|
environment: request.get('environment') || request.get('env'),
|
|
42
70
|
domain: request.get('domain'),
|
|
43
|
-
dryRun,
|
|
44
71
|
})
|
|
45
72
|
}
|
|
46
73
|
catch (error) {
|
|
@@ -50,22 +77,20 @@ export default new Action({
|
|
|
50
77
|
}, { status: 422 })
|
|
51
78
|
}
|
|
52
79
|
|
|
53
|
-
let deployment: Awaited<ReturnType<typeof Deployment.create>>
|
|
80
|
+
let deployment: Awaited<ReturnType<typeof Deployment.create>>
|
|
54
81
|
const startedAt = Date.now()
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return dashboardOperationalError(error, 'Deployment record could not be created.', 'CreateDeployment.record', 500)
|
|
68
|
-
}
|
|
82
|
+
try {
|
|
83
|
+
deployment = await Deployment.create({
|
|
84
|
+
commitHash: gitValue(['rev-parse', '--short=12', 'HEAD']),
|
|
85
|
+
commitMessage: gitValue(['log', '-1', '--pretty=%s']),
|
|
86
|
+
branch: gitValue(['branch', '--show-current']),
|
|
87
|
+
status: 'running',
|
|
88
|
+
environment: args[2],
|
|
89
|
+
author: gitValue(['log', '-1', '--pretty=%an <%ae>']),
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
return dashboardOperationalError(error, 'Deployment record could not be created.', 'CreateDeployment.record', 500)
|
|
69
94
|
}
|
|
70
95
|
|
|
71
96
|
let child: ReturnType<typeof Bun.spawn>
|
|
@@ -78,42 +103,37 @@ export default new Action({
|
|
|
78
103
|
child.unref()
|
|
79
104
|
}
|
|
80
105
|
catch (error) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
dashboardOperationalIssue(updateError, 'Deployment failure status could not be recorded.', 'CreateDeployment.failureRecord')
|
|
91
|
-
}
|
|
106
|
+
try {
|
|
107
|
+
await deployment.update({
|
|
108
|
+
status: 'failed',
|
|
109
|
+
duration: 0,
|
|
110
|
+
errorLog: 'Deployment process could not be started.',
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
catch (updateError) {
|
|
114
|
+
dashboardOperationalIssue(updateError, 'Deployment failure status could not be recorded.', 'CreateDeployment.failureRecord')
|
|
92
115
|
}
|
|
93
116
|
return dashboardOperationalError(error, 'Deployment process could not be started.', 'CreateDeployment.process', 500)
|
|
94
117
|
}
|
|
95
118
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
duration: Math.max(0, Math.round((Date.now() - startedAt) / 1000)),
|
|
103
|
-
errorLog: exitCode === 0 ? '' : `buddy deploy exited with code ${exitCode}`,
|
|
104
|
-
})
|
|
105
|
-
})
|
|
106
|
-
.catch((error) => {
|
|
107
|
-
dashboardOperationalIssue(error, 'Deployment completion could not be recorded.', 'CreateDeployment.completionRecord')
|
|
119
|
+
void child.exited
|
|
120
|
+
.then(async (exitCode) => {
|
|
121
|
+
await deployment.update({
|
|
122
|
+
status: exitCode === 0 ? 'success' : 'failed',
|
|
123
|
+
duration: Math.max(0, Math.round((Date.now() - startedAt) / 1000)),
|
|
124
|
+
errorLog: exitCode === 0 ? '' : `buddy deploy exited with code ${exitCode}`,
|
|
108
125
|
})
|
|
109
|
-
|
|
126
|
+
})
|
|
127
|
+
.catch((error) => {
|
|
128
|
+
dashboardOperationalIssue(error, 'Deployment completion could not be recorded.', 'CreateDeployment.completionRecord')
|
|
129
|
+
})
|
|
110
130
|
|
|
111
131
|
return {
|
|
112
132
|
success: true,
|
|
113
133
|
pid: child.pid,
|
|
114
134
|
command: ['./buddy', ...args],
|
|
115
|
-
deployment: deployment
|
|
116
|
-
message:
|
|
135
|
+
deployment: deployment.toJSON(),
|
|
136
|
+
message: 'Deployment started.',
|
|
117
137
|
}
|
|
118
138
|
},
|
|
119
139
|
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
2
|
+
import { Action } from '@stacksjs/actions'
|
|
3
|
+
import { response } from '@stacksjs/router'
|
|
4
|
+
import { trackOperatorOperation } from '../Operations/control-plane'
|
|
5
|
+
import { stringValue } from '../Operations/recovery-input'
|
|
6
|
+
import { deploymentRollbackInput, DeploymentRollbackError, executeDeploymentRollback } from './deployment-rollback'
|
|
7
|
+
|
|
8
|
+
export default new Action({
|
|
9
|
+
name: 'CreateDeploymentRollback',
|
|
10
|
+
description: 'Executes a previously reviewed deployment rollback and records the operator action.',
|
|
11
|
+
method: 'POST',
|
|
12
|
+
apiResponse: true,
|
|
13
|
+
async handle(request: RequestInstance) {
|
|
14
|
+
const body = request.all() as Record<string, unknown>
|
|
15
|
+
try {
|
|
16
|
+
const input = deploymentRollbackInput(body)
|
|
17
|
+
const tracked = await trackOperatorOperation(request, 'dashboard.deployments.rollback', input, () => executeDeploymentRollback(input, stringValue(body.revision), stringValue(body.confirmation)))
|
|
18
|
+
return { success: true, ...tracked.result, operation: tracked.operation }
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error instanceof DeploymentRollbackError)
|
|
22
|
+
return response.json({ message: error.message }, 409)
|
|
23
|
+
throw error
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
2
|
+
import { Action } from '@stacksjs/actions'
|
|
3
|
+
import { response } from '@stacksjs/router'
|
|
4
|
+
import { dashboardOperationalError } from '../dashboard-response'
|
|
5
|
+
import { deploymentPreviewCommandArgs } from './deployment-input'
|
|
6
|
+
import { runDeploymentPreview } from './deployment-preview'
|
|
7
|
+
|
|
8
|
+
export default new Action({
|
|
9
|
+
name: 'PreviewDeployment',
|
|
10
|
+
description: 'Builds a non-mutating deployment plan from the native Buddy deploy pipeline.',
|
|
11
|
+
method: 'POST',
|
|
12
|
+
apiResponse: true,
|
|
13
|
+
|
|
14
|
+
async handle(request: RequestInstance) {
|
|
15
|
+
let args: string[]
|
|
16
|
+
try {
|
|
17
|
+
args = deploymentPreviewCommandArgs({
|
|
18
|
+
environment: request.get('environment') || request.get('env'),
|
|
19
|
+
domain: request.get('domain'),
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
return response.json({
|
|
24
|
+
success: false,
|
|
25
|
+
message: error instanceof Error ? error.message : 'Deployment preview input is invalid.',
|
|
26
|
+
}, { status: 422 })
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
return {
|
|
31
|
+
success: true,
|
|
32
|
+
plan: await runDeploymentPreview(args),
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
return dashboardOperationalError(error, 'Deployment preview could not be generated.', 'PreviewDeployment', 500)
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
2
|
+
import { Action } from '@stacksjs/actions'
|
|
3
|
+
import { response } from '@stacksjs/router'
|
|
4
|
+
import { dashboardOperationalError } from '../dashboard-response'
|
|
5
|
+
import { deploymentRollbackInput, DeploymentRollbackError, previewDeploymentRollback } from './deployment-rollback'
|
|
6
|
+
|
|
7
|
+
export default new Action({
|
|
8
|
+
name: 'PreviewDeploymentRollback',
|
|
9
|
+
description: 'Builds and validates a native deployment rollback plan without changing the active release.',
|
|
10
|
+
method: 'POST',
|
|
11
|
+
apiResponse: true,
|
|
12
|
+
async handle(request: RequestInstance) {
|
|
13
|
+
try {
|
|
14
|
+
return { success: true, plan: await previewDeploymentRollback(deploymentRollbackInput(request.all() as Record<string, unknown>)) }
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (error instanceof DeploymentRollbackError)
|
|
18
|
+
return response.json({ message: error.message }, 409)
|
|
19
|
+
return dashboardOperationalError(error, 'Deployment rollback could not be previewed.', 'PreviewDeploymentRollback')
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
})
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
averageRecordedDuration,
|
|
4
4
|
booleanValue,
|
|
5
5
|
deploymentCommandArgs,
|
|
6
|
+
deploymentPreviewCommandArgs,
|
|
6
7
|
tailLines,
|
|
7
8
|
} from './deployment-input'
|
|
8
9
|
|
|
@@ -19,7 +20,6 @@ describe('dashboard deployment inputs', () => {
|
|
|
19
20
|
expect(deploymentCommandArgs({
|
|
20
21
|
environment: 'staging',
|
|
21
22
|
domain: 'app.example.com',
|
|
22
|
-
dryRun: true,
|
|
23
23
|
})).toEqual([
|
|
24
24
|
'deploy',
|
|
25
25
|
'--env',
|
|
@@ -28,7 +28,6 @@ describe('dashboard deployment inputs', () => {
|
|
|
28
28
|
'--yes',
|
|
29
29
|
'--domain',
|
|
30
30
|
'app.example.com',
|
|
31
|
-
'--dry-run',
|
|
32
31
|
])
|
|
33
32
|
})
|
|
34
33
|
|
|
@@ -37,6 +36,17 @@ describe('dashboard deployment inputs', () => {
|
|
|
37
36
|
expect(() => deploymentCommandArgs({ domain: 'not a domain' })).toThrow()
|
|
38
37
|
})
|
|
39
38
|
|
|
39
|
+
it('builds a non-mutating machine-readable preview command', () => {
|
|
40
|
+
expect(deploymentPreviewCommandArgs({ environment: 'staging' })).toEqual([
|
|
41
|
+
'deploy',
|
|
42
|
+
'--env',
|
|
43
|
+
'staging',
|
|
44
|
+
'--dry-run',
|
|
45
|
+
'--json',
|
|
46
|
+
'--no-interaction',
|
|
47
|
+
])
|
|
48
|
+
})
|
|
49
|
+
|
|
40
50
|
it('aggregates only recorded durations', () => {
|
|
41
51
|
expect(averageRecordedDuration([record(30), record('90'), record(null), record('bad')])).toBe(60)
|
|
42
52
|
expect(averageRecordedDuration([record('bad')])).toBeNull()
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export interface DeploymentCommandInput {
|
|
2
2
|
environment?: unknown
|
|
3
3
|
domain?: unknown
|
|
4
|
-
dryRun?: unknown
|
|
5
4
|
}
|
|
6
5
|
|
|
7
6
|
export interface DeploymentRecordLike {
|
|
@@ -24,11 +23,14 @@ export function deploymentCommandArgs(input: DeploymentCommandInput): string[] {
|
|
|
24
23
|
throw new Error('Domain must be a valid DNS name.')
|
|
25
24
|
args.push('--domain', domain)
|
|
26
25
|
}
|
|
27
|
-
if (booleanValue(input.dryRun))
|
|
28
|
-
args.push('--dry-run')
|
|
29
26
|
return args
|
|
30
27
|
}
|
|
31
28
|
|
|
29
|
+
export function deploymentPreviewCommandArgs(input: DeploymentCommandInput): string[] {
|
|
30
|
+
const args = deploymentCommandArgs(input)
|
|
31
|
+
return [...args.filter(arg => arg !== '--no-interaction' && arg !== '--yes'), '--dry-run', '--json', '--no-interaction']
|
|
32
|
+
}
|
|
33
|
+
|
|
32
34
|
export function averageRecordedDuration(records: DeploymentRecordLike[]): number | null {
|
|
33
35
|
const durations = records
|
|
34
36
|
.map(record => record.get('duration'))
|