@auersperg/flare 0.3.10 → 0.3.12

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 (44) hide show
  1. package/dist/cli/{chunk-m0698kap.js → chunk-1yvbecac.js} +88 -5
  2. package/dist/cli/{chunk-znktbexz.js → chunk-7ym0f672.js} +6 -6
  3. package/dist/cli/{chunk-jrptpjmm.js → chunk-8gzfscca.js} +5 -7
  4. package/dist/cli/chunk-xn3e3gev.js +9 -0
  5. package/dist/cli/{chunk-zt96ghm7.js → chunk-yhvbksrm.js} +20 -1
  6. package/dist/cli/flare.js +4 -4
  7. package/dist/platform/admin.js +5 -2
  8. package/dist/platform/dashboard/_astro/ai-view.De8j8CVD.js +1 -0
  9. package/dist/platform/dashboard/_astro/{auth-view.BhUFPmNv.js → auth-view.C9j0-OzH.js} +1 -1
  10. package/dist/platform/dashboard/_astro/client.BvQ8bQkQ.js +4 -0
  11. package/dist/platform/dashboard/_astro/{data-view.C-VU8Oxm.js → data-view.Bw2dG8Wg.js} +1 -1
  12. package/dist/platform/dashboard/_astro/{functions-view.3yLrtS0h.js → functions-view.Bsazyfej.js} +1 -1
  13. package/dist/platform/dashboard/_astro/index.CDrYoVkO.css +2 -0
  14. package/dist/platform/dashboard/_astro/index.astro_astro_type_script_index_0_lang.C_U84Sz-.js +3 -0
  15. package/dist/platform/dashboard/_astro/input-group.Dsc4z0iT.js +1 -0
  16. package/dist/platform/dashboard/_astro/{sdk-view.BAiQKNm8.js → sdk-view.vdDnLZ5g.js} +1 -1
  17. package/dist/platform/dashboard/_astro/{services-view.BSrqpyr5.js → services-view.C1FjN8NY.js} +1 -1
  18. package/dist/platform/dashboard/_astro/{settings-view.C_eYjlIE.js → settings-view.DZldw4Vy.js} +1 -1
  19. package/dist/platform/dashboard/bootstrap/action.js +43 -0
  20. package/dist/platform/dashboard/bootstrap/openapi.json +1 -1
  21. package/dist/platform/dashboard/bootstrap/release.json +2 -2
  22. package/dist/platform/dashboard/index.html +1 -1
  23. package/dist/platform/manifest.json +43 -38
  24. package/dist/platform/runtime.js +24294 -836
  25. package/dist/sdk/client.d.ts +6 -0
  26. package/dist/sdk/client.js +29 -0
  27. package/dist/sdk/cloudflare.d.ts +4 -0
  28. package/dist/sdk/cloudflare.js +4 -0
  29. package/dist/sdk/codex-stream.d.ts +28 -0
  30. package/dist/sdk/codex-stream.js +147 -0
  31. package/dist/sdk/codex.d.ts +20 -0
  32. package/dist/sdk/codex.js +17 -0
  33. package/dist/sdk/protocol.d.ts +6 -0
  34. package/dist/sdk/protocol.js +12 -0
  35. package/dist/sdk/react.d.ts +4 -0
  36. package/dist/sdk/search.d.ts +25 -0
  37. package/dist/sdk/search.js +22 -0
  38. package/dist/sdk/server.d.ts +15 -1
  39. package/dist/sdk/server.js +9 -1
  40. package/package.json +1 -1
  41. package/dist/platform/dashboard/_astro/client.4z54xJ_z.js +0 -1
  42. package/dist/platform/dashboard/_astro/index.BPQdjJ4j.css +0 -2
  43. package/dist/platform/dashboard/_astro/index.astro_astro_type_script_index_0_lang.Dsfj1oeZ.js +0 -3
  44. package/dist/platform/dashboard/_astro/input-group.BZ1TZT7L.js +0 -1
@@ -1,3 +1,5 @@
1
+ import { type CodexSubscription } from "./codex-stream.js";
2
+ export type { CodexEvent, CodexSubscription } from "./codex-stream.js";
1
3
  import { FlareError, type FunctionRef } from "./protocol.js";
2
4
  export { reference, FlareError } from "./protocol.js";
3
5
  export type { FunctionRef, Json } from "./protocol.js";
