@astrale-os/adapter-cloudflare 0.1.9 → 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 (80) hide show
  1. package/package.json +1 -1
  2. package/template/.agents/skills/astrale-cli/SKILL.md +1 -1
  3. package/template/.agents/skills/astrale-domain/SKILL.md +5 -5
  4. package/template/.env.example +5 -7
  5. package/template/README.md +2 -2
  6. package/template/client/README.md +79 -62
  7. package/template/client/__tests__/app.test.tsx +188 -99
  8. package/template/client/__tests__/harness.ts +67 -12
  9. package/template/client/__tests__/kernel.test.ts +65 -50
  10. package/template/client/__tests__/seam.test.tsx +111 -0
  11. package/template/client/index.html +1 -1
  12. package/template/client/package.json +1 -0
  13. package/template/client/src/app.tsx +40 -83
  14. package/template/client/src/main.tsx +2 -2
  15. package/template/client/src/monitor/components/MonitorCard.tsx +50 -0
  16. package/template/client/src/monitor/components/index.ts +1 -0
  17. package/template/client/src/monitor/hooks/index.ts +3 -0
  18. package/template/client/src/monitor/hooks/useCheck.mutation.ts +16 -0
  19. package/template/client/src/monitor/hooks/useMonitor.query.ts +64 -0
  20. package/template/client/src/monitor/index.ts +6 -0
  21. package/template/client/src/monitor/monitor.api.ts +11 -0
  22. package/template/client/src/monitor/monitor.mappers.ts +38 -0
  23. package/template/client/src/monitor/monitor.types.ts +23 -0
  24. package/template/client/src/monitor/ui/MonitorDetails.UI.tsx +38 -0
  25. package/template/client/src/monitor/ui/StatusBadge.UI.tsx +14 -0
  26. package/template/client/src/monitor/ui/index.ts +8 -0
  27. package/template/client/src/shell/client.ts +67 -0
  28. package/template/client/src/shell/index.ts +20 -0
  29. package/template/client/src/shell/invoke.ts +35 -0
  30. package/template/client/src/shell/transformers.ts +72 -0
  31. package/template/client/src/shell/use-async.ts +56 -0
  32. package/template/client/src/shell/use-capability.ts +61 -0
  33. package/template/client/src/shell/use-node.ts +61 -0
  34. package/template/client/src/shell/use-shell.ts +91 -0
  35. package/template/client/src/shell/view-router.tsx +98 -0
  36. package/template/client/src/styles.css +177 -4
  37. package/template/client/src/ui/format.ts +24 -0
  38. package/template/client/src/ui/index.ts +9 -0
  39. package/template/client/src/ui/surface.tsx +56 -0
  40. package/template/client/src/ui/value.tsx +32 -0
  41. package/template/client/src/views/monitor.tsx +30 -0
  42. package/template/client/tsconfig.json +2 -1
  43. package/template/client/vite.config.ts +12 -13
  44. package/template/client/vitest.config.ts +12 -5
  45. package/template/core/monitor/health.ts +19 -0
  46. package/template/core/monitor/index.ts +9 -0
  47. package/template/core/monitor/keys.ts +29 -0
  48. package/template/core/monitor/node.ts +51 -0
  49. package/template/deps.ts +8 -8
  50. package/template/domain.ts +1 -1
  51. package/template/env.ts +2 -9
  52. package/template/integrations/prober/http.ts +43 -0
  53. package/template/integrations/prober/mock.ts +22 -0
  54. package/template/integrations/prober/port.ts +28 -0
  55. package/template/integrations/prober/registry.ts +66 -0
  56. package/template/package.json +1 -1
  57. package/template/pnpm-lock.yaml +2766 -0
  58. package/template/runtime/index.ts +36 -19
  59. package/template/runtime/monitor/check.ts +29 -0
  60. package/template/runtime/monitor/dependsOn.ts +16 -0
  61. package/template/runtime/monitor/index.ts +12 -0
  62. package/template/runtime/monitor/seed.ts +74 -0
  63. package/template/runtime/monitor/shared.ts +17 -0
  64. package/template/runtime/monitor/watch.ts +37 -0
  65. package/template/schema/index.ts +11 -4
  66. package/template/schema/monitor.ts +80 -0
  67. package/template/views/index.ts +9 -2
  68. package/template/views/monitor.ts +22 -0
  69. package/template/client/src/lib/kernel.ts +0 -135
  70. package/template/client/src/lib/shell.ts +0 -197
  71. package/template/client/src/lib/use-node.ts +0 -66
  72. package/template/client/src/lib/use-shell.ts +0 -85
  73. package/template/core/keys.ts +0 -28
  74. package/template/core/note.ts +0 -148
  75. package/template/integrations/summary/heuristic.ts +0 -25
  76. package/template/integrations/summary/http.ts +0 -69
  77. package/template/integrations/summary/port.ts +0 -21
  78. package/template/integrations/summary/registry.ts +0 -52
  79. package/template/schema/note.ts +0 -67
  80. package/template/views/note.ts +0 -21
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * Runtime composition root — the ONLY place request context (kernel/self/params/
3
3
  * auth) and `deps` meet the per-feature logic. Each `execute` resolves the port
