@auersperg/flare 0.3.9 → 0.3.11
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 +3 -1
- package/dist/cli/{chunk-0s0vrjtw.js → chunk-1yvbecac.js} +97 -11
- package/dist/cli/{chunk-5yg1zj5n.js → chunk-7ym0f672.js} +6 -6
- package/dist/cli/{chunk-5s6zv1h9.js → chunk-8gzfscca.js} +5 -7
- package/dist/cli/chunk-xn3e3gev.js +9 -0
- package/dist/cli/{chunk-vx7kmczm.js → chunk-yhvbksrm.js} +32 -7
- package/dist/cli/flare.js +5 -5
- package/dist/platform/admin.js +35 -24
- package/dist/platform/dashboard/_astro/ai-view.DxgwI4OB.js +1 -0
- package/dist/platform/dashboard/_astro/{auth-view.EASEF9BN.js → auth-view.C16WPVOx.js} +1 -1
- package/dist/platform/dashboard/_astro/{cli-approval.DwZVHdyo.js → cli-approval.emPTVRqn.js} +1 -1
- package/dist/platform/dashboard/_astro/client.4z54xJ_z.js +1 -0
- package/dist/platform/dashboard/_astro/{common.TAClb6G9.js → common.9pujiaSZ.js} +1 -1
- package/dist/platform/dashboard/_astro/{data-view.BaHEU4Xa.js → data-view.BCmdi_cs.js} +1 -1
- package/dist/platform/dashboard/_astro/{functions-view.BGOYuDwu.js → functions-view.Cf640vhX.js} +1 -1
- package/dist/platform/dashboard/_astro/index.CDrYoVkO.css +2 -0
- package/dist/platform/dashboard/_astro/index.astro_astro_type_script_index_0_lang.cFeBPGtv.js +3 -0
- package/dist/platform/dashboard/_astro/input-group.Dsc4z0iT.js +1 -0
- package/dist/platform/dashboard/_astro/{sdk-view.2ErPn4cO.js → sdk-view.Cs96E0YV.js} +1 -1
- package/dist/platform/dashboard/_astro/{services-view.iZujitxl.js → services-view.DhwKE_RK.js} +1 -1
- package/dist/platform/dashboard/_astro/{settings-view.CqK-mrlB.js → settings-view.Dsc34AAW.js} +1 -1
- package/dist/platform/dashboard/bootstrap/action.js +43 -0
- package/dist/platform/dashboard/bootstrap/facet.js +64 -46
- package/dist/platform/dashboard/bootstrap/openapi.json +1 -1
- package/dist/platform/dashboard/bootstrap/release.json +3 -3
- package/dist/platform/dashboard/index.html +1 -1
- package/dist/platform/manifest.json +48 -43
- package/dist/platform/runtime.js +24267 -865
- package/dist/sdk/backup.js +4 -4
- package/dist/sdk/client.js +22 -41
- package/dist/sdk/cloudflare.d.ts +4 -0
- package/dist/sdk/cloudflare.js +14 -11
- package/dist/sdk/codex.d.ts +20 -0
- package/dist/sdk/codex.js +17 -0
- package/dist/sdk/database.js +45 -27
- package/dist/sdk/http.d.ts +2 -0
- package/dist/sdk/http.js +27 -0
- package/dist/sdk/protocol.d.ts +6 -0
- package/dist/sdk/protocol.js +12 -0
- package/dist/sdk/schema.d.ts +2 -2
- package/dist/sdk/schema.js +7 -6
- package/dist/sdk/search.d.ts +25 -0
- package/dist/sdk/search.js +22 -0
- package/dist/sdk/server.d.ts +14 -1
- package/dist/sdk/server.js +9 -1
- package/package.json +2 -2
- package/dist/platform/dashboard/_astro/client.BUUZgEFj.js +0 -1
- package/dist/platform/dashboard/_astro/index.BPQdjJ4j.css +0 -2
- package/dist/platform/dashboard/_astro/index.astro_astro_type_script_index_0_lang.Blg5hU9g.js +0 -3
- package/dist/platform/dashboard/_astro/input-group.DUp-h0xx.js +0 -1
package/dist/sdk/backup.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { boundedJson, FlareError, stableJson } from "./protocol.js";
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
documentJson,
|
|
4
4
|
quote,
|
|
5
5
|
schemaContract
|
|
6
6
|
} from "./schema.js";
|
|
@@ -66,10 +66,10 @@ export async function restoreData(storage, schema, id, snapshot) {
|
|
|
66
66
|
for (const doc of docs) {
|
|
67
67
|
if (++count > SNAPSHOT_ROWS || !doc || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(doc._id) || !Number.isFinite(doc._creationTime) || doc._creationTime < 0)
|
|
68
68
|
reject("INVALID_BACKUP");
|
|
69
|
-
const value = await
|
|
70
|
-
if (
|
|
69
|
+
const value = await documentJson(schema[name], doc.value);
|
|
70
|
+
if (value !== stableJson(doc.value))
|
|
71
71
|
reject("INVALID_BACKUP");
|
|
72
|
-
storage.sql.exec(`INSERT INTO ${quote(name)} VALUES (?,?,?)`, doc._id, doc._creationTime,
|
|
72
|
+
storage.sql.exec(`INSERT INTO ${quote(name)} VALUES (?,?,?)`, doc._id, doc._creationTime, value);
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
storage.sql.exec("UPDATE _flared_revision SET value=? WHERE id=1", snapshot.revision);
|
package/dist/sdk/client.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readBody } from "./http.js";
|
|
1
2
|
import {
|
|
2
3
|
assertPath,
|
|
3
4
|
boundedJson,
|
|
@@ -37,12 +38,17 @@ export async function uploadFile(grant, file, options = {}) {
|
|
|
37
38
|
body: file,
|
|
38
39
|
redirect: "error",
|
|
39
40
|
credentials: "omit",
|
|
40
|
-
signal:
|
|
41
|
+
signal: AbortSignal.any([
|
|
42
|
+
...options.signal ? [options.signal] : [],
|
|
43
|
+
AbortSignal.timeout(options.timeoutMs ?? 120000)
|
|
44
|
+
])
|
|
41
45
|
});
|
|
42
46
|
} catch {
|
|
43
47
|
throw new FlareError("UPLOAD_UNKNOWN", "Connection interrupted or upload timed out; refresh the file list before trying again", true);
|
|
44
48
|
}
|
|
45
|
-
const result = await response.
|
|
49
|
+
const result = response.body ? await readBody(response.body, LIMITS.outputBytes, () => {
|
|
50
|
+
throw Error("Upload response exceeds limit");
|
|
51
|
+
}).then((bytes) => JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes))).catch(() => null) : null;
|
|
46
52
|
if (!response.ok) {
|
|
47
53
|
const code = result && typeof result === "object" && "error" in result && typeof result.error === "string" ? result.error : `HTTP_${response.status}`;
|
|
48
54
|
throw new FlareError(code, `Upload failed: ${code}`, response.status >= 500);
|
|
@@ -77,7 +83,7 @@ export function createClient(options) {
|
|
|
77
83
|
const notify = (sub, method, value) => {
|
|
78
84
|
for (const observer of sub.observers) {
|
|
79
85
|
try {
|
|
80
|
-
observer[method]?.(method === "next" ? JSON.parse(
|
|
86
|
+
observer[method]?.(method === "next" ? JSON.parse(value) : value);
|
|
81
87
|
} catch {}
|
|
82
88
|
}
|
|
83
89
|
};
|
|
@@ -110,28 +116,9 @@ export function createClient(options) {
|
|
|
110
116
|
});
|
|
111
117
|
if (!response.body)
|
|
112
118
|
throw new FlareError("INVALID_RESPONSE", "Missing response body", write);
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
try {
|
|
117
|
-
for (;; ) {
|
|
118
|
-
const { done, value } = await reader.read();
|
|
119
|
-
if (done)
|
|
120
|
-
break;
|
|
121
|
-
bytes += value.byteLength;
|
|
122
|
-
if (bytes > LIMITS.outputBytes)
|
|
123
|
-
throw new FlareError("INVALID_RESPONSE", "Response exceeds limit", write);
|
|
124
|
-
chunks.push(value);
|
|
125
|
-
}
|
|
126
|
-
} finally {
|
|
127
|
-
await reader.cancel().catch(() => {});
|
|
128
|
-
}
|
|
129
|
-
const combined = new Uint8Array(bytes);
|
|
130
|
-
let offset = 0;
|
|
131
|
-
for (const chunk of chunks) {
|
|
132
|
-
combined.set(chunk, offset);
|
|
133
|
-
offset += chunk.byteLength;
|
|
134
|
-
}
|
|
119
|
+
const combined = await readBody(response.body, LIMITS.outputBytes, () => {
|
|
120
|
+
throw new FlareError("INVALID_RESPONSE", "Response exceeds limit", write);
|
|
121
|
+
});
|
|
135
122
|
let data;
|
|
136
123
|
try {
|
|
137
124
|
data = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(combined));
|
|
@@ -195,7 +182,7 @@ export function createClient(options) {
|
|
|
195
182
|
attempt = 0;
|
|
196
183
|
for (const sub of subscriptions.values()) {
|
|
197
184
|
sub.revision = -1;
|
|
198
|
-
sub.
|
|
185
|
+
sub.snapshot = undefined;
|
|
199
186
|
if (!sub.terminal)
|
|
200
187
|
register(sub);
|
|
201
188
|
}
|
|
@@ -215,8 +202,7 @@ export function createClient(options) {
|
|
|
215
202
|
if (frame.type === "error") {
|
|
216
203
|
sub.terminal = true;
|
|
217
204
|
if (authorizationErrors.has(frame.code)) {
|
|
218
|
-
sub.
|
|
219
|
-
sub.value = undefined;
|
|
205
|
+
sub.snapshot = undefined;
|
|
220
206
|
}
|
|
221
207
|
notify(sub, "error", new FlareError(typeof frame.code === "string" ? frame.code : "LIVE_ERROR", "Live subscription failed"));
|
|
222
208
|
return;
|
|
@@ -225,12 +211,11 @@ export function createClient(options) {
|
|
|
225
211
|
throw new Error("Invalid snapshot");
|
|
226
212
|
const json = boundedJson(frame.result, LIMITS.outputBytes);
|
|
227
213
|
if (frame.revision > sub.revision) {
|
|
228
|
-
const changed =
|
|
214
|
+
const changed = sub.snapshot !== json;
|
|
229
215
|
sub.revision = frame.revision;
|
|
230
|
-
sub.
|
|
231
|
-
sub.hasValue = true;
|
|
216
|
+
sub.snapshot = json;
|
|
232
217
|
if (changed)
|
|
233
|
-
notify(sub, "next", sub.
|
|
218
|
+
notify(sub, "next", sub.snapshot);
|
|
234
219
|
}
|
|
235
220
|
send({ type: "ack", id: sub.id, revision: sub.revision });
|
|
236
221
|
} catch {
|
|
@@ -244,8 +229,7 @@ export function createClient(options) {
|
|
|
244
229
|
socket = undefined;
|
|
245
230
|
for (const sub of subscriptions.values()) {
|
|
246
231
|
if (event.code === 1008) {
|
|
247
|
-
sub.
|
|
248
|
-
sub.value = undefined;
|
|
232
|
+
sub.snapshot = undefined;
|
|
249
233
|
sub.revision = -1;
|
|
250
234
|
}
|
|
251
235
|
if (!sub.terminal)
|
|
@@ -271,8 +255,7 @@ export function createClient(options) {
|
|
|
271
255
|
return;
|
|
272
256
|
for (const sub of subscriptions.values()) {
|
|
273
257
|
if (error instanceof FlareError && authorizationErrors.has(error.code)) {
|
|
274
|
-
sub.
|
|
275
|
-
sub.value = undefined;
|
|
258
|
+
sub.snapshot = undefined;
|
|
276
259
|
sub.revision = -1;
|
|
277
260
|
}
|
|
278
261
|
notify(sub, "error", error instanceof FlareError ? error : new FlareError("LIVE_UNAVAILABLE", "Live connection failed"));
|
|
@@ -319,7 +302,6 @@ export function createClient(options) {
|
|
|
319
302
|
args: JSON.parse(stableJson(args)),
|
|
320
303
|
observers: new Set,
|
|
321
304
|
revision: -1,
|
|
322
|
-
hasValue: false,
|
|
323
305
|
terminal: false
|
|
324
306
|
};
|
|
325
307
|
subscriptions.set(key, sub);
|
|
@@ -328,8 +310,8 @@ export function createClient(options) {
|
|
|
328
310
|
}
|
|
329
311
|
const registration = { next: observer.next, error: observer.error };
|
|
330
312
|
sub.observers.add(registration);
|
|
331
|
-
if (sub.
|
|
332
|
-
notify({ ...sub, observers: new Set([observer]) }, "next", sub.
|
|
313
|
+
if (sub.snapshot !== undefined)
|
|
314
|
+
notify({ ...sub, observers: new Set([observer]) }, "next", sub.snapshot);
|
|
333
315
|
connect();
|
|
334
316
|
let unsubscribed = false;
|
|
335
317
|
return () => {
|
|
@@ -367,8 +349,7 @@ export function createClient(options) {
|
|
|
367
349
|
socket = undefined;
|
|
368
350
|
old?.close(1000, "Authorization changed");
|
|
369
351
|
for (const sub of subscriptions.values()) {
|
|
370
|
-
sub.
|
|
371
|
-
sub.value = undefined;
|
|
352
|
+
sub.snapshot = undefined;
|
|
372
353
|
sub.revision = -1;
|
|
373
354
|
sub.terminal = false;
|
|
374
355
|
notify(sub, "error", new FlareError("AUTH_CHANGED", "Authorization changed; cached value invalidated"));
|
package/dist/sdk/cloudflare.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { type FunctionTree } from "./server.js";
|
|
|
3
3
|
import { type Auth, type FunctionKind, type Json } from "./protocol.js";
|
|
4
4
|
import { type Schema } from "./schema.js";
|
|
5
5
|
export interface Invocation {
|
|
6
|
+
/** Trusted Runtime admission only, never copied from public call input. */
|
|
7
|
+
mcp?: string;
|
|
6
8
|
path: string;
|
|
7
9
|
kind: FunctionKind;
|
|
8
10
|
args: Json;
|
|
@@ -98,6 +100,8 @@ export interface ScopedCapability {
|
|
|
98
100
|
}): Promise<string>;
|
|
99
101
|
download(id: string): Promise<Response>;
|
|
100
102
|
fileCommand(op: string, input: unknown): Promise<any>;
|
|
103
|
+
codexCommand(op: string, input: unknown): Promise<any>;
|
|
104
|
+
searchCommand(op: string, input: unknown): Promise<any>;
|
|
101
105
|
scheduleCommand(op: string, input: unknown): Promise<any>;
|
|
102
106
|
}
|
|
103
107
|
/** Construct the class at build time. Parent supplies a verified identity and release ID,
|
package/dist/sdk/cloudflare.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { codexContext } from "./codex.js";
|
|
2
|
+
import { searchContext } from "./search.js";
|
|
1
3
|
import { DurableObject, WorkerEntrypoint } from "cloudflare:workers";
|
|
2
4
|
import {
|
|
3
5
|
flatten,
|
|
@@ -102,7 +104,7 @@ export function createFacet(app) {
|
|
|
102
104
|
}
|
|
103
105
|
let result = null;
|
|
104
106
|
const changed = new Set;
|
|
105
|
-
await this.ctx.storage.transaction(async () => {
|
|
107
|
+
const revision = await this.ctx.storage.transaction(async () => {
|
|
106
108
|
let active = true;
|
|
107
109
|
const scheduled = [];
|
|
108
110
|
const jobs = Object.freeze({
|
|
@@ -136,9 +138,7 @@ export function createFacet(app) {
|
|
|
136
138
|
if (!active)
|
|
137
139
|
throw new FlareError("CAPABILITY_EXPIRED", "Mutation invocation ended");
|
|
138
140
|
const id = crypto.randomUUID();
|
|
139
|
-
this.ctx.storage.sql.exec("INSERT INTO _flared_outbox (id,release,path,args,auth,dueAt) VALUES (
|
|
140
|
-
if (target.auth === "internal")
|
|
141
|
-
this.ctx.storage.sql.exec("UPDATE _flared_outbox SET internal=1,auth='null' WHERE id=?", id);
|
|
141
|
+
this.ctx.storage.sql.exec("INSERT INTO _flared_outbox (id,release,path,args,auth,dueAt,internal) VALUES (?,?,?,?,?,?,?)", id, this.env.RELEASE_ID, path, JSON.stringify(input), JSON.stringify(target.auth === "internal" ? null : call.auth), Date.now() + Math.ceil(delay * 1000), Number(target.auth === "internal"));
|
|
142
142
|
return id;
|
|
143
143
|
})();
|
|
144
144
|
scheduled.push(pending);
|
|
@@ -161,16 +161,17 @@ export function createFacet(app) {
|
|
|
161
161
|
capability.revoke();
|
|
162
162
|
await Promise.allSettled(scheduled);
|
|
163
163
|
}
|
|
164
|
-
this.commitRevision(changed);
|
|
164
|
+
return this.commitRevision(changed);
|
|
165
165
|
});
|
|
166
166
|
await this.ctx.storage.sync();
|
|
167
|
-
return { result, revision
|
|
167
|
+
return { result, revision, tables: [...changed] };
|
|
168
168
|
});
|
|
169
169
|
}
|
|
170
170
|
commitRevision(changed) {
|
|
171
|
-
this.ctx.storage.sql.exec("UPDATE _flared_revision SET value=value+1 WHERE id=1");
|
|
171
|
+
const revision = Number(this.ctx.storage.sql.exec("UPDATE _flared_revision SET value=value+1 WHERE id=1 RETURNING value").one().value);
|
|
172
172
|
for (const name of changed)
|
|
173
|
-
this.ctx.storage.sql.exec("INSERT INTO _flared_changes VALUES (?,?) ON CONFLICT(name) DO UPDATE SET revision=excluded.revision", name,
|
|
173
|
+
this.ctx.storage.sql.exec("INSERT INTO _flared_changes VALUES (?,?) ON CONFLICT(name) DO UPDATE SET revision=excluded.revision", name, revision);
|
|
174
|
+
return revision;
|
|
174
175
|
}
|
|
175
176
|
pendingJobs(limit = 100) {
|
|
176
177
|
return this.serial(async () => {
|
|
@@ -295,7 +296,7 @@ export function createFacet(app) {
|
|
|
295
296
|
boundedJson(input, LIMITS.inputBytes);
|
|
296
297
|
let rows = [];
|
|
297
298
|
const changed = new Set;
|
|
298
|
-
await this.ctx.storage.transaction(async () => {
|
|
299
|
+
const revision = await this.ctx.storage.transaction(async () => {
|
|
299
300
|
if (input.expectedRevision !== this.revision())
|
|
300
301
|
throw new FlareError("CONFLICT", "Data changed; reload before editing");
|
|
301
302
|
const capability = database(this.ctx.storage.sql, app.schema, true, new Set, changed);
|
|
@@ -317,10 +318,10 @@ export function createFacet(app) {
|
|
|
317
318
|
} finally {
|
|
318
319
|
capability.revoke();
|
|
319
320
|
}
|
|
320
|
-
this.commitRevision(changed);
|
|
321
|
+
return this.commitRevision(changed);
|
|
321
322
|
});
|
|
322
323
|
await this.ctx.storage.sync();
|
|
323
|
-
return { rows, revision
|
|
324
|
+
return { rows, revision };
|
|
324
325
|
});
|
|
325
326
|
}
|
|
326
327
|
};
|
|
@@ -360,6 +361,8 @@ export function createAction(app) {
|
|
|
360
361
|
const capability = this.env.CAPABILITY;
|
|
361
362
|
const context = {
|
|
362
363
|
auth: call.auth,
|
|
364
|
+
codex: codexContext((op, input) => rpcData(capability.codexCommand(op, input))),
|
|
365
|
+
search: searchContext((op, input) => rpcData(capability.searchCommand(op, input))),
|
|
363
366
|
schedules: {
|
|
364
367
|
set: (input) => rpcData(capability.scheduleCommand("set", input)),
|
|
365
368
|
cancel: (name) => rpcData(capability.scheduleCommand("cancel", { name })),
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface CodexTaskInput {
|
|
2
|
+
/** Stable UUID for retries of the same submission; retained for seven days. */
|
|
3
|
+
id: string;
|
|
4
|
+
prompt: string;
|
|
5
|
+
}
|
|
6
|
+
export interface CodexTask {
|
|
7
|
+
id: string;
|
|
8
|
+
state: "running" | "succeeded" | "failed" | "cancelled" | "interrupted";
|
|
9
|
+
startedAt: number;
|
|
10
|
+
finishedAt?: number;
|
|
11
|
+
result?: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface CodexContext {
|
|
15
|
+
start(input: CodexTaskInput): Promise<CodexTask>;
|
|
16
|
+
status(id: string): Promise<CodexTask | null>;
|
|
17
|
+
cancel(id: string): Promise<CodexTask | null>;
|
|
18
|
+
}
|
|
19
|
+
export declare function codexInput(op: string, input: any): void;
|
|
20
|
+
export declare function codexContext(call: (op: string, input: unknown) => Promise<any>): CodexContext;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { FlareError, boundedJson } from "./protocol.js";
|
|
2
|
+
export function codexInput(op, input) {
|
|
3
|
+
boundedJson(input, 20000);
|
|
4
|
+
if (!input || Array.isArray(input) || !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(input.id) || !["start", "status", "cancel"].includes(op) || Object.keys(input).some((k) => !["id", ...op === "start" ? ["prompt"] : []].includes(k)) || op === "start" && (typeof input.prompt !== "string" || !input.prompt.trim() || new TextEncoder().encode(input.prompt).length > 16384))
|
|
5
|
+
throw new FlareError("INVALID_ARGUMENT", "Invalid Codex task");
|
|
6
|
+
}
|
|
7
|
+
export function codexContext(call) {
|
|
8
|
+
const invoke = (op, input) => {
|
|
9
|
+
codexInput(op, input);
|
|
10
|
+
return call(op, input);
|
|
11
|
+
};
|
|
12
|
+
return {
|
|
13
|
+
start: (input) => invoke("start", input),
|
|
14
|
+
status: (id) => invoke("status", { id }),
|
|
15
|
+
cancel: (id) => invoke("cancel", { id })
|
|
16
|
+
};
|
|
17
|
+
}
|
package/dist/sdk/database.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { FlareError, boundedJson, stableJson } from "./protocol.js";
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
documentJson,
|
|
4
4
|
fieldSql,
|
|
5
5
|
quote
|
|
6
6
|
} from "./schema.js";
|
|
@@ -64,7 +64,7 @@ export function database(sql, schema, writable, touched = new Set, changed = new
|
|
|
64
64
|
function query(name) {
|
|
65
65
|
const definition = table(name);
|
|
66
66
|
function make(index, filters, direction) {
|
|
67
|
-
const page = (options) => task(async () => {
|
|
67
|
+
const page = (options, withCursor = true) => task(async () => {
|
|
68
68
|
table(name);
|
|
69
69
|
size(options.limit);
|
|
70
70
|
const fields = [
|
|
@@ -73,9 +73,18 @@ export function database(sql, schema, writable, touched = new Set, changed = new
|
|
|
73
73
|
"_id"
|
|
74
74
|
];
|
|
75
75
|
const expressions = fields.map(fieldSql);
|
|
76
|
-
const signature = stableJson({ name, index, filters, direction });
|
|
76
|
+
const signature = withCursor ? stableJson({ name, index, filters, direction }) : "";
|
|
77
|
+
const fixed = filters.filter((f) => f.operator === "=");
|
|
77
78
|
const where = filters.map((f) => `${fieldSql(f.field)} ${f.operator === "=" ? "IS" : f.operator} ?`);
|
|
78
79
|
const bindings = filters.map((f) => bind(f.value));
|
|
80
|
+
const rowLimit = options.limit + (withCursor ? 1 : 0);
|
|
81
|
+
const rows = [];
|
|
82
|
+
const read = (terms = [], values = [], orderStart = fixed.length) => {
|
|
83
|
+
if (rows.length >= rowLimit)
|
|
84
|
+
return;
|
|
85
|
+
const conditions = [...terms, ...where];
|
|
86
|
+
rows.push(...sql.exec(`SELECT *${withCursor ? "," + expressions.map((e, i) => `${e} AS __k${i}`).join(",") : ""} FROM ${quote(name)} ${conditions.length ? "WHERE " + conditions.join(" AND ") : ""} ORDER BY ${expressions.slice(orderStart).map((e) => `${e} ${direction}`).join(",")} LIMIT ?`, ...values, ...bindings, rowLimit - rows.length).toArray());
|
|
87
|
+
};
|
|
79
88
|
if (options.cursor != null) {
|
|
80
89
|
let token;
|
|
81
90
|
try {
|
|
@@ -85,23 +94,32 @@ export function database(sql, schema, writable, touched = new Set, changed = new
|
|
|
85
94
|
} catch {
|
|
86
95
|
throw new FlareError("INVALID_CURSOR", "Invalid cursor");
|
|
87
96
|
}
|
|
88
|
-
if (token.signature !== signature || !Array.isArray(token.values) || token.values.length !== fields.length || !token.values.every(scalar))
|
|
97
|
+
if (!token || token.signature !== signature || !Array.isArray(token.values) || token.values.length !== fields.length || !token.values.every(scalar) || typeof token.values.at(-2) !== "number" || typeof token.values.at(-1) !== "string")
|
|
89
98
|
throw new FlareError("INVALID_CURSOR", "Cursor belongs to a different query");
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
if (fixed.some((f, i) => bind(f.value) !== bind(token.values[i])))
|
|
100
|
+
throw new FlareError("INVALID_CURSOR", "Cursor violates index range");
|
|
101
|
+
const first = fixed.length;
|
|
102
|
+
for (let i = expressions.length - 2;i >= first && rows.length < rowLimit; i--) {
|
|
103
|
+
const prefix = expressions.slice(first, i).map((e) => `${e} IS ?`);
|
|
104
|
+
const values = token.values.slice(first, i).map(bind);
|
|
105
|
+
const expr = expressions[i], value = token.values[i];
|
|
106
|
+
if (i === expressions.length - 2) {
|
|
107
|
+
read([
|
|
108
|
+
...prefix,
|
|
109
|
+
`(${expressions.slice(i).join(",")}) ${direction === "asc" ? ">" : "<"} (?,?)`
|
|
110
|
+
], [...values, ...token.values.slice(i).map(bind)], i);
|
|
111
|
+
} else if (direction === "asc") {
|
|
112
|
+
read([
|
|
113
|
+
...prefix,
|
|
114
|
+
value === null ? `${expr} IS NOT NULL` : `${expr} > ?`
|
|
115
|
+
], value === null ? values : [...values, bind(value)], i);
|
|
116
|
+
} else if (value !== null) {
|
|
117
|
+
read([...prefix, `${expr} < ?`], [...values, bind(value)], i);
|
|
118
|
+
read([...prefix, `${expr} IS NULL`], values, i + 1);
|
|
99
119
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
const rows = sql.exec(`SELECT *,${expressions.map((e, i) => `${e} AS __k${i}`).join(",")} FROM ${quote(name)} ${where.length ? "WHERE " + where.join(" AND ") : ""} ORDER BY ${expressions.map((e) => `${e} ${direction}`).join(",")} LIMIT ?`, ...bindings, options.limit + 1).toArray();
|
|
120
|
+
}
|
|
121
|
+
} else
|
|
122
|
+
read();
|
|
105
123
|
let more = rows.length > options.limit;
|
|
106
124
|
if (more)
|
|
107
125
|
rows.pop();
|
|
@@ -117,7 +135,7 @@ export function database(sql, schema, writable, touched = new Set, changed = new
|
|
|
117
135
|
bytes += length;
|
|
118
136
|
}
|
|
119
137
|
const last = rows[docs.length - 1];
|
|
120
|
-
const continueCursor = more && last ? btoa(String.fromCharCode(...new TextEncoder().encode(boundedJson({
|
|
138
|
+
const continueCursor = withCursor && more && last ? btoa(String.fromCharCode(...new TextEncoder().encode(boundedJson({
|
|
121
139
|
signature,
|
|
122
140
|
values: expressions.map((_, i) => last[`__k${i}`])
|
|
123
141
|
}, 6144)))) : null;
|
|
@@ -174,14 +192,14 @@ export function database(sql, schema, writable, touched = new Set, changed = new
|
|
|
174
192
|
paginate: page,
|
|
175
193
|
take(limit) {
|
|
176
194
|
return task(async () => {
|
|
177
|
-
const result = await page({ limit });
|
|
195
|
+
const result = await page({ limit }, false);
|
|
178
196
|
if (!result.isDone && result.page.length < limit)
|
|
179
197
|
throw new FlareError("TOO_LARGE", "Use paginate for large documents");
|
|
180
198
|
return result.page;
|
|
181
199
|
}, false);
|
|
182
200
|
},
|
|
183
201
|
first() {
|
|
184
|
-
return task(async () => (await page({ limit: 1 })).page[0] ?? null, false);
|
|
202
|
+
return task(async () => (await page({ limit: 1 }, false)).page[0] ?? null, false);
|
|
185
203
|
}
|
|
186
204
|
});
|
|
187
205
|
}
|
|
@@ -192,10 +210,10 @@ export function database(sql, schema, writable, touched = new Set, changed = new
|
|
|
192
210
|
insert(name, input) {
|
|
193
211
|
return task(async () => {
|
|
194
212
|
writing();
|
|
195
|
-
const definition = table(name), value = await
|
|
213
|
+
const definition = table(name), value = await documentJson(definition, input);
|
|
196
214
|
writing();
|
|
197
215
|
const id = crypto.randomUUID();
|
|
198
|
-
sql.exec(`INSERT INTO ${quote(name)} VALUES (?,?,?)`, id, Date.now(),
|
|
216
|
+
sql.exec(`INSERT INTO ${quote(name)} VALUES (?,?,?)`, id, Date.now(), value);
|
|
199
217
|
changed.add(name);
|
|
200
218
|
return id;
|
|
201
219
|
});
|
|
@@ -211,12 +229,12 @@ export function database(sql, schema, writable, touched = new Set, changed = new
|
|
|
211
229
|
const row = sql.exec(`SELECT _data FROM ${quote(name)} WHERE _id=?`, id).toArray()[0];
|
|
212
230
|
if (!row)
|
|
213
231
|
throw new FlareError("NOT_FOUND", "Document not found");
|
|
214
|
-
const value = await
|
|
232
|
+
const value = await documentJson(definition, {
|
|
215
233
|
...JSON.parse(String(row._data)),
|
|
216
234
|
...patch
|
|
217
235
|
});
|
|
218
236
|
writing();
|
|
219
|
-
sql.exec(`UPDATE ${quote(name)} SET _data=? WHERE _id=?`,
|
|
237
|
+
sql.exec(`UPDATE ${quote(name)} SET _data=? WHERE _id=?`, value, id);
|
|
220
238
|
changed.add(name);
|
|
221
239
|
});
|
|
222
240
|
},
|
|
@@ -225,9 +243,9 @@ export function database(sql, schema, writable, touched = new Set, changed = new
|
|
|
225
243
|
writing();
|
|
226
244
|
const definition = table(name);
|
|
227
245
|
checkedId(id);
|
|
228
|
-
const value = await
|
|
246
|
+
const value = await documentJson(definition, input);
|
|
229
247
|
writing();
|
|
230
|
-
const rows = sql.exec(`UPDATE ${quote(name)} SET _data=? WHERE _id=? RETURNING _id`,
|
|
248
|
+
const rows = sql.exec(`UPDATE ${quote(name)} SET _data=? WHERE _id=? RETURNING _id`, value, id).toArray();
|
|
231
249
|
if (!rows.length)
|
|
232
250
|
throw new FlareError("NOT_FOUND", "Document not found");
|
|
233
251
|
changed.add(name);
|
package/dist/sdk/http.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export async function readBody(body, limit, tooLarge) {
|
|
2
|
+
const reader = body.getReader();
|
|
3
|
+
const chunks = [];
|
|
4
|
+
let size = 0;
|
|
5
|
+
try {
|
|
6
|
+
for (;; ) {
|
|
7
|
+
const { done, value } = await reader.read();
|
|
8
|
+
if (done)
|
|
9
|
+
break;
|
|
10
|
+
size += value.byteLength;
|
|
11
|
+
if (size > limit)
|
|
12
|
+
tooLarge();
|
|
13
|
+
chunks.push(value);
|
|
14
|
+
}
|
|
15
|
+
} finally {
|
|
16
|
+
await reader.cancel().catch(() => {});
|
|
17
|
+
}
|
|
18
|
+
if (chunks.length === 1)
|
|
19
|
+
return chunks[0];
|
|
20
|
+
const bytes = new Uint8Array(size);
|
|
21
|
+
let offset = 0;
|
|
22
|
+
for (const chunk of chunks) {
|
|
23
|
+
bytes.set(chunk, offset);
|
|
24
|
+
offset += chunk.byteLength;
|
|
25
|
+
}
|
|
26
|
+
return bytes;
|
|
27
|
+
}
|
package/dist/sdk/protocol.d.ts
CHANGED
|
@@ -34,7 +34,13 @@ export declare function reference<I, O>(path: string, kind: FunctionKind): Funct
|
|
|
34
34
|
/** Lossless JSON only: no implicit Date, undefined, NaN or prototype coercion. */
|
|
35
35
|
export declare function stableJson(value: unknown): string;
|
|
36
36
|
export declare function boundedJson(value: unknown, maxBytes: number): string;
|
|
37
|
+
export type McpTool = {
|
|
38
|
+
name: string;
|
|
39
|
+
description: string;
|
|
40
|
+
};
|
|
41
|
+
export declare function assertMcpTools(functions: Record<string, FunctionContract>): void;
|
|
37
42
|
export interface FunctionContract {
|
|
43
|
+
mcp?: McpTool;
|
|
38
44
|
kind: FunctionKind;
|
|
39
45
|
auth: "required" | "public" | "internal";
|
|
40
46
|
input: Record<string, unknown>;
|
package/dist/sdk/protocol.js
CHANGED
|
@@ -74,3 +74,15 @@ export function boundedJson(value, maxBytes) {
|
|
|
74
74
|
throw new FlareError("TOO_LARGE", "JSON payload exceeds limit");
|
|
75
75
|
return json;
|
|
76
76
|
}
|
|
77
|
+
export function assertMcpTools(functions) {
|
|
78
|
+
const names = new Set;
|
|
79
|
+
for (const fn of Object.values(functions)) {
|
|
80
|
+
if (fn.mcp === undefined)
|
|
81
|
+
continue;
|
|
82
|
+
const m = fn.mcp;
|
|
83
|
+
if (!m || typeof m !== "object" || Object.keys(m).sort().join() !== "description,name" || typeof m.name !== "string" || !/^[A-Za-z][A-Za-z0-9_.-]{0,63}$/.test(m.name) || typeof m.description !== "string" || !m.description.trim() || m.description.length > 1000 || names.has(m.name) || fn.auth === "internal" || fn.input.type !== "object")
|
|
84
|
+
throw new FlareError("INVALID_CONTRACT", "Invalid MCP tool contract");
|
|
85
|
+
boundedJson({ mcp: m, input: fn.input }, 16384);
|
|
86
|
+
names.add(m.name);
|
|
87
|
+
}
|
|
88
|
+
}
|
package/dist/sdk/schema.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { StandardSchemaV1, StandardJSONSchemaV1 } from "@standard-schema/spec";
|
|
2
|
-
import { type Json } from "./protocol.js";
|
|
3
2
|
export type SqlValue = ArrayBuffer | string | number | null;
|
|
4
3
|
/** The small structural portion of native storage needed by the adapter; no Worker globals in public types. */
|
|
5
4
|
export interface NativeSql {
|
|
@@ -39,5 +38,6 @@ export declare function defineTable<V extends Validator>(validator: V): Table<V,
|
|
|
39
38
|
export declare function schemaContract(schema: Schema): SchemaContract;
|
|
40
39
|
export declare function assertSchemaContract(contract: SchemaContract): void;
|
|
41
40
|
export declare function defineSchema<const S extends Schema>(schema: S): S;
|
|
42
|
-
|
|
41
|
+
/** Validate once and return the canonical JSON written by all storage paths. */
|
|
42
|
+
export declare function documentJson(table: TableDefinition, input: unknown): Promise<string>;
|
|
43
43
|
export declare function applySchema(storage: NativeStorage, schema: Schema): Promise<void>;
|
package/dist/sdk/schema.js
CHANGED
|
@@ -92,7 +92,7 @@ function documentSchema(table) {
|
|
|
92
92
|
}
|
|
93
93
|
return doc;
|
|
94
94
|
}
|
|
95
|
-
export async function
|
|
95
|
+
export async function documentJson(table, input) {
|
|
96
96
|
boundedJson(input, 65536);
|
|
97
97
|
const properties = documentSchema(table).properties ?? {};
|
|
98
98
|
if (!input || Array.isArray(input) || typeof input !== "object" || Object.keys(input).some((k) => !Object.hasOwn(properties, k)))
|
|
@@ -100,7 +100,7 @@ export async function documentValue(table, input) {
|
|
|
100
100
|
const result = await table.validator["~standard"].validate(input);
|
|
101
101
|
if (result.issues || !result.value || Array.isArray(result.value) || typeof result.value !== "object" || Object.keys(result.value).some((k) => !Object.hasOwn(properties, k)))
|
|
102
102
|
throw new FlareError("INVALID_INPUT", "Document failed schema validation");
|
|
103
|
-
return
|
|
103
|
+
return boundedJson(result.value, 65536);
|
|
104
104
|
}
|
|
105
105
|
export async function applySchema(storage, schema) {
|
|
106
106
|
const contract = schemaContract(schema), encoded = stableJson(contract);
|
|
@@ -118,13 +118,14 @@ export async function applySchema(storage, schema) {
|
|
|
118
118
|
}
|
|
119
119
|
for (const [name, definition] of Object.entries(contract)) {
|
|
120
120
|
storage.sql.exec(`CREATE TABLE IF NOT EXISTS ${quote(name)} (_id TEXT PRIMARY KEY, _creationTime REAL NOT NULL, _data TEXT NOT NULL CHECK(json_valid(_data)))`);
|
|
121
|
+
const changed = !old[name] || stableJson(old[name].document) !== stableJson(definition.document);
|
|
121
122
|
let cursor = "";
|
|
122
|
-
while (
|
|
123
|
+
while (changed) {
|
|
123
124
|
const rows = storage.sql.exec(`SELECT _id,_data FROM ${quote(name)} WHERE _id > ? ORDER BY _id LIMIT 100`, cursor).toArray();
|
|
124
125
|
for (const row of rows) {
|
|
125
|
-
const value = await
|
|
126
|
-
if (
|
|
127
|
-
storage.sql.exec(`UPDATE ${quote(name)} SET _data=? WHERE _id=?`,
|
|
126
|
+
const value = await documentJson(schema[name], JSON.parse(String(row._data)));
|
|
127
|
+
if (value !== stableJson(JSON.parse(String(row._data))))
|
|
128
|
+
storage.sql.exec(`UPDATE ${quote(name)} SET _data=? WHERE _id=?`, value, row._id);
|
|
128
129
|
}
|
|
129
130
|
if (rows.length < 100)
|
|
130
131
|
break;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type Json } from "./protocol.js";
|
|
2
|
+
export type SearchState = {
|
|
3
|
+
fileId: string;
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
status: "excluded" | "pending" | "sending" | "queued" | "running" | "completed" | "error" | "skipped" | "outdated" | "deleting";
|
|
6
|
+
error: string | null;
|
|
7
|
+
};
|
|
8
|
+
export type SearchHit = {
|
|
9
|
+
fileId: string;
|
|
10
|
+
name: string;
|
|
11
|
+
text: string;
|
|
12
|
+
score: number;
|
|
13
|
+
};
|
|
14
|
+
export interface SearchContext {
|
|
15
|
+
index(fileId: string): Promise<SearchState>;
|
|
16
|
+
exclude(fileId: string): Promise<SearchState>;
|
|
17
|
+
status(fileId: string): Promise<SearchState>;
|
|
18
|
+
query(input: {
|
|
19
|
+
query: string;
|
|
20
|
+
limit?: number;
|
|
21
|
+
fileIds?: string[];
|
|
22
|
+
}, authorize: (fileIds: string[]) => Promise<string[]>): Promise<SearchHit[]>;
|
|
23
|
+
}
|
|
24
|
+
/** Runs in the trusted action, never in browser code. ACLs remain application data. */
|
|
25
|
+
export declare function searchContext(command: (op: string, input: Json) => Promise<any>): SearchContext;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { FlareError } from "./protocol.js";
|
|
2
|
+
export function searchContext(command) {
|
|
3
|
+
return {
|
|
4
|
+
index: (fileId) => command("index", { fileId }),
|
|
5
|
+
exclude: (fileId) => command("exclude", { fileId }),
|
|
6
|
+
status: (fileId) => command("status", { fileId }),
|
|
7
|
+
async query(input, authorize) {
|
|
8
|
+
if (typeof authorize !== "function")
|
|
9
|
+
throw new FlareError("FORBIDDEN", "A current file access check is required");
|
|
10
|
+
const hits = await command("query", input);
|
|
11
|
+
if (!hits.length)
|
|
12
|
+
return [];
|
|
13
|
+
const ids = [...new Set(hits.map((h) => h.fileId))];
|
|
14
|
+
const allowed = await authorize(ids);
|
|
15
|
+
if (!Array.isArray(allowed) || allowed.some((id) => !ids.includes(id)))
|
|
16
|
+
throw new FlareError("FORBIDDEN", "Invalid file access result");
|
|
17
|
+
const current = await command("validate", { fileIds: allowed });
|
|
18
|
+
const visible = new Set(current);
|
|
19
|
+
return hits.filter((h) => visible.has(h.fileId));
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|