@unicity-astrid/sdk 0.1.0
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/README.md +120 -0
- package/dist/approval.d.ts +23 -0
- package/dist/approval.d.ts.map +1 -0
- package/dist/approval.js +29 -0
- package/dist/approval.js.map +1 -0
- package/dist/capabilities.d.ts +14 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +19 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/capsule.d.ts +39 -0
- package/dist/capsule.d.ts.map +1 -0
- package/dist/capsule.js +67 -0
- package/dist/capsule.js.map +1 -0
- package/dist/contracts.d.ts +1104 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +4 -0
- package/dist/contracts.js.map +1 -0
- package/dist/elicit.d.ts +30 -0
- package/dist/elicit.d.ts.map +1 -0
- package/dist/elicit.js +103 -0
- package/dist/elicit.js.map +1 -0
- package/dist/env.d.ts +19 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +27 -0
- package/dist/env.js.map +1 -0
- package/dist/errors.d.ts +46 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +108 -0
- package/dist/errors.js.map +1 -0
- package/dist/fs.d.ts +135 -0
- package/dist/fs.d.ts.map +1 -0
- package/dist/fs.js +257 -0
- package/dist/fs.js.map +1 -0
- package/dist/http.d.ts +90 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +276 -0
- package/dist/http.js.map +1 -0
- package/dist/identity.d.ts +46 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +69 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/interceptors.d.ts +21 -0
- package/dist/interceptors.d.ts.map +1 -0
- package/dist/interceptors.js +22 -0
- package/dist/interceptors.js.map +1 -0
- package/dist/ipc.d.ts +143 -0
- package/dist/ipc.d.ts.map +1 -0
- package/dist/ipc.js +261 -0
- package/dist/ipc.js.map +1 -0
- package/dist/kv.d.ts +45 -0
- package/dist/kv.d.ts.map +1 -0
- package/dist/kv.js +91 -0
- package/dist/kv.js.map +1 -0
- package/dist/log.d.ts +17 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +40 -0
- package/dist/log.js.map +1 -0
- package/dist/net.d.ts +154 -0
- package/dist/net.d.ts.map +1 -0
- package/dist/net.js +421 -0
- package/dist/net.js.map +1 -0
- package/dist/process.d.ts +77 -0
- package/dist/process.d.ts.map +1 -0
- package/dist/process.js +128 -0
- package/dist/process.js.map +1 -0
- package/dist/runtime/bridge.d.ts +34 -0
- package/dist/runtime/bridge.d.ts.map +1 -0
- package/dist/runtime/bridge.js +326 -0
- package/dist/runtime/bridge.js.map +1 -0
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +3 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/registry.d.ts +58 -0
- package/dist/runtime/registry.d.ts.map +1 -0
- package/dist/runtime/registry.js +129 -0
- package/dist/runtime/registry.js.map +1 -0
- package/dist/runtime.d.ts +36 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +50 -0
- package/dist/runtime.js.map +1 -0
- package/dist/time.d.ts +29 -0
- package/dist/time.d.ts.map +1 -0
- package/dist/time.js +43 -0
- package/dist/time.js.map +1 -0
- package/dist/tool.d.ts +48 -0
- package/dist/tool.d.ts.map +1 -0
- package/dist/tool.js +86 -0
- package/dist/tool.js.map +1 -0
- package/dist/uplink.d.ts +27 -0
- package/dist/uplink.d.ts.map +1 -0
- package/dist/uplink.js +36 -0
- package/dist/uplink.js.map +1 -0
- package/package.json +38 -0
- package/src/approval.ts +38 -0
- package/src/capabilities.ts +22 -0
- package/src/capsule.ts +90 -0
- package/src/contracts.ts +1189 -0
- package/src/elicit.ts +136 -0
- package/src/env.ts +31 -0
- package/src/errors.ts +122 -0
- package/src/fs.ts +357 -0
- package/src/http.ts +345 -0
- package/src/identity.ts +101 -0
- package/src/index.ts +83 -0
- package/src/interceptors.ts +25 -0
- package/src/ipc.ts +354 -0
- package/src/kv.ts +123 -0
- package/src/log.ts +43 -0
- package/src/net.ts +545 -0
- package/src/process.ts +205 -0
- package/src/runtime/bridge.ts +374 -0
- package/src/runtime/index.ts +11 -0
- package/src/runtime/registry.ts +178 -0
- package/src/runtime.ts +70 -0
- package/src/time.ts +48 -0
- package/src/tool.ts +125 -0
- package/src/uplink.ts +49 -0
- package/src/wit-imports.d.ts +689 -0
- package/wit-contracts/astrid-contracts.wit +1266 -0
package/src/ipc.ts
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IPC event bus. Mirrors `astrid_sdk::ipc`.
|
|
3
|
+
*
|
|
4
|
+
* Publish: {@link publish} / {@link publishJson} for the calling capsule's
|
|
5
|
+
* own principal, {@link publishAs} / {@link publishJsonAs} for uplinks
|
|
6
|
+
* asserting an end-user principal (requires `uplink = true` capability —
|
|
7
|
+
* subscribers see the principal as `claimed`, not `verified`).
|
|
8
|
+
*
|
|
9
|
+
* Subscribe: {@link subscribe} returns a {@link Subscription} resource handle.
|
|
10
|
+
* Resources are Component Model objects with a drop step; we surface
|
|
11
|
+
* `Symbol.dispose` so `using sub = ipc.subscribe(...)` cleans up automatically
|
|
12
|
+
* on scope exit, and an explicit `.close()` for codebases that haven't moved
|
|
13
|
+
* to the explicit-resource-management proposal. AsyncIterable convenience
|
|
14
|
+
* preserved from the pre-migration API.
|
|
15
|
+
*
|
|
16
|
+
* Request/response: {@link requestResponse} mirrors `astrid_sdk::ipc::request_response`.
|
|
17
|
+
* Validates the request payload, pre-subscribes to the reply topic, publishes
|
|
18
|
+
* with an auto-injected `correlation_id`, blocks up to `timeoutMs` for the
|
|
19
|
+
* single reply, always tears down the subscription.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
publish as hostPublish,
|
|
24
|
+
publishAs as hostPublishAs,
|
|
25
|
+
subscribe as hostSubscribe,
|
|
26
|
+
getInterceptorBindings as hostGetInterceptorBindings,
|
|
27
|
+
type IpcEnvelope,
|
|
28
|
+
type IpcMessage as WitIpcMessage,
|
|
29
|
+
type InterceptorBinding as WitInterceptorBinding,
|
|
30
|
+
type PrincipalAttribution,
|
|
31
|
+
type Subscription as WitSubscription,
|
|
32
|
+
} from "astrid:ipc/host@1.0.0";
|
|
33
|
+
import { randomBytes as hostRandomBytes } from "astrid:sys/host@1.0.0";
|
|
34
|
+
import { SysError, callHost } from "./errors.js";
|
|
35
|
+
|
|
36
|
+
/** A single IPC message dispatched to a subscriber. */
|
|
37
|
+
export interface IpcMessage {
|
|
38
|
+
topic: string;
|
|
39
|
+
payload: string;
|
|
40
|
+
/** UUID of the capsule that published this message. */
|
|
41
|
+
sourceId: string;
|
|
42
|
+
/**
|
|
43
|
+
* Principal attributed to the publisher. `verified(...)` for kernel-attributed
|
|
44
|
+
* principals, `claimed(...)` for uplink-asserted principals (NOT kernel-
|
|
45
|
+
* verified), `system` for kernel-originated events.
|
|
46
|
+
*
|
|
47
|
+
* Subscribers MUST check this variant on sensitive actions. Multi-message
|
|
48
|
+
* batches MUST be read per-message rather than relying on `runtime.caller()`
|
|
49
|
+
* (which only reflects the first message's publisher).
|
|
50
|
+
*/
|
|
51
|
+
principal: PrincipalAttribution;
|
|
52
|
+
/** Convenience: parse `payload` as JSON. Throws SysError.json on failure. */
|
|
53
|
+
json<T = unknown>(): T;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type { PrincipalAttribution } from "astrid:ipc/host@1.0.0";
|
|
57
|
+
|
|
58
|
+
export interface PollResult {
|
|
59
|
+
messages: IpcMessage[];
|
|
60
|
+
/** Messages dropped due to buffer overflow since the previous poll. */
|
|
61
|
+
dropped: bigint;
|
|
62
|
+
/** Cumulative lag — total messages missed since subscription opened. */
|
|
63
|
+
lagged: bigint;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface InterceptorBinding {
|
|
67
|
+
/** Subscription handle ID. */
|
|
68
|
+
handle: bigint;
|
|
69
|
+
/** Hook action name the kernel dispatches when a message matches. */
|
|
70
|
+
action: string;
|
|
71
|
+
/** Topic pattern this subscription was registered for. */
|
|
72
|
+
topic: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const DEFAULT_RECV_TIMEOUT_MS = 5_000n;
|
|
76
|
+
|
|
77
|
+
export function publish(topic: string, payload: string): void {
|
|
78
|
+
callHost(`ipc.publish(${quote(topic)})`, () => hostPublish(topic, payload));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function publishJson<T>(topic: string, payload: T): void {
|
|
82
|
+
publish(topic, jsonify(`ipc.publishJson(${quote(topic)})`, payload));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Publish on behalf of a specific principal. Requires `uplink = true` in
|
|
87
|
+
* `Capsule.toml [capabilities]`; non-uplinks see `capability-denied`.
|
|
88
|
+
* Subscribers see the principal as `claimed(...)`, NOT `verified(...)` —
|
|
89
|
+
* downstream consumers MUST treat the principal as caller-input, not
|
|
90
|
+
* authenticated context.
|
|
91
|
+
*/
|
|
92
|
+
export function publishAs(topic: string, payload: string, principal: string): void {
|
|
93
|
+
callHost(`ipc.publishAs(${quote(topic)})`, () =>
|
|
94
|
+
hostPublishAs(topic, payload, principal),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function publishJsonAs<T>(topic: string, payload: T, principal: string): void {
|
|
99
|
+
publishAs(topic, jsonify(`ipc.publishJsonAs(${quote(topic)})`, payload), principal);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Subscribe to an IPC topic pattern. Supports exact matches and trailing-suffix
|
|
104
|
+
* wildcards (`foo.bar.*`). Mid-segment wildcards are rejected by the host.
|
|
105
|
+
*
|
|
106
|
+
* The returned {@link Subscription} is a Resource handle. Use `using` for
|
|
107
|
+
* scope-bound cleanup, or call `.close()` explicitly:
|
|
108
|
+
*
|
|
109
|
+
* ```ts
|
|
110
|
+
* using sub = ipc.subscribe("foo.bar"); // disposed at scope exit
|
|
111
|
+
* for await (const msg of sub) { ... }
|
|
112
|
+
*
|
|
113
|
+
* const sub = ipc.subscribe("foo.bar"); // explicit close
|
|
114
|
+
* try { ... } finally { sub.close(); }
|
|
115
|
+
* ```
|
|
116
|
+
*
|
|
117
|
+
* Per-capsule cap: 128 subscriptions.
|
|
118
|
+
*/
|
|
119
|
+
export function subscribe(topicPattern: string): Subscription {
|
|
120
|
+
const inner = callHost(`ipc.subscribe(${quote(topicPattern)})`, () =>
|
|
121
|
+
hostSubscribe(topicPattern),
|
|
122
|
+
);
|
|
123
|
+
return new Subscription(inner, topicPattern);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Pre-registered interceptor handles for run-loop capsules. Returns ONLY the
|
|
128
|
+
* calling capsule's own interceptors. Most authors don't call this — the
|
|
129
|
+
* `@interceptor` decorator + bridge handle dispatch.
|
|
130
|
+
*/
|
|
131
|
+
export function runtimeInterceptors(): InterceptorBinding[] {
|
|
132
|
+
const handles: WitInterceptorBinding[] = callHost("ipc.runtimeInterceptors", () =>
|
|
133
|
+
hostGetInterceptorBindings(),
|
|
134
|
+
);
|
|
135
|
+
return handles.map((h) => ({ handle: h.handleId, action: h.action, topic: h.topic }));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export class Subscription {
|
|
139
|
+
readonly topic: string;
|
|
140
|
+
#inner: WitSubscription | undefined;
|
|
141
|
+
|
|
142
|
+
constructor(inner: WitSubscription, topic: string) {
|
|
143
|
+
this.#inner = inner;
|
|
144
|
+
this.topic = topic;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Non-blocking poll. Returns whatever's already queued. */
|
|
148
|
+
poll(): PollResult {
|
|
149
|
+
const env = callHost(`ipc.poll(${quote(this.topic)})`, () => this.#requireInner().poll());
|
|
150
|
+
return envelopeToPollResult(env);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Blocking receive (timeout capped at 60s by the host). */
|
|
154
|
+
recv(timeoutMs: bigint = DEFAULT_RECV_TIMEOUT_MS): PollResult {
|
|
155
|
+
const env = callHost(`ipc.recv(${quote(this.topic)})`, () =>
|
|
156
|
+
this.#requireInner().recv(timeoutMs),
|
|
157
|
+
);
|
|
158
|
+
return envelopeToPollResult(env);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Idempotent — closing an already-closed subscription is a no-op.
|
|
163
|
+
* Equivalent to the resource Drop. Prefer `using` when the surrounding
|
|
164
|
+
* code can adopt explicit resource management.
|
|
165
|
+
*/
|
|
166
|
+
close(): void {
|
|
167
|
+
if (this.#inner === undefined) return;
|
|
168
|
+
const inner = this.#inner;
|
|
169
|
+
this.#inner = undefined;
|
|
170
|
+
try {
|
|
171
|
+
inner[Symbol.dispose]();
|
|
172
|
+
} catch {
|
|
173
|
+
// Resource may already be released (interceptor-owned handles, etc.).
|
|
174
|
+
// close() is meant to be safe to call from any cleanup path.
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
[Symbol.dispose](): void {
|
|
179
|
+
this.close();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* AsyncIterable convenience. Loops calling `.recv()` and yielding each
|
|
184
|
+
* message. Stops when the subscription is closed. Drops `lagged`/`dropped`
|
|
185
|
+
* info — use `.poll()`/`.recv()` explicitly if you need to react to lag.
|
|
186
|
+
*/
|
|
187
|
+
async *[Symbol.asyncIterator](): AsyncIterableIterator<IpcMessage> {
|
|
188
|
+
while (this.#inner !== undefined) {
|
|
189
|
+
const batch = this.recv();
|
|
190
|
+
for (const msg of batch.messages) {
|
|
191
|
+
yield msg;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
#requireInner(): WitSubscription {
|
|
197
|
+
if (this.#inner === undefined) {
|
|
198
|
+
throw SysError.api(`subscription on ${quote(this.topic)} is closed`);
|
|
199
|
+
}
|
|
200
|
+
return this.#inner;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Send a request on `requestTopic` and await a single response on a scoped
|
|
206
|
+
* reply topic. Mirrors `astrid_sdk::ipc::request_response` exactly.
|
|
207
|
+
*
|
|
208
|
+
* The helper:
|
|
209
|
+
* 1. Generates a v4 correlation ID.
|
|
210
|
+
* 2. Subscribes to `{responseNamespace}.{correlationId}` *before* publishing
|
|
211
|
+
* (so the response can never be missed in the race).
|
|
212
|
+
* 3. Injects the correlation ID into the request payload as a top-level
|
|
213
|
+
* `correlation_id` field.
|
|
214
|
+
* 4. Publishes the request.
|
|
215
|
+
* 5. Blocks up to `timeoutMs` for the response.
|
|
216
|
+
* 6. Unsubscribes (always, even on error).
|
|
217
|
+
* 7. Returns the parsed response payload as `Resp`.
|
|
218
|
+
*
|
|
219
|
+
* `request` must serialize to a JSON object. Primitives, arrays, strings,
|
|
220
|
+
* etc. are rejected synchronously with `SysError.api` because there is
|
|
221
|
+
* nowhere to put the correlation ID.
|
|
222
|
+
*
|
|
223
|
+
* `responseNamespace` should be the dotted topic prefix the responder
|
|
224
|
+
* publishes to, *without* the trailing correlation id segment. For example,
|
|
225
|
+
* if the responder publishes to
|
|
226
|
+
* `registry.v1.response.set_active_model.<corr_id>`, pass
|
|
227
|
+
* `"registry.v1.response.set_active_model"`.
|
|
228
|
+
*
|
|
229
|
+
* `timeoutMs` is capped at 60,000 ms by the host. A timeout throws
|
|
230
|
+
* `SysError.api` with `request_response: no reply within …`.
|
|
231
|
+
*/
|
|
232
|
+
export function requestResponse<Req, Resp = unknown>(
|
|
233
|
+
requestTopic: string,
|
|
234
|
+
responseNamespace: string,
|
|
235
|
+
request: Req,
|
|
236
|
+
timeoutMs: number | bigint,
|
|
237
|
+
): Resp {
|
|
238
|
+
// Validate input before touching the host so bad calls never allocate a
|
|
239
|
+
// subscription handle.
|
|
240
|
+
if (typeof request !== "object" || request === null || Array.isArray(request)) {
|
|
241
|
+
throw SysError.api(
|
|
242
|
+
"request_response: request payload must serialize to a JSON object so the " +
|
|
243
|
+
"correlation_id can be injected",
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const correlationId = randomUuidV4();
|
|
248
|
+
// Defensive copy + injection. Don't mutate the caller's object.
|
|
249
|
+
const augmented: Record<string, unknown> = {
|
|
250
|
+
...(request as Record<string, unknown>),
|
|
251
|
+
correlation_id: correlationId,
|
|
252
|
+
};
|
|
253
|
+
const payload = jsonify("requestResponse", augmented);
|
|
254
|
+
const replyTopic = `${responseNamespace}.${correlationId}`;
|
|
255
|
+
|
|
256
|
+
const sub = subscribe(replyTopic);
|
|
257
|
+
try {
|
|
258
|
+
publish(requestTopic, payload);
|
|
259
|
+
const timeoutBig =
|
|
260
|
+
typeof timeoutMs === "bigint" ? timeoutMs : BigInt(Math.max(0, Math.floor(timeoutMs)));
|
|
261
|
+
const poll = sub.recv(timeoutBig);
|
|
262
|
+
const msg = poll.messages[0];
|
|
263
|
+
if (msg === undefined) {
|
|
264
|
+
throw SysError.api(
|
|
265
|
+
`request_response: no reply on '${replyTopic}' within ${String(timeoutMs)}ms`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
return JSON.parse(msg.payload) as Resp;
|
|
270
|
+
} catch (err) {
|
|
271
|
+
throw SysError.json(
|
|
272
|
+
`request_response: failed to parse reply on '${replyTopic}': ${(err as Error).message}`,
|
|
273
|
+
err,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
} finally {
|
|
277
|
+
sub.close();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function envelopeToPollResult(env: IpcEnvelope): PollResult {
|
|
282
|
+
return {
|
|
283
|
+
messages: env.messages.map(makeIpcMessage),
|
|
284
|
+
dropped: env.dropped,
|
|
285
|
+
lagged: env.lagged,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function makeIpcMessage(m: WitIpcMessage): IpcMessage {
|
|
290
|
+
return {
|
|
291
|
+
topic: m.topic,
|
|
292
|
+
payload: m.payload,
|
|
293
|
+
sourceId: m.sourceId,
|
|
294
|
+
principal: m.principal,
|
|
295
|
+
json<T = unknown>(): T {
|
|
296
|
+
try {
|
|
297
|
+
return JSON.parse(m.payload) as T;
|
|
298
|
+
} catch (err) {
|
|
299
|
+
throw SysError.json(
|
|
300
|
+
`IpcMessage.json() on topic ${quote(m.topic)}: ${(err as Error).message}`,
|
|
301
|
+
err,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function jsonify(label: string, value: unknown): string {
|
|
309
|
+
try {
|
|
310
|
+
return JSON.stringify(value);
|
|
311
|
+
} catch (err) {
|
|
312
|
+
throw SysError.json(`${label}: ${(err as Error).message}`, err);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function quote(s: string): string {
|
|
317
|
+
return `"${s.replace(/"/g, '\\"')}"`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* UUIDv4 generator. The Astrid host exposes `randomBytes`; pulling from
|
|
322
|
+
* `globalThis.crypto` would work too (StarlingMonkey exposes `crypto`), but
|
|
323
|
+
* routing through `sys::randomBytes` keeps the audit trail on the host side.
|
|
324
|
+
*
|
|
325
|
+
* Lazy-loaded to avoid a load-order cycle between ipc.ts and sys.ts.
|
|
326
|
+
*/
|
|
327
|
+
function randomUuidV4(): string {
|
|
328
|
+
// 16 random bytes → format as 8-4-4-4-12 hex with version (0x40) and
|
|
329
|
+
// variant (0x80) bits set. RFC 4122 §4.4.
|
|
330
|
+
const bytes = getRandomBytes();
|
|
331
|
+
bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40;
|
|
332
|
+
bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;
|
|
333
|
+
const hex: string[] = [];
|
|
334
|
+
for (let i = 0; i < 16; i++) {
|
|
335
|
+
hex.push((bytes[i] ?? 0).toString(16).padStart(2, "0"));
|
|
336
|
+
}
|
|
337
|
+
return (
|
|
338
|
+
hex.slice(0, 4).join("") +
|
|
339
|
+
"-" +
|
|
340
|
+
hex.slice(4, 6).join("") +
|
|
341
|
+
"-" +
|
|
342
|
+
hex.slice(6, 8).join("") +
|
|
343
|
+
"-" +
|
|
344
|
+
hex.slice(8, 10).join("") +
|
|
345
|
+
"-" +
|
|
346
|
+
hex.slice(10, 16).join("")
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function getRandomBytes(): Uint8Array {
|
|
351
|
+
// Audit-traced via sys::random-bytes rather than reaching for globalThis.crypto
|
|
352
|
+
// so every UUID generation flows through the kernel's principal-scoped audit.
|
|
353
|
+
return hostRandomBytes(16n);
|
|
354
|
+
}
|
package/src/kv.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Key-value persistent storage. Mirrors `astrid_sdk::kv` semantics.
|
|
3
|
+
*
|
|
4
|
+
* Keys are scoped per-(principal, capsule). Each capsule sees an isolated
|
|
5
|
+
* namespace. Keys are UTF-8 NFC strings (max 256 bytes); values are arbitrary
|
|
6
|
+
* bytes (max 1 MiB per value). Per-(principal, capsule) cumulative quota is
|
|
7
|
+
* bounded; exceeding it returns `quota` from the host.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
kvGet as hostGet,
|
|
12
|
+
kvSet as hostSet,
|
|
13
|
+
kvDelete as hostDelete,
|
|
14
|
+
kvListKeys as hostListKeys,
|
|
15
|
+
kvListKeysPage as hostListKeysPage,
|
|
16
|
+
kvClearPrefix as hostClearPrefix,
|
|
17
|
+
kvCas as hostCas,
|
|
18
|
+
} from "astrid:kv/host@1.0.0";
|
|
19
|
+
import { SysError, callHost } from "./errors.js";
|
|
20
|
+
|
|
21
|
+
const encoder = new TextEncoder();
|
|
22
|
+
const decoder = new TextDecoder();
|
|
23
|
+
|
|
24
|
+
export interface KeyPage {
|
|
25
|
+
keys: string[];
|
|
26
|
+
/** Pass back to {@link listKeysPage} for the next page. `undefined` on the last page. */
|
|
27
|
+
nextCursor: string | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getBytes(key: string): Uint8Array | undefined {
|
|
31
|
+
return callHost(`kv.getBytes(${quote(key)})`, () => hostGet(key));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function setBytes(key: string, value: Uint8Array): void {
|
|
35
|
+
callHost(`kv.setBytes(${quote(key)})`, () => hostSet(key, value));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function has(key: string): boolean {
|
|
39
|
+
return getBytes(key) !== undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function get<T = unknown>(key: string): T | undefined {
|
|
43
|
+
const bytes = getBytes(key);
|
|
44
|
+
if (bytes === undefined || bytes.length === 0) return undefined;
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(decoder.decode(bytes)) as T;
|
|
47
|
+
} catch (err) {
|
|
48
|
+
throw SysError.json(`kv.get(${quote(key)}): ${(err as Error).message}`, err);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function set<T>(key: string, value: T): void {
|
|
53
|
+
let json: string;
|
|
54
|
+
try {
|
|
55
|
+
json = JSON.stringify(value);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
throw SysError.json(`kv.set(${quote(key)}): ${(err as Error).message}`, err);
|
|
58
|
+
}
|
|
59
|
+
setBytes(key, encoder.encode(json));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Idempotent: deleting a non-existent key succeeds silently. */
|
|
63
|
+
export function del(key: string): void {
|
|
64
|
+
callHost(`kv.delete(${quote(key)})`, () => hostDelete(key));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function listKeys(prefix: string): string[] {
|
|
68
|
+
return callHost(`kv.listKeys(${quote(prefix)})`, () => hostListKeys(prefix));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Paginated key listing for unbounded stores. Pass `undefined` cursor on the
|
|
73
|
+
* first call and the `nextCursor` from the previous page on subsequent calls.
|
|
74
|
+
* `limit` is capped at 1024 per page; 0 means "use the server default".
|
|
75
|
+
*/
|
|
76
|
+
export function listKeysPage(
|
|
77
|
+
prefix: string,
|
|
78
|
+
cursor: string | undefined,
|
|
79
|
+
limit: number = 0,
|
|
80
|
+
): KeyPage {
|
|
81
|
+
return callHost(`kv.listKeysPage(${quote(prefix)})`, () =>
|
|
82
|
+
hostListKeysPage(prefix, cursor, limit),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function clearPrefix(prefix: string): bigint {
|
|
87
|
+
return callHost(`kv.clearPrefix(${quote(prefix)})`, () => hostClearPrefix(prefix));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Atomic compare-and-swap. If the current value for `key` equals `expected`,
|
|
92
|
+
* write `newValue` and return `true`. Otherwise leave the store unchanged and
|
|
93
|
+
* return `false` — the routine lost-race retry path. `expected = undefined`
|
|
94
|
+
* means "swap only if the key does not currently exist" (create-if-absent).
|
|
95
|
+
*
|
|
96
|
+
* SDK-level convenience: the underlying WIT host fn surfaces mismatch as
|
|
97
|
+
* `Err(cas-mismatch)`. We catch that here and return `false` so capsule code
|
|
98
|
+
* can branch on success/mismatch with a boolean. Genuine host errors (quota,
|
|
99
|
+
* invalid key, etc.) still throw via `callHost`.
|
|
100
|
+
*
|
|
101
|
+
* Required for any concurrent coordination on shared state — the kernel runs
|
|
102
|
+
* capsule invocations across a multi-threaded worker pool so naive RMW
|
|
103
|
+
* patterns race.
|
|
104
|
+
*/
|
|
105
|
+
export function cas(
|
|
106
|
+
key: string,
|
|
107
|
+
expected: Uint8Array | undefined,
|
|
108
|
+
newValue: Uint8Array,
|
|
109
|
+
): boolean {
|
|
110
|
+
try {
|
|
111
|
+
callHost(`kv.cas(${quote(key)})`, () => hostCas(key, expected, newValue));
|
|
112
|
+
return true;
|
|
113
|
+
} catch (err) {
|
|
114
|
+
if (err instanceof SysError && err.message.includes("cas-mismatch")) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
throw err;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function quote(s: string): string {
|
|
122
|
+
return `"${s.replace(/"/g, '\\"')}"`;
|
|
123
|
+
}
|
package/src/log.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured logging — mirrors `astrid_sdk::log`. Levels match the WIT
|
|
3
|
+
* `log-level` enum exactly. Messages are coerced to string via the host call.
|
|
4
|
+
* Infallible: the host `log()` function returns void.
|
|
5
|
+
*
|
|
6
|
+
* Deliberate non-feature: this module does NOT override `globalThis.console`.
|
|
7
|
+
* The embedded JS engine may already wire `console` to its own sink, and
|
|
8
|
+
* shadowing risks capturing engine-internal log lines. Users who want
|
|
9
|
+
* `console.log` to flow through Astrid must explicitly forward to one of
|
|
10
|
+
* these functions.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { log as hostLog } from "astrid:sys/host@1.0.0";
|
|
14
|
+
|
|
15
|
+
export function trace(message: unknown): void {
|
|
16
|
+
hostLog("trace", format(message));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function debug(message: unknown): void {
|
|
20
|
+
hostLog("debug", format(message));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function info(message: unknown): void {
|
|
24
|
+
hostLog("info", format(message));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function warn(message: unknown): void {
|
|
28
|
+
hostLog("warn", format(message));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function error(message: unknown): void {
|
|
32
|
+
hostLog("error", format(message));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function format(value: unknown): string {
|
|
36
|
+
if (typeof value === "string") return value;
|
|
37
|
+
if (value instanceof Error) return value.stack ?? value.message;
|
|
38
|
+
try {
|
|
39
|
+
return JSON.stringify(value);
|
|
40
|
+
} catch {
|
|
41
|
+
return String(value);
|
|
42
|
+
}
|
|
43
|
+
}
|