@astrale-os/adapter-cloudflare 0.1.8 → 0.1.10

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.
Files changed (123) hide show
  1. package/dist/assets-pack.d.ts +1 -1
  2. package/dist/assets-pack.js +1 -1
  3. package/dist/build.d.ts +15 -0
  4. package/dist/build.d.ts.map +1 -0
  5. package/dist/build.js +15 -0
  6. package/dist/build.js.map +1 -0
  7. package/dist/client.d.ts +9 -0
  8. package/dist/client.d.ts.map +1 -1
  9. package/dist/client.js +10 -1
  10. package/dist/client.js.map +1 -1
  11. package/dist/cloudflare.d.ts +15 -3
  12. package/dist/cloudflare.d.ts.map +1 -1
  13. package/dist/cloudflare.js +52 -18
  14. package/dist/cloudflare.js.map +1 -1
  15. package/dist/codegen/worker.d.ts +26 -6
  16. package/dist/codegen/worker.d.ts.map +1 -1
  17. package/dist/codegen/worker.js +67 -54
  18. package/dist/codegen/worker.js.map +1 -1
  19. package/dist/codegen/wrangler.d.ts +11 -2
  20. package/dist/codegen/wrangler.d.ts.map +1 -1
  21. package/dist/codegen/wrangler.js +11 -5
  22. package/dist/codegen/wrangler.js.map +1 -1
  23. package/dist/index.d.ts +6 -3
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +5 -2
  26. package/dist/index.js.map +1 -1
  27. package/dist/params.d.ts +30 -30
  28. package/dist/params.d.ts.map +1 -1
  29. package/dist/parse-output.d.ts +1 -1
  30. package/dist/parse-output.js +1 -1
  31. package/package.json +6 -2
  32. package/src/assets-pack.ts +1 -1
  33. package/src/build.ts +15 -0
  34. package/src/client.ts +11 -1
  35. package/src/cloudflare.ts +53 -18
  36. package/src/codegen/worker.ts +76 -59
  37. package/src/codegen/wrangler.ts +15 -5
  38. package/src/index.ts +6 -3
  39. package/src/params.ts +32 -31
  40. package/src/parse-output.ts +1 -1
  41. package/template/.agents/skills/astrale-cli/SKILL.md +26 -12
  42. package/template/.agents/skills/astrale-domain/SKILL.md +46 -29
  43. package/template/.env.example +6 -0
  44. package/template/README.md +25 -10
  45. package/template/astrale.config.ts +27 -33
  46. package/template/client/README.md +80 -63
  47. package/template/client/__tests__/app.test.tsx +188 -99
  48. package/template/client/__tests__/harness.ts +67 -12
  49. package/template/client/__tests__/kernel.test.ts +65 -50
  50. package/template/client/__tests__/seam.test.tsx +111 -0
  51. package/template/client/index.html +1 -1
  52. package/template/client/package.json +1 -0
  53. package/template/client/src/app.tsx +40 -83
  54. package/template/client/src/main.tsx +2 -2
  55. package/template/client/src/monitor/components/MonitorCard.tsx +50 -0
  56. package/template/client/src/monitor/components/index.ts +1 -0
  57. package/template/client/src/monitor/hooks/index.ts +3 -0
  58. package/template/client/src/monitor/hooks/useCheck.mutation.ts +16 -0
  59. package/template/client/src/monitor/hooks/useMonitor.query.ts +64 -0
  60. package/template/client/src/monitor/index.ts +6 -0
  61. package/template/client/src/monitor/monitor.api.ts +11 -0
  62. package/template/client/src/monitor/monitor.mappers.ts +38 -0
  63. package/template/client/src/monitor/monitor.types.ts +23 -0
  64. package/template/client/src/monitor/ui/MonitorDetails.UI.tsx +38 -0
  65. package/template/client/src/monitor/ui/StatusBadge.UI.tsx +14 -0
  66. package/template/client/src/monitor/ui/index.ts +8 -0
  67. package/template/client/src/shell/client.ts +67 -0
  68. package/template/client/src/shell/index.ts +20 -0
  69. package/template/client/src/shell/invoke.ts +35 -0
  70. package/template/client/src/shell/transformers.ts +72 -0
  71. package/template/client/src/shell/use-async.ts +56 -0
  72. package/template/client/src/shell/use-capability.ts +61 -0
  73. package/template/client/src/shell/use-node.ts +61 -0
  74. package/template/client/src/shell/use-shell.ts +91 -0
  75. package/template/client/src/shell/view-router.tsx +98 -0
  76. package/template/client/src/styles.css +177 -4
  77. package/template/client/src/ui/format.ts +24 -0
  78. package/template/client/src/ui/index.ts +9 -0
  79. package/template/client/src/ui/surface.tsx +56 -0
  80. package/template/client/src/ui/value.tsx +32 -0
  81. package/template/client/src/views/monitor.tsx +30 -0
  82. package/template/client/tsconfig.json +3 -2
  83. package/template/client/vite.config.ts +14 -15
  84. package/template/client/vitest.config.ts +12 -5
  85. package/template/core/monitor/health.ts +19 -0
  86. package/template/core/monitor/index.ts +9 -0
  87. package/template/core/monitor/keys.ts +29 -0
  88. package/template/core/monitor/node.ts +51 -0
  89. package/template/deps.ts +25 -0
  90. package/template/domain.ts +33 -0
  91. package/template/env.ts +4 -0
  92. package/template/integrations/prober/http.ts +43 -0
  93. package/template/integrations/prober/mock.ts +22 -0
  94. package/template/integrations/prober/port.ts +28 -0
  95. package/template/integrations/prober/registry.ts +66 -0
  96. package/template/package.json +2 -3
  97. package/template/pnpm-lock.yaml +2766 -0
  98. package/template/runtime/index.ts +79 -0
  99. package/template/runtime/monitor/check.ts +29 -0
  100. package/template/runtime/monitor/dependsOn.ts +16 -0
  101. package/template/runtime/monitor/index.ts +12 -0
  102. package/template/runtime/monitor/seed.ts +74 -0
  103. package/template/runtime/monitor/shared.ts +17 -0
  104. package/template/runtime/monitor/watch.ts +37 -0
  105. package/template/schema/index.ts +13 -4
  106. package/template/schema/monitor.ts +80 -0
  107. package/template/tsconfig.json +13 -2
  108. package/template/views/index.ts +9 -2
  109. package/template/views/monitor.ts +22 -0
  110. package/dist/astrale.d.ts +0 -27
  111. package/dist/astrale.d.ts.map +0 -1
  112. package/dist/astrale.js +0 -222
  113. package/dist/astrale.js.map +0 -1
  114. package/src/astrale.ts +0 -259
  115. package/template/client/src/lib/kernel.ts +0 -135
  116. package/template/client/src/lib/shell.ts +0 -197
  117. package/template/client/src/lib/use-node.ts +0 -66
  118. package/template/client/src/lib/use-shell.ts +0 -85
  119. package/template/methods/index.ts +0 -66
  120. package/template/methods/note.ts +0 -131
  121. package/template/schema/compiled.ts +0 -14
  122. package/template/schema/note.ts +0 -64
  123. package/template/views/note.ts +0 -21
