@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.
- 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 +26 -12
- package/template/.agents/skills/astrale-domain/SKILL.md +46 -29
- package/template/.env.example +6 -0
- package/template/README.md +25 -10
- package/template/astrale.config.ts +27 -33
- package/template/client/README.md +80 -63
- package/template/client/__tests__/app.test.tsx +188 -99
- package/template/client/__tests__/harness.ts +67 -12
- package/template/client/__tests__/kernel.test.ts +65 -50
- package/template/client/__tests__/seam.test.tsx +111 -0
- package/template/client/index.html +1 -1
- package/template/client/package.json +1 -0
- package/template/client/src/app.tsx +40 -83
- package/template/client/src/main.tsx +2 -2
- package/template/client/src/monitor/components/MonitorCard.tsx +50 -0
- package/template/client/src/monitor/components/index.ts +1 -0
- package/template/client/src/monitor/hooks/index.ts +3 -0
- package/template/client/src/monitor/hooks/useCheck.mutation.ts +16 -0
- package/template/client/src/monitor/hooks/useMonitor.query.ts +64 -0
- package/template/client/src/monitor/index.ts +6 -0
- package/template/client/src/monitor/monitor.api.ts +11 -0
- package/template/client/src/monitor/monitor.mappers.ts +38 -0
- package/template/client/src/monitor/monitor.types.ts +23 -0
- package/template/client/src/monitor/ui/MonitorDetails.UI.tsx +38 -0
- package/template/client/src/monitor/ui/StatusBadge.UI.tsx +14 -0
- package/template/client/src/monitor/ui/index.ts +8 -0
- package/template/client/src/shell/client.ts +67 -0
- package/template/client/src/shell/index.ts +20 -0
- package/template/client/src/shell/invoke.ts +35 -0
- package/template/client/src/shell/transformers.ts +72 -0
- package/template/client/src/shell/use-async.ts +56 -0
- package/template/client/src/shell/use-capability.ts +61 -0
- package/template/client/src/shell/use-node.ts +61 -0
- package/template/client/src/shell/use-shell.ts +91 -0
- package/template/client/src/shell/view-router.tsx +98 -0
- package/template/client/src/styles.css +177 -4
- package/template/client/src/ui/format.ts +24 -0
- package/template/client/src/ui/index.ts +9 -0
- package/template/client/src/ui/surface.tsx +56 -0
- package/template/client/src/ui/value.tsx +32 -0
- package/template/client/src/views/monitor.tsx +30 -0
- package/template/client/tsconfig.json +3 -2
- package/template/client/vite.config.ts +14 -15
- package/template/client/vitest.config.ts +12 -5
- package/template/core/monitor/health.ts +19 -0
- package/template/core/monitor/index.ts +9 -0
- package/template/core/monitor/keys.ts +29 -0
- package/template/core/monitor/node.ts +51 -0
- package/template/deps.ts +25 -0
- package/template/domain.ts +33 -0
- package/template/env.ts +4 -0
- package/template/integrations/prober/http.ts +43 -0
- package/template/integrations/prober/mock.ts +22 -0
- package/template/integrations/prober/port.ts +28 -0
- package/template/integrations/prober/registry.ts +66 -0
- package/template/package.json +2 -3
- package/template/pnpm-lock.yaml +2766 -0
- package/template/runtime/index.ts +79 -0
- package/template/runtime/monitor/check.ts +29 -0
- package/template/runtime/monitor/dependsOn.ts +16 -0
- package/template/runtime/monitor/index.ts +12 -0
- package/template/runtime/monitor/seed.ts +74 -0
- package/template/runtime/monitor/shared.ts +17 -0
- package/template/runtime/monitor/watch.ts +37 -0
- package/template/schema/index.ts +13 -4
- package/template/schema/monitor.ts +80 -0
- package/template/tsconfig.json +13 -2
- package/template/views/index.ts +9 -2
- package/template/views/monitor.ts +22 -0
- 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/client/src/lib/kernel.ts +0 -135
- package/template/client/src/lib/shell.ts +0 -197
- package/template/client/src/lib/use-node.ts +0 -66
- package/template/client/src/lib/use-shell.ts +0 -85
- package/template/methods/index.ts +0 -66
- package/template/methods/note.ts +0 -131
- package/template/schema/compiled.ts +0 -14
- package/template/schema/note.ts +0 -64
- package/template/views/note.ts +0 -21
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal, self-contained reimplementation of the Astrale shell *child* init
|
|
3
|
-
* handshake — just enough to learn which node this view is mounted for and to
|
|
4
|
-
* keep its delegation token live, without pulling in `@astrale-os/shell` (and
|
|
5
|
-
* its `kernel-client` transitive deps).
|
|
6
|
-
*
|
|
7
|
-
* Protocol (mirrors `shell/packages/shell/src/application/windowing/`):
|
|
8
|
-
* 1. child posts `{ type: 'astrale-shell/init-request', version: 1 }` to the
|
|
9
|
-
* parent window via `window.postMessage(..., '*')`.
|
|
10
|
-
* 2. parent replies with `{ type: 'astrale-shell/init-response', windowId }`
|
|
11
|
-
* and *transfers a MessagePort* (`event.ports[0]`).
|
|
12
|
-
* 3. over that port the parent sends a `ctrl:handshake` carrying the session
|
|
13
|
-
* data — `windowId`, `kernelUrl`, `delegationToken`, `tokenExpiresAt`,
|
|
14
|
-
* `functionId`, and the `targetNodeId` (the node this view should render).
|
|
15
|
-
* 4. child replies with `ctrl:handshakeAck` and keeps the port open.
|
|
16
|
-
*
|
|
17
|
-
* Steady state on the port:
|
|
18
|
-
* - `intent` `setTarget { nodeId }` — hot-swap the target node (re-render).
|
|
19
|
-
* - `ctrl:tokenRefresh { delegationToken, tokenExpiresAt }` — the parent
|
|
20
|
-
* pushes a FRESH token before the old one expires. We update the mutable
|
|
21
|
-
* `currentToken` so the next kernel call uses it (see `getToken()`).
|
|
22
|
-
*
|
|
23
|
-
* The iframe authenticates with the handshake `delegationToken` ONLY: the
|
|
24
|
-
* parent minted it for this kernel, so the audience already matches — no
|
|
25
|
-
* cookie, no whoami, no client-side minting. Actually CALLING the kernel lives
|
|
26
|
-
* in `lib/kernel.ts` (a minimal inline JSON client).
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
const INIT_REQUEST_TYPE = 'astrale-shell/init-request'
|
|
30
|
-
const INIT_RESPONSE_TYPE = 'astrale-shell/init-response'
|
|
31
|
-
|
|
32
|
-
export type HandshakeData = {
|
|
33
|
-
windowId: string
|
|
34
|
-
kernelUrl?: string
|
|
35
|
-
functionId?: string
|
|
36
|
-
/** Opaque delegation credential the parent minted for this view. */
|
|
37
|
-
delegationToken?: string
|
|
38
|
-
/** Unix-ms expiry of `delegationToken`; advances on each `tokenRefresh`. */
|
|
39
|
-
tokenExpiresAt?: number
|
|
40
|
-
/** The node this view is mounted for — what we want to render. */
|
|
41
|
-
targetNodeId?: string
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export type ShellSession = {
|
|
45
|
-
/** Handshake data captured at handshake time (token here is the INITIAL one). */
|
|
46
|
-
readonly data: HandshakeData
|
|
47
|
-
/** Convenience mirror of `data.kernelUrl` (satisfies `NodeSession`). */
|
|
48
|
-
readonly kernelUrl: string | undefined
|
|
49
|
-
/** The LIVE delegation token — reflects `tokenRefresh` pushes. */
|
|
50
|
-
getToken(): string | undefined
|
|
51
|
-
/** The LIVE token expiry (Unix ms) — reflects `tokenRefresh` pushes. */
|
|
52
|
-
getTokenExpiresAt(): number | undefined
|
|
53
|
-
/** Subscribe to `targetNodeId` hot-swaps pushed via `setTarget` intents. */
|
|
54
|
-
onTargetChange(cb: (nodeId: string | undefined) => void): () => void
|
|
55
|
-
/** Tear down listeners + the message port. */
|
|
56
|
-
dispose(): void
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
type CtrlMessage = {
|
|
60
|
-
type: 'ctrl'
|
|
61
|
-
version: number
|
|
62
|
-
action: string
|
|
63
|
-
data?: Record<string, unknown>
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
type IntentMessage = {
|
|
67
|
-
type: 'intent'
|
|
68
|
-
envelope?: { name?: string; payload?: Record<string, unknown> }
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const isObj = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null
|
|
72
|
-
|
|
73
|
-
const asString = (v: unknown): string | undefined => (typeof v === 'string' ? v : undefined)
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Run the child handshake. Resolves once the parent completes `ctrl:handshake`.
|
|
77
|
-
* Rejects on timeout (no parent / not sandboxed) — callers should render a
|
|
78
|
-
* standalone fallback in that case.
|
|
79
|
-
*/
|
|
80
|
-
export function connectShell(timeoutMs = 5000): Promise<ShellSession> {
|
|
81
|
-
return new Promise((resolve, reject) => {
|
|
82
|
-
const parent = window.parent
|
|
83
|
-
if (!parent || parent === window) {
|
|
84
|
-
reject(new Error('not running inside a parent frame'))
|
|
85
|
-
return
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
let port: MessagePort | null = null
|
|
89
|
-
// Mutable token state — `tokenRefresh` updates these in place so later
|
|
90
|
-
// kernel calls read the fresh credential via `getToken()`.
|
|
91
|
-
let currentToken: string | undefined
|
|
92
|
-
let currentExpiresAt: number | undefined
|
|
93
|
-
const targetListeners = new Set<(nodeId: string | undefined) => void>()
|
|
94
|
-
|
|
95
|
-
const timer = setTimeout(() => {
|
|
96
|
-
cleanupWindow()
|
|
97
|
-
reject(new Error(`shell handshake timed out after ${timeoutMs}ms`))
|
|
98
|
-
}, timeoutMs)
|
|
99
|
-
|
|
100
|
-
function cleanupWindow() {
|
|
101
|
-
clearTimeout(timer)
|
|
102
|
-
window.removeEventListener('message', onWindowMessage)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Steady-state port listener: target hot-swaps + token refreshes.
|
|
106
|
-
function onPortMessage(ev: MessageEvent) {
|
|
107
|
-
const msg = ev.data as unknown
|
|
108
|
-
if (!isObj(msg)) return
|
|
109
|
-
|
|
110
|
-
// Token refresh: the parent pushed a fresh credential. Swap it in place.
|
|
111
|
-
if (msg.type === 'ctrl' && (msg as CtrlMessage).action === 'tokenRefresh') {
|
|
112
|
-
const d = (msg as CtrlMessage).data ?? {}
|
|
113
|
-
const next = asString(d.delegationToken)
|
|
114
|
-
if (next) currentToken = next
|
|
115
|
-
if (typeof d.tokenExpiresAt === 'number') currentExpiresAt = d.tokenExpiresAt
|
|
116
|
-
return
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Hot-swap: `setTarget` intent carries a new nodeId.
|
|
120
|
-
if (msg.type === 'intent') {
|
|
121
|
-
const intent = msg as IntentMessage
|
|
122
|
-
if (intent.envelope?.name === 'setTarget') {
|
|
123
|
-
const raw = intent.envelope.payload?.nodeId
|
|
124
|
-
const nodeId = asString(raw)
|
|
125
|
-
for (const cb of targetListeners) cb(nodeId)
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function onWindowMessage(ev: MessageEvent) {
|
|
131
|
-
const msg = ev.data as unknown
|
|
132
|
-
if (ev.source !== parent || !isObj(msg)) return
|
|
133
|
-
if (msg.type !== INIT_RESPONSE_TYPE) return
|
|
134
|
-
|
|
135
|
-
const received = ev.ports[0]
|
|
136
|
-
if (!received) return
|
|
137
|
-
port = received
|
|
138
|
-
|
|
139
|
-
port.onmessage = (portEv: MessageEvent) => {
|
|
140
|
-
const ctrl = portEv.data as unknown
|
|
141
|
-
if (!isObj(ctrl) || ctrl.type !== 'ctrl') {
|
|
142
|
-
onPortMessage(portEv)
|
|
143
|
-
return
|
|
144
|
-
}
|
|
145
|
-
const m = ctrl as CtrlMessage
|
|
146
|
-
if (m.action !== 'handshake' || !port) return
|
|
147
|
-
|
|
148
|
-
const d = (m.data ?? {}) as Record<string, unknown>
|
|
149
|
-
currentToken = asString(d.delegationToken)
|
|
150
|
-
currentExpiresAt = typeof d.tokenExpiresAt === 'number' ? d.tokenExpiresAt : undefined
|
|
151
|
-
const kernelUrl = asString(d.kernelUrl)
|
|
152
|
-
|
|
153
|
-
// Ack so the parent considers the child live.
|
|
154
|
-
port.postMessage({
|
|
155
|
-
type: 'ctrl',
|
|
156
|
-
version: 1,
|
|
157
|
-
action: 'handshakeAck',
|
|
158
|
-
data: { windowId: d.windowId },
|
|
159
|
-
})
|
|
160
|
-
// Switch the port to the steady-state listener (intents + tokenRefresh).
|
|
161
|
-
port.onmessage = onPortMessage
|
|
162
|
-
cleanupWindow()
|
|
163
|
-
|
|
164
|
-
resolve({
|
|
165
|
-
data: {
|
|
166
|
-
windowId: String(d.windowId ?? ''),
|
|
167
|
-
kernelUrl,
|
|
168
|
-
functionId: asString(d.functionId),
|
|
169
|
-
delegationToken: currentToken,
|
|
170
|
-
tokenExpiresAt: currentExpiresAt,
|
|
171
|
-
targetNodeId: asString(d.targetNodeId),
|
|
172
|
-
},
|
|
173
|
-
kernelUrl,
|
|
174
|
-
getToken: () => currentToken,
|
|
175
|
-
getTokenExpiresAt: () => currentExpiresAt,
|
|
176
|
-
onTargetChange(cb) {
|
|
177
|
-
targetListeners.add(cb)
|
|
178
|
-
return () => targetListeners.delete(cb)
|
|
179
|
-
},
|
|
180
|
-
dispose() {
|
|
181
|
-
cleanupWindow()
|
|
182
|
-
targetListeners.clear()
|
|
183
|
-
if (port) {
|
|
184
|
-
port.onmessage = null
|
|
185
|
-
port.close()
|
|
186
|
-
}
|
|
187
|
-
},
|
|
188
|
-
})
|
|
189
|
-
}
|
|
190
|
-
port.start()
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
window.addEventListener('message', onWindowMessage)
|
|
194
|
-
// targetOrigin '*' — the parent verifies our origin on its side.
|
|
195
|
-
parent.postMessage({ type: INIT_REQUEST_TYPE, version: 1 }, '*')
|
|
196
|
-
})
|
|
197
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
2
|
-
|
|
3
|
-
import { kernelCall, type KernelNode } from './kernel'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* The slice of the shell session this hook needs: where to call the kernel
|
|
7
|
-
* (`kernelUrl`) and how to read the LIVE delegation token (`getToken()` —
|
|
8
|
-
* which advances on `ctrl:tokenRefresh`). `ShellSession` satisfies this, and is
|
|
9
|
-
* a stable reference for the component's lifetime (set once on handshake).
|
|
10
|
-
*/
|
|
11
|
-
export type NodeSession = {
|
|
12
|
-
readonly kernelUrl: string | undefined
|
|
13
|
-
getToken(): string | undefined
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export type NodeState =
|
|
17
|
-
| { status: 'idle' }
|
|
18
|
-
| { status: 'loading' }
|
|
19
|
-
| { status: 'ok'; node: KernelNode }
|
|
20
|
-
| { status: 'error'; message: string }
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Fetch a node by id via `@<id>::get`, using the session's kernel URL + live
|
|
24
|
-
* delegation token. Re-fetches when the target node id changes (`setTarget`
|
|
25
|
-
* hot-swap) or when a token first becomes available. Stays `idle` until both a
|
|
26
|
-
* target node id and a token exist.
|
|
27
|
-
*
|
|
28
|
-
* The effect keys on the token's PRESENCE (`hasToken`), not its value: a
|
|
29
|
-
* `ctrl:tokenRefresh` swaps the token in place WITHOUT re-firing, and the next
|
|
30
|
-
* fetch (triggered by a `setTarget` or remount) reads the fresh token via
|
|
31
|
-
* `session.getToken()` at call time. `session` is stable, so listing it as a
|
|
32
|
-
* dep is safe — it never causes a re-fire on its own.
|
|
33
|
-
*/
|
|
34
|
-
export function useNode(session: NodeSession, nodeId: string | undefined): NodeState {
|
|
35
|
-
const hasToken = session.getToken() !== undefined
|
|
36
|
-
const [state, setState] = useState<NodeState>({ status: 'idle' })
|
|
37
|
-
|
|
38
|
-
useEffect(() => {
|
|
39
|
-
const token = session.getToken()
|
|
40
|
-
if (!nodeId || !session.kernelUrl || !token) {
|
|
41
|
-
setState({ status: 'idle' })
|
|
42
|
-
return
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
let cancelled = false
|
|
46
|
-
setState({ status: 'loading' })
|
|
47
|
-
kernelCall(session.kernelUrl, token, `@${nodeId}::get`, {})
|
|
48
|
-
.then((result) => {
|
|
49
|
-
if (!cancelled) setState({ status: 'ok', node: result as KernelNode })
|
|
50
|
-
})
|
|
51
|
-
.catch((err: unknown) => {
|
|
52
|
-
if (!cancelled) {
|
|
53
|
-
setState({
|
|
54
|
-
status: 'error',
|
|
55
|
-
message: err instanceof Error ? err.message : String(err),
|
|
56
|
-
})
|
|
57
|
-
}
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
return () => {
|
|
61
|
-
cancelled = true
|
|
62
|
-
}
|
|
63
|
-
}, [session, hasToken, nodeId])
|
|
64
|
-
|
|
65
|
-
return state
|
|
66
|
-
}
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
2
|
-
|
|
3
|
-
import { connectShell, type HandshakeData, type ShellSession } from './shell'
|
|
4
|
-
|
|
5
|
-
export type ShellStatus = 'loading' | 'ready' | 'standalone'
|
|
6
|
-
|
|
7
|
-
export type ShellState = {
|
|
8
|
-
status: ShellStatus
|
|
9
|
-
/** Handshake data once `ready` (kernelUrl, token, etc). */
|
|
10
|
-
data: HandshakeData | null
|
|
11
|
-
/**
|
|
12
|
-
* The live session once `ready` — exposes `getToken()` (which reflects
|
|
13
|
-
* `tokenRefresh`) and `kernelUrl`. The node-fetch hook reads through this so
|
|
14
|
-
* it always uses the CURRENT token, not the one captured at handshake time.
|
|
15
|
-
*/
|
|
16
|
-
session: ShellSession | null
|
|
17
|
-
/** Current target node id — updates on `setTarget` hot-swaps. */
|
|
18
|
-
nodeId: string | undefined
|
|
19
|
-
/** Reason we fell back to `standalone` (no parent / timeout). */
|
|
20
|
-
reason: string | null
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Runs the inline shell handshake once on mount and tracks the target node id.
|
|
25
|
-
*
|
|
26
|
-
* Three outcomes:
|
|
27
|
-
* - `loading` — handshake in flight
|
|
28
|
-
* - `ready` — parent completed the handshake; `data`/`session`/`nodeId`
|
|
29
|
-
* populated
|
|
30
|
-
* - `standalone` — no parent or it timed out (e.g. opened directly in a tab);
|
|
31
|
-
* the view renders a self-describing fallback
|
|
32
|
-
*/
|
|
33
|
-
export function useShell(): ShellState {
|
|
34
|
-
const [state, setState] = useState<ShellState>({
|
|
35
|
-
status: 'loading',
|
|
36
|
-
data: null,
|
|
37
|
-
session: null,
|
|
38
|
-
nodeId: undefined,
|
|
39
|
-
reason: null,
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
useEffect(() => {
|
|
43
|
-
let cancelled = false
|
|
44
|
-
let dispose: (() => void) | undefined
|
|
45
|
-
|
|
46
|
-
connectShell()
|
|
47
|
-
.then((session) => {
|
|
48
|
-
if (cancelled) {
|
|
49
|
-
session.dispose()
|
|
50
|
-
return
|
|
51
|
-
}
|
|
52
|
-
setState({
|
|
53
|
-
status: 'ready',
|
|
54
|
-
data: session.data,
|
|
55
|
-
session,
|
|
56
|
-
nodeId: session.data.targetNodeId,
|
|
57
|
-
reason: null,
|
|
58
|
-
})
|
|
59
|
-
const off = session.onTargetChange((nodeId) => {
|
|
60
|
-
setState((prev) => ({ ...prev, nodeId }))
|
|
61
|
-
})
|
|
62
|
-
dispose = () => {
|
|
63
|
-
off()
|
|
64
|
-
session.dispose()
|
|
65
|
-
}
|
|
66
|
-
})
|
|
67
|
-
.catch((err: unknown) => {
|
|
68
|
-
if (cancelled) return
|
|
69
|
-
setState({
|
|
70
|
-
status: 'standalone',
|
|
71
|
-
data: null,
|
|
72
|
-
session: null,
|
|
73
|
-
nodeId: undefined,
|
|
74
|
-
reason: err instanceof Error ? err.message : String(err),
|
|
75
|
-
})
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
return () => {
|
|
79
|
-
cancelled = true
|
|
80
|
-
dispose?.()
|
|
81
|
-
}
|
|
82
|
-
}, [])
|
|
83
|
-
|
|
84
|
-
return state
|
|
85
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Worker method wiring — the `methods` the codegen'd worker entry mounts. Thin
|
|
3
|
-
* adapters around the shared logic in `./note`: the only thing that lives here
|
|
4
|
-
* is the SDK typing and the `kernel` null-guard.
|
|
5
|
-
*
|
|
6
|
-
* - `createNote` is interface-hosted (static) → `remoteInterfaceMethods`.
|
|
7
|
-
* - `reference` is class-hosted (instance) → `remoteMethod` +
|
|
8
|
-
* `remoteClassMethods`.
|
|
9
|
-
* - `seed` is class-hosted (static) — the `postInstall` bootstrap.
|
|
10
|
-
*
|
|
11
|
-
* SDK-level `authorize` is an additive throw-to-deny check (returns void). For
|
|
12
|
-
* finer worker checks see `assertPerm` / `requireOwnership` from
|
|
13
|
-
* `@astrale-os/sdk`.
|
|
14
|
-
*/
|
|
15
|
-
import {
|
|
16
|
-
remoteClassMethods,
|
|
17
|
-
remoteInterfaceMethods,
|
|
18
|
-
remoteMethod,
|
|
19
|
-
type SchemaMethodsImpl,
|
|
20
|
-
} from '@astrale-os/sdk'
|
|
21
|
-
|
|
22
|
-
import type { Env } from '../env'
|
|
23
|
-
|
|
24
|
-
import { schema } from '../schema'
|
|
25
|
-
import {
|
|
26
|
-
createNote as createNoteLogic,
|
|
27
|
-
reference as referenceLogic,
|
|
28
|
-
seed as seedLogic,
|
|
29
|
-
} from './note'
|
|
30
|
-
|
|
31
|
-
const method = remoteMethod<Env>()
|
|
32
|
-
const interfaceMethods = remoteInterfaceMethods<Env>()
|
|
33
|
-
const classMethods = remoteClassMethods<Env>()
|
|
34
|
-
|
|
35
|
-
const NoteOpsMethods = interfaceMethods(schema, 'NoteOps', {
|
|
36
|
-
createNote: {
|
|
37
|
-
authorize: async () => undefined,
|
|
38
|
-
execute: ({ kernel, params }) => {
|
|
39
|
-
if (!kernel) throw new Error('createNote requires a kernel credential')
|
|
40
|
-
return createNoteLogic(kernel, params)
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
const reference = method(schema, 'Note', 'reference', {
|
|
46
|
-
authorize: async () => undefined,
|
|
47
|
-
execute: ({ kernel, self, params }) => {
|
|
48
|
-
if (!kernel) throw new Error('reference requires a kernel credential')
|
|
49
|
-
return referenceLogic(kernel, self.path.raw, params)
|
|
50
|
-
},
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
const seed = method(schema, 'Note', 'seed', {
|
|
54
|
-
authorize: async () => undefined,
|
|
55
|
-
execute: ({ kernel }) => {
|
|
56
|
-
if (!kernel) throw new Error('seed requires a kernel credential')
|
|
57
|
-
return seedLogic(kernel)
|
|
58
|
-
},
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
const NoteMethods = classMethods(schema, 'Note', { reference, seed })
|
|
62
|
-
|
|
63
|
-
export const methods: SchemaMethodsImpl<typeof schema, Env> = {
|
|
64
|
-
interface: { NoteOps: NoteOpsMethods },
|
|
65
|
-
class: { Note: NoteMethods },
|
|
66
|
-
}
|
package/template/methods/note.ts
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Note context — method LOGIC, written once, transport-agnostic.
|
|
3
|
-
*
|
|
4
|
-
* Each handler touches the kernel ONLY through `kernel.call(...)` (the universal
|
|
5
|
-
* syscalls) plus its `params` and, for instance methods, the source node's
|
|
6
|
-
* `path.raw`. Address callables/edges with layout-independent forms — a
|
|
7
|
-
* `ClassPath` for the edge class, the `<id>::link` instance form, and an
|
|
8
|
-
* `@<id>` id-form target. Reserve an absolute path string for a node *location*.
|
|
9
|
-
*/
|
|
10
|
-
import { K } from '@astrale-os/kernel-core'
|
|
11
|
-
|
|
12
|
-
import { D } from '../schema/compiled'
|
|
13
|
-
|
|
14
|
-
/** The minimal kernel surface the worker runtime exposes to a handler. */
|
|
15
|
-
export type CallableKernel = { call(path: string, params: unknown): Promise<unknown> }
|
|
16
|
-
|
|
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
|
-
/** Notes live under a `/notes` folder at the graph root (created by `seed`). */
|
|
25
|
-
const NOTES_PARENT = '/notes'
|
|
26
|
-
|
|
27
|
-
/** URL-safe slug + short random suffix to dodge same-ms collisions. */
|
|
28
|
-
function slugify(text: string): string {
|
|
29
|
-
const stem =
|
|
30
|
-
text
|
|
31
|
-
.toLowerCase()
|
|
32
|
-
.normalize('NFD')
|
|
33
|
-
.replace(/[̀-ͯ]/g, '')
|
|
34
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
35
|
-
.replace(/^-|-$/g, '')
|
|
36
|
-
.slice(0, 40) || 'note'
|
|
37
|
-
return `${stem}-${Date.now().toString(36).slice(-4)}${Math.random().toString(36).slice(2, 6)}`
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** `NoteOps.createNote` (static) — create a Note under `/notes`. */
|
|
41
|
-
export async function createNote(
|
|
42
|
-
kernel: CallableKernel,
|
|
43
|
-
params: { title: string; body: string },
|
|
44
|
-
): Promise<{ id: string; path: string }> {
|
|
45
|
-
const path = `${NOTES_PARENT}/${slugify(params.title)}`
|
|
46
|
-
const created = (await kernel.call(NODE_CREATE, {
|
|
47
|
-
class: NOTE_CLASS,
|
|
48
|
-
path,
|
|
49
|
-
props: { [NAME_KEY]: params.title, [TITLE_KEY]: params.title, [BODY_KEY]: params.body },
|
|
50
|
-
})) as { id: string }
|
|
51
|
-
return { id: created.id, path }
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** `Note.reference` (instance) — link this Note to another via `references`. */
|
|
55
|
-
export async function reference(
|
|
56
|
-
kernel: CallableKernel,
|
|
57
|
-
selfPathRaw: string,
|
|
58
|
-
params: { target: string },
|
|
59
|
-
): Promise<{ linked: string }> {
|
|
60
|
-
await kernel.call(`${selfPathRaw}::link`, {
|
|
61
|
-
edgeClass: REFERENCES_EDGE,
|
|
62
|
-
target: params.target,
|
|
63
|
-
})
|
|
64
|
-
return { linked: params.target }
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const FOLDER_CLASS = K.Folder.path.class.raw
|
|
68
|
-
|
|
69
|
-
const STARTERS: ReadonlyArray<{ slug: string; title: string; body: string }> = [
|
|
70
|
-
{
|
|
71
|
-
slug: 'welcome',
|
|
72
|
-
title: 'Welcome',
|
|
73
|
-
body: 'This note was created by `seed` after install. Edit it freely.',
|
|
74
|
-
},
|
|
75
|
-
{ slug: 'getting-started', title: 'Getting started', body: 'Call `createNote` to add your own.' },
|
|
76
|
-
]
|
|
77
|
-
|
|
78
|
-
function isPathConflict(e: unknown): boolean {
|
|
79
|
-
return e instanceof Error && e.message.includes('PATH_CONFLICT')
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* `Note.seed` (static) — the domain's post-install bootstrap. The kernel calls
|
|
84
|
-
* it ONCE after install, as __SYSTEM__ (see `postInstall` in
|
|
85
|
-
* `astrale.config.ts`), so the domain can lay down its initial state and
|
|
86
|
-
* grants: the `/notes` folder, a couple of starter Notes, and a `references`
|
|
87
|
-
* edge between them. Idempotent: a re-run swallows `PATH_CONFLICT` per node.
|
|
88
|
-
*/
|
|
89
|
-
export async function seed(kernel: CallableKernel): Promise<{ seeded: number }> {
|
|
90
|
-
// 1. The `/notes` folder at the graph root.
|
|
91
|
-
try {
|
|
92
|
-
await kernel.call(NODE_CREATE, {
|
|
93
|
-
class: FOLDER_CLASS,
|
|
94
|
-
path: NOTES_PARENT,
|
|
95
|
-
props: { [NAME_KEY]: 'notes' },
|
|
96
|
-
})
|
|
97
|
-
} catch (e) {
|
|
98
|
-
if (!isPathConflict(e)) throw e
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// 2. A couple of starter Notes under it.
|
|
102
|
-
const ids: Record<string, string> = {}
|
|
103
|
-
let seeded = 0
|
|
104
|
-
for (const s of STARTERS) {
|
|
105
|
-
try {
|
|
106
|
-
const created = (await kernel.call(NODE_CREATE, {
|
|
107
|
-
class: NOTE_CLASS,
|
|
108
|
-
path: `${NOTES_PARENT}/${s.slug}`,
|
|
109
|
-
props: { [NAME_KEY]: s.title, [TITLE_KEY]: s.title, [BODY_KEY]: s.body },
|
|
110
|
-
})) as { id: string }
|
|
111
|
-
ids[s.slug] = created.id
|
|
112
|
-
seeded++
|
|
113
|
-
} catch (e) {
|
|
114
|
-
if (!isPathConflict(e)) throw e
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// 3. Link welcome → getting-started with a `references` edge (id-form on
|
|
119
|
-
// both sides — layout-independent). Best-effort.
|
|
120
|
-
const from = ids.welcome
|
|
121
|
-
const to = ids['getting-started']
|
|
122
|
-
if (from && to) {
|
|
123
|
-
try {
|
|
124
|
-
await kernel.call(`@${from}::link`, { edgeClass: REFERENCES_EDGE, target: `@${to}` })
|
|
125
|
-
} catch (e) {
|
|
126
|
-
if (!isPathConflict(e)) throw e
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return { seeded }
|
|
131
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Compiled view of the schema — resolved class/interface paths + prop keys.
|
|
3
|
-
* `compileDomain` is pure (no env), so this is safe to import from the worker
|
|
4
|
-
* and from build tooling alike.
|
|
5
|
-
*
|
|
6
|
-
* - `D.Note.path.class.raw` → the Note ClassPath, graph-facing form
|
|
7
|
-
* - `D.references.path.class.raw` → the references edge ClassPath
|
|
8
|
-
* - `D.Note.title.key` → the qualified prop key for `title`
|
|
9
|
-
*/
|
|
10
|
-
import { compileDomain, type Domain } from '@astrale-os/kernel-core/domain'
|
|
11
|
-
|
|
12
|
-
import { schema } from './index'
|
|
13
|
-
|
|
14
|
-
export const D: Domain<typeof schema> = compileDomain(schema)
|
package/template/schema/note.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Note 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 `NoteOps` interface, the `Note`
|
|
6
|
-
* class that implements it, and the `references` edge from one Note to
|
|
7
|
-
* another. To grow the domain, add `schema/<context>.ts` and register its
|
|
8
|
-
* members in `schema/index.ts`.
|
|
9
|
-
*
|
|
10
|
-
* - Interface `NoteOps` one static op, `createNote`. Static → the impl
|
|
11
|
-
* gets no `self`; it creates a brand-new Note.
|
|
12
|
-
* - Class `Note` implements `[NoteOps, Container]`, inheriting
|
|
13
|
-
* `createNote` and adding the instance method
|
|
14
|
-
* `reference` (links this Note to another).
|
|
15
|
-
* - Edge `references` Note → Note. Materialized at runtime by
|
|
16
|
-
* `reference` (and by `seed`).
|
|
17
|
-
*/
|
|
18
|
-
import { edgeClass, KernelSchema, nodeClass, nodeInterface } from '@astrale-os/kernel-core'
|
|
19
|
-
import { fn } from '@astrale-os/kernel-dsl'
|
|
20
|
-
import { z } from 'zod'
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Thin ref to a created node — what node-creating ops return. A remote method
|
|
24
|
-
* returns a plain `{ id, path }`, never `ref(SELF)` (whose full-Node value
|
|
25
|
-
* does not round-trip over the worker wire).
|
|
26
|
-
*/
|
|
27
|
-
export const NoteRef = z.object({ id: z.string(), path: z.string() })
|
|
28
|
-
|
|
29
|
-
export const NoteOps = nodeInterface({
|
|
30
|
-
methods: {
|
|
31
|
-
createNote: fn({
|
|
32
|
-
static: true,
|
|
33
|
-
params: { title: z.string(), body: z.string() },
|
|
34
|
-
returns: NoteRef,
|
|
35
|
-
}),
|
|
36
|
-
},
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
export const Note = nodeClass({
|
|
40
|
-
implements: [NoteOps, KernelSchema.interfaces.Container],
|
|
41
|
-
props: {
|
|
42
|
-
title: z.string(),
|
|
43
|
-
body: z.string(),
|
|
44
|
-
},
|
|
45
|
-
methods: {
|
|
46
|
-
reference: fn({
|
|
47
|
-
params: { target: z.string() },
|
|
48
|
-
returns: z.object({ linked: z.string() }),
|
|
49
|
-
}),
|
|
50
|
-
// Post-install bootstrap (wired as `postInstall` in astrale.config.ts).
|
|
51
|
-
// Static: the kernel calls it ONCE after install, as __SYSTEM__, with no
|
|
52
|
-
// `self`. Must stay idempotent — a re-install runs it again.
|
|
53
|
-
seed: fn({
|
|
54
|
-
static: true,
|
|
55
|
-
returns: z.object({ seeded: z.number().int() }),
|
|
56
|
-
}),
|
|
57
|
-
},
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
export const references = edgeClass(
|
|
61
|
-
{ as: 'from_note', types: [Note] },
|
|
62
|
-
{ as: 'to_note', types: [Note] },
|
|
63
|
-
{ props: { reason: z.string().optional() } },
|
|
64
|
-
)
|
package/template/views/note.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `ui-note` — 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 `<serving url>/ui/note`
|
|
5
|
-
* — the SDK stamps it from the worker's live URL when it builds the install
|
|
6
|
-
* bundle. The Cloudflare adapter serves `/ui/*` from `../dist-client` (built by
|
|
7
|
-
* `client/` with base `/ui/`) via the Worker's `ASSETS` binding.
|
|
8
|
-
*
|
|
9
|
-
* `viewFor: selfOf(Note)` attaches a `view_for` edge to the `Note` class
|
|
10
|
-
* meta-node, so the GUI offers this view for any Note instance.
|
|
11
|
-
*/
|
|
12
|
-
import { selfOf } from '@astrale-os/kernel-dsl'
|
|
13
|
-
import { defineView } from '@astrale-os/sdk'
|
|
14
|
-
|
|
15
|
-
import { Note } from '../schema/note'
|
|
16
|
-
|
|
17
|
-
export const note = defineView({
|
|
18
|
-
auth: 'public',
|
|
19
|
-
mount: '/ui/note',
|
|
20
|
-
viewFor: selfOf(Note),
|
|
21
|
-
})
|