@astrale-os/adapter-cloudflare 0.1.8 → 0.1.9
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/dist/assets-pack.d.ts +1 -1
- package/dist/assets-pack.js +1 -1
- package/dist/build.d.ts +15 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +15 -0
- package/dist/build.js.map +1 -0
- package/dist/client.d.ts +9 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +10 -1
- package/dist/client.js.map +1 -1
- package/dist/cloudflare.d.ts +15 -3
- package/dist/cloudflare.d.ts.map +1 -1
- package/dist/cloudflare.js +52 -18
- package/dist/cloudflare.js.map +1 -1
- package/dist/codegen/worker.d.ts +26 -6
- package/dist/codegen/worker.d.ts.map +1 -1
- package/dist/codegen/worker.js +67 -54
- package/dist/codegen/worker.js.map +1 -1
- package/dist/codegen/wrangler.d.ts +11 -2
- package/dist/codegen/wrangler.d.ts.map +1 -1
- package/dist/codegen/wrangler.js +11 -5
- package/dist/codegen/wrangler.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/params.d.ts +30 -30
- package/dist/params.d.ts.map +1 -1
- package/dist/parse-output.d.ts +1 -1
- package/dist/parse-output.js +1 -1
- package/package.json +6 -2
- package/src/assets-pack.ts +1 -1
- package/src/build.ts +15 -0
- package/src/client.ts +11 -1
- package/src/cloudflare.ts +53 -18
- package/src/codegen/worker.ts +76 -59
- package/src/codegen/wrangler.ts +15 -5
- package/src/index.ts +6 -3
- package/src/params.ts +32 -31
- package/src/parse-output.ts +1 -1
- package/template/.agents/skills/astrale-cli/SKILL.md +25 -11
- package/template/.agents/skills/astrale-domain/SKILL.md +46 -29
- package/template/.env.example +8 -0
- package/template/README.md +24 -9
- package/template/astrale.config.ts +27 -33
- package/template/client/README.md +2 -2
- package/template/client/tsconfig.json +1 -1
- package/template/client/vite.config.ts +3 -3
- package/template/client/vitest.config.ts +1 -1
- package/template/core/keys.ts +28 -0
- package/template/{methods → core}/note.ts +42 -25
- package/template/deps.ts +25 -0
- package/template/domain.ts +33 -0
- package/template/env.ts +11 -0
- package/template/integrations/summary/heuristic.ts +25 -0
- package/template/integrations/summary/http.ts +69 -0
- package/template/integrations/summary/port.ts +21 -0
- package/template/integrations/summary/registry.ts +52 -0
- package/template/package.json +2 -3
- package/template/runtime/index.ts +62 -0
- package/template/schema/index.ts +2 -0
- package/template/schema/note.ts +5 -2
- package/template/tsconfig.json +13 -2
- package/template/views/note.ts +1 -1
- package/dist/astrale.d.ts +0 -27
- package/dist/astrale.d.ts.map +0 -1
- package/dist/astrale.js +0 -222
- package/dist/astrale.js.map +0 -1
- package/src/astrale.ts +0 -259
- package/template/methods/index.ts +0 -66
- package/template/schema/compiled.ts +0 -14
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
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
*
|
|
82
|
-
*
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
}
|
package/src/parse-output.ts
CHANGED
|
@@ -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
|
|
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
|
|
@@ -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.**
|
|
168
|
-
|
|
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/
|
|
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
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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 `
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
45
|
+
# + compiled.ts (D = compileDomain) — the public compiled entry
|
|
46
|
+
core/ # pure, transport-agnostic logic + keys.ts (compiled accessors)
|
|
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
|
-
|
|
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
|
|
101
|
-
client + ports + params (testable with fakes). Wiring = `method()` /
|
|
102
|
-
`classMethods()` / `interfaceMethods()` in `
|
|
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 `
|
|
117
|
-
|
|
118
|
-
|
|
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.summarizer()` — 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
|
|
159
|
-
(`WeatherClient { forecast(city) }
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
+
`Summarizer { summarize(body) }`). `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.summarizer()`) 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
|
|
206
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
`wrangler`
|
|
283
|
-
|
|
284
|
-
|
|
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
|
|
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:
|
package/template/.env.example
CHANGED
|
@@ -4,3 +4,11 @@
|
|
|
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
|
+
# ── Summarizer (the example external-API integration) ──────────────────────
|
|
9
|
+
# The scaffold summarizes notes with a no-network heuristic by default. To use a
|
|
10
|
+
# real OpenAI-compatible model instead, set NOTE_SUMMARIZER=http and provide:
|
|
11
|
+
# NOTE_SUMMARIZER=http
|
|
12
|
+
# SUMMARIZER_API_KEY=sk-...
|
|
13
|
+
# SUMMARIZER_BASE_URL=https://api.openai.com/v1
|
|
14
|
+
# SUMMARIZER_MODEL=gpt-4o-mini
|
package/template/README.md
CHANGED
|
@@ -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
|
|
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/
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
schema/ classes + interfaces (the data model) + compiled accessors
|
|
30
|
+
core/ pure, transport-agnostic logic + keys.ts (never hand-write keys)
|
|
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,8 +87,8 @@ adapter-owned keys `name`, `main`, `assets`, `routes` are rejected (use
|
|
|
72
87
|
|
|
73
88
|
## The loop
|
|
74
89
|
|
|
75
|
-
- **Edit a handler** (`
|
|
76
|
-
- **Edit the schema** (`schema/`) → rebuilds the graph; reinstall with `astrale
|
|
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`.
|
|
77
92
|
- **`postInstall`** (the static `Note.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
|
|
@@ -1,43 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* astrale.config.ts — the
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
10
|
-
* pnpm prod #
|
|
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 {
|
|
13
|
-
import {
|
|
14
|
-
//
|
|
15
|
-
// the
|
|
16
|
-
//
|
|
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 {
|
|
20
|
-
//
|
|
21
|
-
// dev: { secrets: '.env.dev' },
|
|
22
|
-
// 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 {
|
|
25
|
+
import { domain } from './domain'
|
|
26
26
|
|
|
27
|
-
export default
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
//
|
|
36
|
-
|
|
37
|
-
//
|
|
38
|
-
//
|
|
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
|
+
)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
A small React + Vite SPA that renders the domain's `ui-note` View. It is loaded
|
|
4
4
|
inside an iframe mounted by the Astrale shell, runs the shell handshake to learn
|
|
5
5
|
its target node id and a delegation token, fetches that Note from the kernel,
|
|
6
|
-
and renders its title/body. Built into
|
|
6
|
+
and renders its title/body. Built into `../.dist/` and served by the
|
|
7
7
|
generated worker (`.astrale/`) under `/ui/*` via its `ASSETS` binding.
|
|
8
8
|
|
|
9
9
|
## Self-contained on purpose
|
|
@@ -38,7 +38,7 @@ __tests__/
|
|
|
38
38
|
## Dev loops
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
-
pnpm dev # vite build --watch →
|
|
41
|
+
pnpm dev # vite build --watch → ../.dist/ (worker auto-reloads, no HMR)
|
|
42
42
|
pnpm dev:hmr # vite dev on http://127.0.0.1:5173/ (React fast-refresh)
|
|
43
43
|
pnpm test # vitest run (happy-dom; fake parent + fake kernel, JSON-only)
|
|
44
44
|
```
|
|
@@ -2,10 +2,10 @@ import viteReact from '@vitejs/plugin-react'
|
|
|
2
2
|
import { defineConfig } from 'vite'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Client SPA for the domain's `ui-note` View. Built into
|
|
5
|
+
* Client SPA for the domain's `ui-note` View. Built into `../.dist/`,
|
|
6
6
|
* served by the generated worker via its `ASSETS` binding (`.astrale/`).
|
|
7
7
|
*
|
|
8
|
-
* `base: '/ui/'` + `outDir: '
|
|
8
|
+
* `base: '/ui/'` + `outDir: '../.dist'`: Vite emits asset refs as
|
|
9
9
|
* `/ui/assets/<hash>.js`; the worker strips the `/ui` prefix before delegating
|
|
10
10
|
* to `ASSETS`, so files resolve from the directory root. `index.html` is the
|
|
11
11
|
* SPA fallback for `/ui/<anything>`.
|
|
@@ -26,7 +26,7 @@ export default defineConfig({
|
|
|
26
26
|
base: '/ui/',
|
|
27
27
|
plugins: [viteReact()],
|
|
28
28
|
build: {
|
|
29
|
-
outDir: '
|
|
29
|
+
outDir: '../.dist',
|
|
30
30
|
emptyOutDir: true,
|
|
31
31
|
sourcemap: false,
|
|
32
32
|
},
|
|
@@ -5,7 +5,7 @@ import { defineConfig } from 'vitest/config'
|
|
|
5
5
|
* Vitest config for the self-contained client. A DOM env (`happy-dom`) backs
|
|
6
6
|
* the React render + `window.postMessage`/`MessageChannel` used by the fake
|
|
7
7
|
* shell-parent harness. Kept separate from `vite.config.ts` (which sets the
|
|
8
|
-
* `/ui/` base +
|
|
8
|
+
* `/ui/` base + `../.dist` build) so tests don't inherit the SPA build
|
|
9
9
|
* options.
|
|
10
10
|
*/
|
|
11
11
|
export default defineConfig({
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compiled-schema accessors — the ONE place class paths, method paths, and
|
|
3
|
+
* qualified prop keys come from. Pure (schema-derived); never hand-write key
|
|
4
|
+
* strings. `D` (the compiled domain) is the public `schema/compiled` entry,
|
|
5
|
+
* re-exported here so `core/` and `runtime/` read it — plus the named keys —
|
|
6
|
+
* from one place.
|
|
7
|
+
*/
|
|
8
|
+
import { K } from '@astrale-os/kernel-core'
|
|
9
|
+
|
|
10
|
+
import { D } from '../schema/compiled'
|
|
11
|
+
|
|
12
|
+
export { D, K }
|
|
13
|
+
|
|
14
|
+
/** Kernel ops/classes the logic addresses. */
|
|
15
|
+
export const NODE_CREATE = K.Node.createNode.path.method.raw
|
|
16
|
+
export const FOLDER_CLASS = K.Folder.path.class.raw
|
|
17
|
+
export const NAME_KEY = K.Named.name.key
|
|
18
|
+
|
|
19
|
+
/** Domain class paths. */
|
|
20
|
+
export const NOTE_CLASS = D.Note.path.class.raw
|
|
21
|
+
export const REFERENCES_EDGE = D.references.path.class.raw
|
|
22
|
+
|
|
23
|
+
/** Qualified storage keys for Note node props. */
|
|
24
|
+
export const NOTE_KEYS = {
|
|
25
|
+
title: D.Note.title.key,
|
|
26
|
+
body: D.Note.body.key,
|
|
27
|
+
summary: D.Note.summary.key,
|
|
28
|
+
} as const
|
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Note context — method LOGIC, written once, transport-agnostic.
|
|
2
|
+
* Note context — method LOGIC, written once, transport-agnostic and testable.
|
|
3
3
|
*
|
|
4
4
|
* Each handler touches the kernel ONLY through `kernel.call(...)` (the universal
|
|
5
|
-
* syscalls) plus its `params
|
|
6
|
-
* `path.raw`.
|
|
7
|
-
* `
|
|
8
|
-
*
|
|
5
|
+
* syscalls) plus its `params`, the resolved `Summarizer` PORT, and — for
|
|
6
|
+
* instance methods — the source node's `path.raw`. It never names `fetch`, the
|
|
7
|
+
* worker `env`, or a concrete provider: the port arrives ready-to-use from
|
|
8
|
+
* `runtime/` (which built it from `deps`). Address callables/edges with
|
|
9
|
+
* layout-independent forms — a `ClassPath` for the edge class, the `<id>::link`
|
|
10
|
+
* instance form, and an `@<id>` id-form target.
|
|
9
11
|
*/
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
import type { Summarizer } from '../integrations/summary/port'
|
|
13
|
+
import {
|
|
14
|
+
FOLDER_CLASS,
|
|
15
|
+
NAME_KEY,
|
|
16
|
+
NODE_CREATE,
|
|
17
|
+
NOTE_CLASS,
|
|
18
|
+
NOTE_KEYS,
|
|
19
|
+
REFERENCES_EDGE,
|
|
20
|
+
} from './keys'
|
|
13
21
|
|
|
14
22
|
/** The minimal kernel surface the worker runtime exposes to a handler. */
|
|
15
23
|
export type CallableKernel = { call(path: string, params: unknown): Promise<unknown> }
|
|
16
24
|
|
|
17
|
-
const NODE_CREATE = K.Node.createNode.path.method.raw
|
|
18
|
-
const NAME_KEY = K.Named.name.key
|
|
19
|
-
const NOTE_CLASS = D.Note.path.class.raw
|
|
20
|
-
const REFERENCES_EDGE = D.references.path.class.raw
|
|
21
|
-
const TITLE_KEY = D.Note.title.key
|
|
22
|
-
const BODY_KEY = D.Note.body.key
|
|
23
|
-
|
|
24
25
|
/** Notes live under a `/notes` folder at the graph root (created by `seed`). */
|
|
25
26
|
const NOTES_PARENT = '/notes'
|
|
26
27
|
|
|
@@ -37,16 +38,26 @@ function slugify(text: string): string {
|
|
|
37
38
|
return `${stem}-${Date.now().toString(36).slice(-4)}${Math.random().toString(36).slice(2, 6)}`
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* `NoteOps.createNote` (static) — create a Note under `/notes`, stamping a
|
|
43
|
+
* one-line `summary` from the resolved summarizer port (the external-API seam).
|
|
44
|
+
*/
|
|
41
45
|
export async function createNote(
|
|
42
46
|
kernel: CallableKernel,
|
|
47
|
+
summarizer: Summarizer,
|
|
43
48
|
params: { title: string; body: string },
|
|
44
49
|
): Promise<{ id: string; path: string }> {
|
|
50
|
+
const summary = await summarizer.summarize(params.body)
|
|
45
51
|
const path = `${NOTES_PARENT}/${slugify(params.title)}`
|
|
46
52
|
const created = (await kernel.call(NODE_CREATE, {
|
|
47
53
|
class: NOTE_CLASS,
|
|
48
54
|
path,
|
|
49
|
-
props: {
|
|
55
|
+
props: {
|
|
56
|
+
[NAME_KEY]: params.title,
|
|
57
|
+
[NOTE_KEYS.title]: params.title,
|
|
58
|
+
[NOTE_KEYS.body]: params.body,
|
|
59
|
+
[NOTE_KEYS.summary]: summary,
|
|
60
|
+
},
|
|
50
61
|
})) as { id: string }
|
|
51
62
|
return { id: created.id, path }
|
|
52
63
|
}
|
|
@@ -64,8 +75,6 @@ export async function reference(
|
|
|
64
75
|
return { linked: params.target }
|
|
65
76
|
}
|
|
66
77
|
|
|
67
|
-
const FOLDER_CLASS = K.Folder.path.class.raw
|
|
68
|
-
|
|
69
78
|
const STARTERS: ReadonlyArray<{ slug: string; title: string; body: string }> = [
|
|
70
79
|
{
|
|
71
80
|
slug: 'welcome',
|
|
@@ -81,12 +90,15 @@ function isPathConflict(e: unknown): boolean {
|
|
|
81
90
|
|
|
82
91
|
/**
|
|
83
92
|
* `Note.seed` (static) — the domain's post-install bootstrap. The kernel calls
|
|
84
|
-
* it ONCE after install, as __SYSTEM__ (see `postInstall` in
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
93
|
+
* it ONCE after install, as __SYSTEM__ (see `postInstall` in `domain.ts`), so
|
|
94
|
+
* the domain can lay down its initial state: the `/notes` folder, a couple of
|
|
95
|
+
* starter Notes (each summarized through the port), and a `references` edge
|
|
96
|
+
* between them. Idempotent: a re-run swallows `PATH_CONFLICT` per node.
|
|
88
97
|
*/
|
|
89
|
-
export async function seed(
|
|
98
|
+
export async function seed(
|
|
99
|
+
kernel: CallableKernel,
|
|
100
|
+
summarizer: Summarizer,
|
|
101
|
+
): Promise<{ seeded: number }> {
|
|
90
102
|
// 1. The `/notes` folder at the graph root.
|
|
91
103
|
try {
|
|
92
104
|
await kernel.call(NODE_CREATE, {
|
|
@@ -106,7 +118,12 @@ export async function seed(kernel: CallableKernel): Promise<{ seeded: number }>
|
|
|
106
118
|
const created = (await kernel.call(NODE_CREATE, {
|
|
107
119
|
class: NOTE_CLASS,
|
|
108
120
|
path: `${NOTES_PARENT}/${s.slug}`,
|
|
109
|
-
props: {
|
|
121
|
+
props: {
|
|
122
|
+
[NAME_KEY]: s.title,
|
|
123
|
+
[NOTE_KEYS.title]: s.title,
|
|
124
|
+
[NOTE_KEYS.body]: s.body,
|
|
125
|
+
[NOTE_KEYS.summary]: await summarizer.summarize(s.body),
|
|
126
|
+
},
|
|
110
127
|
})) as { id: string }
|
|
111
128
|
ids[s.slug] = created.id
|
|
112
129
|
seeded++
|