@@ -41,6 +43,10 @@ export declare function createClient(options: ClientOptions): {
41
43
  query: <I, O>(ref: FunctionRef<I, O>, args: I) => Promise<O>;
42
44
  mutation: <I, O>(ref: FunctionRef<I, O>, args: I) => Promise<O>;
43
45
  action: <I, O>(ref: FunctionRef<I, O>, args: I) => Promise<O>;
46
+ codexEvents<I>(ref: FunctionRef<I, CodexSubscription>, args: I, streamOptions?: {
47
+ signal?: AbortSignal;
48
+ after?: number;
49
+ }): AsyncGenerator<import("./codex-stream.js").CodexEvent, void, any>;
44
50
  /** One subscription delivers the initial snapshot and subsequent updates. */
45
51
  onUpdate<I, O>(ref: FunctionRef<I, O>, args: I, next: (value: O) => void, error?: (error: FlareError) => void): () => void;
46
52
  /** Call on login/logout/account switch; never reuse cached values across identities. */
@@ -1,3 +1,4 @@
1
+ import { readCodexEvents } from "./codex-stream.js";
1
2
  import { readBody } from "./http.js";
2
3
  import {
3
4
  assertPath,
@@ -279,6 +280,34 @@ export function createClient(options) {
279
280
  query: invoke("query"),
280
281
  mutation: invoke("mutation"),
281
282
  action: invoke("action"),
283
+ async* codexEvents(ref, args, streamOptions = {}) {
284
+ ensureOpen();
285
+ assertPath(ref.path);
286
+ const controller = new AbortController;
287
+ const abort = () => controller.abort();
288
+ cleanups.add(abort);
289
+ streamOptions.signal?.addEventListener("abort", abort, { once: true });
290
+ if (streamOptions.signal?.aborted)
291
+ abort();
292
+ try {
293
+ yield* readCodexEvents({
294
+ url: endpoint("/v1/codex/events"),
295
+ ref,
296
+ args,
297
+ after: streamOptions.after,
298
+ transport,
299
+ signal: controller.signal,
300
+ token: async () => {
301
+ ensureOpen();
302
+ return await options.getToken?.() ?? null;
303
+ }
304
+ });
305
+ } finally {
306
+ abort();
307
+ cleanups.delete(abort);
308
+ streamOptions.signal?.removeEventListener("abort", abort);
309
+ }
310
+ },
282
311
  onUpdate(ref, args, next, error) {
283
312
  const observer = { next, error };
284
313
  ensureOpen();
@@ -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,
@@ -359,6 +361,8 @@ export function createAction(app) {
359
361
  const capability = this.env.CAPABILITY;
360
362
  const context = {
361
363
  auth: call.auth,
364
+ codex: codexContext((op, input) => rpcData(capability.codexCommand(op, input))),
365
+ search: searchContext((op, input) => rpcData(capability.searchCommand(op, input))),
362
366
  schedules: {
363
367
  set: (input) => rpcData(capability.scheduleCommand("set", input)),
364
368
  cancel: (name) => rpcData(capability.scheduleCommand("cancel", { name })),
@@ -0,0 +1,28 @@
1
+ import { type FunctionRef } from "./protocol.js";
2
+ import type { CodexTask } from "./codex.js";
3
+ export type CodexSubscription = {
4
+ type: "codex-events";
5
+ id: string;
6
+ };
7
+ export type CodexEvent = {
8
+ type: "task";
9
+ task: CodexTask;
10
+ } | {
11
+ type: "status" | "text" | "output" | "item" | "truncated";
12
+ seq: number;
13
+ at: number;
14
+ itemId?: string;
15
+ text?: string;
16
+ kind?: string;
17
+ state?: string;
18
+ exitCode?: number | null;
19
+ };
20
+ export declare function readCodexEvents<I>(options: {
21
+ url: string;
22
+ ref: FunctionRef<I, CodexSubscription>;
23
+ args: I;
24
+ token: () => Promise<string | null>;
25
+ transport: typeof fetch;
26
+ signal: AbortSignal;
27
+ after?: number;
28
+ }): AsyncGenerator<CodexEvent>;
@@ -0,0 +1,147 @@
1
+ import {
2
+ FlareError,
3
+ boundedJson,
4
+ LIMITS
5
+ } from "./protocol.js";
6
+ import { readBody } from "./http.js";
7
+ export async function* readCodexEvents(options) {
8
+ if (options.ref.kind !== "query")
9
+ throw new FlareError("INVALID_KIND", "Stream authorization must be a read-only query");
10
+ let cursor = options.after ?? 0, failures = 0;
11
+ if (!Number.isSafeInteger(cursor) || cursor < 0 || cursor > 4096)
12
+ throw new FlareError("INVALID_ARGUMENT", "Invalid cursor");
13
+ const signal = options.signal;
14
+ while (!signal.aborted) {
15
+ let reader;
16
+ const connection = new AbortController;
17
+ const abort = () => connection.abort(signal.reason);
18
+ signal.addEventListener("abort", abort, { once: true });
19
+ if (signal.aborted)
20
+ abort();
21
+ let idle;
22
+ const touch = () => {
23
+ clearTimeout(idle);
24
+ idle = setTimeout(() => connection.abort(), 45000);
25
+ };
26
+ try {
27
+ touch();
28
+ const token = await options.token();
29
+ if (signal.aborted)
30
+ return;
31
+ const response = await options.transport(options.url, {
32
+ method: "POST",
33
+ redirect: "error",
34
+ signal: connection.signal,
35
+ headers: {
36
+ "Content-Type": "application/json",
37
+ ...token ? { Authorization: `Bearer ${token}` } : {}
38
+ },
39
+ body: boundedJson({
40
+ path: options.ref.path,
41
+ kind: "query",
42
+ args: options.args,
43
+ after: cursor
44
+ }, LIMITS.inputBytes)
45
+ });
46
+ if (!response.ok) {
47
+ const bytes = response.body ? await readBody(response.body, 8192, () => {
48
+ throw new FlareError("INVALID_RESPONSE", "Oversized error");
49
+ }) : new Uint8Array;
50
+ let code = `HTTP_${response.status}`;
51
+ try {
52
+ const value = JSON.parse(new TextDecoder().decode(bytes));
53
+ if (/^[A-Z][A-Z0-9_]{0,63}$/.test(value.error))
54
+ code = value.error;
55
+ } catch {}
56
+ throw new FlareError(code, "Event subscription failed");
57
+ }
58
+ if (!response.headers.get("content-type")?.startsWith("text/event-stream") || !response.body)
59
+ throw new FlareError("INVALID_RESPONSE", "Expected SSE");
60
+ reader = response.body.getReader();
61
+ const decoder = new TextDecoder("utf-8", { fatal: true });
62
+ let pending = "", total = 0;
63
+ while (!signal.aborted) {
64
+ const next = await reader.read();
65
+ if (next.done)
66
+ break;
67
+ touch();
68
+ total += next.value.byteLength;
69
+ if (total > 1048576)
70
+ throw new FlareError("INVALID_RESPONSE", "Stream exceeds bound");
71
+ pending += decoder.decode(next.value, { stream: true });
72
+ let end;
73
+ while ((end = pending.indexOf(`
74
+
75
+ `)) >= 0) {
76
+ const frame = pending.slice(0, end);
77
+ pending = pending.slice(end + 2);
78
+ if (frame.length > 524288)
79
+ throw new FlareError("INVALID_RESPONSE", "Oversized event");
80
+ let name = "", data = "", id = "";
81
+ for (const line of frame.split(`
82
+ `)) {
83
+ if (line.startsWith("event:"))
84
+ name = line.slice(6).trim();
85
+ if (line.startsWith("data:"))
86
+ data += line.slice(5).trimStart();
87
+ if (line.startsWith("id:"))
88
+ id = line.slice(3).trim();
89
+ }
90
+ if (!data)
91
+ continue;
92
+ if (name === "reconnect")
93
+ continue;
94
+ const value = JSON.parse(data);
95
+ if (name === "task") {
96
+ if (!value || typeof value.id !== "string" || !["succeeded", "failed", "cancelled", "interrupted"].includes(value.state))
97
+ throw new FlareError("INVALID_RESPONSE", "Invalid task");
98
+ yield { type: "task", task: value };
99
+ return;
100
+ }
101
+ if (name !== "progress" || !Number.isSafeInteger(value.seq) || String(value.seq) !== id || value.seq > 4096 || !["status", "text", "output", "item", "truncated"].includes(value.type))
102
+ throw new FlareError("INVALID_RESPONSE", "Invalid event");
103
+ if (value.seq <= cursor)
104
+ continue;
105
+ if (value.seq !== cursor + 1)
106
+ throw new FlareError("CODEX_EVENT_GAP", "Event gap");
107
+ cursor = value.seq;
108
+ failures = 0;
109
+ yield value;
110
+ }
111
+ if (pending.length > 524288)
112
+ throw new FlareError("INVALID_RESPONSE", "Oversized frame");
113
+ }
114
+ } catch (error) {
115
+ if (signal.aborted)
116
+ return;
117
+ const code = error instanceof FlareError ? error.code : "";
118
+ if (code && ![
119
+ "HTTP_429",
120
+ "HTTP_500",
121
+ "HTTP_502",
122
+ "HTTP_503",
123
+ "HTTP_504",
124
+ "CODEX_UNAVAILABLE"
125
+ ].includes(code))
126
+ throw error;
127
+ if (++failures > 5)
128
+ throw new FlareError("CODEX_STREAM_INTERRUPTED", "Reconnect failed; task continues independently");
129
+ } finally {
130
+ clearTimeout(idle);
131
+ signal.removeEventListener("abort", abort);
132
+ connection.abort();
133
+ await reader?.cancel().catch(() => {});
134
+ }
135
+ if (signal.aborted)
136
+ return;
137
+ await new Promise((resolve) => {
138
+ const done = () => {
139
+ clearTimeout(timer);
140
+ signal.removeEventListener("abort", done);
141
+ resolve();
142
+ };
143
+ const timer = setTimeout(done, Math.min(5000, 500 * 2 ** failures));
144
+ signal.addEventListener("abort", done, { once: true });
145
+ });
146
+ }
147
+ }
@@ -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
+ }
@@ -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
+ }
@@ -9,6 +9,10 @@ export declare function FlareProvider({ client, children, }: {
9
9
  query: <I, O>(ref: FunctionRef<I, O>, args: I) => Promise<O>;
10
10
  mutation: <I, O>(ref: FunctionRef<I, O>, args: I) => Promise<O>;
11
11
  action: <I, O>(ref: FunctionRef<I, O>, args: I) => Promise<O>;
12
+ codexEvents<I>(ref: FunctionRef<I, import("./codex-stream.js").CodexSubscription>, args: I, streamOptions?: {
13
+ signal?: AbortSignal;
14
+ after?: number;
15
+ }): AsyncGenerator<import("./codex-stream.js").CodexEvent, void, any>;
12
16
  onUpdate<I, O>(ref: FunctionRef<I, O>, args: I, next: (value: O) => void, error?: (error: FlareError) => void): () => void;
13
17
  refreshAuth(): void;
14
18
  dispose(): void;
@@ -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
+ }
@@ -1,3 +1,8 @@
1
+ export type { CodexSubscription } from "./codex-stream.js";
2
+ import type { CodexContext } from "./codex.js";
3
+ export type { CodexContext, CodexTask, CodexTaskInput } from "./codex.js";
4
+ import type { SearchContext } from "./search.js";
5
+ export type { SearchContext, SearchHit, SearchState } from "./search.js";
1
6
  export { z } from "zod";
2
7
  import type { StandardSchemaV1 } from "@standard-schema/spec";
3
8
  import type { Validator, Schema } from "./schema.js";
@@ -5,7 +10,7 @@ import type { ReadDatabase, WriteDatabase } from "./database.js";
5
10
  export { defineSchema, defineTable, schemaContract } from "./schema.js";
6
11
  export type { Validator, Schema, Doc, Value, Input, SchemaContract, } from "./schema.js";
7
12
  export type { ReadDatabase, WriteDatabase, Page } from "./database.js";
8
- import { type Auth, type FunctionKind, type FunctionRef, type Json, type Manifest } from "./protocol.js";
13
+ import { type McpTool, type Auth, type FunctionKind, type FunctionRef, type Json, type Manifest } from "./protocol.js";
9
14
  export interface QueryContext<S extends Schema = Schema> {
10
15
  readonly db: ReadDatabase<S>;
11
16
  readonly auth: Auth | null;
@@ -21,6 +26,8 @@ export interface MutationContext<S extends Schema = Schema> {
21
26
  };
22
27
  }
23
28
  export interface ActionContext {
29
+ readonly codex: CodexContext;
30
+ readonly search: SearchContext;
24
31
  readonly auth: Auth | null;
25
32
  runQuery<I, O>(ref: FunctionRef<I, O>, args: I): Promise<O>;
26
33
  runMutation<I, O>(ref: FunctionRef<I, O>, args: I): Promise<O>;
@@ -86,6 +93,7 @@ export interface ActionContext {
86
93
  type Context<K extends FunctionKind, S extends Schema = Schema> = K extends "action" ? ActionContext : K extends "mutation" ? MutationContext<S> : QueryContext<S>;
87
94
  export interface Definition<K extends FunctionKind = FunctionKind, I = any, O = any, S extends Schema = Schema> {
88
95
  readonly kind: K;
96
+ readonly mcp?: McpTool;
89
97
  readonly input: Validator<any, I>;
90
98
  readonly output: Validator<O, any>;
91
99
  readonly auth: "required" | "public" | "internal";
@@ -95,18 +103,21 @@ export declare const query: <IV extends Validator, OV extends Validator>(options
95
103
  input: IV;
96
104
  output: OV;
97
105
  auth?: "required" | "public" | "internal";
106
+ mcp?: McpTool;
98
107
  handler: (ctx: QueryContext<Schema>, args: StandardSchemaV1.InferOutput<IV>) => StandardSchemaV1.InferInput<OV> | Promise<StandardSchemaV1.InferInput<OV>>;
99
108
  }) => Definition<"query", StandardSchemaV1.InferOutput<IV>, StandardSchemaV1.InferInput<OV>, Schema>;
100
109
  export declare const mutation: <IV extends Validator, OV extends Validator>(options: {
101
110
  input: IV;
102
111
  output: OV;
103
112
  auth?: "required" | "public" | "internal";
113
+ mcp?: McpTool;
104
114
  handler: (ctx: MutationContext<Schema>, args: StandardSchemaV1.InferOutput<IV>) => StandardSchemaV1.InferInput<OV> | Promise<StandardSchemaV1.InferInput<OV>>;
105
115
  }) => Definition<"mutation", StandardSchemaV1.InferOutput<IV>, StandardSchemaV1.InferInput<OV>, Schema>;
106
116
  export declare const action: <IV extends Validator, OV extends Validator>(options: {
107
117
  input: IV;
108
118
  output: OV;
109
119
  auth?: "required" | "public" | "internal";
120
+ mcp?: McpTool;
110
121
  handler: (ctx: ActionContext, args: StandardSchemaV1.InferOutput<IV>) => StandardSchemaV1.InferInput<OV> | Promise<StandardSchemaV1.InferInput<OV>>;
111
122
  }) => Definition<"action", StandardSchemaV1.InferOutput<IV>, StandardSchemaV1.InferInput<OV>, Schema>;
112
123
  export declare function createFunctions<S extends Schema>(): {
@@ -114,18 +125,21 @@ export declare function createFunctions<S extends Schema>(): {
114
125
  input: IV;
115
126
  output: OV;
116
127
  auth?: "required" | "public" | "internal";
128
+ mcp?: McpTool;
117
129
  handler: (ctx: QueryContext<S>, args: StandardSchemaV1.InferOutput<IV>) => StandardSchemaV1.InferInput<OV> | Promise<StandardSchemaV1.InferInput<OV>>;
118
130
  }) => Definition<"query", StandardSchemaV1.InferOutput<IV>, StandardSchemaV1.InferInput<OV>, S>;
119
131
  mutation: <IV extends Validator, OV extends Validator>(options: {
120
132
  input: IV;
121
133
  output: OV;
122
134
  auth?: "required" | "public" | "internal";
135
+ mcp?: McpTool;
123
136
  handler: (ctx: MutationContext<S>, args: StandardSchemaV1.InferOutput<IV>) => StandardSchemaV1.InferInput<OV> | Promise<StandardSchemaV1.InferInput<OV>>;
124
137
  }) => Definition<"mutation", StandardSchemaV1.InferOutput<IV>, StandardSchemaV1.InferInput<OV>, S>;
125
138
  action: <IV extends Validator, OV extends Validator>(options: {
126
139
  input: IV;
127
140
  output: OV;
128
141
  auth?: "required" | "public" | "internal";
142
+ mcp?: McpTool;
129
143
  handler: (ctx: ActionContext, args: StandardSchemaV1.InferOutput<IV>) => StandardSchemaV1.InferInput<OV> | Promise<StandardSchemaV1.InferInput<OV>>;
130
144
  }) => Definition<"action", StandardSchemaV1.InferOutput<IV>, StandardSchemaV1.InferInput<OV>, Schema>;
131
145
  };
@@ -3,6 +3,7 @@ import { schemaContract } from "./schema.js";
3
3
  export { defineSchema, defineTable, schemaContract } from "./schema.js";
4
4
  import {
5
5
  assertPath,
6
+ assertMcpTools,
6
7
  boundedJson,
7
8
  LIMITS,
8
9
  FlareError
@@ -63,8 +64,15 @@ export function manifest(app) {
63
64
  });
64
65
  if (!input || !output)
65
66
  throw new FlareError("INVALID_CONTRACT", `${path} requires JSON Schema converters`);
66
- functions[path] = { kind: fn.kind, auth: fn.auth, input, output };
67
+ functions[path] = {
68
+ kind: fn.kind,
69
+ auth: fn.auth,
70
+ input,
71
+ output,
72
+ ...fn.mcp !== undefined ? { mcp: fn.mcp } : {}
73
+ };
67
74
  }
75
+ assertMcpTools(functions);
68
76
  boundedJson(functions, 1048576);
69
77
  return { protocol: 1, runtimeAbi: 4, functions };
70
78
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@auersperg/flare",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Flare client, server declarations and development CLI",
@@ -1 +0,0 @@
1
- async function e(e,t,n){let r=e.getReader(),i=[],a=0;try{for(;;){let{done:e,value:o}=await r.read();if(e)break;a+=o.byteLength,a>t&&n(),i.push(o)}}finally{await r.cancel().catch(()=>{})}if(i.length===1)return i[0];let o=new Uint8Array(a),s=0;for(let e of i)o.set(e,s),s+=e.byteLength;return o}var t=Object.freeze({inputBytes:65536,outputBytes:262144,frameBytes:8192,subscriptions:64,fileBytes:10485760}),n=class extends Error{code;ambiguous;constructor(e,t,n=!1){super(t),this.code=e,this.ambiguous=n,this.name=`FlareError`}};function r(e){if(e.length>256||!/^[A-Za-z][A-Za-z0-9_]*(\/[A-Za-z][A-Za-z0-9_]*)*$/.test(e)||e.split(`/`).some(e=>[`__proto__`,`prototype`,`constructor`].includes(e)))throw new n(`INVALID_PATH`,`Invalid function path`)}function i(e,t){return r(e),Object.freeze({path:e,kind:t})}function a(e){let t=new Set;function r(e,i){if(i>40)throw new n(`INVALID_JSON`,`JSON nesting limit exceeded`);if(e===null||typeof e==`string`||typeof e==`boolean`||typeof e==`number`&&Number.isFinite(e))return e;if(typeof e!=`object`||!e||t.has(e))throw new n(`INVALID_JSON`,`Expected lossless JSON`);if(!Array.isArray(e)&&Object.getPrototypeOf(e)!==Object.prototype&&Object.getPrototypeOf(e)!==null)throw new n(`INVALID_JSON`,`Expected plain JSON object`);if(Object.getOwnPropertySymbols(e).length)throw new n(`INVALID_JSON`,`Symbol keys are unsupported`);t.add(e);let a;if(Array.isArray(e))a=Array.from(e,e=>r(e,i+1));else{let t=Object.create(null);for(let a of Object.keys(e).sort()){let o=Object.getOwnPropertyDescriptor(e,a);if(!(`value`in o))throw new n(`INVALID_JSON`,`Getters are unsupported`);t[a]=r(o.value,i+1)}a=t}return t.delete(e),a}return JSON.stringify(r(e,0))}function o(e,t){let r=a(e);if(new TextEncoder().encode(r).byteLength>t)throw new n(`TOO_LARGE`,`JSON payload exceeds limit`);return r}function s(e){if(e.size>t.fileBytes)throw new n(`TOO_LARGE`,`Maximum file size: 10 MiB`);if(!e.name||e.name.length>120||/[\x00-\x1f\x7f/\\]/.test(e.name))throw new n(`INVALID_INPUT`,`File name must be 1–120 characters, without path separators`);return{name:e.name,size:e.size,type:[`text/plain`,`image/png`,`image/jpeg`,`image/webp`,`application/pdf`].includes(e.type)?e.type:`application/octet-stream`}}async function c(r,i,a={}){if(i.size>t.fileBytes)throw new n(`TOO_LARGE`,`Maximum file size: 10 MiB`);let o=new URL(r.url);if(o.username||o.password||o.protocol!==`https:`&&!(o.protocol===`http:`&&[`localhost`,`127.0.0.1`,`[::1]`].includes(o.hostname)))throw new n(`INVALID_URL`,`Expected secure upload URL`);let s;try{s=await(a.fetch??globalThis.fetch)(o.href,{method:`POST`,body:i,redirect:`error`,credentials:`omit`,signal:AbortSignal.any([...a.signal?[a.signal]:[],AbortSignal.timeout(a.timeoutMs??12e4)])})}catch{throw new n(`UPLOAD_UNKNOWN`,`Connection interrupted or upload timed out; refresh the file list before trying again`,!0)}let c=s.body?await e(s.body,t.outputBytes,()=>{throw Error(`Upload response exceeds limit`)}).then(e=>JSON.parse(new TextDecoder(`utf-8`,{fatal:!0}).decode(e))).catch(()=>null):null;if(!s.ok){let e=c&&typeof c==`object`&&`error`in c&&typeof c.error==`string`?c.error:`HTTP_${s.status}`;throw new n(e,`Upload failed: ${e}`,s.status>=500)}if(!c||typeof c!=`object`||!(`id`in c)||typeof c.id!=`string`)throw new n(`INVALID_RESPONSE`,`Upload outcome is unknown; refresh the file list before uploading again`,!0);return{id:c.id}}var l=new Set([`UNAUTHORIZED`,`AUTH_EXPIRED`,`FORBIDDEN`,`AUTH_DISABLED`,`HTTP_401`,`HTTP_403`]);function u(i){let s=new URL(i.url);if(!/^https?:$/.test(s.protocol)||s.username||s.password||s.search||s.hash)throw new n(`INVALID_URL`,`Expected backend HTTP URL`);if(s.protocol===`http:`&&![`localhost`,`127.0.0.1`,`[::1]`].includes(s.hostname))throw new n(`INVALID_URL`,`Remote backends require HTTPS`);s.pathname=s.pathname.replace(/\/$/,``);let c=i.fetch??globalThis.fetch;if(!c)throw new n(`TRANSPORT_UNAVAILABLE`,`Provide a fetch implementation`);let u=new Map,d=!1,f,p=!1,m=0,h=0,g=0,_,v=new Set,y=e=>`${s.href.replace(/\/$/,``)}${e}`,b=(e,t,n)=>{for(let r of e.observers)try{r[t]?.(t===`next`?JSON.parse(n):n)}catch{}};function x(){if(d)throw new n(`DISPOSED`,`Client is disposed`)}async function S(r,a,s){x();let l=o(a,t.inputBytes),u;try{u=await i.getToken?.()??null}catch{throw new n(`AUTH_UNAVAILABLE`,`Cannot obtain access token`)}x();let d=new AbortController,f=setTimeout(()=>d.abort(),i.requestTimeoutMs??3e4);try{let i=await c(y(r),{method:`POST`,headers:{"Content-Type":`application/json`,...u?{Authorization:`Bearer ${u}`}:{}},body:l,signal:d.signal,redirect:`error`});if(!i.body)throw new n(`INVALID_RESPONSE`,`Missing response body`,s);let a=await e(i.body,t.outputBytes,()=>{throw new n(`INVALID_RESPONSE`,`Response exceeds limit`,s)}),f;try{f=JSON.parse(new TextDecoder(`utf-8`,{fatal:!0}).decode(a))}catch(e){throw i.ok?e:new n(`HTTP_${i.status}`,`Backend request failed`,s&&i.status>=500)}if(!i.ok)throw new n(f&&typeof f==`object`&&`error`in f&&typeof f.error==`string`&&/^[A-Z][A-Z0-9_]{0,63}$/.test(f.error)?f.error:`HTTP_${i.status}`,`Backend request failed`,s&&i.status>=500);if(!f||typeof f!=`object`||!(`result`in f))throw new n(`INVALID_RESPONSE`,`Invalid protocol envelope`,s);return o(f.result,t.outputBytes),f.result}catch(e){throw e instanceof n?e:e instanceof SyntaxError?new n(`INVALID_RESPONSE`,`Invalid JSON response`,s):new n(`TRANSPORT_ERROR`,s?`Write outcome is unknown; do not replay automatically`:`Backend request interrupted`,s)}finally{clearTimeout(f)}}function C(e){f?.send(o(e,t.frameBytes))}function w(e){C({type:`subscribe`,id:e.id,path:e.ref.path,args:e.args})}function T(){if(d||!u.size||_)return;let e=Math.min(3e4,250*2**Math.min(m++,7));_=setTimeout(()=>{_=void 0,E()},e)}async function E(){if(d||p||f||!u.size)return;p=!0;let e=++h;try{let r=await S(`/v1/live/ticket`,{},!1);if(d||e!==h||!u.size)return;if(!r||typeof r.ticket!=`string`||r.ticket.length>1024)throw new n(`INVALID_RESPONSE`,`Invalid live ticket`);let a=new URL(y(`/v1/live`));a.protocol=a.protocol===`https:`?`wss:`:`ws:`,a.searchParams.set(`ticket`,r.ticket);let s=i.createWebSocket?i.createWebSocket(a.href):new globalThis.WebSocket(a.href);f=s;let c=()=>{if(!(f!==s||d)){m=0;for(let e of u.values())e.revision=-1,e.snapshot=void 0,e.terminal||w(e)}},p=e=>{if(!(f!==s||d))try{if(typeof e.data!=`string`||new TextEncoder().encode(e.data).byteLength>t.outputBytes+1024)throw Error(`Invalid frame`);let r=JSON.parse(e.data);if(r.type===`ready`)return;let i=[...u.values()].find(e=>e.id===r.id);if(!i||i.terminal)return;if(r.type===`error`){i.terminal=!0,l.has(r.code)&&(i.snapshot=void 0),b(i,`error`,new n(typeof r.code==`string`?r.code:`LIVE_ERROR`,`Live subscription failed`));return}if(r.type!==`snapshot`||!Number.isSafeInteger(r.revision)||r.revision<0||!(`result`in r))throw Error(`Invalid snapshot`);let a=o(r.result,t.outputBytes);if(r.revision>i.revision){let e=i.snapshot!==a;i.revision=r.revision,i.snapshot=a,e&&b(i,`next`,i.snapshot)}C({type:`ack`,id:i.id,revision:i.revision})}catch{s.close(1002,`Invalid protocol frame`)}},g=e=>{if(x(),f===s){f=void 0;for(let t of u.values())e.code===1008&&(t.snapshot=void 0,t.revision=-1),t.terminal||b(t,`error`,new n(e.code===1008?`AUTH_EXPIRED`:`LIVE_DISCONNECTED`,`Live reconnecting; next snapshot will be current`));T()}},_=()=>s.close(),x=()=>{s.removeEventListener(`open`,c),s.removeEventListener(`message`,p),s.removeEventListener(`close`,g),s.removeEventListener(`error`,_),v.delete(x)};v.add(x),s.addEventListener(`open`,c),s.addEventListener(`message`,p),s.addEventListener(`close`,g),s.addEventListener(`error`,_)}catch(t){if(d||e!==h)return;for(let e of u.values())t instanceof n&&l.has(t.code)&&(e.snapshot=void 0,e.revision=-1),b(e,`error`,t instanceof n?t:new n(`LIVE_UNAVAILABLE`,`Live connection failed`));T()}finally{p=!1,!d&&e!==h&&!f&&u.size&&T()}}function D(e){return async(t,i)=>{if(r(t.path),t.kind!==e)throw new n(`INVALID_KIND`,`Expected a ${e} function`);return S(`/v1/call`,{path:t.path,kind:t.kind,args:i},t.kind===`mutation`||t.kind===`action`)}}return{query:D(`query`),mutation:D(`mutation`),action:D(`action`),onUpdate(e,i,s,c){let l={next:s,error:c};if(x(),r(e.path),e.kind!==`query`)throw new n(`INVALID_KIND`,`Only queries are subscribable`);o({type:`subscribe`,id:`subscription-9999999999`,path:e.path,args:i},t.frameBytes);let d=a({path:e.path,args:i}),p=u.get(d);if(!p){if(u.size>=t.subscriptions)throw new n(`TOO_MANY_SUBSCRIPTIONS`,`Subscription limit exceeded`);p={id:`subscription-${++g}`,ref:e,args:JSON.parse(a(i)),observers:new Set,revision:-1,terminal:!1},u.set(d,p),f?.readyState===1&&w(p)}let m={next:l.next,error:l.error};p.observers.add(m),p.snapshot!==void 0&&b({...p,observers:new Set([l])},`next`,p.snapshot),E();let y=!1;return()=>{if(!y&&(y=!0,p.observers.delete(m),p.observers.size||(u.delete(d),f?.readyState===1&&C({type:`unsubscribe`,id:p.id})),!u.size)){++h,_&&clearTimeout(_),_=void 0;let e=f;f=void 0;for(let e of[...v])e();e?.close(1e3,`No subscriptions`)}}},refreshAuth(){x(),++h,_&&clearTimeout(_),_=void 0;for(let e of[...v])e();let e=f;f=void 0,e?.close(1e3,`Authorization changed`);for(let e of u.values())e.snapshot=void 0,e.revision=-1,e.terminal=!1,b(e,`error`,new n(`AUTH_CHANGED`,`Authorization changed; cached value invalidated`));E()},dispose(){if(!d){d=!0,++h,_&&clearTimeout(_),_=void 0;for(let e of[...v])e();u.clear(),f?.close(1e3,`Disposed`),f=void 0}}}}export{i,s as n,c as r,u as t};