@stacksjs/defaults 0.72.63 → 0.72.66

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.
@@ -276,14 +276,31 @@ Supported webhook event types: `payment_intent.succeeded`, `payment_intent.payme
276
276
 
277
277
  ### Setup Products from SaaS Config
278
278
 
279
+ Prefer the command; it is what an app author can discover:
280
+
281
+ ```bash
282
+ buddy stripe:setup --dry-run # report the plan of record, write nothing
283
+ buddy stripe:setup # apply it
284
+ ```
285
+
279
286
  ```typescript
280
- import { createStripeProduct } from '@stacksjs/payments'
287
+ import { createStripeProduct, formatSetupReport } from '@stacksjs/payments'
281
288
 
282
- const result = await createStripeProduct()
283
- // Creates Stripe products and prices from config/saas.ts plans
289
+ const result = await createStripeProduct({ dryRun: true })
290
+ if (!result.isErr)
291
+ console.log(formatSetupReport(result.value).join('\n'))
284
292
  ```
285
293
 
286
- This iterates `saas.plans`, creates a Stripe product per plan, then creates prices for each pricing option using `lookup_key` from the `key` field.
294
+ This iterates `saas.plans` and reconciles each one against the live account: a
295
+ product is matched by name and reused, and each pricing option is matched by its
296
+ `lookup_key` (from the `key` field). Prices are immutable in Stripe, so a changed
297
+ amount is applied by creating a new price with `transfer_lookup_key: true`, which
298
+ moves the key atomically and leaves the superseded price active so existing
299
+ subscriptions keep billing.
300
+
301
+ Re-running is safe and converges. It does not use `products.search` on purpose:
302
+ that index is eventually consistent, so a second run inside the lag window would
303
+ find nothing and create a duplicate.
287
304
 
288
305
  ### Utility Functions
289
306
 
package/bootstrap.ts CHANGED
@@ -134,7 +134,27 @@ if (mounts('dashboard', feature('dashboard'))) {
134
134
  await route.register(frameworkPath('defaults/routes/dashboard-api.ts'))
135
135
  // The dev dashboard boots through this file rather than the application
136
136
  // router's importRoutes() path, so load model-declared useApi routes here too.
137
- await import(frameworkPath('orm/routes.ts'))
137
+ //
138
+ // Package first, vendored second, matching importRoutes() and start.ts. This
139
+ // line used to import the vendored copy only, and nothing re-vendors that
140
+ // file: `@stacksjs/orm` publishes `dist/routes.js` and no `routes.ts`, so an
141
+ // app kept whatever generator its copy froze at however often it upgraded.
142
+ // An old enough copy compares route paths literally, so a generated
143
+ // `PATCH /api/sites/{id}` does not recognise a hand-written
144
+ // `/api/sites/{siteId}` as the same endpoint and registers alongside it,
145
+ // and the hand-written handler's authorization check stops running
146
+ // (stacksjs/stacks#2364).
147
+ //
148
+ // Held in a variable so the specifier resolves at runtime rather than while
149
+ // transpiling, where an unresolvable literal would fail this module instead
150
+ // of throwing where it can be caught.
151
+ const ormRoutesPackage = '@stacksjs/orm/routes'
152
+ try {
153
+ await import(ormRoutesPackage)
154
+ }
155
+ catch {
156
+ await import(frameworkPath('orm/routes.ts'))
157
+ }
138
158
  }
139
159
 
140
160
  // Email webhook + unsubscribe routes (stacksjs/stacks#1881, #1880).
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.72.63",
5
+ "version": "0.72.66",
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.72.63",
5
+ "version": "0.72.66",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/stacksjs/stacks.git",
@@ -51,7 +51,7 @@
51
51
  "dependencies": {
52
52
  "@iconify-json/f7": "^1.2.2",
53
53
  "@iconify-json/hugeicons": "^1.2.27",
54
- "@stacksjs/mobile": "^0.72.63",
54
+ "@stacksjs/mobile": "^0.72.66",
55
55
  "@stacksjs/sanitizer": "^0.2.113"
56
56
  },
57
57
  "scripts": {
@@ -0,0 +1,77 @@
1
+ name: Deploy
2
+
3
+ # Push to deploy.
4
+ #
5
+ # Gated on CI rather than firing on the push itself: `buddy deploy` activates a
6
+ # release atomically, so the failure mode of deploying an untested commit is a
7
+ # live site running it, not a red check somebody notices later.
8
+ on:
9
+ workflow_run:
10
+ workflows: [CI]
11
+ types: [completed]
12
+ branches: [main]
13
+
14
+ # Ship a specific commit without pushing, and reach environments that no
15
+ # branch maps to.
16
+ workflow_dispatch:
17
+ inputs:
18
+ environment:
19
+ description: Which environment to deploy
20
+ type: choice
21
+ options: [production, staging, development]
22
+ default: production
23
+
24
+ concurrency:
25
+ group: deploy-${{ github.event.inputs.environment || 'production' }}
26
+ # Never cancel a deploy in flight. Half of one is a release uploaded but not
27
+ # activated, or services restarted against the previous release's env.
28
+ cancel-in-progress: false
29
+
30
+ jobs:
31
+ deploy:
32
+ # `workflow_run` fires for failed runs too — that is the point of the
33
+ # gate, so check the conclusion.
34
+ if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success'
35
+ runs-on: ubuntu-latest
36
+ # Attach a GitHub Environment to get required reviewers and to scope these
37
+ # secrets to it.
38
+ environment: ${{ github.event.inputs.environment || 'production' }}
39
+
40
+ steps:
41
+ - name: Checkout
42
+ uses: actions/checkout@v6
43
+ with:
44
+ # The exact commit CI approved, not whatever main points at now.
45
+ ref: ${{ github.event.workflow_run.head_sha || github.ref }}
46
+
47
+ - name: Setup Bun
48
+ uses: oven-sh/setup-bun@v2
49
+
50
+ - name: Setup Pantry
51
+ uses: home-lang/pantry/packages/action@main
52
+
53
+ - name: Install dependencies
54
+ run: bun install --frozen-lockfile
55
+
56
+ # The deploy talks to the server as root over SSH with BatchMode on, so
57
+ # the key has to be in an agent before it runs — there is no prompt to
58
+ # fall back to.
59
+ - name: Load the deploy key
60
+ uses: webfactory/ssh-agent@v0.9.0
61
+ with:
62
+ ssh-private-key: ${{ secrets.DEPLOY_SSH_KEY }}
63
+
64
+ - name: Deploy
65
+ run: ./buddy deploy --${{ github.event.inputs.environment == 'staging' && 'staging' || github.event.inputs.environment == 'development' && 'dev' || 'prod' }} --yes
66
+ env:
67
+ # Decrypts .env.production. Without it the encrypted values silently
68
+ # fall back to defaults, which is how an app deploys with the wrong
69
+ # APP_KEY and invalidates every session it had.
70
+ DOTENV_PRIVATE_KEY_PRODUCTION: ${{ secrets.DOTENV_PRIVATE_KEY_PRODUCTION }}
71
+ # DNS records are written through the registrar that administers the
72
+ # zone, declared in config/cloud.ts. Keep these in the environment
73
+ # even when they are also in .env.production — a missing one now
74
+ # fails loudly instead of reaching for another provider's keys.
75
+ PORKBUN_API_KEY: ${{ secrets.PORKBUN_API_KEY }}
76
+ PORKBUN_SECRET_KEY: ${{ secrets.PORKBUN_SECRET_KEY }}
77
+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}