@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
@@ -3,11 +3,19 @@
3
3
  --fg: #171717;
4
4
  --muted: #737373;
5
5
  --border: #e5e5e5;
6
+ --border-strong: #d4d4d8;
6
7
  --card: #ffffff;
7
8
  --accent: #2563eb;
9
+ --accent-soft: rgba(37, 99, 235, 0.1);
8
10
  --warn-bg: #fffbeb;
9
11
  --warn-border: #fde68a;
10
12
  --warn-fg: #92400e;
13
+ --err: #dc2626;
14
+ --err-soft: rgba(220, 38, 38, 0.1);
15
+ --up: #16a34a;
16
+ --down: #dc2626;
17
+ --unknown: #737373;
18
+ --mono: ui-monospace, SFMono-Regular, Menlo, monospace;
11
19
  }
12
20
 
13
21
  * {
@@ -49,7 +57,7 @@ body {
49
57
  }
50
58
 
51
59
  .subline {
52
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
60
+ font-family: var(--mono);
53
61
  font-size: 0.72rem;
54
62
  color: var(--muted);
55
63
  margin: 0.25rem 0 1rem;
@@ -61,6 +69,8 @@ body {
61
69
  margin: 0 0 1rem;
62
70
  }
63
71
 
72
+ /* ─── Banners ───────────────────────────────────────────────────────────── */
73
+
64
74
  .banner {
65
75
  border: 1px solid var(--warn-border);
66
76
  background: var(--warn-bg);
@@ -71,11 +81,57 @@ body {
71
81
  margin-bottom: 1rem;
72
82
  }
73
83
 
84
+ .banner-error {
85
+ border-color: var(--err);
86
+ background: var(--err-soft);
87
+ color: var(--err);
88
+ word-break: break-word;
89
+ }
90
+
91
+ /* ─── Panel ─────────────────────────────────────────────────────────────── */
92
+
93
+ .panel {
94
+ background: var(--card);
95
+ border: 1px solid var(--border);
96
+ border-radius: 0.7rem;
97
+ padding: 1.05rem 1.2rem;
98
+ }
99
+
100
+ .panel-danger {
101
+ border-color: color-mix(in srgb, var(--err) 45%, var(--border));
102
+ }
103
+
104
+ .panel-head {
105
+ display: flex;
106
+ align-items: center;
107
+ justify-content: space-between;
108
+ gap: 0.75rem;
109
+ margin-bottom: 0.85rem;
110
+ }
111
+
112
+ .panel-title {
113
+ font-size: 1.1rem;
114
+ font-weight: 650;
115
+ margin: 0;
116
+ }
117
+
118
+ .panel-danger .panel-title {
119
+ color: var(--err);
120
+ }
121
+
122
+ .panel-actions {
123
+ display: inline-flex;
124
+ align-items: center;
125
+ gap: 0.5rem;
126
+ }
127
+
128
+ /* ─── Key/value ─────────────────────────────────────────────────────────── */
129
+
74
130
  .kv {
75
131
  border: 1px solid var(--border);
76
132
  border-radius: 0.5rem;
77
133
  overflow: hidden;
78
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
134
+ font-family: var(--mono);
79
135
  font-size: 0.72rem;
80
136
  }
81
137
 
@@ -97,14 +153,131 @@ body {
97
153
 
98
154
  .kv-val {
99
155
  word-break: break-all;
156
+ min-width: 0;
100
157
  }
101
158
 
102
159
  .muted {
103
160
  color: var(--muted);
104
161
  }
105
162
 
106
- .section-label {
163
+ .mono {
164
+ font-family: var(--mono);
107
165
  font-size: 0.78rem;
166
+ }
167
+
168
+ .link {
169
+ color: var(--accent);
170
+ text-decoration: none;
171
+ }
172
+
173
+ .link:hover {
174
+ text-decoration: underline;
175
+ }
176
+
177
+ /* ─── Status row + badge ────────────────────────────────────────────────── */
178
+
179
+ .status-row {
180
+ display: flex;
181
+ align-items: center;
182
+ justify-content: space-between;
183
+ gap: 0.75rem;
184
+ margin: 0 0 1rem;
185
+ }
186
+
187
+ .status-badge {
188
+ display: inline-block;
189
+ font-weight: 700;
190
+ font-size: 1rem;
191
+ letter-spacing: 0.04em;
192
+ padding: 0.3rem 0.85rem;
193
+ border-radius: 0.5rem;
194
+ color: #ffffff;
195
+ }
196
+
197
+ .status-up {
198
+ background: var(--up);
199
+ }
200
+
201
+ .status-down {
202
+ background: var(--down);
203
+ }
204
+
205
+ .status-unknown {
206
+ background: var(--unknown);
207
+ }
208
+
209
+ /* ─── Buttons ───────────────────────────────────────────────────────────── */
210
+
211
+ .check-btn {
212
+ font: inherit;
108
213
  font-weight: 600;
109
- margin: 0 0 0.5rem;
214
+ color: #ffffff;
215
+ background: var(--accent);
216
+ border: 0;
217
+ border-radius: 0.5rem;
218
+ padding: 0.4rem 0.85rem;
219
+ cursor: pointer;
220
+ }
221
+
222
+ .check-btn:hover:not(:disabled) {
223
+ opacity: 0.9;
224
+ }
225
+
226
+ .check-btn:disabled {
227
+ opacity: 0.55;
228
+ cursor: default;
229
+ }
230
+
231
+ /* ─── Empty state ───────────────────────────────────────────────────────── */
232
+
233
+ .empty {
234
+ border: 1px dashed var(--border-strong);
235
+ border-radius: 0.6rem;
236
+ padding: 1.4rem 1.2rem;
237
+ text-align: center;
238
+ display: flex;
239
+ flex-direction: column;
240
+ gap: 0.6rem;
241
+ align-items: center;
242
+ }
243
+
244
+ .empty-text {
245
+ margin: 0;
246
+ color: var(--muted);
247
+ font-size: 0.85rem;
248
+ }
249
+
250
+ .empty-hint {
251
+ font-family: var(--mono);
252
+ font-size: 0.72rem;
253
+ color: var(--muted);
254
+ background: var(--bg);
255
+ border: 1px solid var(--border);
256
+ border-radius: 0.4rem;
257
+ padding: 0.3rem 0.6rem;
258
+ word-break: break-all;
259
+ }
260
+
261
+ /* ─── Loading ───────────────────────────────────────────────────────────── */
262
+
263
+ .loading-line {
264
+ display: flex;
265
+ align-items: center;
266
+ gap: 0.5rem;
267
+ margin: 0;
268
+ }
269
+
270
+ .spinner {
271
+ width: 0.85rem;
272
+ height: 0.85rem;
273
+ border-radius: 50%;
274
+ border: 2px solid var(--border-strong);
275
+ border-top-color: var(--accent);
276
+ animation: spin 0.8s linear infinite;
277
+ }
278
+
279
+ @keyframes spin {
280
+ to {
281
+ transform: rotate(360deg);
282
+ }
110
283
  }
@@ -0,0 +1,24 @@
1
+ /** Shared display formatters — pure, presentation-side (used across views). */
2
+
3
+ /**
4
+ * Coarse relative time for `lastCheckedAt`-style ISO stamps:
5
+ * `just now` / `5m ago` / `2h ago` / a `YYYY-MM-DD` date for anything older than
6
+ * 30 days. Returns `—` for a missing stamp and the raw string for an unparseable
7
+ * one.
8
+ */
9
+ export function relativeTime(iso: string | undefined, now = Date.now()): string {
10
+ if (!iso) return '—'
11
+ const t = Date.parse(iso)
12
+ if (Number.isNaN(t)) return iso
13
+ const diff = now - t
14
+ if (diff < 0) return 'just now'
15
+ const s = Math.floor(diff / 1000)
16
+ if (s < 45) return 'just now'
17
+ const m = Math.floor(s / 60)
18
+ if (m < 60) return `${m}m ago`
19
+ const h = Math.floor(m / 60)
20
+ if (h < 48) return `${h}h ago`
21
+ const d = Math.floor(h / 24)
22
+ if (d < 30) return `${d}d ago`
23
+ return new Date(t).toISOString().slice(0, 10)
24
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The design system — generic, feature-AGNOSTIC presentational primitives +
3
+ * display formatters, no domain knowledge and no kernel session. Feature-specific
4
+ * UI (e.g. the Monitor's StatusBadge) lives in that feature's own `ui/`, not here.
5
+ * Import from the barrel: `import { Panel, KV, relativeTime } from '@/ui'`.
6
+ */
7
+ export { EmptyState, ErrorBanner, Panel, Spinner } from './surface'
8
+ export { ExternalLink, KV, Mono } from './value'
9
+ export { relativeTime } from './format'
@@ -0,0 +1,56 @@
1
+ /** Containers + feedback surfaces — pure presentational. */
2
+ import type { ReactNode } from 'react'
3
+
4
+ /**
5
+ * A titled card section. `actions` render in the header (e.g. a button); `tone:
6
+ * 'danger'` tints the border/title for destructive panels.
7
+ */
8
+ export function Panel({
9
+ title,
10
+ actions,
11
+ tone,
12
+ children,
13
+ }: {
14
+ title: string
15
+ actions?: ReactNode
16
+ tone?: 'danger'
17
+ children: ReactNode
18
+ }) {
19
+ return (
20
+ <section className={tone === 'danger' ? 'panel panel-danger' : 'panel'}>
21
+ <header className="panel-head">
22
+ <h2 className="panel-title">{title}</h2>
23
+ {actions && <div className="panel-actions">{actions}</div>}
24
+ </header>
25
+ {children}
26
+ </section>
27
+ )
28
+ }
29
+
30
+ /** A red alert banner for a failed call. */
31
+ export function ErrorBanner({ children }: { children: ReactNode }) {
32
+ return (
33
+ <div className="banner banner-error" role="alert">
34
+ {children}
35
+ </div>
36
+ )
37
+ }
38
+
39
+ /** Teaching empty state — what's missing and an optional command that fixes it. */
40
+ export function EmptyState({ children, hint }: { children: ReactNode; hint?: string }) {
41
+ return (
42
+ <div className="empty">
43
+ <p className="empty-text">{children}</p>
44
+ {hint && <code className="empty-hint">{hint}</code>}
45
+ </div>
46
+ )
47
+ }
48
+
49
+ /** A spinner with a label, for in-flight loads. */
50
+ export function Spinner({ label }: { label: string }) {
51
+ return (
52
+ <p className="muted loading-line">
53
+ <span className="spinner" aria-hidden="true" /> {label}
54
+ </p>
55
+ )
56
+ }
@@ -0,0 +1,32 @@
1
+ /** Inline value renderers — pure presentational. */
2
+ import type { ReactNode } from 'react'
3
+
4
+ /** One label/value row in a `.kv` grid. */
5
+ export function KV({ label, children }: { label: string; children: ReactNode }) {
6
+ return (
7
+ <div className="kv-row">
8
+ <div className="kv-key">{label}</div>
9
+ <div className="kv-val">{children}</div>
10
+ </div>
11
+ )
12
+ }
13
+
14
+ /** Monospace inline value with a soft em-dash fallback. */
15
+ export function Mono({ value, title }: { value: string | undefined; title?: string }) {
16
+ if (!value) return <span className="muted">—</span>
17
+ return (
18
+ <span className="mono" title={title ?? value}>
19
+ {value}
20
+ </span>
21
+ )
22
+ }
23
+
24
+ /** An external link rendered without its scheme, opening in a new tab. */
25
+ export function ExternalLink({ url }: { url: string | undefined }) {
26
+ if (!url) return <span className="muted">—</span>
27
+ return (
28
+ <a className="mono link" href={url} target="_blank" rel="noreferrer noopener" title={url}>
29
+ {url.replace(/^https?:\/\//, '')}
30
+ </a>
31
+ )
32
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The `ui-monitor` view (mount path `/ui/monitor`) — the Monitor detail panel.
3
+ *
4
+ * Mounted by the Astrale shell as a sandboxed iframe; `@/shell` (built on the
5
+ * real `@astrale-os/shell`) completes the handshake and hands over the kernel
6
+ * session + the target node id. `ViewFrame` gates the handshake (loading /
7
+ * standalone / ready); the `ready` body delegates to `MonitorCard`, the feature
8
+ * container that loads the node, renders its status/url/latency, and exposes a
9
+ * "Check now" probe.
10
+ *
11
+ * Pure composition — no data/transport logic lives here (that's `@/monitor` +
12
+ * `@/shell`).
13
+ */
14
+ import { MonitorCard } from '@/monitor'
15
+ import { type ShellState, ViewFrame } from '@/shell'
16
+
17
+ export function MonitorView(shell: ShellState) {
18
+ return (
19
+ <ViewFrame shell={shell} title="Status monitor" subline="ui-monitor · Astrale view SPA">
20
+ {(session, nodeId) =>
21
+ nodeId ? (
22
+ // Keyed by node id so a target hot-swap remounts with fresh state.
23
+ <MonitorCard key={nodeId} session={session} nodeId={nodeId} />
24
+ ) : (
25
+ <div className="banner">No target Monitor — open this view from a Monitor node.</div>
26
+ )
27
+ }
28
+ </ViewFrame>
29
+ )
30
+ }
@@ -11,7 +11,8 @@
11
11
  "skipLibCheck": true,
12
12
  "isolatedModules": true,
13
13
  "resolveJsonModule": true,
14
- "verbatimModuleSyntax": true
14
+ "verbatimModuleSyntax": true,
15
+ "paths": { "@/*": ["./src/*"] }
15
16
  },
16
17
  "include": [
17
18
  "src/**/*.ts",
@@ -21,5 +22,5 @@
21
22
  "vite.config.ts",
22
23
  "vitest.config.ts"
23
24
  ],
24
- "exclude": ["node_modules", "../dist-client"]
25
+ "exclude": ["node_modules", "../.dist"]
25
26
  }
@@ -1,32 +1,31 @@
1
+ import { fileURLToPath } from 'node:url'
2
+
1
3
  import viteReact from '@vitejs/plugin-react'
2
4
  import { defineConfig } from 'vite'
3
5
 
4
6
  /**
5
- * Client SPA for the domain's `ui-note` View. Built into `../dist-client/`,
6
- * served by the generated worker via its `ASSETS` binding (`.astrale/`).
7
+ * Client SPA for the domain's Views. Built into `../.dist/`, served by the
8
+ * generated worker via its `ASSETS` binding (`.astrale/`).
7
9
  *
8
- * `base: '/ui/'` + `outDir: '../dist-client'`: Vite emits asset refs as
10
+ * `base: '/ui/'` + `outDir: '../.dist'`: Vite emits asset refs as
9
11
  * `/ui/assets/<hash>.js`; the worker strips the `/ui` prefix before delegating
10
12
  * to `ASSETS`, so files resolve from the directory root. `index.html` is the
11
- * SPA fallback for `/ui/<anything>`.
13
+ * SPA fallback for `/ui/<anything>` — `src/app.tsx` routes on the mount path.
12
14
  *
13
- * Deliberately self-contained only react/react-dom/vite/@vitejs/plugin-react,
14
- * all on public npm so the template builds without the @astrale-os registry
15
- * or workspace `link:`s. Two small subsets are reimplemented inline: the shell
16
- * child-handshake (`src/lib/shell.ts`, including `ctrl:tokenRefresh` so a
17
- * pushed credential is picked up) and a minimal JSON kernel client
18
- * (`src/lib/kernel.ts`, `@<id>::get`). Tests live in `__tests__/` and run on
19
- * `vitest`/`happy-dom` (see `vitest.config.ts`) — vite never bundles them.
15
+ * Consumes `@astrale-os/shell` for the child handshake + the real kernel client:
16
+ * `src/shell/use-shell.ts` boots `createShell({ mode: 'sandboxed' })` and the
17
+ * feature hooks call kernel methods through `shell.kernel` (token refresh, codec
18
+ * negotiation, redirect following, delegation all handled by the SDK).
20
19
  *
21
- * Deferred on purpose (grow via `@astrale-os/kernel-client` if needed): no
22
- * msgpack, no streaming/binary, no redirect following, no client-side minting,
23
- * no writes. See `README.md`.
20
+ * `@` is aliased to `src/` (mirrors tsconfig `paths`) so feature code imports
21
+ * `@/shell`, `@/ui`, `@/monitor`.
24
22
  */
25
23
  export default defineConfig({
26
24
  base: '/ui/',
27
25
  plugins: [viteReact()],
26
+ resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } },
28
27
  build: {
29
- outDir: '../dist-client',
28
+ outDir: '../.dist',
30
29
  emptyOutDir: true,
31
30
  sourcemap: false,
32
31
  },
@@ -1,18 +1,25 @@
1
+ import { fileURLToPath } from 'node:url'
2
+
1
3
  import viteReact from '@vitejs/plugin-react'
2
4
  import { defineConfig } from 'vitest/config'
3
5
 
4
6
  /**
5
- * Vitest config for the self-contained client. A DOM env (`happy-dom`) backs
6
- * the React render + `window.postMessage`/`MessageChannel` used by the fake
7
- * shell-parent harness. Kept separate from `vite.config.ts` (which sets the
8
- * `/ui/` base + `../dist-client` build) so tests don't inherit the SPA build
9
- * options.
7
+ * Vitest config for the client SPA. A DOM env (`happy-dom`) backs the React
8
+ * render + the shell handshake the tests exercise. Kept separate from
9
+ * `vite.config.ts` (which sets the `/ui/` base + `../.dist` build) so tests
10
+ * don't inherit the SPA build options.
10
11
  */
11
12
  export default defineConfig({
12
13
  plugins: [viteReact()],
14
+ resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } },
13
15
  test: {
14
16
  environment: 'happy-dom',
15
17
  globals: true,
16
18
  include: ['__tests__/**/*.test.{ts,tsx}', 'src/**/*.test.{ts,tsx}'],
19
+ // `@astrale-os/*` ship bundler-targeted ESM (extensionless relative imports).
20
+ // vitest externalizes node_modules and uses Node's stricter ESM resolver,
21
+ // which wants explicit `.js`; inlining the scope makes vitest transform them
22
+ // through vite — the same context the build uses. Standard for these deps.
23
+ server: { deps: { inline: [/@astrale-os\//] } },
17
24
  },
18
25
  })
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Health logic — PURE status rule, no I/O. Maps an observed probe result to the
3
+ * monitor's verdict. (Node identity/layout/seed data lives in `./node`; the
4
+ * kernel reads/writes live in `runtime/monitor/`; the network probe behind the
5
+ * `Prober` port in `integrations/prober/`.)
6
+ */
7
+
8
+ /** A monitor's health verdict. `unknown` is the pre-first-check state. */
9
+ export type HealthStatus = 'up' | 'down' | 'unknown'
10
+
11
+ /**
12
+ * Map an observed HTTP status code to a verdict. `0` means the host was
13
+ * unreachable (DNS/timeout/refused) — that's `down`. 2xx/3xx is `up`; anything
14
+ * else (4xx/5xx) is `down`. Pure.
15
+ */
16
+ export function classify(statusCode: number): HealthStatus {
17
+ if (statusCode >= 200 && statusCode < 400) return 'up'
18
+ return 'down'
19
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The monitor bounded context's PURE core, in one place: schema accessors
3
+ * (`keys`), the health status rule (`health`), and node identity/layout/seed
4
+ * data (`node`). No I/O, no clock/RNG — all deterministic + testable.
5
+ * `runtime/monitor/` imports the operations' logic from here.
6
+ */
7
+ export * from './keys'
8
+ export * from './health'
9
+ export * from './node'
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Compiled-schema accessors for the monitor context — the ONE place class paths,
3
+ * method paths, and qualified prop keys come from. Pure (schema-derived); never
4
+ * hand-write key strings. `D` (the compiled domain) comes from `schema/index`,
5
+ * re-exported here so the rest of the context reads it from one place.
6
+ */
7
+ import { K } from '@astrale-os/kernel-core'
8
+
9
+ import { D } from '../../schema'
10
+
11
+ export { D, K }
12
+
13
+ /** Kernel ops/classes the logic addresses. */
14
+ export const NODE_CREATE = K.Node.createNode.path.method.raw
15
+ export const FOLDER_CLASS = K.Folder.path.class.raw
16
+ export const NAME_KEY = K.Named.name.key
17
+
18
+ /** Domain class paths. */
19
+ export const MONITOR_CLASS = D.Monitor.path.class.raw
20
+ export const DEPENDS_ON_EDGE = D.depends_on.path.class.raw
21
+
22
+ /** Qualified storage keys for Monitor node props. */
23
+ export const MONITOR_KEYS = {
24
+ url: D.Monitor.url.key,
25
+ status: D.Monitor.status.key,
26
+ statusCode: D.Monitor.statusCode.key,
27
+ latencyMs: D.Monitor.latencyMs.key,
28
+ lastCheckedAt: D.Monitor.lastCheckedAt.key,
29
+ } as const
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Monitor node identity — PURE domain constants + slug logic (no I/O): where
3
+ * monitors live in the graph, how a new node's slug is formed, and the seed set.
4
+ * `runtime/monitor/` consumes these. The IMPURE bits (the slug's entropy suffix,
5
+ * the actual node writes) stay in `runtime/`, so this file is deterministic and
6
+ * trivially testable.
7
+ */
8
+
9
+ /** Where Monitor nodes live in the graph — a domain layout choice (`seed` creates the folder). */
10
+ export const MONITORS_PARENT = '/monitors'
11
+
12
+ /**
13
+ * Deterministic URL-safe stem for a monitor's node slug (host + path, lowercased,
14
+ * non-alnum → `-`). Pure.
15
+ */
16
+ export function slugForUrl(url: string): string {
17
+ return (
18
+ url
19
+ .replace(/^https?:\/\//i, '')
20
+ .toLowerCase()
21
+ .replace(/[^a-z0-9]+/g, '-')
22
+ .replace(/^-|-$/g, '')
23
+ .slice(0, 40) || 'monitor'
24
+ )
25
+ }
26
+
27
+ /**
28
+ * Slug for a NEW monitor node: the url stem + a caller-supplied `suffix`. Pure —
29
+ * `runtime/monitor/watch` injects the `suffix` (clock/RNG entropy for collision
30
+ * safety) so core stays deterministic. `seed` doesn't use this (it pins fixed
31
+ * slugs for idempotency); `watch` does.
32
+ */
33
+ export function monitorSlug(url: string, suffix: string): string {
34
+ return `${slugForUrl(url)}-${suffix}`
35
+ }
36
+
37
+ export interface StarterMonitor {
38
+ slug: string
39
+ name: string
40
+ url: string
41
+ }
42
+
43
+ /**
44
+ * Monitors laid down by `seed` (postInstall). httpbin returns a deterministic
45
+ * status code, so the second starter is a stable demo target; the first points at
46
+ * a real site. Fixed slugs keep `seed` idempotent across reinstalls.
47
+ */
48
+ export const STARTERS: readonly StarterMonitor[] = [
49
+ { slug: 'astrale', name: 'Astrale', url: 'https://astrale.ai' },
50
+ { slug: 'httpbin-200', name: 'httpbin (200)', url: 'https://httpbin.org/status/200' },
51
+ ]
@@ -0,0 +1,25 @@
1
+ /**
2
+ * env → handler dependency container — the ONE place the worker env becomes the
3
+ * typed `ctx.deps` every method reads. `defineDomain({ deps })` mounts it; the
4
+ * generated worker imports it. Runs ONCE per cold isolate (NOT per request), so
5
+ * it's where ports/clients are wired once instead of being re-derived in every
6
+ * handler.
7
+ *
8
+ * The prober port is resolved PER-REQUEST and PER-NODE (each `prober(target)`
9
+ * call), not here — so this stays a cheap synchronous wiring step and the backend
10
+ * can be chosen from the monitor being probed, not just the env.
11
+ *
12
+ * Omit the `deps` field in `domain.ts` for the trivial case (handlers then get
13
+ * raw `Env`). Transform here the moment a handler should depend on a PORT instead
14
+ * of raw config — that's the whole point of this seam.
15
+ */
16
+ import { buildProberRegistry, type ProberRegistry } from './integrations/prober/registry'
17
+ import type { Env } from './env'
18
+
19
+ /** Typed dependency container handed to every method as `ctx.deps`. */
20
+ export interface Deps extends ProberRegistry {}
21
+
22
+ /** Map the worker env to the handler deps (the seam `defineDomain({ deps })` mounts). */
23
+ export function deps(env: Env): Deps {
24
+ return buildProberRegistry(env)
25
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * domain.ts — the WORKER-SAFE domain definition: what the domain IS (its
3
+ * `schema`, `methods`, `deps`, `views`, `functions`, `client`) plus its identity
4
+ * (`origin` defaults to `schema.domain`; `postInstall`). Everything is wired
5
+ * EXPLICITLY here — a renamed or mistyped module is a compile error at this
6
+ * call, never a silently-missing route. The handlers live in `runtime/` (the
7
+ * composition root), the pure logic in `core/`, the external-API ports in
8
+ * `integrations/` — but the worker only ever sees this one wired definition.
9
+ *
10
+ * The generated worker (`.astrale/worker.gen.ts`) imports THIS and spreads it,
11
+ * so your folder layout is invisible to it — reorganize freely; only this wiring
12
+ * has to stay put. The deploy adapter is attached separately in
13
+ * `astrale.config.ts` via `deploy(domain, …)`, keeping its node-only code
14
+ * (wrangler, filesystem) out of the worker bundle.
15
+ */
16
+ import { defineDomain } from '@astrale-os/sdk'
17
+
18
+ import { deps } from './deps'
19
+ import { functions } from './functions'
20
+ import { methods } from './runtime'
21
+ import { schema } from './schema'
22
+ import { views } from './views'
23
+
24
+ export const domain = defineDomain({
25
+ schema,
26
+ methods,
27
+ deps,
28
+ views,
29
+ functions,
30
+ client: { dir: 'client' },
31
+ postInstall: `/:${schema.domain}:class.Monitor:seed`,
32
+ requires: [],
33
+ })
package/template/env.ts CHANGED
@@ -18,5 +18,9 @@ export interface Env {
18
18
  SELF?: { fetch(request: Request): Promise<Response> }
19
19
  /** Dev-only: forward /ui/* to a running Vite dev server. */
20
20
  VIEW_DEV_URL?: string
21
+
22
+ /** `http` only — per-probe timeout in ms (default 10000). */
23
+ PROBE_TIMEOUT_MS?: string
24
+
21
25
  [key: string]: unknown
22
26
  }