@akanjs/cli 3.0.2 → 3.1.0-rc.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/.build-stamp +1 -1
- package/{CodeAgent-szbjv4gv.js → CodeAgent-kyprmy06.js} +2 -2
- package/{CodeTui-5nw86ab6.js → CodeTui-21mtze3j.js} +1 -1
- package/{TunnelShare-cwh9ezsm.js → TunnelShare-q4jq08qg.js} +2 -2
- package/{agent.command-np1dffkb.js → agent.command-f3gsc1gd.js} +5 -5
- package/akanCode.js +2 -2
- package/{application.command-gk69v1xs.js → application.command-5s780vnz.js} +37 -15
- package/{applicationBuildRunner-wx09n9g6.js → applicationBuildRunner-1yaqsrdq.js} +115 -103
- package/buildBatch.proc.js +16 -5
- package/cloud.command-yj6xb2a7.js +192 -0
- package/cloud.script-h6pvcpa2.js +20 -0
- package/{code.command-hmtx14b2.js → code.command-3wkn4ja5.js} +6 -6
- package/commandManifest.json +1 -1
- package/{context.command-6aqvdx43.js → context.command-9jjn3gcw.js} +13 -13
- package/context.runner-atcrzz5b.js +21 -0
- package/{guideline.command-axvf68gc.js → guideline.command-dtczrqpf.js} +2 -2
- package/guidelines/agentRule/agentRule.instruction.md +10 -2
- package/guidelines/conventions/conventions.instruction.md +70 -25
- package/guidelines/cssRule/cssRule.instruction.md +19 -0
- package/guidelines/mcpRule/mcpRule.instruction.md +72 -12
- package/guidelines/queryRule/queryRule.instruction.md +11 -1
- package/guidelines/runtimeRule/runtimeRule.instruction.md +97 -16
- package/guidelines/ssrRule/ssrRule.instruction.md +10 -7
- package/guidelines/transportRule/transportRule.instruction.md +19 -0
- package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +6 -6
- package/incrementalBuilder.proc.js +24 -3
- package/{index-h9ht6s60.js → index-05g3jayg.js} +1 -1
- package/{index-jk9xnck6.js → index-1w2qaee2.js} +240 -133
- package/{index-g38nhv87.js → index-2etcv0sc.js} +1 -1
- package/{index-zyw622y1.js → index-2mx692qq.js} +3 -3
- package/{index-50wb2a26.js → index-55mynnqs.js} +354 -129
- package/{index-2wc41k5w.js → index-56d2g8ms.js} +2 -2
- package/{index-q6ft9fkc.js → index-6jjtnhkw.js} +8 -3
- package/index-6w0rbr9d.js +68 -0
- package/{index-hxnnfy57.js → index-8ggztp8k.js} +204 -8
- package/{index-zwd84m0w.js → index-8r4d7cmt.js} +4 -4
- package/{index-j38qkvkk.js → index-ae5pwhgs.js} +12 -10
- package/{index-gtjgnv36.js → index-aprt7pkz.js} +6 -9
- package/{index-1bjkfvm9.js → index-c1x0be5j.js} +2 -2
- package/{index-z750gk1j.js → index-c2yrsxdh.js} +13 -7
- package/{index-cb6yc5mb.js → index-c76k723e.js} +2 -2
- package/{index-6keyf4sg.js → index-ctanya41.js} +4 -4
- package/{index-f4e08ptp.js → index-e0cvze1s.js} +4 -4
- package/{index-hp3stk1m.js → index-fm46xrf1.js} +3 -3
- package/{index-3xmzr1p1.js → index-gnyf1fqy.js} +2 -2
- package/{index-nwd6qzvv.js → index-jx4bzvwg.js} +138 -55
- package/{index-jb0hmt3k.js → index-krq7btbt.js} +3 -3
- package/{index-94s080qm.js → index-pgj2rt78.js} +139 -1
- package/index-q958tqqg.js +77 -0
- package/{index-50jjwg8y.js → index-qt22xstj.js} +1 -1
- package/index-rc5rwyps.js +201 -0
- package/{index-xwprn9nr.js → index-rtct7d43.js} +1 -1
- package/{index-bsv901fd.js → index-rwq3ekqc.js} +7 -7
- package/{index-1gs9g12n.js → index-v29746vb.js} +4 -4
- package/index-ww9bhp67.js +62 -0
- package/index.js +22 -21
- package/{library.command-9m2rdwwz.js → library.command-gaga5bmq.js} +3 -3
- package/{localRegistry.command-kda81jyk.js → localRegistry.command-vsreyy42.js} +9 -9
- package/{module.command-q63dffya.js → module.command-7vpf6db4.js} +5 -5
- package/{package.command-y2jh0m99.js → package.command-twmvvf69.js} +3 -3
- package/package.json +2 -2
- package/{page.command-f3e4cwhh.js → page.command-5asq7fdf.js} +3 -3
- package/pipeline.command-d0v38572.js +1334 -0
- package/{primitive.command-r4kq75qj.js → primitive.command-dyrs2fm8.js} +6 -6
- package/projectTypecheck-cvtkrzp2.js +7 -0
- package/{quality.command-f7gzh26z.js → quality.command-6emsqpbg.js} +5 -4
- package/{repair.command-0qmyr6qa.js → repair.command-8t6aegv5.js} +5 -5
- package/{scalar.command-kddn7av4.js → scalar.command-bnggy1x7.js} +4 -4
- package/{subspace.command-ft65qvc4.js → subspace.command-md2teygq.js} +158 -91
- package/templates/app/env/env.client.type.ts +2 -3
- package/templates/app/page/styles.css.template +8 -0
- package/templates/appSample/page/task/[taskId]/_index.tsx +2 -2
- package/templates/appSample/page/task/[taskId]/edit.tsx +2 -2
- package/templates/appSample/page/task/_index.tsx +2 -2
- package/templates/crudPages/[__model__Id]/edit/page.tsx +2 -2
- package/templates/crudPages/[__model__Id]/page.tsx +3 -3
- package/templates/crudPages/page.tsx +2 -2
- package/templates/crudSinglePage/page.tsx +3 -3
- package/templates/lib/cnst.ts +2 -2
- package/templates/lib/sig.ts +3 -3
- package/{tunnel.command-b3ymdk4f.js → tunnel.command-jx3tdm2d.js} +16 -13
- package/{workflow.command-z1srcdfw.js → workflow.command-jx0wag5r.js} +10 -10
- package/{workspace.command-vp6vmx3z.js → workspace.command-yxd7rr7g.js} +57 -27
- package/cloud.command-tw06613r.js +0 -95
- package/cloud.script-64gzba8r.js +0 -20
- package/context.runner-hnkvv3z3.js +0 -21
- package/index-t91a7339.js +0 -104
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Runtime Rule — Serving, Processes, Logging, Image, Native Apps, Desktop Server, Assets
|
|
1
|
+
# Runtime Rule — Serving, Processes, Logging, Image, Native Apps, Desktop Server, Assets, Build Pipeline
|
|
2
2
|
|
|
3
3
|
How an Akan app is built, what it serves, how many processes it runs, where its logs go, and what ends up in
|
|
4
4
|
its image. Everything here is declared in `akan.config.ts` or narrowed by an env at boot; none of it is reached
|
|
@@ -255,31 +255,62 @@ const config: AppConfig = {
|
|
|
255
255
|
other CPU of that OS: the Rust library, Bun's executable, the server's `bun install --cpu` and each `bin` file
|
|
256
256
|
follow it, and a server addon with no binary for that CPU stops the build. A macOS app is Apple silicon only —
|
|
257
257
|
Intel Macs are not a target, so `--arch x64` on macOS is refused.
|
|
258
|
-
- **A
|
|
258
|
+
- **A release is signed only with `--sign` and notarized only with `--notarize`** (`build-desktop`,
|
|
259
|
+
`publish-update`); the environment names the identity and never decides to use it, so a shell exporting a Developer
|
|
260
|
+
ID signs and uploads nothing unasked. Before either, the server the app carries is searched for every secret value
|
|
261
|
+
of the workspace's server env files and refused with the paths, and one line names the identity and the service
|
|
262
|
+
the app goes to.
|
|
263
|
+
- **A macOS release for download is signed with a Developer ID and notarized.**
|
|
259
264
|
`AKAN_NATIVE_MACOS_IDENTITY` names a keychain identity, or `AKAN_NATIVE_MACOS_CERTIFICATE` +
|
|
260
265
|
`_CERTIFICATE_PASSWORD` a `.p12` the build imports into a keychain of its own (a CI runner);
|
|
261
|
-
`AKAN_NATIVE_MACOS_NOTARY_KEY` + `_KEY_ID` + `_ISSUER` (an App Store Connect API key) or `_NOTARY_PROFILE`
|
|
262
|
-
|
|
266
|
+
`AKAN_NATIVE_MACOS_NOTARY_KEY` + `_KEY_ID` + `_ISSUER` (an App Store Connect API key) or `_NOTARY_PROFILE` are the
|
|
267
|
+
credentials `--notarize` staples the app and the dmg with. Every Mach-O file — the server's addons and `bin` included — is
|
|
263
268
|
signed inside out with the hardened runtime and a timestamp; the executable gets Bun's JIT entitlements and the
|
|
264
269
|
camera's or microphone's when a usage text asks for them, and `desktop.entitlements` adds the app's own.
|
|
265
|
-
`publish-update` signs with the same identity, since the updater checks the installed app's signature. Without a
|
|
270
|
+
`publish-update --sign` signs with the same identity, since the updater checks the installed app's signature. Without a
|
|
266
271
|
Developer ID the build warns, because Gatekeeper blocks a downloaded copy.
|
|
267
|
-
- **A Windows release is signed with Authenticode
|
|
272
|
+
- **A Windows release is signed with Authenticode.** `AKAN_NATIVE_WINDOWS_CERTIFICATE` +
|
|
268
273
|
`_CERTIFICATE_PASSWORD` (a `.pfx`), `AKAN_NATIVE_WINDOWS_THUMBPRINT` (a certificate in the store, a token's or an
|
|
269
274
|
HSM's), or `AKAN_NATIVE_WINDOWS_SIGN_COMMAND` (a JSON array run per file with `{file}`, e.g. signtool with Azure
|
|
270
275
|
Trusted Signing's dlib) signs every PE file of the app — the server's `.node` and `bin` included — the setup program
|
|
271
276
|
and the uninstaller it writes, SHA-256 with a timestamp (`AKAN_NATIVE_WINDOWS_TIMESTAMP_URL`).
|
|
272
|
-
- **`updates: { url,
|
|
277
|
+
- **`updates: { url, publicKeys }` lets an installed app update itself** — the whole app on a desktop, the web
|
|
273
278
|
bundle on a phone. `akan update-keygen <app>` makes the signing key once
|
|
274
|
-
(`~/.akan/native/keys/<app id>.update.key`, or the path `AKAN_NATIVE_UPDATE_KEY` names) and prints
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
+
(`~/.akan/native/keys/<app id>.update.key`, or the path `AKAN_NATIVE_UPDATE_KEY` names) and prints its public
|
|
280
|
+
key. List the online signing key first and offline spares after it (up to three): a phone binary trusts all of
|
|
281
|
+
them, and a key may revoke only the keys listed before it, so a leaked key cannot lock out its spares. A desktop
|
|
282
|
+
app trusts the first; `publicKey` is the one-key form. `akan publish-update <app>` builds and signs a release
|
|
283
|
+
into `updates/`: upload that folder to `url`, the manifests last, or let `--upload s3://<bucket>/<prefix>` do it
|
|
284
|
+
(credentials and endpoint from `S3_*` or `AWS_*`; a release file the bucket holds is skipped). A phone reads only
|
|
285
|
+
`<url>/<platform>/<channel>/<nativeApi>.json`, so each store generation on a channel takes its own fixes.
|
|
286
|
+
`akan pack-update` writes an unsigned phone bundle for whoever holds the key to sign and records it in
|
|
287
|
+
`<out>/pack.json`, exiting 2 when `--against` says it needs a new store build; `akan push-update` packs and uploads to
|
|
288
|
+
an akasys project that holds the key, and `akan register-binary` registers a store build after `release-*`. A
|
|
289
|
+
release that hangs is rolled back while the app runs, and `report: { url }` has a phone post what became of each
|
|
290
|
+
release (applied, confirmed, rolled back and why) at its next launch, so rollback rates are visible. `devtools: true`
|
|
291
|
+
opens the web inspector of a release build for an internal test target. The channel is the backend env the binary was built for unless `channel` names one;
|
|
292
|
+
on a desktop, `updates.setChannel({ channel })` moves the installed app to another manifest until set back with
|
|
293
|
+
`null` — a server putting a device on a pilot channel — and never to a release older than the one running. A
|
|
294
|
+
release that does not come up is rolled back to the one before.
|
|
279
295
|
- **An unattended screen** — signage, a kiosk — takes `desktop.recovery: "reload"` (a crashed page loads again,
|
|
280
|
-
and the app relaunches when the webview ends), `desktop.window: { fullscreen, skipTaskbar }`,
|
|
296
|
+
and the app relaunches when the webview ends), `desktop.window: { fullscreen, skipTaskbar, frameless }`,
|
|
281
297
|
`desktop.screenCapture: "auto"` (Windows answers `getDisplayMedia()` with the first screen, no picker) and
|
|
282
|
-
`android.autoplay` (media plays with sound without a tap).
|
|
298
|
+
`android.autoplay` (media plays with sound without a tap). Its carried server takes `desktop.server: { restart:
|
|
299
|
+
"forever", watchdog: true }` — restarted after every crash, never given up on, and killed and restarted when its
|
|
300
|
+
health route stops answering (every 10 s, 3 misses; `{ intervalMs, failures }` to tune) — and
|
|
301
|
+
`desktop.webview2: "standalone"` puts the whole WebView2 Runtime (~200 MB) in the Windows setup for a PC offline at
|
|
302
|
+
install. `desktop.server.env` adds variables to the server's start; the build refuses a secret-named one and those
|
|
303
|
+
the stage owns. The `window` plugin places it: `placeOnDisplay({ index, bounds, unit: "physical" })` puts a window
|
|
304
|
+
on one display in that display's device pixels — a 640×128 LED panel — lowering the minimum size to fit, beside
|
|
305
|
+
`setAlwaysOnTop`, `setCursorVisible` (which hides the pointer over its external views too, on Windows) and
|
|
306
|
+
`setDecorations`. `native.backgroundColor` (or `{ light, dark }`) is what a window and an external view show before
|
|
307
|
+
their page paints, so a dark board never flashes white, and `desktop.publisher` names who publishes a Windows app in
|
|
308
|
+
Apps & features and its executable's company (the app name when unset, never Bun's own).
|
|
309
|
+
- **A third-party page shown first-party** — a scoreboard, a menu board — is the `external-view` plugin, listed in
|
|
310
|
+
`native.plugins`: a webview over the window with a browser profile of its own, held to the origins it was opened
|
|
311
|
+
for, with an init script, `evaluate`, a trusted `sendKey`, `capture`, and `loadFailed` / `gone` (it reloads, or is
|
|
312
|
+
rebuilt, by itself) / `consoleMessage` events. Every method on Windows; macOS 14+ lacks `sendKey`, `setMuted` and
|
|
313
|
+
`capture`.
|
|
283
314
|
|
|
284
315
|
## A Desktop App's Server — `bin` And `trustedDependencies`
|
|
285
316
|
|
|
@@ -340,7 +371,8 @@ const config: AppConfig = {
|
|
|
340
371
|
- **An executable the desktop app spawns goes in `bin`** — its server's code or its native plugins, whether or not
|
|
341
372
|
it carries a server — keyed by the name the code spawns and then by
|
|
342
373
|
`${process.platform}-${process.arch}` (`darwin-arm64`, `darwin-x64`, `linux-arm64`, `linux-x64`, `win32-arm64`,
|
|
343
|
-
`win32-x64`)
|
|
374
|
+
`win32-x64`) — Node's names, unlike an update feed's folder, which is `<os>-<arch>` with the OS spelled `macos`,
|
|
375
|
+
`windows` or `linux` (`windows-x64/<channel>.json`). A source is `{ url, sha256, file? }` — downloaded when the app is built, refused unless it hashes
|
|
344
376
|
to `sha256` (so plain http is as safe as https), kept in `apps/<app>/.akan/cache/bin/<sha256>` — or
|
|
345
377
|
`{ path, file? }`, relative to the `akan.config.ts` that declares it. `file` is the executable inside an archive
|
|
346
378
|
(`.zip`, `.tar.gz`, `.tgz`, `.tar.xz`, `.tar.bz2`, `.tar`), unpacked with the OS's own `tar` (`unzip` for a zip
|
|
@@ -378,6 +410,18 @@ was still starting — the server sees its parent gone and ends the group itself
|
|
|
378
410
|
later for whatever ignored it. On Windows the job object ends them all with the app. A process started `detached`
|
|
379
411
|
leaves the group, or the job, and keeps running.
|
|
380
412
|
|
|
413
|
+
**An edge server carries no cloud secret.** The carried server runs `operationMode` edge, and the same app's cloud
|
|
414
|
+
deployment runs cloud: a module whose `serve(ref, { operationMode: ["edge"] }, …)` lists the modes it belongs to mounts
|
|
415
|
+
in those alone, and `modules: ["@edge"]` (or `AKAN_MODULES=@edge`) names every module that lists edge, with what they
|
|
416
|
+
inject, cascade to and hold a relation to. `desktop: { server: { profile: "edge", modules: ["@edge"] } }` builds the
|
|
417
|
+
carried server that way: `env/env.server.<env>.edge.ts` is bundled in place of `env/env.server.<env>.ts`, which never
|
|
418
|
+
enters it, and the build evaluates the profile file with no app env and refuses one whose secret-named keys hold a
|
|
419
|
+
value — a `process.env.X ?? "literal"` counts as the literal. Each lib's env defaults (`env.server.testing.ts`) are
|
|
420
|
+
swapped the same way: for the lib's `env/env.server.testing.edge.ts`, held to the same check, or for the defaults with
|
|
421
|
+
every secret-named value left out, which the build names in a warning. `akan build <app> --profile edge` does the
|
|
422
|
+
same for an image. A module listing only edge does not mount under `akan start` (local): list `local` too to develop
|
|
423
|
+
it there.
|
|
424
|
+
|
|
381
425
|
**A file the user picks reaches the server as a grant, never as a copy or a path.** With `"file-picker"` in
|
|
382
426
|
`native.plugins`, `filePicker.pickFiles({ forServer: true })` (also `pickDirectory` and `saveFile`,
|
|
383
427
|
from `akanjs/client/native`) copies nothing, whatever the size: its FileRefs serve the originals for a preview, and
|
|
@@ -417,7 +461,7 @@ lacks is added there: an app's own plugin runs as Bun code in the plugin host an
|
|
|
417
461
|
`single` and a cloud cluster in `cluster`: declare both.
|
|
418
462
|
- **Where the data lives is the deployment's env, ahead of `env.server.ts`**: `SQLITE_DATABASE_PATH`,
|
|
419
463
|
`AKAN_SOLID_DB_PATH`, `POSTGRES_URL` (or `POSTGRES_HOST`/`PORT`/`DATABASE`/`USER`/`PASSWORD`),
|
|
420
|
-
`POSTGRES_INSIGHT_URL
|
|
464
|
+
`POSTGRES_INSIGHT_URL`. `REDIS_URI` is required outside local development. Pool size, SSL and
|
|
421
465
|
prepared statements ride the Postgres URL (`?max=20&ssl=require`, `prepare=false` behind PgBouncer).
|
|
422
466
|
- **Uploads**: a deployed `multiple`/`cluster` app uses object storage, or a volume every instance mounts with
|
|
423
467
|
`AKAN_STORAGE_SHARED=true`.
|
|
@@ -508,3 +552,40 @@ const config: AppConfig = { assets: { pruneFonts: false } }; // emergency valve,
|
|
|
508
552
|
- The phase is `assets`, between `csr` and `compress`. An api-only build (`web: false`) copies no `public/` and
|
|
509
553
|
the phase finds nothing to do.
|
|
510
554
|
|
|
555
|
+
## The Build Pipeline — `pipeline` In `akan.config.ts`
|
|
556
|
+
|
|
557
|
+
Akan Cloud builds and deploys each app as `build` → `docker` → `deploy`, and `ota` after them when asked. **An app's own stages go in
|
|
558
|
+
`apps/<app>/akan.config.ts` under `pipeline`; stages around every app go in the workspace root's
|
|
559
|
+
`akan.config.ts`**, typed `WorkspaceConfig` (`before`, `after`, `post`, `env`, `timeout`). Both are `import type`
|
|
560
|
+
objects (or `(ctx) => …`), and `akan pipeline dry-run | validate | plan` turns them into the plan Akan Cloud runs —
|
|
561
|
+
locally, running nothing.
|
|
562
|
+
|
|
563
|
+
```ts
|
|
564
|
+
const config: AppConfig = {
|
|
565
|
+
pipeline: { stages: [{ use: "build" }, { name: "Test", sh: "bun run akan test web" }, { use: "docker" }, { use: "deploy" }] },
|
|
566
|
+
};
|
|
567
|
+
const config: WorkspaceConfig = {
|
|
568
|
+
pipeline: { post: { failure: [{ name: "Notify", sh: "./scripts/notify.sh", secrets: ["DISCORD_WEBHOOK"] }] } },
|
|
569
|
+
};
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
- **A declared list is the whole pipeline.** Without `pipeline` an app runs the three actions; with it, write them
|
|
573
|
+
— a list without `{ use: "deploy" }` never deploys. Each action appears at most once, in that order, and
|
|
574
|
+
`docker` needs a `build` before it (the image is made from that build).
|
|
575
|
+
- **`{ use: "ota" }` ships the native web bundle over the air**, after `deploy` so the API it calls is already up.
|
|
576
|
+
It is opt-in, makes its own CSR build, and packs every native target that declares `native.updates` for iOS
|
|
577
|
+
and Android — `targets` and `platforms` narrow it, and a target without `updates` is a plan error. Akan Cloud
|
|
578
|
+
signs and publishes the packs; the plan never holds a key.
|
|
579
|
+
- **A stage runs after the action before it.** Stages between two actions share a phase across apps, and a phase
|
|
580
|
+
waits for every stage of the one before; `deploy` runs the apps one at a time in `after` order.
|
|
581
|
+
- **App stages stand on their own.** Never rely on what a root `before` stage left on disk: the root config never
|
|
582
|
+
travels to a subspace, so an app built there runs its own stages only. Keep the files an app stage needs inside
|
|
583
|
+
the app or its libs.
|
|
584
|
+
- **Secrets are named and write-only.** `akan upload-secret <NAME>` stores one per project (`--branch` for a
|
|
585
|
+
branch-only value); a step lists it in `secrets` and reads `$NAME`. Anyone who can push to the repo can make a
|
|
586
|
+
step print it — the GitHub Actions model — so push access is secret access.
|
|
587
|
+
- **`allowFailure` only means "this failure does not fail the run"**; how the run is reported is Akan Cloud's
|
|
588
|
+
call. `env` and secret names may not be `CI`, `AKAN_CI_*` or shell names such as `PATH`, and every step gets
|
|
589
|
+
`AKAN_CI_BRANCH`, `AKAN_CI_APP` and the rest of the build's facts.
|
|
590
|
+
- `akan pipeline` exits 0 with a plan, 1 with an envelope of errors, and 2 for anything else (no envelope). A lib
|
|
591
|
+
config takes no `pipeline`; the root config takes no app or lib settings.
|
|
@@ -125,21 +125,24 @@ Three rules the shape enforces:
|
|
|
125
125
|
**A failure now surfaces where it happened.** One `Promise.all` in the page body reports nothing about which of
|
|
126
126
|
eight queries threw; per-section boundaries put each rejection at its own boundary.
|
|
127
127
|
|
|
128
|
-
**
|
|
129
|
-
|
|
130
|
-
|
|
128
|
+
**Know which redirects are real.** A streaming response starts while layout and page bodies still run, so a redirect
|
|
129
|
+
is a real 3xx only when it was raised before any I/O was awaited, the body's own or a layout's above it (a layout that
|
|
130
|
+
awaits `fetch.viewOrg` makes every page under it late); after one — `getSelf({ unauthorize: "/signin" })` fetches —
|
|
131
|
+
it reaches the browser as an HTTP 200 that navigates in the page, which a crawler or `curl` does not
|
|
132
|
+
follow. Either way no markup below the redirect is sent. A route that must answer a real 3xx after I/O — a gate an
|
|
133
|
+
external client hits, a moved URL — takes `.config({ ssr: "block" })`: the response waits for the whole render.
|
|
134
|
+
Never redirect from inside a streamed section.
|
|
131
135
|
|
|
132
136
|
**⑥ Use named `ReactNode` slots, not just `children`.** A client shell that takes one slot per region lets a
|
|
133
137
|
server component supply each of them, instead of absorbing the whole subtree into the bundle. `Layout.Navbar`
|
|
134
|
-
|
|
135
|
-
`children` — copy the shape, not that component.
|
|
138
|
+
is the shape: `title` / `left` / `right` beside `back` and `children`, each filled by the server.
|
|
136
139
|
|
|
137
140
|
**⑦ Let the server do the derived work.** Display and predicate logic belongs on `Light<Model>` (`isNew()`,
|
|
138
141
|
`canWrite(user?)`, `formatTimes()`), and enum→class lookups belong in a module-scope `as const` map. Both sides call
|
|
139
142
|
the same method, so a client component that exists only to compute a label is markup in the wrong place.
|
|
140
143
|
|
|
141
|
-
**⑧ Gate auth on the server.** `getSelf({ unauthorize: "/signin" })` in `_layout.tsx` redirects before any
|
|
142
|
-
|
|
144
|
+
**⑧ Gate auth on the server.** `getSelf({ unauthorize: "/signin" })` in `_layout.tsx` redirects before any of the
|
|
145
|
+
page's markup is rendered. A client-side check costs a hydration round-trip and flashes the wrong UI first.
|
|
143
146
|
|
|
144
147
|
**⑨ Prefer CSS over client state for pure visibility.** A `data-*` attribute plus `group-data-[…]` variants (see
|
|
145
148
|
`libs/util/ui/Grid/*`) or `<details>`/`<summary>` keeps both branches server-rendered. Reach for `useState` when the
|
|
@@ -21,8 +21,27 @@ mutation is mounted under. `conventions` carries the invariants — this is the
|
|
|
21
21
|
`null` rather than a placeholder when genuinely unknown — a loopback-looking address for an unknown caller
|
|
22
22
|
is the failure this replaced. The gateway also forwards the client port (`x-forwarded-port`, read with
|
|
23
23
|
`context.getClientPort()`) along with host and protocol.
|
|
24
|
+
- **A device or another server proves itself with a machine token.** `MachineToken.issue({ kind, id, version },
|
|
25
|
+
{ expiresAt? })` in `akanjs/server` signs an EdDSA JWT (`typ: akan-machine+jwt`, `aud: <app>/<environment>`) with
|
|
26
|
+
`AKAN_MACHINE_PRIVATE_KEY`; it never expires unless asked. `akan machine-keygen <file>` writes that key to a file
|
|
27
|
+
and prints only the public one. A server with either key verifies the bearer of every call — HTTP, and the
|
|
28
|
+
websocket's auth frame — after every lib middleware, and sets `account.machine = { kind, id, version }` from a
|
|
29
|
+
verified token; it takes off a `machine` any other middleware or a session's claims put there, so a staff token
|
|
30
|
+
can never pass for one, and a session middleware already reads the EdDSA token as anonymous. `.with(Machine)` hands
|
|
31
|
+
the identity (401 without one; `{ nullable: true }` takes `null`), and a pubsub room's guards see it on subscribe
|
|
32
|
+
and on every credential change. Revocation is the app's: a guard compares `machine.version` with the record's
|
|
33
|
+
`tokenVersion`, which a removal or a reinstall bumps. An endpoint taking `.with(Machine)` on a server with no key
|
|
34
|
+
is an error at boot. The calling side is `fetch.clone({ origin, jwt: token, connect: true })`.
|
|
24
35
|
- **Every socket carries a `socketId`, and only the framework mints one.** `AppWsData` assigns it at the handshake, and a `message` / `pubsub` handler reads it off the `Ws` internal arg — `.with(Ws)` hands `{ ws, socketId, subscribe, on, off }`. Never mint your own from `ws.data`: the room bookkeeping keys on the framework's id, and a second one fails to match it silently. It identifies a **connection**, not a caller — a reconnect gets a new id and the federation gateway's own socket is a different one — so per-user state keys on the account, never on this.
|
|
25
36
|
|
|
37
|
+
- **A socket that dropped comes back on a backoff, and asks for what it missed.** The client waits 1 s doubling to
|
|
38
|
+
30 s, cut at random so a deploy's dropped clients return apart; `fetch.clone({ ws: { reconnect: { min, max, jitter,
|
|
39
|
+
maxAttempts } | false, headers, tls, onStateChange, onRevoked, onError } })` (or `fetch.instance.configureWs(…)`)
|
|
40
|
+
tunes it, and a server-side client sends `headers` and its token with the upgrade. A server numbers the JSON frames
|
|
41
|
+
each room delivers (`seq`); a client drops a repeat, and a hole — a frame dropped under backpressure — calls the
|
|
42
|
+
room's `onResync`. A resubscribe names its last frame (`since`): a server that saw every publish (a solo process)
|
|
43
|
+
replays what followed it ahead of the ack, and otherwise answers a gap, which calls `onResync` as before. Binary
|
|
44
|
+
rooms are not numbered.
|
|
26
45
|
- **A cleanup registered with `ws.on("disconnect" | "unsubscribe", fn)` is scoped to the call that registered it.**
|
|
27
46
|
From a `pubsub` subscribe it belongs to that room — `unsubscribe` runs when the client leaves it or a credential
|
|
28
47
|
change revokes it, `disconnect` when the socket closes while still subscribed — so a room already left runs
|
|
@@ -187,8 +187,8 @@ export class TaskFilter extends from(cnst.Task, (filter) => ({
|
|
|
187
187
|
}))
|
|
188
188
|
|
|
189
189
|
// 4. In page — init the slice inside the page chain's render and hand the init to a Zone.
|
|
190
|
-
export default page().render(
|
|
191
|
-
const
|
|
190
|
+
export default page().render(() => {
|
|
191
|
+
const { taskInitInTodo } = fetch.initTaskInTodo();
|
|
192
192
|
return <Task.Zone.Card init={taskInitInTodo} sliceName="taskInTodo" />;
|
|
193
193
|
});
|
|
194
194
|
```
|
|
@@ -349,8 +349,8 @@ export function generateHead({ params }: PageProps) {
|
|
|
349
349
|
}
|
|
350
350
|
export default async function Page({ params: { projectId }, searchParams }: PageProps) {
|
|
351
351
|
const { l } = usePage();
|
|
352
|
-
const {
|
|
353
|
-
return <Project.Zone.View view={
|
|
352
|
+
const { projectView } = await fetch.viewProject(projectId);
|
|
353
|
+
return <Project.Zone.View view={projectView} tab={searchParams.tab} />;
|
|
354
354
|
}
|
|
355
355
|
export const pageConfig = { transition: "stack" } satisfies PageConfig;
|
|
356
356
|
|
|
@@ -367,8 +367,8 @@ export default page()
|
|
|
367
367
|
.prompt("briefProject", "Brief one project: its status and what is due.")
|
|
368
368
|
.render(async ({ projectId, tab }) => {
|
|
369
369
|
const { l } = usePage();
|
|
370
|
-
const {
|
|
371
|
-
return <Project.Zone.View view={
|
|
370
|
+
const { projectView } = await fetch.viewProject(projectId);
|
|
371
|
+
return <Project.Zone.View view={projectView} tab={tab} />;
|
|
372
372
|
});
|
|
373
373
|
```
|
|
374
374
|
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
CodegenLock,
|
|
9
9
|
WorkspaceExecutor,
|
|
10
10
|
AppExecutor
|
|
11
|
-
} from "./index-
|
|
11
|
+
} from "./index-55mynnqs.js";
|
|
12
12
|
import"./index-52w0fvh8.js";
|
|
13
13
|
import {
|
|
14
14
|
GraphClientEntryDiscovery,
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
DevChangePlanner,
|
|
21
21
|
DevGeneratedIndexSync,
|
|
22
22
|
SsrDevBundler
|
|
23
|
-
} from "./index-
|
|
23
|
+
} from "./index-aprt7pkz.js";
|
|
24
24
|
import {
|
|
25
25
|
prepareDevWatchBatch
|
|
26
26
|
} from "./index-a5vmm6x4.js";
|
|
@@ -235,6 +235,7 @@ class IncrementalBuilder {
|
|
|
235
235
|
#autoImportSync;
|
|
236
236
|
#watcher = null;
|
|
237
237
|
#generation = 0;
|
|
238
|
+
#shellGeneration = null;
|
|
238
239
|
#discoveredThrough = 0;
|
|
239
240
|
#csrActive = IncrementalBuilder.#csrArmedByEnv();
|
|
240
241
|
#csrBundler;
|
|
@@ -852,7 +853,7 @@ class IncrementalBuilder {
|
|
|
852
853
|
if (crashedNeeds.includes("ssr"))
|
|
853
854
|
this.#ssrPatcher?.releaseHandedOver();
|
|
854
855
|
for (const need of crashedNeeds)
|
|
855
|
-
if (need !== "base")
|
|
856
|
+
if (need !== "base" && need !== "shell")
|
|
856
857
|
this.#sendBuildStatus(need, {
|
|
857
858
|
generation,
|
|
858
859
|
ok: false,
|
|
@@ -916,6 +917,10 @@ class IncrementalBuilder {
|
|
|
916
917
|
this.#logger.verbose(`announced boot state after recycle generation=${generation}`);
|
|
917
918
|
}
|
|
918
919
|
async handleBuildCsr(msg) {
|
|
920
|
+
if (msg.shell) {
|
|
921
|
+
await this.#buildShell(msg);
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
919
924
|
this.#beginCsrArm();
|
|
920
925
|
this.#csrActive = true;
|
|
921
926
|
const armed = this.#workQueue.enqueue("build-csr", async () => {
|
|
@@ -940,6 +945,22 @@ class IncrementalBuilder {
|
|
|
940
945
|
this.#csrGate = this.#armingCsr(armed);
|
|
941
946
|
await armed;
|
|
942
947
|
}
|
|
948
|
+
async#buildShell(msg) {
|
|
949
|
+
await this.#workQueue.enqueue("build-shell", async () => {
|
|
950
|
+
const generation = this.#generation;
|
|
951
|
+
let error;
|
|
952
|
+
if (this.#shellGeneration !== generation)
|
|
953
|
+
try {
|
|
954
|
+
const result = await this.#batchRunner.run(await this.#batchRequest({ generation, needs: ["shell"], changedFiles: [] }));
|
|
955
|
+
error = result.errors.shell;
|
|
956
|
+
if (!error)
|
|
957
|
+
this.#shellGeneration = generation;
|
|
958
|
+
} catch (err) {
|
|
959
|
+
error = ApplicationBuildReporter.formatError(err, this.#app.workspace.workspaceRoot);
|
|
960
|
+
}
|
|
961
|
+
await BuilderChannel.send(error ? { type: "build-csr-res", id: msg.id, ok: false, error } : { type: "build-csr-res", id: msg.id, ok: true });
|
|
962
|
+
});
|
|
963
|
+
}
|
|
943
964
|
#beginCsrArm() {
|
|
944
965
|
if (this.#csrArms === 0) {
|
|
945
966
|
this.#csrWasActive = this.#csrActive;
|