4
- * it needs (`deps.summarizer()`, on-request) and delegates to the transport-
5
- * agnostic functions in `core/`. No business logic lives here.
4
+ * it needs (`deps.prober()`, on-request) and delegates to the per-operation I/O
5
+ * functions in `runtime/monitor/` (`watch`/`check`/`dependsOn`/`seed`), which in
6
+ * turn call `core/` for pure decisions. No business logic lives here.
6
7
  *
7
- * - `createNote` is interface-hosted (static) → `remoteInterfaceMethods`.
8
- * - `reference` is class-hosted (instance) → `remoteMethod` + `remoteClassMethods`.
8
+ * - `watch` is interface-hosted (static) → `remoteInterfaceMethods`.
9
+ * - `check` is class-hosted (instance) → `remoteMethod` + `remoteClassMethods`.
10
+ * - `dependsOn` is class-hosted (instance).
9
11
  * - `seed` is class-hosted (static) — the `postInstall` bootstrap.
10
12
  *
11
13
  * SDK-level `authorize` is an additive throw-to-deny check (returns void). For
@@ -20,43 +22,58 @@ import {
20
22
  type SchemaMethodsImpl,
21
23
  } from '@astrale-os/sdk'
22
24
 
23
- import { createNote, reference, seed } from '../core/note'
24
25
  import type { Deps } from '../deps'
26
+
27
+ import { MONITOR_KEYS } from '../core/monitor'
25
28
  import { schema } from '../schema'
29
+ import { check, dependsOn, seed, watch } from './monitor'
26
30
 
27
31
  const method = remoteMethod<Deps>()
28
32
  const interfaceMethods = remoteInterfaceMethods<Deps>()
29
33
  const classMethods = remoteClassMethods<Deps>()
30
34
 
