@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.
Files changed (50) hide show
  1. package/README.md +3 -1
  2. package/dist/cli/{chunk-0s0vrjtw.js → chunk-1yvbecac.js} +97 -11
  3. package/dist/cli/{chunk-5yg1zj5n.js → chunk-7ym0f672.js} +6 -6
  4. package/dist/cli/{chunk-5s6zv1h9.js → chunk-8gzfscca.js} +5 -7
  5. package/dist/cli/chunk-xn3e3gev.js +9 -0
  6. package/dist/cli/{chunk-vx7kmczm.js → chunk-yhvbksrm.js} +32 -7
  7. package/dist/cli/flare.js +5 -5
  8. package/dist/platform/admin.js +35 -24
  9. package/dist/platform/dashboard/_astro/ai-view.DxgwI4OB.js +1 -0
  10. package/dist/platform/dashboard/_astro/{auth-view.EASEF9BN.js → auth-view.C16WPVOx.js} +1 -1
  11. package/dist/platform/dashboard/_astro/{cli-approval.DwZVHdyo.js → cli-approval.emPTVRqn.js} +1 -1
  12. package/dist/platform/dashboard/_astro/client.4z54xJ_z.js +1 -0
  13. package/dist/platform/dashboard/_astro/{common.TAClb6G9.js → common.9pujiaSZ.js} +1 -1
  14. package/dist/platform/dashboard/_astro/{data-view.BaHEU4Xa.js → data-view.BCmdi_cs.js} +1 -1
  15. package/dist/platform/dashboard/_astro/{functions-view.BGOYuDwu.js → functions-view.Cf640vhX.js} +1 -1
  16. package/dist/platform/dashboard/_astro/index.CDrYoVkO.css +2 -0
  17. package/dist/platform/dashboard/_astro/index.astro_astro_type_script_index_0_lang.cFeBPGtv.js +3 -0
  18. package/dist/platform/dashboard/_astro/input-group.Dsc4z0iT.js +1 -0
  19. package/dist/platform/dashboard/_astro/{sdk-view.2ErPn4cO.js → sdk-view.Cs96E0YV.js} +1 -1
  20. package/dist/platform/dashboard/_astro/{services-view.iZujitxl.js → services-view.DhwKE_RK.js} +1 -1
  21. package/dist/platform/dashboard/_astro/{settings-view.CqK-mrlB.js → settings-view.Dsc34AAW.js} +1 -1
  22. package/dist/platform/dashboard/bootstrap/action.js +43 -0
  23. package/dist/platform/dashboard/bootstrap/facet.js +64 -46
  24. package/dist/platform/dashboard/bootstrap/openapi.json +1 -1
  25. package/dist/platform/dashboard/bootstrap/release.json +3 -3
  26. package/dist/platform/dashboard/index.html +1 -1
  27. package/dist/platform/manifest.json +48 -43
  28. package/dist/platform/runtime.js +24267 -865
  29. package/dist/sdk/backup.js +4 -4
  30. package/dist/sdk/client.js +22 -41
  31. package/dist/sdk/cloudflare.d.ts +4 -0
  32. package/dist/sdk/cloudflare.js +14 -11
  33. package/dist/sdk/codex.d.ts +20 -0
  34. package/dist/sdk/codex.js +17 -0
  35. package/dist/sdk/database.js +45 -27
  36. package/dist/sdk/http.d.ts +2 -0
  37. package/dist/sdk/http.js +27 -0
  38. package/dist/sdk/protocol.d.ts +6 -0
  39. package/dist/sdk/protocol.js +12 -0
  40. package/dist/sdk/schema.d.ts +2 -2
  41. package/dist/sdk/schema.js +7 -6
  42. package/dist/sdk/search.d.ts +25 -0
  43. package/dist/sdk/search.js +22 -0
  44. package/dist/sdk/server.d.ts +14 -1
  45. package/dist/sdk/server.js +9 -1
  46. package/package.json +2 -2
  47. package/dist/platform/dashboard/_astro/client.BUUZgEFj.js +0 -1
  48. package/dist/platform/dashboard/_astro/index.BPQdjJ4j.css +0 -2
  49. package/dist/platform/dashboard/_astro/index.astro_astro_type_script_index_0_lang.Blg5hU9g.js +0 -3
  50. package/dist/platform/dashboard/_astro/input-group.DUp-h0xx.js +0 -1
