@auersperg/flare 0.0.0-stage → 0.3.2

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.
@@ -0,0 +1,565 @@
1
+ import {
2
+ assertPath,
3
+ boundedJson,
4
+ LIMITS,
5
+ FlareError,
6
+ stableJson
7
+ } from "./protocol.js";
8
+ export { reference, FlareError } from "./protocol.js";
9
+ export function fileMetadata(file) {
10
+ if (file.size > LIMITS.fileBytes)
11
+ throw new FlareError("TOO_LARGE", "Maximum file size: 10 MiB");
12
+ if (!file.name || file.name.length > 120 || /[\x00-\x1f\x7f/\\]/.test(file.name))
13
+ throw new FlareError("INVALID_INPUT", "File name must be 1–120 characters, without path separators");
14
+ const safeTypes = [
15
+ "text/plain",
16
+ "image/png",
17
+ "image/jpeg",
18
+ "image/webp",
19
+ "application/pdf"
20
+ ];
21
+ return {
22
+ name: file.name,
23
+ size: file.size,
24
+ type: safeTypes.includes(file.type) ? file.type : "application/octet-stream"
25
+ };
26
+ }
27
+ export async function uploadFile(grant, file, options = {}) {
28
+ if (file.size > LIMITS.fileBytes)
29
+ throw new FlareError("TOO_LARGE", "Maximum file size: 10 MiB");
30
+ const url = new URL(grant.url);
31
+ if (url.username || url.password || url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)))
32
+ throw new FlareError("INVALID_URL", "Expected secure upload URL");
33
+ let response;
34
+ try {
35
+ response = await (options.fetch ?? globalThis.fetch)(url.href, {
36
+ method: "POST",
37
+ body: file,
38
+ redirect: "error",
39
+ credentials: "omit",
40
+ signal: options.signal ?? AbortSignal.timeout(options.timeoutMs ?? 120000)
41
+ });
42
+ } catch {
43
+ throw new FlareError("UPLOAD_UNKNOWN", "Connection interrupted or upload timed out; refresh the file list before trying again", true);
44
+ }
45
+ const result = await response.json().catch(() => null);
46
+ if (!response.ok) {
47
+ const code = result && typeof result === "object" && "error" in result && typeof result.error === "string" ? result.error : `HTTP_${response.status}`;
48
+ throw new FlareError(code, `Upload failed: ${code}`, response.status >= 500);
49
+ }
50
+ if (!result || typeof result !== "object" || !("id" in result) || typeof result.id !== "string")
51
+ throw new FlareError("INVALID_RESPONSE", "Upload outcome is unknown; refresh the file list before uploading again", true);
52
+ return { id: result.id };
53
+ }
54
+ export function createClient(options) {
55
+ const base = new URL(options.url);
56
+ if (!/^https?:$/.test(base.protocol) || base.username || base.password || base.search || base.hash)
57
+ throw new FlareError("INVALID_URL", "Expected backend HTTP URL");
58
+ if (base.protocol === "http:" && !["localhost", "127.0.0.1", "[::1]"].includes(base.hostname))
59
+ throw new FlareError("INVALID_URL", "Remote backends require HTTPS");
60
+ base.pathname = base.pathname.replace(/\/$/, "");
61
+ const transport = options.fetch ?? globalThis.fetch;
62
+ if (!transport)
63
+ throw new FlareError("TRANSPORT_UNAVAILABLE", "Provide a fetch implementation");
64
+ const subscriptions = new Map;
65
+ let disposed = false, socket, connecting = false, attempt = 0, generation = 0, counter = 0;
66
+ let retry;
67
+ const cleanups = new Set;
68
+ const endpoint = (path) => `${base.href.replace(/\/$/, "")}${path}`;
69
+ const notify = (sub, method, value) => {
70
+ for (const observer of sub.observers) {
71
+ try {
72
+ observer[method]?.(method === "next" ? JSON.parse(stableJson(value)) : value);
73
+ } catch {}
74
+ }
75
+ };
76
+ function ensureOpen() {
77
+ if (disposed)
78
+ throw new FlareError("DISPOSED", "Client is disposed");
79
+ }
80
+ async function request(path, body, write) {
81
+ ensureOpen();
82
+ const json = boundedJson(body, LIMITS.inputBytes);
83
+ let token;
84
+ try {
85
+ token = await options.getToken?.() ?? null;
86
+ } catch {
87
+ throw new FlareError("AUTH_UNAVAILABLE", "Cannot obtain access token");
88
+ }
89
+ ensureOpen();
90
+ const controller = new AbortController;
91
+ const timeout = setTimeout(() => controller.abort(), options.requestTimeoutMs ?? 30000);
92
+ try {
93
+ const response = await transport(endpoint(path), {
94
+ method: "POST",
95
+ headers: {
96
+ "Content-Type": "application/json",
97
+ ...token ? { Authorization: `Bearer ${token}` } : {}
98
+ },
99
+ body: json,
100
+ signal: controller.signal,
101
+ redirect: "error"
102
+ });
103
+ if (!response.body)
104
+ throw new FlareError("INVALID_RESPONSE", "Missing response body", write);
105
+ const reader = response.body.getReader();
106
+ let bytes = 0;
107
+ const chunks = [];
108
+ try {
109
+ for (;; ) {
110
+ const { done, value } = await reader.read();
111
+ if (done)
112
+ break;
113
+ bytes += value.byteLength;
114
+ if (bytes > LIMITS.outputBytes)
115
+ throw new FlareError("INVALID_RESPONSE", "Response exceeds limit", write);
116
+ chunks.push(value);
117
+ }
118
+ } finally {
119
+ await reader.cancel().catch(() => {});
120
+ }
121
+ const combined = new Uint8Array(bytes);
122
+ let offset = 0;
123
+ for (const chunk of chunks) {
124
+ combined.set(chunk, offset);
125
+ offset += chunk.byteLength;
126
+ }
127
+ let data;
128
+ try {
129
+ data = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(combined));
130
+ } catch (error) {
131
+ if (response.ok)
132
+ throw error;
133
+ throw new FlareError(`HTTP_${response.status}`, "Backend request failed", write && response.status >= 500);
134
+ }
135
+ if (!response.ok) {
136
+ const code = data && typeof data === "object" && "error" in data && typeof data.error === "string" && /^[A-Z][A-Z0-9_]{0,63}$/.test(data.error) ? data.error : `HTTP_${response.status}`;
137
+ throw new FlareError(code, "Backend request failed", write && response.status >= 500);
138
+ }
139
+ if (!data || typeof data !== "object" || !("result" in data))
140
+ throw new FlareError("INVALID_RESPONSE", "Invalid protocol envelope", write);
141
+ boundedJson(data.result, LIMITS.outputBytes);
142
+ return data.result;
143
+ } catch (error) {
144
+ if (error instanceof FlareError)
145
+ throw error;
146
+ if (error instanceof SyntaxError)
147
+ throw new FlareError("INVALID_RESPONSE", "Invalid JSON response", write);
148
+ throw new FlareError("TRANSPORT_ERROR", write ? "Write outcome is unknown; do not replay automatically" : "Backend request interrupted", write);
149
+ } finally {
150
+ clearTimeout(timeout);
151
+ }
152
+ }
153
+ function send(value) {
154
+ socket?.send(boundedJson(value, LIMITS.frameBytes));
155
+ }
156
+ function register(sub) {
157
+ send({ type: "subscribe", id: sub.id, path: sub.ref.path, args: sub.args });
158
+ }
159
+ function reconnect() {
160
+ if (disposed || !subscriptions.size || retry)
161
+ return;
162
+ const delay = Math.min(30000, 250 * 2 ** Math.min(attempt++, 7));
163
+ retry = setTimeout(() => {
164
+ retry = undefined;
165
+ connect();
166
+ }, delay);
167
+ }
168
+ async function connect() {
169
+ if (disposed || connecting || socket || !subscriptions.size)
170
+ return;
171
+ connecting = true;
172
+ const current = ++generation;
173
+ try {
174
+ const ticket = await request("/v1/live/ticket", {}, false);
175
+ if (disposed || current !== generation || !subscriptions.size)
176
+ return;
177
+ if (!ticket || typeof ticket.ticket !== "string" || ticket.ticket.length > 1024)
178
+ throw new FlareError("INVALID_RESPONSE", "Invalid live ticket");
179
+ const url = new URL(endpoint("/v1/live"));
180
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
181
+ url.searchParams.set("ticket", ticket.ticket);
182
+ const ws = options.createWebSocket ? options.createWebSocket(url.href) : new globalThis.WebSocket(url.href);
183
+ socket = ws;
184
+ const onOpen = () => {
185
+ if (socket !== ws || disposed)
186
+ return;
187
+ attempt = 0;
188
+ for (const sub of subscriptions.values()) {
189
+ sub.revision = -1;
190
+ sub.hasValue = false;
191
+ if (!sub.terminal)
192
+ register(sub);
193
+ }
194
+ };
195
+ const onMessage = (event) => {
196
+ if (socket !== ws || disposed)
197
+ return;
198
+ try {
199
+ if (typeof event.data !== "string" || new TextEncoder().encode(event.data).byteLength > LIMITS.outputBytes + 1024)
200
+ throw new Error("Invalid frame");
201
+ const frame = JSON.parse(event.data);
202
+ if (frame.type === "ready")
203
+ return;
204
+ const sub = [...subscriptions.values()].find((s) => s.id === frame.id);
205
+ if (!sub || sub.terminal)
206
+ return;
207
+ if (frame.type === "error") {
208
+ sub.terminal = true;
209
+ if (["AUTH_EXPIRED", "UNAUTHORIZED", "FORBIDDEN"].includes(frame.code)) {
210
+ sub.hasValue = false;
211
+ sub.value = undefined;
212
+ }
213
+ notify(sub, "error", new FlareError(typeof frame.code === "string" ? frame.code : "LIVE_ERROR", "Live subscription failed"));
214
+ return;
215
+ }
216
+ if (frame.type !== "snapshot" || !Number.isSafeInteger(frame.revision) || frame.revision < 0 || !("result" in frame))
217
+ throw new Error("Invalid snapshot");
218
+ const json = boundedJson(frame.result, LIMITS.outputBytes);
219
+ if (frame.revision > sub.revision) {
220
+ const changed = !sub.hasValue || stableJson(sub.value) !== json;
221
+ sub.revision = frame.revision;
222
+ sub.value = frame.result;
223
+ sub.hasValue = true;
224
+ if (changed)
225
+ notify(sub, "next", sub.value);
226
+ }
227
+ send({ type: "ack", id: sub.id, revision: sub.revision });
228
+ } catch {
229
+ ws.close(1002, "Invalid protocol frame");
230
+ }
231
+ };
232
+ const onClose = (event) => {
233
+ cleanup();
234
+ if (socket !== ws)
235
+ return;
236
+ socket = undefined;
237
+ for (const sub of subscriptions.values()) {
238
+ if (event.code === 1008) {
239
+ sub.hasValue = false;
240
+ sub.value = undefined;
241
+ sub.revision = -1;
242
+ }
243
+ if (!sub.terminal)
244
+ notify(sub, "error", new FlareError(event.code === 1008 ? "AUTH_EXPIRED" : "LIVE_DISCONNECTED", "Live reconnecting; next snapshot will be current"));
245
+ }
246
+ reconnect();
247
+ };
248
+ const onError = () => ws.close();
249
+ const cleanup = () => {
250
+ ws.removeEventListener("open", onOpen);
251
+ ws.removeEventListener("message", onMessage);
252
+ ws.removeEventListener("close", onClose);
253
+ ws.removeEventListener("error", onError);
254
+ cleanups.delete(cleanup);
255
+ };
256
+ cleanups.add(cleanup);
257
+ ws.addEventListener("open", onOpen);
258
+ ws.addEventListener("message", onMessage);
259
+ ws.addEventListener("close", onClose);
260
+ ws.addEventListener("error", onError);
261
+ } catch (error) {
262
+ if (disposed || current !== generation)
263
+ return;
264
+ for (const sub of subscriptions.values()) {
265
+ if (error instanceof FlareError && ["HTTP_401", "HTTP_403"].includes(error.code)) {
266
+ sub.hasValue = false;
267
+ sub.value = undefined;
268
+ sub.revision = -1;
269
+ }
270
+ notify(sub, "error", error instanceof FlareError ? error : new FlareError("LIVE_UNAVAILABLE", "Live connection failed"));
271
+ }
272
+ reconnect();
273
+ } finally {
274
+ connecting = false;
275
+ if (!disposed && current !== generation && !socket && subscriptions.size)
276
+ reconnect();
277
+ }
278
+ }
279
+ function invoke(kind) {
280
+ return async (ref, args) => {
281
+ assertPath(ref.path);
282
+ if (ref.kind !== kind)
283
+ throw new FlareError("INVALID_KIND", `Expected a ${kind} function`);
284
+ return request("/v1/call", { path: ref.path, kind: ref.kind, args }, ref.kind === "mutation" || ref.kind === "action");
285
+ };
286
+ }
287
+ return {
288
+ query: invoke("query"),
289
+ mutation: invoke("mutation"),
290
+ action: invoke("action"),
291
+ onUpdate(ref, args, next, error) {
292
+ const observer = { next, error };
293
+ ensureOpen();
294
+ assertPath(ref.path);
295
+ if (ref.kind !== "query")
296
+ throw new FlareError("INVALID_KIND", "Only queries are subscribable");
297
+ boundedJson({
298
+ type: "subscribe",
299
+ id: "subscription-9999999999",
300
+ path: ref.path,
301
+ args
302
+ }, LIMITS.frameBytes);
303
+ const key = stableJson({ path: ref.path, args });
304
+ let sub = subscriptions.get(key);
305
+ if (!sub) {
306
+ if (subscriptions.size >= LIMITS.subscriptions)
307
+ throw new FlareError("TOO_MANY_SUBSCRIPTIONS", "Subscription limit exceeded");
308
+ sub = {
309
+ id: `subscription-${++counter}`,
310
+ ref,
311
+ args: JSON.parse(stableJson(args)),
312
+ observers: new Set,
313
+ revision: -1,
314
+ hasValue: false,
315
+ terminal: false
316
+ };
317
+ subscriptions.set(key, sub);
318
+ if (socket?.readyState === 1)
319
+ register(sub);
320
+ }
321
+ const registration = { next: observer.next, error: observer.error };
322
+ sub.observers.add(registration);
323
+ if (sub.hasValue)
324
+ notify({ ...sub, observers: new Set([observer]) }, "next", sub.value);
325
+ connect();
326
+ let unsubscribed = false;
327
+ return () => {
328
+ if (unsubscribed)
329
+ return;
330
+ unsubscribed = true;
331
+ sub.observers.delete(registration);
332
+ if (!sub.observers.size) {
333
+ subscriptions.delete(key);
334
+ if (socket?.readyState === 1)
335
+ send({ type: "unsubscribe", id: sub.id });
336
+ }
337
+ if (!subscriptions.size) {
338
+ ++generation;
339
+ if (retry)
340
+ clearTimeout(retry);
341
+ retry = undefined;
342
+ const old = socket;
343
+ socket = undefined;
344
+ for (const cleanup of [...cleanups])
345
+ cleanup();
346
+ old?.close(1000, "No subscriptions");
347
+ }
348
+ };
349
+ },
350
+ refreshAuth() {
351
+ ensureOpen();
352
+ ++generation;
353
+ if (retry)
354
+ clearTimeout(retry);
355
+ retry = undefined;
356
+ for (const cleanup of [...cleanups])
357
+ cleanup();
358
+ const old = socket;
359
+ socket = undefined;
360
+ old?.close(1000, "Authorization changed");
361
+ for (const sub of subscriptions.values()) {
362
+ sub.hasValue = false;
363
+ sub.value = undefined;
364
+ sub.revision = -1;
365
+ sub.terminal = false;
366
+ notify(sub, "error", new FlareError("AUTH_CHANGED", "Authorization changed; cached value invalidated"));
367
+ }
368
+ connect();
369
+ },
370
+ dispose() {
371
+ if (disposed)
372
+ return;
373
+ disposed = true;
374
+ ++generation;
375
+ if (retry)
376
+ clearTimeout(retry);
377
+ retry = undefined;
378
+ for (const cleanup of [...cleanups])
379
+ cleanup();
380
+ subscriptions.clear();
381
+ socket?.close(1000, "Disposed");
382
+ socket = undefined;
383
+ }
384
+ };
385
+ }
386
+ export function createPasswordAuth(options) {
387
+ const base = options.url.replace(/\/$/, "");
388
+ const key = "flared:session:" + encodeURIComponent(base);
389
+ let storage = options.storage ?? null;
390
+ let browserStorage = null;
391
+ if (typeof window !== "undefined") {
392
+ try {
393
+ browserStorage = window.localStorage;
394
+ } catch {}
395
+ if (options.storage === undefined)
396
+ storage = browserStorage;
397
+ }
398
+ let session = null;
399
+ let user = null;
400
+ let version = 0;
401
+ let listening = false;
402
+ let restoring = null;
403
+ const listeners = new Set;
404
+ const changed = () => {
405
+ for (const listener of listeners)
406
+ listener();
407
+ };
408
+ const persist = () => {
409
+ if (!storage)
410
+ return;
411
+ try {
412
+ if (session)
413
+ storage.setItem(key, JSON.stringify(session));
414
+ else
415
+ storage.removeItem(key);
416
+ } catch {}
417
+ };
418
+ const clear = () => {
419
+ ++version;
420
+ session = null;
421
+ user = null;
422
+ persist();
423
+ changed();
424
+ };
425
+ const request = async (op, input, token = session?.token) => {
426
+ const response = await fetch(base + "/v1/auth/" + op, {
427
+ method: "POST",
428
+ redirect: "error",
429
+ credentials: "omit",
430
+ headers: {
431
+ "content-type": "application/json",
432
+ ...token ? { authorization: "Bearer " + token } : {}
433
+ },
434
+ body: JSON.stringify(input),
435
+ signal: AbortSignal.timeout(15000)
436
+ });
437
+ const data = await response.json();
438
+ if (!response.ok)
439
+ throw new FlareError(data.error ?? "AUTH_FAILED", "Authentication request failed");
440
+ return data;
441
+ };
442
+ const restore = () => {
443
+ if (!listening && browserStorage && storage === browserStorage) {
444
+ window.addEventListener("storage", onStorage);
445
+ listening = true;
446
+ }
447
+ if (restoring?.version === version)
448
+ return restoring.promise;
449
+ const current = version;
450
+ const promise = (async () => {
451
+ let saved = session;
452
+ if (storage) {
453
+ try {
454
+ const raw = storage.getItem(key);
455
+ saved = raw ? JSON.parse(raw) : null;
456
+ } catch {
457
+ saved = null;
458
+ }
459
+ }
460
+ if (!saved || !/^flared_session_[a-f0-9]{64}$/.test(saved.token) || !Number.isSafeInteger(saved.expiresAt) || saved.expiresAt <= Date.now()) {
461
+ if (current === version)
462
+ clear();
463
+ return null;
464
+ }
465
+ try {
466
+ const verified = await request("me", {}, saved.token);
467
+ if (current !== version)
468
+ return null;
469
+ session = { token: saved.token, expiresAt: saved.expiresAt };
470
+ user = verified;
471
+ changed();
472
+ return verified;
473
+ } catch (error) {
474
+ if (current !== version)
475
+ return null;
476
+ if (error instanceof FlareError && ["UNAUTHORIZED", "AUTH_EXPIRED", "AUTH_DISABLED"].includes(error.code)) {
477
+ clear();
478
+ return null;
479
+ }
480
+ throw error;
481
+ }
482
+ })();
483
+ restoring = { version: current, promise };
484
+ promise.finally(() => {
485
+ if (restoring?.promise === promise)
486
+ restoring = null;
487
+ }).catch(() => {});
488
+ return promise;
489
+ };
490
+ function onStorage(event) {
491
+ if (event.storageArea !== browserStorage || event.key !== key && event.key !== null)
492
+ return;
493
+ ++version;
494
+ session = null;
495
+ user = null;
496
+ changed();
497
+ restore().catch(() => {});
498
+ }
499
+ const signIn = async (op, login, password) => {
500
+ const current = ++version;
501
+ const result = await request(op, { login, password }, undefined);
502
+ if (current !== version) {
503
+ await request("logout", {}, result.token).catch(() => {});
504
+ throw new FlareError("AUTH_CHANGED", "Authentication changed during sign-in");
505
+ }
506
+ session = { token: result.token, expiresAt: result.expiresAt };
507
+ user = result.user;
508
+ persist();
509
+ changed();
510
+ return result.user;
511
+ };
512
+ const getToken = () => session && session.expiresAt > Date.now() ? session.token : null;
513
+ return {
514
+ getToken,
515
+ getUser: () => getToken() ? user : null,
516
+ restore,
517
+ onChange(listener) {
518
+ listeners.add(listener);
519
+ return () => listeners.delete(listener);
520
+ },
521
+ register: (login, password) => signIn("register", login, password),
522
+ login: (login, password) => signIn("login", login, password),
523
+ async me() {
524
+ const current = version;
525
+ try {
526
+ const value = await request("me", {});
527
+ if (current === version)
528
+ user = value;
529
+ return value;
530
+ } catch (error) {
531
+ if (current === version && error instanceof FlareError && ["UNAUTHORIZED", "AUTH_EXPIRED", "AUTH_DISABLED"].includes(error.code))
532
+ clear();
533
+ throw error;
534
+ }
535
+ },
536
+ async logout() {
537
+ let token = session?.token;
538
+ if (!token && storage) {
539
+ try {
540
+ const saved = JSON.parse(storage.getItem(key) ?? "null");
541
+ if (saved && /^flared_session_[a-f0-9]{64}$/.test(saved.token))
542
+ token = saved.token;
543
+ } catch {}
544
+ }
545
+ clear();
546
+ if (token)
547
+ await request("logout", {}, token);
548
+ },
549
+ async changePassword(currentPassword, password) {
550
+ const current = version;
551
+ await request("change-password", { currentPassword, password });
552
+ if (current === version)
553
+ clear();
554
+ },
555
+ dispose() {
556
+ ++version;
557
+ if (listening)
558
+ window.removeEventListener("storage", onStorage);
559
+ listening = false;
560
+ listeners.clear();
561
+ session = null;
562
+ user = null;
563
+ }
564
+ };
565
+ }
@@ -0,0 +1,112 @@
1
+ import { DurableObject, WorkerEntrypoint } from "cloudflare:workers";
2
+ import { type FunctionTree } from "./server.js";
3
+ import { type Auth, type FunctionKind, type Json } from "./protocol.js";
4
+ import { type Schema } from "./schema.js";
5
+ export interface Invocation {
6
+ path: string;
7
+ kind: FunctionKind;
8
+ args: Json;
9
+ auth: Auth | null;
10
+ internal?: boolean;
11
+ }
12
+ export interface OutboxJob {
13
+ id: string;
14
+ release: string;
15
+ path: string;
16
+ args: Json;
17
+ auth: Auth | null;
18
+ dueAt: number;
19
+ internal?: boolean;
20
+ }
21
+ export interface FacetRpc {
22
+ snapshot(): Promise<{
23
+ revision: number;
24
+ changes: Record<string, number>;
25
+ }>;
26
+ execute(call: Invocation): Promise<{
27
+ result: Json;
28
+ revision: number;
29
+ tables: string[];
30
+ }>;
31
+ pendingJobs(limit?: number): Promise<OutboxJob[]>;
32
+ markJobsSent(ids: string[]): Promise<void>;
33
+ cancelJob(id: string): Promise<boolean>;
34
+ pendingJobReferences(): Promise<{
35
+ paths: string[];
36
+ releases: string[];
37
+ truncated: boolean;
38
+ }>;
39
+ enqueueJob(job: OutboxJob): Promise<void>;
40
+ validateJob(path: string, args: Json): Promise<void>;
41
+ inspect(input: {
42
+ table?: string;
43
+ cursor?: string | null;
44
+ limit?: number;
45
+ }): Promise<any>;
46
+ edit(input: {
47
+ table: string;
48
+ operation: "insert" | "update" | "delete";
49
+ values?: Record<string, Json>;
50
+ key?: string;
51
+ expectedRevision: number;
52
+ }): Promise<any>;
53
+ invoke(call: Invocation): Promise<{
54
+ ok: true;
55
+ value: {
56
+ result: Json;
57
+ revision: number;
58
+ tables: string[];
59
+ };
60
+ } | {
61
+ ok: false;
62
+ code: string;
63
+ }>;
64
+ administrative(operation: string, input: any): Promise<{
65
+ ok: true;
66
+ value: any;
67
+ } | {
68
+ ok: false;
69
+ code: string;
70
+ }>;
71
+ }
72
+ export type FacetConstructor = new (ctx: DurableObjectState, env: {
73
+ RELEASE_ID: string;
74
+ }) => DurableObject<{
75
+ RELEASE_ID: string;
76
+ }> & FacetRpc;
77
+ export type ActionConstructor = new (ctx: ExecutionContext, env: {
78
+ CAPABILITY: ScopedCapability;
79
+ }) => WorkerEntrypoint<{
80
+ CAPABILITY: ScopedCapability;
81
+ }> & {
82
+ execute(call: Invocation): Promise<Json>;
83
+ invoke(call: Invocation): Promise<{
84
+ ok: true;
85
+ value: Json;
86
+ } | {
87
+ ok: false;
88
+ code: string;
89
+ }>;
90
+ };
91
+ export interface ScopedCapability {
92
+ runQuery(path: string, args: unknown, auth?: Auth | null): Promise<any>;
93
+ runMutation(path: string, args: unknown, auth?: Auth | null): Promise<any>;
94
+ secret(name: string): Promise<string>;
95
+ upload(body: ReadableStream<Uint8Array>, contentType: string, options: {
96
+ name: string;
97
+ size: number;
98
+ }): Promise<string>;
99
+ download(id: string): Promise<Response>;
100
+ fileCommand(op: string, input: unknown): Promise<any>;
101
+ scheduleCommand(op: string, input: unknown): Promise<any>;
102
+ }
103
+ /** Construct the class at build time. Parent supplies a verified identity and release ID,
104
+ * never D1, R2, secrets or its own storage bindings. The parent must arm recovery first. */
105
+ export declare function createFacet(app: {
106
+ functions: FunctionTree;
107
+ schema: Schema;
108
+ }): FacetConstructor;
109
+ /** A separate Dynamic Worker role. Capabilities are scoped by the parent, not the app. */
110
+ export declare function createAction(app: {
111
+ functions: FunctionTree;
112
+ }): ActionConstructor;