@stacksjs/defaults 0.75.87 → 0.75.89
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-deploy/SKILL.md +17 -0
- package/ai/skills/stacks-payments/SKILL.md +2 -1
- package/app/Actions/Dashboard/Email/captured-mail.ts +2 -2
- package/ide/vscode/package.json +1 -1
- package/package.json +3 -3
- package/routes/payments.ts +4 -3
- package/vcs/github/workflows/README.md +4 -0
- package/vcs/github/workflows/preview.yml +141 -0
|
@@ -20,6 +20,7 @@ environment overrides it. With neither set, the provider is `aws`.
|
|
|
20
20
|
| `'aws'` (default) | AWS | Generates infrastructure, creates or updates a CloudFormation stack |
|
|
21
21
|
| `'hetzner'` | Hetzner Cloud | Provisions the server through the Hetzner API, then deploys over SSH |
|
|
22
22
|
| `'ssh'` | A host you already own | Adopts and bootstraps the host over SSH, then deploys to it |
|
|
23
|
+
| `'fly'` | Fly.io Machines | Builds `storage/framework/Dockerfile`, pushes it to `registry.fly.io`, rolls the Machines under their leases |
|
|
23
24
|
|
|
24
25
|
Read the repo's own `config/cloud.ts` before assuming a target. This repo uses `'hetzner'`.
|
|
25
26
|
|
|
@@ -68,6 +69,22 @@ a versioned `STACKS_DEPLOY_PREVIEW_JSON=` line for API and tool consumers.
|
|
|
68
69
|
2. A key that reaches it, and passwordless sudo when the user is not root
|
|
69
70
|
3. `buddy server:doctor` clean
|
|
70
71
|
|
|
72
|
+
**Fly.io** (`provider: 'fly'`)
|
|
73
|
+
|
|
74
|
+
1. `FLY_API_TOKEN` (an org token lets the first deploy create the app)
|
|
75
|
+
2. Docker where the deploy runs, for the image build and push
|
|
76
|
+
3. `fly.*` in `config/cloud.ts`: `regions`, `count`, `vm`, `volume`, `hostnames`. Every `.env.<env>`
|
|
77
|
+
value becomes a Fly secret. A deploy never destroys Machines; extras are reported.
|
|
78
|
+
|
|
79
|
+
## Preview deployments (Hetzner and `ssh`)
|
|
80
|
+
|
|
81
|
+
`buddy deploy:preview pr-123` deploys the app as its own tenant (slug `<slug>-pr-123`, attached to
|
|
82
|
+
the app's box) at `pr-123.<cloud.previews.domain | PREVIEW_DOMAIN>`; `deploy:preview:remove pr-123`
|
|
83
|
+
removes its units, gateway route, cert units, files and DNS records by exact name. Secrets come
|
|
84
|
+
from `.env.preview` only (the deploy refuses without it), and a relative SQLite path gets its own
|
|
85
|
+
data dir. `--base staging` uses the staging box. New apps ship `.github/workflows/preview.yml`
|
|
86
|
+
(same-repo PRs only; needs `DEPLOY_SSH_KEY`, `DOTENV_PRIVATE_KEY_PREVIEW`, `vars.PREVIEW_DOMAIN`).
|
|
87
|
+
|
|
71
88
|
## Deployment Flow
|
|
72
89
|
|
|
73
90
|
**AWS**
|
|
@@ -12,13 +12,14 @@ Full Stripe integration via the Payment facade. Uses Stripe API version `2026-01
|
|
|
12
12
|
|
|
13
13
|
## Payment Drivers (provider-neutral)
|
|
14
14
|
|
|
15
|
-
`paymentDriver()` (also `Payment.driver()`) returns the driver `config.payment.driver` selects: `stripe` (default), `adyen` or `
|
|
15
|
+
`paymentDriver()` (also `Payment.driver()`) returns the driver `config.payment.driver` selects: `stripe` (default), `adyen`, `paddle` or `lemonsqueezy`, or one added with `registerPaymentDriver(name, factory)`. Source: `storage/framework/core/payments/src/driver/`.
|
|
16
16
|
|
|
17
17
|
- Money is `{ amount, currency }` in minor units; `assertMoney` refuses floats before a request leaves.
|
|
18
18
|
- Methods: `customer`, `charge(payer, money, paymentMethodId)`, `createPayment` (browser completes: `clientConfirmation` is a Stripe client secret or an Adyen session), `refund`, `checkout(payer, { mode, lines, successUrl, ... })`, `paymentMethods`, `removePaymentMethod`, `subscribe`, `cancelSubscription`, `subscriptions`, `verifyWebhook({ payload, headers })` -> `PaymentEvent[]`, `acknowledgeWebhook()`.
|
|
19
19
|
- `checkout` copies `reference` and `metadata` onto the payment / subscription / setup intent it creates (Stripe: `payment_intent_data`, `subscription_data`, `setup_intent_data`), so their webhooks can be attributed. `allowPromotionCodes` and `automaticTax` are Stripe only; Adyen refuses them.
|
|
20
20
|
- Unsupported operations throw `PaymentUnsupportedError` (driver + operation). Adyen has no catalog, subscriptions, invoices, coupons or portal; Paddle has no server-side charge, server-side subscribe, setup checkout, promotion-code or trial option on a checkout, or separate cancel URL.
|
|
21
21
|
- Paddle (`driver: 'paddle'`, Billing API v1, merchant of record): customers found or created by email; every payment is a transaction completed in Paddle.js (`clientConfirmation: { provider: 'paddle', transactionId, successUrl? }`), so `charge` and `subscribe` are unsupported and subscriptions start at `checkout` in `subscription` mode with catalog prices; refunds are adjustments Paddle reviews (`pending`, settled by `adjustment.updated`; a refund's event id is the adjustment's, so a created-and-updated approval counts once); webhooks `Paddle-Signature` = HMAC-SHA256 hex of `<ts>:<raw body>` under the secret, 5 s tolerance. Paddle has no hosted page: `GET /payments/checkout` (mounted while the driver is `paddle`) is the default payment link page, reading `stacks_success_url` from the transaction for Paddle.js. Env: `PADDLE_API_KEY`, `PADDLE_ENVIRONMENT`, `PADDLE_CLIENT_TOKEN`, `PADDLE_WEBHOOK_SECRET`, `PADDLE_TAX_CATEGORY`.
|
|
22
|
+
- Lemon Squeezy (`driver: 'lemonsqueezy'`, API v1, merchant of record): customers found or created by email in the store; `checkout` sells exactly one variant (a `price` line is a variant id; a line priced here sells as `LEMONSQUEEZY_VARIANT_ID` at a custom price in the store's currency); `charge`, `createPayment`, `subscribe` and payment methods are unsupported; refunds are immediate against the order; `cancelSubscription` needs `{ atPeriodEnd: true }`; webhooks `X-Signature` = HMAC-SHA256 hex of the raw body, event id = digest of the bytes, `order_refunded` carries `refundedTotal` (the order's running total), which commerce applies as a difference. Env: `LEMONSQUEEZY_API_KEY`, `LEMONSQUEEZY_STORE_ID`, `LEMONSQUEEZY_WEBHOOK_SECRET`, `LEMONSQUEEZY_VARIANT_ID`, `LEMONSQUEEZY_TEST_MODE`.
|
|
22
23
|
- Adyen: Checkout API v72, `X-API-Key`; shopper reference `user-<id>` (no PII); full refunds go to `/reversals`; webhooks verified per item with HMAC-SHA256 over 8 colon-joined fields under the hex key, acknowledged with 202.
|
|
23
24
|
- `PaymentEvent` carries `provider` and `id` (together, the retry key), `reference` (the payment; for a refund, the payment refunded), `amount` (for a refund, that one refund - Stripe's cumulative `amount_refunded` is turned into a delta from `previous_attributes`) and `reason`. Adyen's id is `eventCode:pspReference:success`, since Adyen may resend a pair with the outcome changed.
|
|
24
25
|
- The Stripe driver delegates to the existing billable modules, so idempotency keys and `stripe_id` handling are unchanged.
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { readdir, readFile, stat } from 'node:fs/promises'
|
|
2
2
|
import { basename, join } from 'node:path'
|
|
3
3
|
import { log as logDriver } from '@stacksjs/email'
|
|
4
|
-
import { logsPath } from '@stacksjs/path'
|
|
5
4
|
|
|
6
5
|
const { LogEmailDriver } = logDriver
|
|
7
6
|
|
|
@@ -36,8 +35,9 @@ export interface CapturedMailListing {
|
|
|
36
35
|
problems: CapturedMailProblem[]
|
|
37
36
|
}
|
|
38
37
|
|
|
38
|
+
// The driver's own answer, so the page reads exactly where captures are written.
|
|
39
39
|
function capturedMailDirectory(): string {
|
|
40
|
-
return
|
|
40
|
+
return LogEmailDriver.directory()
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
function formatAddress(value: unknown): string {
|
package/ide/vscode/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"publisher": "Stacks",
|
|
3
3
|
"name": "vscode-stacks",
|
|
4
4
|
"displayName": "Stacks",
|
|
5
|
-
"version": "0.75.
|
|
5
|
+
"version": "0.75.89",
|
|
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.
|
|
5
|
+
"version": "0.75.89",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/stacksjs/stacks.git",
|
|
@@ -55,8 +55,8 @@
|
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@iconify-json/f7": "^1.2.2",
|
|
57
57
|
"@iconify-json/hugeicons": "^1.2.35",
|
|
58
|
-
"@stacksjs/mobile": "^0.75.
|
|
59
|
-
"@stacksjs/sanitizer": "^0.2.
|
|
58
|
+
"@stacksjs/mobile": "^0.75.89",
|
|
59
|
+
"@stacksjs/sanitizer": "^0.2.395",
|
|
60
60
|
"@xterm/addon-fit": "^0.11.0",
|
|
61
61
|
"@xterm/xterm": "^6.0.0",
|
|
62
62
|
"ts-qr-codes": "^0.1.8"
|
package/routes/payments.ts
CHANGED
|
@@ -6,9 +6,10 @@ import { route } from '@stacksjs/router'
|
|
|
6
6
|
*
|
|
7
7
|
* Point the provider at `POST /webhooks/payments`: Stripe's endpoint with its
|
|
8
8
|
* signing secret in `STRIPE_WEBHOOK_SECRET`, or Adyen's standard webhook with
|
|
9
|
-
* its HMAC key in `ADYEN_HMAC_KEY`,
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* its HMAC key in `ADYEN_HMAC_KEY`, a Paddle notification destination with its
|
|
10
|
+
* secret key in `PADDLE_WEBHOOK_SECRET`, or a Lemon Squeezy store webhook with
|
|
11
|
+
* its signing secret in `LEMONSQUEEZY_WEBHOOK_SECRET`, whichever
|
|
12
|
+
* `config.payment.driver` names.
|
|
12
13
|
* Payments and refunds then reach their orders without app code; orders used
|
|
13
14
|
* to sit at PENDING whenever the webhook was the only news of a payment.
|
|
14
15
|
*
|
|
@@ -9,7 +9,11 @@ This folder contains the following GitHub Actions:
|
|
|
9
9
|
- runs on `ubuntu-latest`
|
|
10
10
|
- [Release][Release] — on a `v*` tag, generates the changelog and creates the GitHub release
|
|
11
11
|
- [Labeler][Labeler] — labels pull requests from `.github/labeler.yml`
|
|
12
|
+
- [Deploy][Deploy] — deploys each green push to `main` with `buddy deploy`
|
|
13
|
+
- [Preview][Preview] — deploys every pull request to `pr-<number>.<preview domain>` with `buddy deploy:preview`, comments the URL, and removes it when the pull request closes
|
|
12
14
|
|
|
13
15
|
[CI]: ./ci.yml
|
|
14
16
|
[Release]: ./release.yml
|
|
15
17
|
[Labeler]: ./labeler.yml
|
|
18
|
+
[Deploy]: ./deploy.yml
|
|
19
|
+
[Preview]: ./preview.yml
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
name: Preview
|
|
2
|
+
|
|
3
|
+
# A preview deployment for every pull request (stacksjs/stacks#735).
|
|
4
|
+
#
|
|
5
|
+
# Each pull request is deployed as its own tenant on the app's box, at
|
|
6
|
+
# pr-<number>.<preview domain>, with its own database and its own secrets
|
|
7
|
+
# from .env.preview, and removed when the pull request closes. The URL is
|
|
8
|
+
# kept current in one comment on the pull request.
|
|
9
|
+
#
|
|
10
|
+
# Set up once:
|
|
11
|
+
# - PREVIEW_DOMAIN as a repository variable (or cloud.previews.domain in
|
|
12
|
+
# config/cloud.ts), with its DNS at a provider whose keys are below
|
|
13
|
+
# - .env.preview, committed encrypted, holding credentials that are NOT
|
|
14
|
+
# production's: its own database, mail and API keys
|
|
15
|
+
# - DOTENV_PRIVATE_KEY_PREVIEW and DEPLOY_SSH_KEY as secrets
|
|
16
|
+
on:
|
|
17
|
+
pull_request:
|
|
18
|
+
types: [opened, synchronize, reopened, closed]
|
|
19
|
+
|
|
20
|
+
# One run per pull request at a time, in order: a removal must not race the
|
|
21
|
+
# deploy it follows, and half a deploy is a release uploaded but not live.
|
|
22
|
+
concurrency:
|
|
23
|
+
group: preview-${{ github.event.pull_request.number }}
|
|
24
|
+
cancel-in-progress: false
|
|
25
|
+
|
|
26
|
+
permissions:
|
|
27
|
+
contents: read
|
|
28
|
+
pull-requests: write
|
|
29
|
+
|
|
30
|
+
jobs:
|
|
31
|
+
preview:
|
|
32
|
+
# Only pull requests from this repository. A fork's pull request runs
|
|
33
|
+
# without secrets, and deploying its code with them would hand the fork
|
|
34
|
+
# this app's preview credentials and SSH access to the box.
|
|
35
|
+
if: github.event.pull_request.head.repo.full_name == github.repository
|
|
36
|
+
runs-on: ubuntu-latest
|
|
37
|
+
environment: preview
|
|
38
|
+
env:
|
|
39
|
+
PREVIEW: pr-${{ github.event.pull_request.number }}
|
|
40
|
+
|
|
41
|
+
steps:
|
|
42
|
+
# A repository that has not set previews up finishes green with a notice,
|
|
43
|
+
# rather than failing every pull request.
|
|
44
|
+
- name: Are previews configured?
|
|
45
|
+
id: check
|
|
46
|
+
env:
|
|
47
|
+
DOTENV_PRIVATE_KEY_PREVIEW: ${{ secrets.DOTENV_PRIVATE_KEY_PREVIEW }}
|
|
48
|
+
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
|
49
|
+
run: |
|
|
50
|
+
if [ -n "$DOTENV_PRIVATE_KEY_PREVIEW" ] && [ -n "$DEPLOY_SSH_KEY" ]; then
|
|
51
|
+
echo 'configured=true' >> "$GITHUB_OUTPUT"
|
|
52
|
+
else
|
|
53
|
+
echo 'configured=false' >> "$GITHUB_OUTPUT"
|
|
54
|
+
echo '::notice::Previews are not configured, so this run did nothing. Add DEPLOY_SSH_KEY and DOTENV_PRIVATE_KEY_PREVIEW (from .env.keys), and set PREVIEW_DOMAIN.'
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
- name: Checkout
|
|
58
|
+
if: steps.check.outputs.configured == 'true'
|
|
59
|
+
uses: actions/checkout@v6
|
|
60
|
+
with:
|
|
61
|
+
# The pull request's head, not the merge commit GitHub builds.
|
|
62
|
+
ref: ${{ github.event.pull_request.head.sha }}
|
|
63
|
+
|
|
64
|
+
- name: Setup Bun
|
|
65
|
+
if: steps.check.outputs.configured == 'true'
|
|
66
|
+
uses: oven-sh/setup-bun@v2
|
|
67
|
+
|
|
68
|
+
- name: Setup Pantry
|
|
69
|
+
if: steps.check.outputs.configured == 'true'
|
|
70
|
+
uses: home-lang/pantry/packages/action@main
|
|
71
|
+
|
|
72
|
+
- name: Install dependencies
|
|
73
|
+
if: steps.check.outputs.configured == 'true'
|
|
74
|
+
run: bun install --frozen-lockfile
|
|
75
|
+
|
|
76
|
+
- name: Load the deploy key
|
|
77
|
+
if: steps.check.outputs.configured == 'true'
|
|
78
|
+
env:
|
|
79
|
+
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
|
80
|
+
run: |
|
|
81
|
+
mkdir -p ~/.ssh
|
|
82
|
+
chmod 700 ~/.ssh
|
|
83
|
+
printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519
|
|
84
|
+
chmod 600 ~/.ssh/id_ed25519
|
|
85
|
+
ssh-keygen -y -f ~/.ssh/id_ed25519 > ~/.ssh/id_ed25519.pub
|
|
86
|
+
chmod 644 ~/.ssh/id_ed25519.pub
|
|
87
|
+
eval "$(ssh-agent -s)" >/dev/null
|
|
88
|
+
ssh-add ~/.ssh/id_ed25519
|
|
89
|
+
echo "SSH_AUTH_SOCK=$SSH_AUTH_SOCK" >> "$GITHUB_ENV"
|
|
90
|
+
|
|
91
|
+
- name: Deploy the preview
|
|
92
|
+
id: deploy
|
|
93
|
+
if: steps.check.outputs.configured == 'true' && github.event.action != 'closed'
|
|
94
|
+
run: ./buddy deploy:preview "$PREVIEW"
|
|
95
|
+
env:
|
|
96
|
+
DOTENV_PRIVATE_KEY_PREVIEW: ${{ secrets.DOTENV_PRIVATE_KEY_PREVIEW }}
|
|
97
|
+
PREVIEW_DOMAIN: ${{ vars.PREVIEW_DOMAIN }}
|
|
98
|
+
HCLOUD_TOKEN: ${{ secrets.HCLOUD_TOKEN }}
|
|
99
|
+
PORKBUN_API_KEY: ${{ secrets.PORKBUN_API_KEY }}
|
|
100
|
+
PORKBUN_SECRET_KEY: ${{ secrets.PORKBUN_SECRET_KEY }}
|
|
101
|
+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
102
|
+
COPYFILE_DISABLE: '1'
|
|
103
|
+
|
|
104
|
+
- name: Remove the preview
|
|
105
|
+
id: remove
|
|
106
|
+
if: steps.check.outputs.configured == 'true' && github.event.action == 'closed'
|
|
107
|
+
run: ./buddy deploy:preview:remove "$PREVIEW"
|
|
108
|
+
env:
|
|
109
|
+
DOTENV_PRIVATE_KEY_PREVIEW: ${{ secrets.DOTENV_PRIVATE_KEY_PREVIEW }}
|
|
110
|
+
PREVIEW_DOMAIN: ${{ vars.PREVIEW_DOMAIN }}
|
|
111
|
+
HCLOUD_TOKEN: ${{ secrets.HCLOUD_TOKEN }}
|
|
112
|
+
PORKBUN_API_KEY: ${{ secrets.PORKBUN_API_KEY }}
|
|
113
|
+
PORKBUN_SECRET_KEY: ${{ secrets.PORKBUN_SECRET_KEY }}
|
|
114
|
+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
115
|
+
|
|
116
|
+
# One comment per pull request, edited in place rather than added to on
|
|
117
|
+
# every push, found by the marker on its first line.
|
|
118
|
+
- name: Say where it is
|
|
119
|
+
if: always() && steps.check.outputs.configured == 'true' && (steps.deploy.outcome != 'skipped' || steps.remove.outcome != 'skipped')
|
|
120
|
+
env:
|
|
121
|
+
GH_TOKEN: ${{ github.token }}
|
|
122
|
+
NUMBER: ${{ github.event.pull_request.number }}
|
|
123
|
+
URL: ${{ steps.deploy.outputs.url }}
|
|
124
|
+
DEPLOYED: ${{ steps.deploy.outcome }}
|
|
125
|
+
REMOVED: ${{ steps.remove.outcome }}
|
|
126
|
+
SHA: ${{ github.event.pull_request.head.sha }}
|
|
127
|
+
run: |
|
|
128
|
+
marker='<!-- stacks-preview -->'
|
|
129
|
+
if [ "$REMOVED" = 'success' ]; then
|
|
130
|
+
body="$marker"$'\n'"Preview removed: the pull request is closed."
|
|
131
|
+
elif [ "$DEPLOYED" = 'success' ]; then
|
|
132
|
+
body="$marker"$'\n'"Preview of ${SHA::7}: $URL"
|
|
133
|
+
else
|
|
134
|
+
body="$marker"$'\n'"The preview of ${SHA::7} failed to deploy; see the run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
|
135
|
+
fi
|
|
136
|
+
existing=$(gh api "repos/${{ github.repository }}/issues/$NUMBER/comments" --paginate --jq ".[] | select(.body | startswith(\"$marker\")) | .id" | head -n 1)
|
|
137
|
+
if [ -n "$existing" ]; then
|
|
138
|
+
gh api -X PATCH "repos/${{ github.repository }}/issues/comments/$existing" -f body="$body" >/dev/null
|
|
139
|
+
else
|
|
140
|
+
gh api "repos/${{ github.repository }}/issues/$NUMBER/comments" -f body="$body" >/dev/null
|
|
141
|
+
fi
|