@@ -1,6 +1,6 @@
1
1
  import { boundedJson, FlareError, stableJson } from "./protocol.js";
2
2
  import {
3
- documentValue,
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 documentValue(schema[name], doc.value);
70
- if (stableJson(value) !== stableJson(doc.value))
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, stableJson(value));
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);
@@ -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: options.signal ?? AbortSignal.timeout(options.timeoutMs ?? 120000)
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.json().catch(() => null);
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(stableJson(value)) : value);
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 reader = response.body.getReader();
114
- let bytes = 0;
115
- const chunks = [];
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.hasValue = false;
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.hasValue = false;
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 = !sub.hasValue || stableJson(sub.value) !== json;
214
+ const changed = sub.snapshot !== json;
229
215
  sub.revision = frame.revision;
230
- sub.value = frame.result;
231
- sub.hasValue = true;
216
+ sub.snapshot = json;
232
217
  if (changed)
233
- notify(sub, "next", sub.value);
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.hasValue = false;
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.hasValue = false;
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.hasValue)
332
- notify({ ...sub, observers: new Set([observer]) }, "next", sub.value);
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.hasValue = false;
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"));
@@ -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,
@@ -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 (?,?,?,?,?,?)", id, this.env.RELEASE_ID, path, JSON.stringify(input), JSON.stringify(call.auth), Date.now() + Math.ceil(delay * 1000));
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: this.revision(), tables: [...changed] };
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, this.revision());
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: this.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
+ }
@@ -1,6 +1,6 @@
1
1
  import { FlareError, boundedJson, stableJson } from "./protocol.js";
2
2
  import {
3
- documentValue,
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
- const terms = expressions.map((expr, i) => {
91
- const prefix = expressions.slice(0, i).map((e) => `${e} IS ?`);
92
- bindings.push(...token.values.slice(0, i).map(bind));
93
- const value = token.values[i];
94
- if (value === null)
95
- prefix.push(direction === "asc" ? `${expr} IS NOT NULL` : "0");
96
- else {
97
- prefix.push(direction === "asc" ? `${expr} > ?` : `(${expr} < ? OR ${expr} IS NULL)`);
98
- bindings.push(bind(value));
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
- return "(" + prefix.join(" AND ") + ")";
101
- });
102
- where.push("(" + terms.join(" OR ") + ")");
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 documentValue(definition, input);
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(), stableJson(value));
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 documentValue(definition, {
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=?`, stableJson(value), 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 documentValue(definition, input);
246
+ const value = await documentJson(definition, input);
229
247
  writing();
230
- const rows = sql.exec(`UPDATE ${quote(name)} SET _data=? WHERE _id=? RETURNING _id`, stableJson(value), id).toArray();
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);
@@ -0,0 +1,2 @@
1
+ /** Internal bounded transport reader shared by the SDK and platform. */
2
+ export declare function readBody(body: ReadableStream<Uint8Array>, limit: number, tooLarge: () => never): Promise<Uint8Array>;
@@ -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
+ }
@@ -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>;
@@ -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
+ }
@@ -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
- export declare function documentValue(table: TableDefinition, input: unknown): Promise<Record<string, Json>>;
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>;
@@ -92,7 +92,7 @@ function documentSchema(table) {
92
92
  }
93
93
  return doc;
94
94
  }
95
- export async function documentValue(table, input) {
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 JSON.parse(boundedJson(result.value, 65536));
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 (!old[name] || stableJson(old[name].document) !== stableJson(definition.document)) {
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 documentValue(schema[name], JSON.parse(String(row._data)));
126
- if (stableJson(value) !== stableJson(JSON.parse(String(row._data))))
127
- storage.sql.exec(`UPDATE ${quote(name)} SET _data=? WHERE _id=?`, stableJson(value), row._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
+ }