create-substrat 0.5.0 → 0.6.1

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/dev-servers.js ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The dev servers a scaffolded vertical runs — declared once, client-neutrally.
3
+ *
4
+ * `.claude/launch.json` is a Claude Desktop **adapter**: it lets the agent start
5
+ * the server, open it in the Browser pane, and verify its own changes. Under
6
+ * design/agent-surface.md §3 an adapter may route, describe and trigger but may
7
+ * never *hold* substance — so the topology lives here, in the `substrat` block of
8
+ * package.json that `substrat push` and the SessionStart hook already read, and
9
+ * the client file is emitted from it (`pnpm lint:launch`, guarded in CI).
10
+ *
11
+ * A port is deliberately NOT a field. `portEnv` + `portFrom` say *where* the port
12
+ * is bound; the number is read out of that file. Otherwise the declaration becomes
13
+ * a second copy of a number whose first copy is the one that actually runs, and
14
+ * with `autoPort: false` a stale copy is a hard boot failure — for an agent, mid
15
+ * session, which is the worst possible audience for it.
16
+ *
17
+ * @typedef {object} DevServer
18
+ * @property {string} name Entry name. Mirrors the `-n` label of the `dev` script.
19
+ * @property {string} run pnpm script to run. With `dir`, the script in that subdir.
20
+ * @property {string} [dir] Subdirectory of the project (a Vite app), if any.
21
+ * @property {string} portEnv The env var that moves this port (`PORT`, `WEB_PORT`, …).
22
+ * @property {string} portFrom Project-relative file binding it: `process.env.<portEnv> ?? N`.
23
+ * @property {Record<string,string>} [env] Env the `dev` script sets for this process.
24
+ */
25
+
26
+ /** The template ships one process: the Hono API. There is no web app to scaffold yet. */
27
+ export const DEV_SERVERS = [
28
+ { name: 'api', run: 'server', portEnv: 'PORT', portFrom: 'src/server.ts' },
29
+ ];
package/index.js CHANGED
@@ -16,6 +16,8 @@ import { cpSync, existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:
16
16
  import { basename, dirname, join, resolve } from 'node:path';
17
17
  import { fileURLToPath } from 'node:url';
18
18
 
19
+ import { DEV_SERVERS } from './dev-servers.js';
20
+
19
21
  const HERE = dirname(fileURLToPath(import.meta.url));
20
22
  const TEMPLATE = join(HERE, 'template');
21
23
 
@@ -73,7 +75,9 @@ function packageJson(name) {
73
75
  // What `substrat push` reads: the permission surface (the registry the
74
76
  // promotion checkpoint diffs) and the runtime needs the deploy config is
75
77
  // derived from — you never author wrangler config (src/worker.ts is the
76
- // entry; ScopeDO is the store it exports).
78
+ // entry; ScopeDO is the store it exports). Also the client-neutral home of
79
+ // the dev topology `.claude/launch.json` is emitted from — see
80
+ // dev-servers.js for why the port is read from the code and never declared.
77
81
  substrat: {
78
82
  permissions: 'src/provision.ts',
79
83
  runtimeNeeds: {
@@ -84,6 +88,7 @@ function packageJson(name) {
84
88
  { binding: 'SWEEPER', class: 'SweeperDO' },
85
89
  ],
86
90
  },
91
+ devServers: DEV_SERVERS,
87
92
  },
88
93
  scripts: {
89
94
  dev: 'tsx watch src/server.ts',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-substrat",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "Scaffold a Substrat vertical — `npm create substrat <dir>`.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -24,6 +24,7 @@
24
24
  },
25
25
  "files": [
26
26
  "index.js",
27
+ "dev-servers.js",
27
28
  "template"
28
29
  ],
29
30
  "engines": {
@@ -0,0 +1,15 @@
1
+ {
2
+ "version": "0.0.1",
3
+ "configurations": [
4
+ {
5
+ "name": "api",
6
+ "runtimeExecutable": "pnpm",
7
+ "runtimeArgs": [
8
+ "run",
9
+ "server"
10
+ ],
11
+ "port": 8873,
12
+ "autoPort": false
13
+ }
14
+ ]
15
+ }
@@ -190,10 +190,21 @@ plain language, so nothing there is a surprise:
190
190
  cross-tenant attacker gets nothing).
191
191
  9. **Open decisions** — each with a **recommended default**, so the user chooses rather
192
192
  than specifies:
193
- - **Auth.** Local dev uses an `x-principal` header — a dev seam, not a login. Default to
194
- it and note it must be replaced before anything real; offer to wire a real login (the
195
- bike-shop reference shows the Better Auth pattern) now if they want it. Real auth gates
196
- *exposing* the app, not *building* it.
193
+ - **Auth.** Local dev uses an `x-principal` header — a dev seam, not a login. Real auth
194
+ gates *exposing* the app, not *building* it — but if the app will be deployed for real
195
+ users, wire the OIDC seam from the start: the standard is a **separate OIDC issuer**
196
+ (an Auth Server app in the same team, or an external issuer — Supabase/Auth0/AuthHero/
197
+ Keycloak), never per-app credential storage. The vertical is a pure OIDC **relying
198
+ party**: depend on `@substrat-run/vertical-auth`, bind its `IdentityDO` (the
199
+ `sub → principal` directory, TOFU owner claim, invites) as a third DO store, read the
200
+ platform-delivered `substrat:auth` config per scope (`authWiring`), and build
201
+ `oidcRpAuthProvider` per request — see https://substrat.net/concepts/identity. The
202
+ dashboard's New-app **Identity** section does the automatic half (dynamic client
203
+ registration at the issuer + `/internal/configure` delivery) — but ONLY at app
204
+ creation, and only for verticals that implement this seam. An install created without
205
+ the identity choice stays `builtin`/unwired forever (switching issuers is deliberately
206
+ create-time only); a starter worker whose `authenticatedPrincipal` returns null answers
207
+ 401 to everything deployed, however many auth servers exist in the team.
197
208
  - **Deploy or stay local.** Local-first is a legitimate endpoint; default to it.
198
209
  10. **Out of scope / deferred** — what you are deliberately not building, so the review is
199
210
  about a bounded thing.
@@ -428,7 +439,9 @@ only in seed.ts would run locally and silently not deploy. The deploy path is th
428
439
  authenticated CLI, and the author never holds a Cloudflare token:
429
440
 
430
441
  - `substrat login` / `substrat whoami` — authenticate against the control plane.
431
- - `substrat push` — push the vertical; the version auto-bumps. A **private** (tenant-owned)
442
+ - `substrat push` — push the vertical. By default the version is the registry's highest
443
+ semver, patch-bumped; `package.json`'s version is only a **seed for the first push of a
444
+ new slug**, and an explicit `--version` always wins. A **private** (tenant-owned)
432
445
  vertical is admitted automatically; a **listed/shared** one waits for staff admission.
433
446
  - `substrat promote <slug> --channel dev|staging|prod --version … [--ack-permissions]
434
447
  [--ack-migrations]` — the owner promotes every channel, prod included, for their own
@@ -436,6 +449,50 @@ authenticated CLI, and the author never holds a Cloudflare token:
436
449
  - `substrat hostnames bind <slug> --surface <s> [--domain <d>]` — mint a live hostname, or
437
450
  record a custom domain pending DNS validation (`substrat hostnames verify`).
438
451
 
452
+ **A SPA ships as NATIVE assets — never inline it into the worker.** Declare it in
453
+ `runtimeNeeds` and `substrat push` builds, hashes, and uploads the directory to the
454
+ runtime's own asset store, served from the edge without invoking the worker:
455
+
456
+ ```jsonc
457
+ "substrat": {
458
+ "runtimeNeeds": {
459
+ "entry": "src/worker.ts",
460
+ "build": "npm --prefix app install && npm --prefix app run build",
461
+ "assets": {
462
+ "directory": "app/dist",
463
+ "notFoundHandling": "single-page-application", // deep client routes → index.html
464
+ "runWorkerFirst": ["/api/*", "/internal/*"] // only these reach the worker
465
+ }
466
+ }
467
+ }
468
+ ```
469
+
470
+ `build` runs before assets are collected, so the directory may be pure build output. Never
471
+ base64-inline a built `app/dist` into a generated worker module: it costs ~+33 % script
472
+ size and a worker invocation per image.
473
+
474
+ **Let changesets own the version, and pass it to push explicitly** — the default bump walks
475
+ the registry forward on its own, so `package.json` and the registry drift apart within a
476
+ few deploys. Set this up when the vertical first deploys:
477
+
478
+ ```sh
479
+ pnpm add -D @changesets/cli && npx changeset init
480
+ ```
481
+
482
+ Then in `.changeset/config.json` add `"privatePackages": { "version": true, "tag": false }`
483
+ (the vertical is a private package, never npm-published), make sure `pnpm-workspace.yaml`
484
+ lists `packages: ["."]` so changesets can see the root package, and add the scripts:
485
+
486
+ ```json
487
+ "changeset": "changeset",
488
+ "release": "pnpm test && pnpm typecheck && pnpm lint:boundaries && changeset version && substrat push --version $(node -p \"require('./package.json').version\") --promote prod"
489
+ ```
490
+
491
+ Read the version with `node -p` at that point in the script, not `$npm_package_version` —
492
+ the latter is captured before `changeset version` rewrites `package.json`, so it would push
493
+ the version you just replaced. Changesets needs a git repo with a commit on the base branch
494
+ (`git init -b main`) — a scaffold that isn't a repo yet must init before the first release.
495
+
439
496
  Updates deploy **in place** from one stable script — data carries forward, migrations run
440
497
  against prod data, backout is a time-boxed PITR rewind.
441
498