package/src/params.ts CHANGED
@@ -55,37 +55,38 @@ export interface CloudflareParams {
55
55
  * worker-entry codegen and aren't supported through this field yet.
56
56
  */
57
57
  wrangler?: Record<string, unknown>
58
- }
59
-
60
- /** Params for the Astrale-managed adapter (`astrale(envs)`). */
61
- export interface AstraleParams {
62
58
  /**
63
- * Target instance slug the domain installs onto (e.g. `'bryan-e2e5'`).
64
- * Required for `deploy` (managed publish + install); ignored by `watch`
65
- * (local dev)so dev-only envs may omit it.
66
- */
67
- instance?: string
68
- /** Catalog package name. Default: the origin's first DNS label. */
69
- name?: string
70
- /** Admin kernel URL the publish calls target. Default the platform admin. */
71
- adminUrl?: string
72
- /** Routing region of the `.svc` service URL. Default `'eu'`. */
73
- region?: string
74
- /** CLI identity (`--as`) for the publish calls. Default: the CLI's active identity. */
75
- identity?: string
76
- /** Gitignored secrets file. `watch`: injected into local wrangler dev. `deploy`: shipped on the install call and applied to the managed service env (encrypted at rest platform-side; platform keys win precedence). */
77
- secrets?: string
78
- /** Local dev port for `wrangler dev`. Default 8787. */
79
- port?: number
80
- /**
81
- * Public URL the dev server is reached through (tunnel / sandbox preview /
82
- * reverse proxy), e.g. `'https://my-box.preview.dev'`. Sets BOTH effects an
83
- * off-localhost dev needs: wrangler binds 0.0.0.0 (reachable from outside)
84
- * and `WORKER_URL` is pinned to this URL so the worker's per-request-Host
85
- * identity (`iss`) doesn't drift to the proxy's internal hostname.
86
- * Usually injected by `pnpm dev --host <url>` rather than written here.
59
+ * Route subrequests to platform INSTANCE hostnames through a service binding
60
+ * instead of the public edge. A Cloudflare Worker can't fetch a same-zone
61
+ * hostname served by another Worker Cloudflare 522s it so without this the
62
+ * worker can't fetch an instance kernel's JWKS to verify an inbound credential
63
+ * from that instance, and every cross-instance call (install hooks, the GUI's
64
+ * delegated calls, authed views) fails with `2002 Credential verification
65
+ * failed`. Point it at a router Worker that reaches those hosts internally
66
+ * (Astrale: `admin-router`).
67
+ *
68
+ * Wires BOTH halves the SDK needs: the `<binding>` service binding in the
69
+ * generated wrangler config, and a `routeSubrequest` predicate in the worker
70
+ * entry that diverts matching hosts through it.
71
+ *
72
+ * ON BY DEFAULT (targets `admin-router`) so every adapter-deployed worker can
73
+ * verify cross-instance credentials out of the box. Pass `false` to disable
74
+ * (a domain that never talks to another instance, or an account without a
75
+ * router), or an object to override the target / binding name / host match.
76
+ *
77
+ * router: false // opt out
78
+ * router: { service: 'my-router' } // override target
87
79
  */
88
- host?: string
89
- /** Extra plain vars for local `watch`. */
90
- vars?: Record<string, string>
80
+ router?:
81
+ | false
82
+ | {
83
+ /** Router Worker the binding targets. Default `'admin-router'`. */
84
+ service?: string
85
+ /** Service-binding name the worker reads. Default `'ROUTER'`. */
86
+ binding?: string
87
+ /** Host suffix that marks an instance host. Default `'.astrale.ai'`. */
88
+ hostSuffix?: string
89
+ /** Min dot-separated label count for an instance host. Default `4` (`<slug>.<region>.astrale.ai`). */
90
+ minLabels?: number
91
+ }
91
92
  }
