@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.
Files changed (124) hide show
  1. package/README.md +120 -0
  2. package/dist/approval.d.ts +23 -0
  3. package/dist/approval.d.ts.map +1 -0
  4. package/dist/approval.js +29 -0
  5. package/dist/approval.js.map +1 -0
  6. package/dist/capabilities.d.ts +14 -0
  7. package/dist/capabilities.d.ts.map +1 -0
  8. package/dist/capabilities.js +19 -0
  9. package/dist/capabilities.js.map +1 -0
  10. package/dist/capsule.d.ts +39 -0
  11. package/dist/capsule.d.ts.map +1 -0
  12. package/dist/capsule.js +67 -0
  13. package/dist/capsule.js.map +1 -0
  14. package/dist/contracts.d.ts +1104 -0
  15. package/dist/contracts.d.ts.map +1 -0
  16. package/dist/contracts.js +4 -0
  17. package/dist/contracts.js.map +1 -0
  18. package/dist/elicit.d.ts +30 -0
  19. package/dist/elicit.d.ts.map +1 -0
  20. package/dist/elicit.js +103 -0
  21. package/dist/elicit.js.map +1 -0
  22. package/dist/env.d.ts +19 -0
  23. package/dist/env.d.ts.map +1 -0
  24. package/dist/env.js +27 -0
  25. package/dist/env.js.map +1 -0
  26. package/dist/errors.d.ts +46 -0
  27. package/dist/errors.d.ts.map +1 -0
  28. package/dist/errors.js +108 -0
  29. package/dist/errors.js.map +1 -0
  30. package/dist/fs.d.ts +135 -0
  31. package/dist/fs.d.ts.map +1 -0
  32. package/dist/fs.js +257 -0
  33. package/dist/fs.js.map +1 -0
  34. package/dist/http.d.ts +90 -0
  35. package/dist/http.d.ts.map +1 -0
  36. package/dist/http.js +276 -0
  37. package/dist/http.js.map +1 -0
  38. package/dist/identity.d.ts +46 -0
  39. package/dist/identity.d.ts.map +1 -0
  40. package/dist/identity.js +69 -0
  41. package/dist/identity.js.map +1 -0
  42. package/dist/index.d.ts +30 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +27 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/interceptors.d.ts +21 -0
  47. package/dist/interceptors.d.ts.map +1 -0
  48. package/dist/interceptors.js +22 -0
  49. package/dist/interceptors.js.map +1 -0
  50. package/dist/ipc.d.ts +143 -0
  51. package/dist/ipc.d.ts.map +1 -0
  52. package/dist/ipc.js +261 -0
  53. package/dist/ipc.js.map +1 -0
  54. package/dist/kv.d.ts +45 -0
  55. package/dist/kv.d.ts.map +1 -0
  56. package/dist/kv.js +91 -0
  57. package/dist/kv.js.map +1 -0
  58. package/dist/log.d.ts +17 -0
  59. package/dist/log.d.ts.map +1 -0
  60. package/dist/log.js +40 -0
  61. package/dist/log.js.map +1 -0
  62. package/dist/net.d.ts +154 -0
  63. package/dist/net.d.ts.map +1 -0
  64. package/dist/net.js +421 -0
  65. package/dist/net.js.map +1 -0
  66. package/dist/process.d.ts +77 -0
  67. package/dist/process.d.ts.map +1 -0
  68. package/dist/process.js +128 -0
  69. package/dist/process.js.map +1 -0
  70. package/dist/runtime/bridge.d.ts +34 -0
  71. package/dist/runtime/bridge.d.ts.map +1 -0
  72. package/dist/runtime/bridge.js +326 -0
  73. package/dist/runtime/bridge.js.map +1 -0
  74. package/dist/runtime/index.d.ts +3 -0
  75. package/dist/runtime/index.d.ts.map +1 -0
  76. package/dist/runtime/index.js +3 -0
  77. package/dist/runtime/index.js.map +1 -0
  78. package/dist/runtime/registry.d.ts +58 -0
  79. package/dist/runtime/registry.d.ts.map +1 -0
  80. package/dist/runtime/registry.js +129 -0
  81. package/dist/runtime/registry.js.map +1 -0
  82. package/dist/runtime.d.ts +36 -0
  83. package/dist/runtime.d.ts.map +1 -0
  84. package/dist/runtime.js +50 -0
  85. package/dist/runtime.js.map +1 -0
  86. package/dist/time.d.ts +29 -0
  87. package/dist/time.d.ts.map +1 -0
  88. package/dist/time.js +43 -0
  89. package/dist/time.js.map +1 -0
  90. package/dist/tool.d.ts +48 -0
  91. package/dist/tool.d.ts.map +1 -0
  92. package/dist/tool.js +86 -0
  93. package/dist/tool.js.map +1 -0
  94. package/dist/uplink.d.ts +27 -0
  95. package/dist/uplink.d.ts.map +1 -0
  96. package/dist/uplink.js +36 -0
  97. package/dist/uplink.js.map +1 -0
  98. package/package.json +38 -0
  99. package/src/approval.ts +38 -0
  100. package/src/capabilities.ts +22 -0
  101. package/src/capsule.ts +90 -0
  102. package/src/contracts.ts +1189 -0
  103. package/src/elicit.ts +136 -0
  104. package/src/env.ts +31 -0
  105. package/src/errors.ts +122 -0
  106. package/src/fs.ts +357 -0
  107. package/src/http.ts +345 -0
  108. package/src/identity.ts +101 -0
  109. package/src/index.ts +83 -0
  110. package/src/interceptors.ts +25 -0
  111. package/src/ipc.ts +354 -0
  112. package/src/kv.ts +123 -0
  113. package/src/log.ts +43 -0
  114. package/src/net.ts +545 -0
  115. package/src/process.ts +205 -0
  116. package/src/runtime/bridge.ts +374 -0
  117. package/src/runtime/index.ts +11 -0
  118. package/src/runtime/registry.ts +178 -0
  119. package/src/runtime.ts +70 -0
  120. package/src/time.ts +48 -0
  121. package/src/tool.ts +125 -0
  122. package/src/uplink.ts +49 -0
  123. package/src/wit-imports.d.ts +689 -0
  124. 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
+ }