31
- const NoteOpsMethods = interfaceMethods(schema, 'NoteOps', {
32
- createNote: {
35
+ const MonitorOpsMethods = interfaceMethods(schema, 'MonitorOps', {
36
+ watch: {
33
37
  authorize: async () => undefined,
34
- execute: ({ kernel, params, deps }) => {
35
- if (!kernel) throw new Error('createNote requires a kernel credential')
36
- return createNote(kernel, deps.summarizer(), params)
38
+ execute: ({ kernel, params }) => {
39
+ return watch(kernel, params)
37
40
  },
38
41
  },
39
42
  })
40
43
 
41
- const referenceMethod = method(schema, 'Note', 'reference', {
44
+ const checkMethod = method(schema, 'Monitor', 'check', {
45
+ authorize: async () => undefined,
46
+ execute: async ({ kernel, self, deps }) => {
47
+ if (!kernel) throw new Error('check requires a kernel credential')
48
+ const node = await self.node()
49
+ const url = node.props[MONITOR_KEYS.url]
50
+ if (typeof url !== 'string') throw new Error('Monitor node is missing its url property')
51
+ const prober = deps.prober({ class: node.class.raw, url })
52
+ return check(kernel, prober, self.path.raw, url)
53
+ },
54
+ })
55
+
56
+ const dependsOnMethod = method(schema, 'Monitor', 'dependsOn', {
42
57
  authorize: async () => undefined,
43
58
  execute: ({ kernel, self, params }) => {
44
- if (!kernel) throw new Error('reference requires a kernel credential')
45
- return reference(kernel, self.path.raw, params)
59
+ return dependsOn(kernel, self.path.raw, params)
46
60
  },
47
61
  })
48
62
 
49
- const seedMethod = method(schema, 'Note', 'seed', {
63
+ const seedMethod = method(schema, 'Monitor', 'seed', {
50
64
  authorize: async () => undefined,
51
65
  execute: ({ kernel, deps }) => {
52
- if (!kernel) throw new Error('seed requires a kernel credential')
53
- return seed(kernel, deps.summarizer())
66
+ return seed(kernel, deps.prober())
54
67
  },
55
68
  })
56
69
 
57
- const NoteMethods = classMethods(schema, 'Note', { reference: referenceMethod, seed: seedMethod })
70
+ const MonitorMethods = classMethods(schema, 'Monitor', {
71
+ check: checkMethod,
72
+ dependsOn: dependsOnMethod,
73
+ seed: seedMethod,
74
+ })
58
75
 
59
76
  export const methods: SchemaMethodsImpl<typeof schema, Deps> = {
60
- interface: { NoteOps: NoteOpsMethods },
61
- class: { Note: NoteMethods },
77
+ interface: { MonitorOps: MonitorOpsMethods },
78
+ class: { Monitor: MonitorMethods },
62
79
  }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `Monitor.check` (instance) — probe the target via the resolved `Prober` port,
3
+ * classify the result (pure, `core/health`), and record status/latency back onto
4
+ * the node. `url` is the monitor's target and `prober` is already selected FOR
5
+ * this node (both resolved by the wiring in `runtime/index.ts` from `self.node()`).
6
+ */
7
+ import { classify, MONITOR_KEYS } from '../../core/monitor'
8
+ import type { Prober } from '../../integrations/prober/port'
9
+
10
+ import type { CallableKernel } from './shared'
11
+
12
+ export async function check(
13
+ kernel: CallableKernel,
14
+ prober: Prober,
15
+ selfPathRaw: string,
16
+ url: string,
17
+ ): Promise<{ status: string; statusCode: number; latencyMs: number }> {
18
+ const result = await prober.probe(url)
19
+ const status = classify(result.statusCode)
20
+ await kernel.call(`${selfPathRaw}::update`, {
21
+ props: {
22
+ [MONITOR_KEYS.status]: status,
23
+ [MONITOR_KEYS.statusCode]: result.statusCode,
24
+ [MONITOR_KEYS.latencyMs]: result.latencyMs,
25
+ [MONITOR_KEYS.lastCheckedAt]: new Date().toISOString(),
26
+ },
27
+ })
28
+ return { status, statusCode: result.statusCode, latencyMs: result.latencyMs }
29
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * `Monitor.dependsOn` (instance) — link this Monitor to another it relies on via
3
+ * a `depends_on` edge. `target` is any node address (a tree path or `@<id>`).
4
+ */
5
+ import { DEPENDS_ON_EDGE } from '../../core/monitor'
6
+
7
+ import type { CallableKernel } from './shared'
8
+
9
+ export async function dependsOn(
10
+ kernel: CallableKernel,
11
+ selfPathRaw: string,
12
+ params: { target: string },
13
+ ): Promise<{ linked: string }> {
14
+ await kernel.call(`${selfPathRaw}::link`, { edgeClass: DEPENDS_ON_EDGE, target: params.target })
15
+ return { linked: params.target }
16
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Monitor runtime operations — one file per method, assembled here. The
3
+ * composition root (`runtime/index.ts`) imports these and wires them into the
4
+ * methods map. Each is transport-agnostic logic over a `CallableKernel` (+ the
5
+ * `Prober` port where it probes), delegating pure decisions to `core/` — so all
6
+ * are unit-testable with fakes.
7
+ */
8
+ export { check } from './check'
9
+ export { dependsOn } from './dependsOn'
10
+ export { seed } from './seed'
11
+ export { watch } from './watch'
12
+ export type { CallableKernel } from './shared'
@@ -0,0 +1,74 @@
1
+ /**
2
+ * `Monitor.seed` (static) — the domain's post-install bootstrap. The kernel calls
3
+ * it ONCE after install, as __SYSTEM__ (see `postInstall` in `domain.ts`), so the
4
+ * domain can lay down its initial state: the `/monitors` folder, a couple of
5
+ * starter Monitors (each probed once so they show real status immediately), and a
6
+ * `depends_on` edge between them. Idempotent: a re-run swallows `PATH_CONFLICT`
7
+ * per node (the starters use fixed slugs).
8
+ */
9
+ import {
10
+ classify,
11
+ DEPENDS_ON_EDGE,
12
+ FOLDER_CLASS,
13
+ MONITOR_CLASS,
14
+ MONITOR_KEYS,
15
+ MONITORS_PARENT,
16
+ NAME_KEY,
17
+ NODE_CREATE,
18
+ STARTERS,
19
+ } from '../../core/monitor'
20
+ import type { Prober } from '../../integrations/prober/port'
21
+
22
+ import { type CallableKernel, isPathConflict } from './shared'
23
+
24
+ export async function seed(kernel: CallableKernel, prober: Prober): Promise<{ seeded: number }> {
25
+ // 1. The `/monitors` folder at the graph root.
26
+ try {
27
+ await kernel.call(NODE_CREATE, {
28
+ class: FOLDER_CLASS,
29
+ path: MONITORS_PARENT,
30
+ props: { [NAME_KEY]: 'monitors' },
31
+ })
32
+ } catch (e) {
33
+ if (!isPathConflict(e)) throw e
34
+ }
35
+
36
+ // 2. A couple of starter Monitors, each probed once for an initial status.
37
+ const ids: Record<string, string> = {}
38
+ let seeded = 0
39
+ for (const s of STARTERS) {
40
+ try {
41
+ const result = await prober.probe(s.url)
42
+ const created = (await kernel.call(NODE_CREATE, {
43
+ class: MONITOR_CLASS,
44
+ path: `${MONITORS_PARENT}/${s.slug}`,
45
+ props: {
46
+ [NAME_KEY]: s.name,
47
+ [MONITOR_KEYS.url]: s.url,
48
+ [MONITOR_KEYS.status]: classify(result.statusCode),
49
+ [MONITOR_KEYS.statusCode]: result.statusCode,
50
+ [MONITOR_KEYS.latencyMs]: result.latencyMs,
51
+ [MONITOR_KEYS.lastCheckedAt]: new Date().toISOString(),
52
+ },
53
+ })) as { id: string }
54
+ ids[s.slug] = created.id
55
+ seeded++
56
+ } catch (e) {
57
+ if (!isPathConflict(e)) throw e
58
+ }
59
+ }
60
+
61
+ // 3. Link the first starter as depending on the second (id-form on both sides
62
+ // — layout-independent). Best-effort.
63
+ const from = ids[STARTERS[0]?.slug ?? '']
64
+ const to = ids[STARTERS[1]?.slug ?? '']
65
+ if (from && to) {
66
+ try {
67
+ await kernel.call(`@${from}::link`, { edgeClass: DEPENDS_ON_EDGE, target: `@${to}` })
68
+ } catch (e) {
69
+ if (!isPathConflict(e)) throw e
70
+ }
71
+ }
72
+
73
+ return { seeded }
74
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Runtime-internal helpers shared across the Monitor operations (`watch`,
3
+ * `check`, `dependsOn`, `seed`). Not exported from the domain — just the seam the
4
+ * operation files agree on.
5
+ */
6
+
7
+ /**
8
+ * The minimal kernel surface the operations need — kept narrow so each is
9
+ * unit-testable with a fake `{ call }`. The SDK hands the real client at the
10
+ * `runtime/index.ts` seam.
11
+ */
12
+ export type CallableKernel = { call(path: string, params: unknown): Promise<unknown> }
13
+
14
+ /** True for the kernel's create-collision error — lets `seed` stay idempotent. */
15
+ export function isPathConflict(e: unknown): boolean {
16
+ return e instanceof Error && e.message.includes('PATH_CONFLICT')
17
+ }
@@ -0,0 +1,37 @@
1
+ import type { CallableKernel } from './shared'
2
+
3
+ /**
4
+ * `MonitorOps.watch` (static) — create a Monitor node under `/monitors`. The slug
5
+ * FORMAT is pure (`core/monitor`'s `monitorSlug`); the entropy suffix (impure,
6
+ * clock/RNG) is generated HERE, keeping core deterministic.
7
+ */
8
+ import {
9
+ MONITOR_CLASS,
10
+ MONITOR_KEYS,
11
+ MONITORS_PARENT,
12
+ monitorSlug,
13
+ NAME_KEY,
14
+ NODE_CREATE,
15
+ } from '../../core/monitor'
16
+
17
+ /** Short clock+RNG entropy to dodge same-ms / same-host slug collisions. */
18
+ function randomSuffix(): string {
19
+ return `${Date.now().toString(36).slice(-4)}${Math.random().toString(36).slice(2, 6)}`
20
+ }
21
+
22
+ export async function watch(
23
+ kernel: CallableKernel,
24
+ params: { url: string; name?: string },
25
+ ): Promise<{ id: string; path: string }> {
26
+ const path = `${MONITORS_PARENT}/${monitorSlug(params.url, randomSuffix())}`
27
+ const created = (await kernel.call(NODE_CREATE, {
28
+ class: MONITOR_CLASS,
29
+ path,
30
+ props: {
31
+ [NAME_KEY]: params.name ?? params.url,
32
+ [MONITOR_KEYS.url]: params.url,
33
+ [MONITOR_KEYS.status]: 'unknown',
34
+ },
35
+ })) as { id: string }
36
+ return { id: created.id, path }
37
+ }
@@ -11,13 +11,20 @@
11
11
  import { defineSchema, KernelSchema } from '@astrale-os/kernel-core'
12
12
  import { compileDomain, type Domain } from '@astrale-os/kernel-core/domain'
13
13
 
14
- import { Note, NoteOps, references } from './note'
14
+ import { depends_on, Monitor, MonitorOps } from './monitor'
15
15
 
16
16
  export const schema = defineSchema('astrale-domain.example.dev', {
17
- interfaces: { NoteOps },
18
- classes: { Note, references },
17
+ interfaces: { MonitorOps },
18
+ classes: { Monitor, depends_on },
19
19
  imports: [KernelSchema],
20
20
  })
21
+
22
+ /**
23
+ * The compiled domain — resolved class/interface paths + qualified prop keys.
24
+ * `compileDomain` is pure (no env), so this is safe to import from the worker
25
+ * and from build tooling alike. `core/keys.ts` re-exports it as the single source
26
+ * of graph-facing strings (class paths, prop keys) — never hand-write those.
27
+ */
21
28
  export const D: Domain<typeof schema> = compileDomain(schema)
22
29
 
23
- export * from './note'
30
+ export * from './monitor'
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Monitor context — the domain's single bounded slice.
3
+ *
4
+ * One file per context: a class (or a few tightly-related classes) plus the
5
+ * edges that bind them. Here that is the `MonitorOps` interface, the `Monitor`
6
+ * class that implements it, and the `depends_on` edge from one Monitor to
7
+ * another. To grow the domain, add `schema/<context>.ts` and register its
8
+ * members in `schema/index.ts`.
9
+ *
10
+ * - Interface `MonitorOps` one static op, `watch`. Static → the impl gets no
11
+ * `self`; it creates a brand-new Monitor.
12
+ * - Class `Monitor` implements `[MonitorOps, Container]`, inheriting
13
+ * `watch` and adding instance methods `check`
14
+ * (probe + record live status) and `dependsOn`
15
+ * (links this Monitor to one it relies on).
16
+ * - Edge `depends_on` Monitor → Monitor. A dependency graph of checks,
17
+ * materialized by `dependsOn` (and by `seed`).
18
+ */
19
+ import { edgeClass, KernelSchema, nodeClass, nodeInterface } from '@astrale-os/kernel-core'
20
+ import { fn } from '@astrale-os/kernel-dsl'
21
+ import { z } from 'zod'
22
+
23
+ /**
24
+ * Thin ref to a created node — what node-creating ops return. A remote method
25
+ * returns a plain `{ id, path }`, never `ref(SELF)` (whose full-Node value does
26
+ * not round-trip over the worker wire).
27
+ */
28
+ export const MonitorRef = z.object({ id: z.string(), path: z.string() })
29
+
30
+ /** What a single probe records — returned by `check`. */
31
+ export const ProbeOutcome = z.object({
32
+ status: z.string(),
33
+ statusCode: z.number().int(),
34
+ latencyMs: z.number().int(),
35
+ })
36
+
37
+ export const MonitorOps = nodeInterface({
38
+ methods: {
39
+ watch: fn({
40
+ static: true,
41
+ params: { url: z.string(), name: z.string().optional() },
42
+ returns: MonitorRef,
43
+ }),
44
+ },
45
+ })
46
+
47
+ export const Monitor = nodeClass({
48
+ implements: [MonitorOps, KernelSchema.interfaces.Container],
49
+ props: {
50
+ /** The target URL this monitor probes. */
51
+ url: z.string(),
52
+ // Live status, written by `check`. PLAIN STRING, not `z.enum()`: `::update`
53
+ // currently drops `z.enum()` props silently, and `check` updates this.
54
+ // Values: 'up' | 'down' | 'unknown' (the initial value before the first check).
55
+ status: z.string().optional(),
56
+ /** Last observed HTTP status code (0 = host unreachable). */
57
+ statusCode: z.number().int().optional(),
58
+ /** Last observed round-trip latency, in ms. */
59
+ latencyMs: z.number().int().optional(),
60
+ /** ISO timestamp of the last check. */
61
+ lastCheckedAt: z.string().optional(),
62
+ },
63
+ methods: {
64
+ // Instance: probe the target via the resolved `Prober` port (the external
65
+ // integration), then record status/latency back onto this node.
66
+ check: fn({ returns: ProbeOutcome }),
67
+ // Instance: link this Monitor to another it depends on (a `depends_on` edge).
68
+ dependsOn: fn({ params: { target: z.string() }, returns: z.object({ linked: z.string() }) }),
69
+ // Post-install bootstrap (wired as `postInstall` in domain.ts). Static: the
70
+ // kernel calls it ONCE after install, as __SYSTEM__, with no `self`. Must
71
+ // stay idempotent — a re-install runs it again.
72
+ seed: fn({ static: true, returns: z.object({ seeded: z.number().int() }) }),
73
+ },
74
+ })
75
+
76
+ export const depends_on = edgeClass(
77
+ { as: 'dependent', types: [Monitor] },
78
+ { as: 'dependency', types: [Monitor] },
79
+ { props: { reason: z.string().optional() } },
80
+ )
@@ -3,8 +3,15 @@
3
3
  * key; each becomes a View node at `/<origin>/core/views/<slug>` whose iframe
4
4
  * binding the SDK stamps with the worker's live serving URL when it builds the
5
5
  * install bundle.
6
+ *
7
+ * `welcome` is a worker-rendered inline-HTML view (no SPA). `ui-monitor` and
8
+ * `ui-monitor-badge` are BOTH served by the one `client/` SPA — it routes on the
9
+ * mount path (`/ui/monitor` vs `/ui/monitor-badge`) to pick which view to render.
6
10
  */
7
- import { note } from './note'
11
+ import { monitor } from './monitor'
8
12
  import { welcome } from './welcome'
9
13
 
10
- export const views = { welcome, 'ui-note': note }
14
+ export const views = {
15
+ welcome,
16
+ 'ui-monitor': monitor,
17
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * `ui-monitor` — a rich View backed by the `client/` React + Vite SPA (instead of
3
+ * an inline-HTML `render` like `welcome`). Because it declares `mount` rather
4
+ * than `render`, the View node's iframe binding points at
5
+ * `<serving url>/ui/monitor` — the SDK stamps it from the worker's live URL when
6
+ * it builds the install bundle. The Cloudflare adapter serves `/ui/*` from
7
+ * `../.dist` (built by `client/` with base `/ui/`) via the Worker's `ASSETS`
8
+ * binding.
9
+ *
10
+ * `viewFor: selfOf(Monitor)` attaches a `view_for` edge to the `Monitor` class
11
+ * meta-node, so the GUI offers this view for any Monitor instance.
12
+ */
13
+ import { selfOf } from '@astrale-os/kernel-dsl'
14
+ import { defineView } from '@astrale-os/sdk'
15
+
16
+ import { Monitor } from '../schema/monitor'
17
+
18
+ export const monitor = defineView({
19
+ auth: 'public',
20
+ mount: '/ui/monitor',
21
+ viewFor: selfOf(Monitor),
22
+ })
@@ -1,135 +0,0 @@
1
- /**
2
- * Minimal, self-contained kernel JSON client — just enough to load a node via
3
- * `@<id>::get`, without pulling in `@astrale-os/kernel-client` (and its
4
- * transitive deps). It reproduces the kernel ENVELOPE wire shape inline.
5
- *
6
- * Wire contract (authoritative source: `kernel/api/envelope/`):
7
- * - Request: POST <kernelUrl> with headers
8
- * content-type: application/vnd.astrale.kernel+json
9
- * accept: application/vnd.astrale.kernel+json
10
- * authorization: <delegationToken> (BARE token — no "Bearer " prefix)
11
- * body JSON `{ method, params, id }` (see `encode.ts:encodeKernelRequest`).
12
- * - Response: JSON, exactly one of (decode precedence error → redirect → result,
13
- * mirroring `decode.ts:decodeKernelResponse`):
14
- * { error: { code, message }, id } → throw Error("<code>: <message>")
15
- * { redirect, id } → throw (we don't follow redirects here)
16
- * { result, id } → return result
17
- *
18
- * Deliberately JSON-only: no msgpack codec, no streaming/binary, no redirect
19
- * following, no schema/batching. Those live in `@astrale-os/kernel-client`,
20
- * which this self-contained build omits on purpose.
21
- */
22
-
23
- // From `kernel/api/envelope/types.ts` (KERNEL_CONTENT_TYPE_JSON). Inlined so the
24
- // template needs no @astrale-os import.
25
- const KERNEL_CONTENT_TYPE_JSON = 'application/vnd.astrale.kernel+json'
26
-
27
- // Module-local monotonic request id. Strings keep ids stable across reloads and
28
- // distinguishable in logs; the kernel echoes `id` back but we don't correlate
29
- // (one request per fetch).
30
- let idSeq = 0
31
- function nextId(): string {
32
- idSeq += 1
33
- return `c${idSeq}`
34
- }
35
-
36
- /**
37
- * Prop key constants — the graph stores props with fully-qualified keys
38
- * (`<domain>:<member>.property.<name>`). The kernel `Named.name` key is fixed
39
- * and known; domain props (`title`, `body`) are qualified by the (build-time
40
- * unknown) domain origin — read those by suffix with `readPropBySuffix`.
41
- */
42
- export const PROP = {
43
- named: {
44
- name: 'kernel.astrale.ai:interface.Named.property.name',
45
- },
46
- } as const
47
-
48
- export type KernelNode = {
49
- id: string
50
- path: string
51
- class: string | { raw?: string }
52
- props: Record<string, unknown>
53
- }
54
-
55
- export function readProp(props: Record<string, unknown>, key: string): string | undefined {
56
- const v = props[key]
57
- return typeof v === 'string' ? v : undefined
58
- }
59
-
60
- /**
61
- * Read a string prop by key suffix — for domain-qualified props whose full key
62
- * embeds the (build-time-unknown) domain origin. e.g.
63
- * `readPropBySuffix(props, '.property.body')`.
64
- */
65
- export function readPropBySuffix(
66
- props: Record<string, unknown>,
67
- suffix: string,
68
- ): string | undefined {
69
- for (const [k, v] of Object.entries(props)) {
70
- if (k.endsWith(suffix) && typeof v === 'string') return v
71
- }
72
- return undefined
73
- }
74
-
75
- /** Short class name from a `class.raw` path `/:<domain>:class.<Name>` → `<Name>`. */
76
- export function classShortName(node: KernelNode): string {
77
- const raw = (typeof node.class === 'string' ? node.class : node.class?.raw) ?? ''
78
- const last = raw.split(':').pop() ?? ''
79
- const dot = last.indexOf('.')
80
- return dot >= 0 ? last.slice(dot + 1) : last
81
- }
82
-
83
- /**
84
- * POST a single kernel call and return its `result`. Throws on a kernel error
85
- * envelope or an unexpected redirect. `token` is the bare delegation credential
86
- * from the shell handshake; the iframe authenticates SOLELY with it (the parent
87
- * minted it for this kernel, so the audience already matches — no cookie/mint).
88
- */
89
- export async function kernelCall(
90
- kernelUrl: string,
91
- token: string,
92
- method: string,
93
- params: Record<string, unknown> = {},
94
- ): Promise<unknown> {
95
- // The kernel routes on a trailing slash; the parent absolutizes the URL, but
96
- // not always with the slash, so normalize here.
97
- const url = kernelUrl.endsWith('/') ? kernelUrl : `${kernelUrl}/`
98
- const id = nextId()
99
-
100
- const res = await fetch(url, {
101
- method: 'POST',
102
- headers: {
103
- 'content-type': KERNEL_CONTENT_TYPE_JSON,
104
- accept: KERNEL_CONTENT_TYPE_JSON,
105
- authorization: token,
106
- },
107
- body: JSON.stringify({ method, params, id }),
108
- })
109
-
110
- let body: unknown
111
- try {
112
- body = await res.json()
113
- } catch {
114
- throw new Error(`kernel returned a non-JSON response (HTTP ${res.status})`)
115
- }
116
-
117
- if (body === null || typeof body !== 'object') {
118
- throw new Error(`kernel returned an unexpected response (HTTP ${res.status})`)
119
- }
120
- const obj = body as Record<string, unknown>
121
-
122
- // Decode precedence error → redirect → result (mirrors decodeKernelResponse).
123
- if ('error' in obj && obj.error && typeof obj.error === 'object') {
124
- const err = obj.error as { code?: unknown; message?: unknown }
125
- const code = typeof err.code === 'number' ? err.code : 5000
126
- const message = typeof err.message === 'string' ? err.message : 'Unknown error'
127
- throw new Error(`${code}: ${message}`)
128
- }
129
- if ('redirect' in obj && obj.redirect) {
130
- // Redirects (remote-domain Functions) aren't followed by this minimal
131
- // client — they require credential re-minting against the target worker.
132
- throw new Error('unexpected redirect from kernel (not supported by the template client)')
133
- }
134
- return obj.result
135
- }