@@ -14,7 +14,7 @@ const WORKERS_DEV_RE = /(https:\/\/[^\s]+\.workers\.dev)/i
14
14
  * The fallback (when no explicit "Ready on" line) only accepts a localhost URL
15
15
  * on the EXPECTED port — wrangler may print unrelated localhost URLs (e.g. an
16
16
  * inspector/devtools endpoint on a different port) before readiness, and taking
17
- * one of those would point `astrale instance install <url>` at a dead endpoint.
17
+ * one of those would point `astrale domain install <url>` at a dead endpoint.
18
18
  */
19
19
  export function parseDevReadyUrl(text: string, port: number): string | undefined {
20
20
  const ready = READY_RE.exec(text)
@@ -38,6 +38,7 @@ astrale logs <service>
38
38
  astrale status
39
39
  astrale browser
40
40
  astrale instance ...
41
+ astrale domain ...
41
42
  astrale admin ...
42
43
  astrale identity ...
43
44
  astrale auth ...
@@ -50,7 +51,7 @@ Command groups:
50
51
  |---|---|
51
52
  | Kernel | `call`, `token`, `get`, `ls`, `describe`, `query`, `logs` |
52
53
  | Context | `status`, `whoami`, `use` |
53
- | Management | `admin`, `instance`, `identity`, `auth`, `idp`, `update` |
54
+ | Management | `admin`, `instance`, `domain`, `identity`, `auth`, `idp`, `update` |
54
55
  | Agent | `browser` |
55
56
 
56
57
  Shared kernel options are merged onto kernel-touching commands at registration
@@ -90,7 +91,7 @@ Examples:
90
91
 
91
92
  ```bash
92
93
  astrale call /:blog.acme.com:class.Author:list
93
- astrale call /:blog.acme.com:interface.NoteOps:createNote title=Hello
94
+ astrale call /:blog.acme.com:interface.MonitorOps:watch url=https://astrale.ai
94
95
  astrale call /blog.acme.com/alice::deactivate
95
96
  astrale call @f00d...::deactivate
96
97
  ```
@@ -148,7 +149,6 @@ astrale instance use my-app
148
149
  astrale instance active
149
150
  astrale instance bookmark staging --url https://kernel.example.com
150
151
  astrale instance forget staging
151
- astrale instance install https://domain.example.com
152
152
  ```
153
153
 
154
154
  Important distinctions:
@@ -164,23 +164,37 @@ Important distinctions:
164
164
  ## Domain Dev Workflow
165
165
 
166
166
  **The `astrale` CLI is connect-only — it does not build, run, or deploy
167
- domains.** There is no `astrale domain …` command group. Domain development
168
- lives in two separate tools:
167
+ domains.** The `astrale domain` group manages the admin catalog and installs a
168
+ running domain onto an instance (`list`, `publish`, `install`); it does NOT
169
+ build or run domains. `astrale domain list` shows the published catalog (add
170
+ `--check` to probe each URL's reachability, `--default-only` for the
171
+ install-on-every-instance set, `-q` to pipe install URLs). Building, running,
172
+ and deploying live in two separate tools:
169
173
 
170
174
  - **`create-astrale-domain`** scaffolds a new standalone domain project
171
175
  (`pnpm create astrale-domain <slug>`), writing an `astrale.config.ts`.
172
- - **`astrale-domain`** (the `@astrale-os/devkit` bin, behind the project's
176
+ - **`astrale-domain`** (the `@astrale-os/sdk` bin, behind the project's
173
177
  `pnpm dev` / `pnpm prod` scripts) runs `dev | prod | deploy <env> | build`.
174
178
 
175
- Domains are **installed by URL**, never from a file: run or deploy the domain
176
- worker, then `astrale instance install <url> -i <slug>` — the kernel fetches a
177
- signed install bundle from the running worker. There is no committed
178
- `spec.json` and no `astrale domain install`; install lives under the
179
- `instance` group because it operates on an instance graph.
179
+ Domains are **installed by URL or catalog origin**, never from a file (there is
180
+ no committed `spec.json`). `astrale domain install` has two modes:
181
+
182
+ - **default (via admin)** — `astrale domain install <origin|url> -i <slug>`
183
+ installs a PUBLISHED domain through the admin control plane
184
+ (`DomainEntry.install`), addressed by its catalog `origin` (the unique
185
+ registry key) or `url`. Run it bare to pick from the catalog interactively.
186
+ The target instance is the active one or `-i <slug>` and **must be
187
+ admin-managed** (otherwise it fails loudly and points you at `--direct`).
188
+ - **`--direct`** — `astrale domain install <url> --direct` installs a url
189
+ straight onto the instance kernel (`Root.installDomain`), bypassing the
190
+ catalog. Works on ANY instance you can authenticate to (managed, bookmarked,
191
+ or local), using your own authority, and is the only mode that runs the
192
+ identity-override consent gate. Use it for dev/local instances and
193
+ freshly-deployed, not-yet-published workers.
180
194
 
181
195
  With the **managed (`astrale`) adapter**, `pnpm prod` publishes the bundle
182
196
  through the platform AND installs it on the configured instance in one step —
183
- no manual `instance install`. The service serves at
197
+ no manual `domain install`. The service serves at
184
198
  `https://<name>-<hash>.svc.<region>.astrale.ai` (the CLI session is the auth).
185
199
 
186
200
  For authoring domains end-to-end (schema, handlers, external APIs, deploys),
@@ -36,14 +36,21 @@ pnpm prod # deploy + (astrale adapter) auto-install on your instance
36
36
  Project anatomy (the scaffold is the reference — read its comments):
37
37
 
38
38
  ```
39
- astrale.config.ts # THE manifest: schema + postInstall + adapter (where it ships)
39
+ domain.ts # THE manifest wires it ALL: defineDomain({ schema, methods,
40
+ # deps, views, functions, client }) + origin/postInstall.
41
+ # Modules are imported & passed EXPLICITLY (no folder magic);
42
+ # a renamed module is a compile error here, not a missing route.
43
+ astrale.config.ts # binds the domain to its deploy adapter (deploy(domain, …)) — node-only
40
44
  schema/ # classes/interfaces/edges — the contract (zod props, fn signatures)
41
- methods/ # handler logic (transport-agnostic fns) + thin wiring (index.ts)
42
- services/ # RECOMMENDED: ports + factories for external APIs (see §4)
45
+ # + index.ts exports D = compileDomain(schema) (resolved paths/keys)
46
+ core/<context>/ # pure, transport-agnostic logic per bounded context (e.g. core/monitor: keys/health/node)
47
+ integrations/ # external-API ports + adapters + a lazy registry (see §4) — what deps is built from
48
+ runtime/index.ts # composition root: the methods map; each execute resolves deps → calls core logic
43
49
  functions/ # standalone remote functions (webhook-shaped endpoints)
44
50
  views/ # iframe-mountable UI declarations (defineView)
45
51
  client/ # the SPA served under /ui (vite)
46
- env.ts # typed worker env config + secrets arrive here, as ctx.deps
52
+ deps.ts # env typed Deps container (the seam defineDomain({ deps }) mounts)
53
+ env.ts # typed worker env — config + secrets arrive here, mapped by deps.ts → ctx.deps
47
54
  ```
48
55
 
49
56
  Iteration loop: edit → `pnpm prod` → call it. Managed redeploys keep the same
@@ -97,9 +104,10 @@ Conventions and rules:
97
104
 
98
105
  ## 2 · Handlers
99
106
 
100
- Separate LOGIC from WIRING. Logic = plain async functions taking the kernel
101
- client + ports + params (testable with fakes). Wiring = `method()` /
102
- `classMethods()` / `interfaceMethods()` in `methods/index.ts`:
107
+ Separate LOGIC from WIRING. Logic = plain async functions in `core/` taking the
108
+ kernel client + ports + params (testable with fakes). Wiring = `method()` /
109
+ `classMethods()` / `interfaceMethods()` in `runtime/index.ts` (the composition
110
+ root — the only place request context + `deps` meet the `core/` logic):
103
111
 
104
112
  ```ts
105
113
  const kickoff = method(schema, 'Project', 'kickoff', {
@@ -113,9 +121,11 @@ const kickoff = method(schema, 'Project', 'kickoff', {
113
121
 
114
122
  Handler context: `kernel` (callback client bound to the composed credential —
115
123
  caller's delegated authority ∪ this function's own), `self` (instance methods),
116
- `params` (zod-validated), `deps` (your typed `Env`), `auth` (principal,
117
- verified claims). Construct external clients from `deps` per request NEVER
118
- at module load (workers must be import-side-effect-free).
124
+ `params` (zod-validated), `deps` (your typed `Deps` from `deps.ts`; raw `Env` if
125
+ you omit the mapper), `auth` (principal, verified claims). Resolve ports from
126
+ `deps` per request (`deps.prober()` the registry builds + caches them per
127
+ isolate) — NEVER construct external clients at module load (workers must be
128
+ import-side-effect-free).
119
129
 
120
130
  ## 3 · Talking to the kernel (and other domains)
121
131
 
@@ -155,16 +165,20 @@ Almost every real domain wraps an external API (payment, calendar, LLM
155
165
  gateway, cloud provider). The shape (see admin/domain for the full-scale
156
166
  example — Scaleway/WorkOS/KV):
157
167
 
158
- 1. **Port** — a narrow interface declaring only what the logic needs
159
- (`WeatherClient { forecast(city) }`). Logic depends on the port, never on
160
- fetch/SDKs/env.
161
- 2. **Factory** — `createXClient(env)` in `services/`: reads config + secrets
162
- from `env`, validates LOUDLY (`if (!env.X_API_KEY) throw`), sets base URLs
163
- (overridable so tests point at a stub), timeouts (`AbortSignal.timeout`),
164
- and maps upstream failures to meaningful errors with the upstream detail in
165
- `cause` (the kernel threads cause chains to callers).
166
- 3. **Wiring** — the `execute` hook builds the client from `deps` per request
167
- and passes it to the logic.
168
+ 1. **Port** — a narrow interface in `integrations/<feature>/port.ts` declaring
169
+ only what the logic needs (`WeatherClient { forecast(city) }`, the scaffold's
170
+ `Prober { probe(url) }`). `core/` logic depends on the port, never
171
+ on fetch/SDKs/env.
172
+ 2. **Adapter(s) + registry** `createXClient(config)` in
173
+ `integrations/<feature>/` (one per backend), plus a `registry.ts` that reads
174
+ config + secrets from `env`, validates LOUDLY (`if (!env.X_API_KEY) throw`),
175
+ sets base URLs (overridable so tests point at a stub), timeouts
176
+ (`AbortSignal.timeout`), and maps upstream failures to errors with the
177
+ upstream detail in `cause`. The registry builds the chosen adapter LAZILY +
178
+ caches it per isolate (a worker never validates an unused backend's env).
179
+ 3. **Wiring** — `deps.ts` mounts the registry (`defineDomain({ deps })`); the
180
+ `execute` hook resolves the PORT from `deps` (`deps.prober()`) per
181
+ request and passes it to the `core/` logic.
168
182
 
169
183
  Secrets & config:
170
184
  - Declare every var as a typed field on `Env` in `env.ts`. Secrets ship via
@@ -202,8 +216,10 @@ Anti-patterns (all observed in production code — don't):
202
216
  ## 5 · Views & standalone functions (webhooks)
203
217
 
204
218
  - `defineView({ auth, mount: '/ui/contact', viewFor: selfOf(Contact) })` in
205
- `views/` + register in `views/index.ts` — the MAP KEY is the view's node
206
- slug (`'ui-contact'` `/<origin>/core/views/ui-contact`). Installs a View
219
+ `views/`, collected into the `views` map in `views/index.ts`, which
220
+ `astrale.config.ts` imports and passes to `defineDomain({ views })`. The MAP
221
+ KEY is the view's node slug (`'ui-contact'` → `/<origin>/core/views/ui-contact`).
222
+ Installs a View
207
223
  node whose binding URL = `<serving url><mount>` (managed:
208
224
  `https://<slug>.svc.<region>.astrale.ai/ui/contact`). The client/ SPA
209
225
  serves `/ui/*` — BOTH adapters ship it (cloudflare via Workers Assets;
@@ -276,12 +292,13 @@ capture when the sender's shape is fixed (see distribution's proxy functions).
276
292
 
277
293
  ## 7 · Deploy & install
278
294
 
279
- Adapter choice in `astrale.config.ts`:
280
- - `cloudflare({...})` your CF account; `dev` (wrangler dev), `prod` (route or
281
- workers.dev); ships secrets + SPA assets; extra bindings via a deep-merged
282
- `wrangler` block.
283
- - `astrale({ dev: {...}, prod: { instance: '<slug>' } })` — managed (the
284
- scaffold's DEFAULT): publishes
295
+ Adapter choice in `astrale.config.ts` (each adapter is its OWN package — swap
296
+ BOTH the import and the call):
297
+ - `cloudflare({...})` from `@astrale-os/adapter-cloudflare` your CF account;
298
+ `dev` (wrangler dev), `prod` (route or workers.dev); ships secrets + SPA
299
+ assets; extra bindings via a deep-merged `wrangler` block.
300
+ - `astrale({ dev: {...}, prod: { instance: '<slug>' } })` from
301
+ `@astrale-os/adapter-astrale` — managed (the scaffold's DEFAULT): publishes
285
302
  the bundle THROUGH the platform and installs it as a host-local service next
286
303
  to your instance (`https://<name>-<hash>.svc.<region>.astrale.ai`). No CF
287
304
  account; auth = your `astrale auth login` session. Ships the client SPA
@@ -303,7 +320,7 @@ service URL stable).
303
320
  (`/:origin:class.X:seed`; tree paths are rejected) and MUST be idempotent
304
321
  (catch path-conflicts). Seed folders, defaults, and demo data here.
305
322
 
306
- Manual install of any served domain: `astrale instance install <url>`.
323
+ Manual install of any served domain: `astrale domain install <url> --direct`.
307
324
 
308
325
  The managed catalog surface (what `pnpm prod` shells into) is callable
309
326
  directly — useful for recovery and inspection:
@@ -4,3 +4,9 @@
4
4
  # prod. These files are gitignored — copy this to `.env.dev` and fill in.
5
5
  #
6
6
  # EXAMPLE_API_KEY=sk-...
7
+ #
8
+ # ── Prober (the example external-API integration) ──────────────────────────
9
+ # The scaffold probes monitor targets with a real, KEYLESS HTTP request by
10
+ # default (no secret needed). These knobs are optional config, not secrets:
11
+ # PROBER=http # `http` (default) | `mock` (offline/deterministic)
12
+ # PROBE_TIMEOUT_MS=10000
@@ -19,20 +19,35 @@ calls), use the `astrale-cli` skill.
19
19
  ```bash
20
20
  pnpm install
21
21
  pnpm dev # wrangler dev → prints a local URL
22
- astrale instance install <url> # mount it on an instance (CLI)
22
+ astrale domain install <url> --direct # mount it on an instance (CLI)
23
23
  pnpm prod # deploy prod → prints a URL
24
24
  ```
25
25
 
26
- ## What you write (★)
26
+ ## What you write
27
27
 
28
28
  ```
29
- schema/classes + interfaces (the data model)
30
- methods/ ★ the execute() handlers
31
- views/ ★ iframe-mountable UIs
32
- functions/ standalone Functions (e.g. the post-install seed)
33
- astrale.config.ts identity (origin) · requires · postInstall · adapter
29
+ schema/ classes + interfaces (the data model) + compiled accessors
30
+ core/<context>/ pure, transport-agnostic logic per bounded context (e.g. core/monitor)
31
+ integrations/ external-API ports + adapters + a lazy registry (built into deps)
32
+ runtime/ the execute() handlers the composition root (the methods map)
33
+ views/ iframe-mountable UIs
34
+ functions/ standalone Functions (e.g. webhooks)
35
+ client/ the SPA served under /ui
36
+ deps.ts env → typed Deps container (what handlers read as ctx.deps)
37
+ domain.ts wires it all together — the worker-safe definition
38
+ astrale.config.ts binds the domain to its deploy adapter (node-only)
34
39
  ```
35
40
 
41
+ `domain.ts` is the one place everything is declared: it imports the modules
42
+ above and passes them to `defineDomain({ schema, methods, deps, views,
43
+ functions, client, … })` alongside the identity (`origin`, `requires`,
44
+ `postInstall`). Nothing is discovered by folder name — a renamed or mistyped
45
+ module is a compile error at that call, never a silently-missing route. Drop a
46
+ field (`deps`, `views`, `functions`, `client`) when the domain has none.
47
+ `astrale.config.ts` then binds that domain to a deploy adapter with
48
+ `deploy(domain, cloudflare({ … }))` — keeping the node-only adapter (wrangler,
49
+ filesystem) out of the worker bundle.
50
+
36
51
  Everything else — the Worker entry, the wrangler config, the signing identity —
37
52
  is generated under `.astrale/` (gitignored) by the adapter. You never edit it.
38
53
 
@@ -72,9 +87,9 @@ adapter-owned keys `name`, `main`, `assets`, `routes` are rejected (use
72
87
 
73
88
  ## The loop
74
89
 
75
- - **Edit a handler** (`methods/`) → hot-reloads at the same URL. Nothing to reinstall.
76
- - **Edit the schema** (`schema/`) → rebuilds the graph; reinstall with `astrale instance install <url>`.
77
- - **`postInstall`** (the static `Note.seed` method) runs once after install, as
90
+ - **Edit a handler** (`core/` logic or `runtime/` wiring) → hot-reloads at the same URL. Nothing to reinstall.
91
+ - **Edit the schema** (`schema/`) → rebuilds the graph; reinstall with `astrale domain install <url> --direct`.
92
+ - **`postInstall`** (the static `Monitor.seed` method) runs once after install, as
78
93
  the system identity, so the domain can seed itself and set its own grants. It
79
94
  must be a class-hosted static addressed by a typed colon-path
80
95
  (`/:origin:class.X:seed`) — the kernel refuses tree paths here.
@@ -1,43 +1,37 @@
1
1
  /**
2
- * astrale.config.ts — the single source of truth for this domain's identity and
3
- * deployment. `origin` defaults to `schema.domain`; `postInstall` points at the
4
- * static `Note.seed` method the kernel runs after install (a typed colon-path —
5
- * the kernel refuses tree paths here, since they can't prove their origin);
6
- * `adapter` chooses the target.
2
+ * astrale.config.ts — binds the worker-safe domain (`domain.ts`) to its deploy
3
+ * adapter for the `astrale-domain` CLI. This is a NODE-only module: it imports
4
+ * the adapter (wrangler + filesystem). The generated worker never imports this
5
+ * file it imports `domain.ts` directly so the adapter stays out of the bundle.
6
+ *
7
+ * This variant deploys to YOUR OWN Cloudflare account (the `cloudflare` adapter).
7
8
  *
8
9
  * pnpm dev # wrangler dev → prints a local URL
9
- * astrale instance install <url> # mount the dev URL on an instance
10
- * pnpm prod # managed deploy prints a URL + installs
10
+ * astrale domain install <url> --direct # mount the dev URL on an instance
11
+ * pnpm prod # deploy to your Cloudflare account
11
12
  */
12
- import { astrale } from '@astrale-os/adapter-cloudflare'
13
- import { defineDomain } from '@astrale-os/devkit'
14
- // Have your own Cloudflare account and prefer deploying there directly? Swap
15
- // the adapter — same bundle, shipped with wrangler under YOUR account (then
16
- // mount it yourself: `astrale instance install <url>`). Extra bindings (KV,
17
- // R2, D1, queues, …) ride a `wrangler` block on any env (deep-merged):
13
+ import { cloudflare } from '@astrale-os/adapter-cloudflare'
14
+ import { deploy } from '@astrale-os/sdk'
15
+ // No Cloudflare account? Swap the adapter for the Astrale-managed one — it
16
+ // publishes the same bundle THROUGH the platform and installs it on your
17
+ // instance (auth = your `astrale auth login` session):
18
18
  //
19
- // import { cloudflare } from '@astrale-os/adapter-cloudflare'
20
- // adapter: cloudflare({
21
- // dev: { secrets: '.env.dev' }, // local wrangler dev, unchanged
22
- // prod: { route: 'astrale-domain.example.dev', secrets: '.env.prod' },
23
- // }),
19
+ // import { astrale } from '@astrale-os/adapter-astrale'
20
+ // export default deploy(domain, astrale({
21
+ // dev: { secrets: '.env.dev' }, // local wrangler dev, unchanged
22
+ // prod: { instance: '<your-instance-slug>' }, // pnpm prod → managed deploy
23
+ // }))
24
24
 
25
- import { schema } from './schema'
25
+ import { domain } from './domain'
26
26
 
27
- export default defineDomain({
28
- schema,
29
- // origin defaults to `schema.domain`. Set it to another domain's origin to
30
- // deliberately alias that identity (you'll get a DANGER prompt at install).
31
- postInstall: `/:${schema.domain}:class.Note:seed`,
32
- adapter: astrale({
27
+ export default deploy(
28
+ domain,
29
+ cloudflare({
33
30
  // Local dev: `wrangler dev`. No route → URL is http://localhost:8787.
34
31
  dev: { secrets: '.env.dev' },
35
- // Managed deploy: `pnpm prod` publishes the bundle through the platform and
36
- // installs the domain on this instance — no Cloudflare account needed
37
- // (auth = your `astrale auth login` session). The slug comes from
38
- // `astrale instance create <slug>` / `astrale instance status`.
39
- prod: { instance: 'my-instance-slug' },
40
- // Author secrets ship via `secrets: '.env.prod'` on any env (encrypted at
41
- // rest platform-side, re-applied on redeploys; omit = keep, `{}` = clear).
32
+ // Custom-domain prod. Drop `route` to ship to *.workers.dev instead.
33
+ prod: { route: 'astrale-domain.example.dev', secrets: '.env.prod' },
34
+ // Author secrets ship via `secrets: '.env.prod'` on any env. Extra bindings
35
+ // (KV, R2, D1, queues, …) ride a `wrangler` block on any env (deep-merged).
42
36
  }),
43
- })
37
+ )
@@ -1,46 +1,72 @@
1
1
  # astrale-domain-client — SPA for domain views
2
2
 
3
- A small React + Vite SPA that renders the domain's `ui-note` View. It is loaded
4
- inside an iframe mounted by the Astrale shell, runs the shell handshake to learn
5
- its target node id and a delegation token, fetches that Note from the kernel,
6
- and renders its title/body. Built into `../dist-client/` and served by the
7
- generated worker (`.astrale/`) under `/ui/*` via its `ASSETS` binding.
8
-
9
- ## Self-contained on purpose
10
-
11
- This client depends only on `react`, `react-dom`, `vite`, and
12
- `@vitejs/plugin-react` all on public npm. It does **not** import
13
- `@astrale-os/shell`, `@astrale-os/kernel-client`, or `@astrale-os/ui-components`,
14
- so the template builds with no registry auth and no workspace `link:`s. Two
15
- small subsets are reimplemented inline:
16
-
17
- - the shell child-handshake (`src/lib/shell.ts`), and
18
- - a minimal JSON kernel client (`src/lib/kernel.ts`).
3
+ A small React + Vite SPA that renders the domain's Views. It is loaded inside an
4
+ iframe mounted by the Astrale shell, runs the shell handshake (via the real
5
+ `@astrale-os/shell`) to learn its target node id and a kernel session, fetches
6
+ that node from the kernel, and renders it. The bundled `ui-monitor` view loads a
7
+ Monitor and renders its status/url/latency, with a "Check now" button that calls
8
+ the Monitor's `check` instance method and reloads the node. Built into `../.dist/`
9
+ and served by the generated worker (`.astrale/`) under `/ui/*` via its `ASSETS`
10
+ binding.
11
+
12
+ ## Built on the real shell SDK
13
+
14
+ This client consumes `@astrale-os/shell` for the child handshake AND the kernel
15
+ client: `src/shell/use-shell.ts` boots `createShell({ mode: 'sandboxed' })`, and
16
+ feature hooks call kernel methods through `shell.kernel` (token refresh, codec
17
+ negotiation, redirect following, and delegation are all handled by the SDK no
18
+ inline wire code). `@` is aliased to `src/` (mirrors `tsconfig` paths), so feature
19
+ code imports `@/shell`, `@/ui`, `@/monitor`.
19
20
 
20
21
  ## Layout
21
22
 
23
+ Feature-first: each feature owns its types/api/mappers/hooks/components. The
24
+ `shell/` and `ui/` folders are the generic, domain-neutral seams every feature
25
+ builds on.
26
+
22
27
  ```
23
28
  src/
24
29
  main.tsx # entry → createRoot(App)
25
- app.tsx # the ui-note view (renders the loaded Note)
26
- lib/
27
- shell.ts # inline shell child handshake (postMessage + MessagePort)
28
- use-shell.ts # React hook over the handshake (status + live session)
29
- kernel.ts # inline kernel JSON client (kernelCall + prop readers)
30
- use-node.ts # React hook: fetch a node via @<id>::get
31
- styles.css # self-contained styles (no Tailwind)
30
+ app.tsx # path router: ROUTES { '/ui/monitor': MonitorView }
31
+ styles.css # self-contained styles (no Tailwind), design tokens
32
+ shell/ # GENERIC kernel/shell adapter on @astrale-os/shell
33
+ client.ts # prop readers (PROP, readProp, readPropBySuffix) + errors
34
+ invoke.ts # callMethod / invokeNode (@<id>::<method>) / nodeAddr
35
+ use-shell.ts # useShell() createShell({ mode: 'sandboxed' })
36
+ use-node.ts # useNode(session, id) @<id>::get (+ reload)
37
+ use-async.ts # generic reloadable async resource
38
+ use-capability.ts # write lifecycle (idle → running → done/failed)
39
+ transformers.ts # qualified-prop / link / node-array shaping
40
+ view-router.tsx # resolveView(routes) + ViewFrame (handshake gate)
41
+ index.ts # barrel → @/shell
42
+ ui/ # PURE presentation — no kernel, no hooks
43
+ surface.tsx # Panel / ErrorBanner / EmptyState / Spinner
44
+ badge.tsx # StatusBadge (up | down | unknown)
45
+ value.tsx # KV / Mono / ExternalLink
46
+ format.ts # relativeTime
47
+ index.ts # barrel → @/ui
48
+ monitor/ # THE Monitor feature (template example)
49
+ monitor.types.ts # MonitorRecord
50
+ monitor.api.ts # check(session, id) → @<id>::check
51
+ monitor.mappers.ts # monitorFromNode (node → record)
52
+ hooks/ # useMonitor.query / useCheck.mutation
53
+ components/ # MonitorCard (container: hooks + @/ui)
54
+ index.ts # barrel → @/monitor
55
+ views/
56
+ monitor.tsx # MonitorView = <ViewFrame>…<MonitorCard/></ViewFrame>
32
57
  __tests__/
33
- harness.ts # fake shell parent + fake kernel (fetch stub)
34
- app.test.tsx # handshake → render → setTarget → tokenRefresh
35
- kernel.test.ts # kernelCall wire shape + prop readers
58
+ harness.ts # fake shell parent (real protocol) + fake kernel (fetch stub)
59
+ app.test.tsx # handshake → render → check now → setTarget → fallback
60
+ seam.test.tsx # pure @/ui + @/monitor hooks in isolation (no handshake)
61
+ kernel.test.ts # pure helpers: prop readers, mapper, relativeTime
36
62
  ```
37
63
 
38
64
  ## Dev loops
39
65
 
40
66
  ```bash
41
- pnpm dev # vite build --watch → ../dist-client/ (worker auto-reloads, no HMR)
67
+ pnpm dev # vite build --watch → ../.dist/ (worker auto-reloads, no HMR)
42
68
  pnpm dev:hmr # vite dev on http://127.0.0.1:5173/ (React fast-refresh)
43
- pnpm test # vitest run (happy-dom; fake parent + fake kernel, JSON-only)
69
+ pnpm test # vitest run (happy-dom; fake parent speaks the real shell protocol)
44
70
  ```
45
71
 
46
72
  For HMR, set `VIEW_DEV_URL=http://127.0.0.1:5173` in the worker's `.dev.vars`;
@@ -48,38 +74,29 @@ the generated worker forwards `/ui/*` to vite.
48
74
 
49
75
  ## How it connects
50
76
 
51
- 1. The domain declares the View in `../views/note.ts` with `mount: '/ui/note'`.
52
- The SDK points the View node's iframe at `<serving url>/ui/note`.
53
- 2. The shell mounts that iframe and completes the handshake: it transfers a
54
- `MessagePort` and sends a `ctrl:handshake` carrying `kernelUrl`, a
55
- `delegationToken` (+ `tokenExpiresAt`), and the `targetNodeId`. `useShell()`
56
- surfaces a live session and tracks `setTarget` hot-swaps.
57
- 3. `useNode(session, nodeId)` POSTs `@<id>::get` to `kernelUrl` with the
58
- delegation token as `authorization` and renders the Note's `title`/`body`
59
- (props are read by key suffix, since the domain origin is unknown at build
60
- time). The iframe authenticates with the handshake token ONLY the parent
61
- minted it for this kernel, so the audience already matches (no cookie, no
62
- whoami, no client-side minting).
63
-
64
- ### Token refresh
65
-
66
- The parent pushes a fresh credential over the port as `ctrl:tokenRefresh`
67
- before the current one expires. `shell.ts` swaps it into a mutable `currentToken`
68
- so the next `kernelCall` (e.g. on the next `setTarget` or remount) uses it —
69
- `getToken()` always returns the live token, never the one captured at handshake.
70
-
71
- ## Deferred kept minimal on purpose
72
-
73
- The inline kernel client is **JSON-only** and read-only for this template:
74
-
75
- - no msgpack codec (the `application/vnd.astrale.kernel+msgpack` body),
76
- - no streaming (`output: 'stream'`) or binary (`output: 'binary'`) responses,
77
- - no redirect following — a `{ redirect }` envelope (remote-domain Functions)
78
- throws rather than re-minting against the target worker,
79
- - no client-side credential minting / `whoami` (the handshake token is used
80
- as-is), and
81
- - no writes — only `@<id>::get`.
82
-
83
- These all live in `@astrale-os/kernel-client` / `@astrale-os/shell`. To grow
84
- past the template, pull those in (and `link:` them in dev) instead of extending
85
- the inline subset.
77
+ 1. The domain declares each View in `../views/` with `mount: '/ui/<path>'`. The
78
+ SDK points the View node's iframe at `<serving url>/ui/<path>`.
79
+ 2. The shell mounts that iframe and completes the handshake (a transferred
80
+ `MessagePort` + a `ctrl:handshake` carrying `kernelUrl`, the delegation token,
81
+ and the `targetNodeId`). `useShell()` boots `@astrale-os/shell`, surfaces the
82
+ kernel session, and tracks `setTarget` hot-swaps.
83
+ 3. A feature loads its node through the session: `useNode(session, nodeId)` calls
84
+ `@<id>::get` over `shell.kernel`, and a mapper (`monitorFromNode`) projects it
85
+ to a typed record. Domain props are read by key SUFFIX (`.property.url`, …)
86
+ because the domain origin is unknown at build time; the name uses the kernel
87
+ `Named.name` key.
88
+ 4. Writes go through `useCapability`: "Check now" runs `@<id>::check`, then calls
89
+ `useMonitor`'s `reload()` so the fresh status/latency re-render. The shell's
90
+ kernel client owns the credential and refreshes it before expiry — features
91
+ never touch the token.
92
+
93
+ ## Adding a view
94
+
95
+ 1. Write a `ViewComponent` (usually wrapping `ViewFrame`) under `src/views/`.
96
+ 2. Add a `ROUTES` entry keyed by its mount path in `src/app.tsx`.
97
+ 3. Register a matching `defineView({ mount: '/ui/<path>' })` in the domain's
98
+ `views/` so the shell mounts an iframe there.
99
+
100
+ For a NEW feature, mirror `src/monitor/`: a `*.types.ts`, `*.api.ts`,
101
+ `*.mappers.ts`, a `hooks/` folder of `.query`/`.mutation` hooks, and a
102
+ `components/` folder of containers that compose `@/ui`.