lopata 0.0.1

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 (147) hide show
  1. package/README.md +15 -0
  2. package/package.json +51 -0
  3. package/runtime/bindings/ai.ts +132 -0
  4. package/runtime/bindings/analytics-engine.ts +96 -0
  5. package/runtime/bindings/browser.ts +64 -0
  6. package/runtime/bindings/cache.ts +179 -0
  7. package/runtime/bindings/cf-streams.ts +56 -0
  8. package/runtime/bindings/container-docker.ts +225 -0
  9. package/runtime/bindings/container.ts +662 -0
  10. package/runtime/bindings/crypto-extras.ts +89 -0
  11. package/runtime/bindings/d1.ts +315 -0
  12. package/runtime/bindings/do-executor-inprocess.ts +140 -0
  13. package/runtime/bindings/do-executor-worker.ts +368 -0
  14. package/runtime/bindings/do-executor.ts +45 -0
  15. package/runtime/bindings/do-websocket-bridge.ts +70 -0
  16. package/runtime/bindings/do-worker-entry.ts +220 -0
  17. package/runtime/bindings/do-worker-env.ts +74 -0
  18. package/runtime/bindings/durable-object.ts +992 -0
  19. package/runtime/bindings/email.ts +180 -0
  20. package/runtime/bindings/html-rewriter.ts +84 -0
  21. package/runtime/bindings/hyperdrive.ts +130 -0
  22. package/runtime/bindings/images.ts +381 -0
  23. package/runtime/bindings/kv.ts +359 -0
  24. package/runtime/bindings/queue.ts +507 -0
  25. package/runtime/bindings/r2.ts +759 -0
  26. package/runtime/bindings/rpc-stub.ts +267 -0
  27. package/runtime/bindings/scheduled.ts +172 -0
  28. package/runtime/bindings/service-binding.ts +217 -0
  29. package/runtime/bindings/static-assets.ts +481 -0
  30. package/runtime/bindings/websocket-pair.ts +182 -0
  31. package/runtime/bindings/workflow.ts +858 -0
  32. package/runtime/bunflare-config.ts +56 -0
  33. package/runtime/cli/cache.ts +39 -0
  34. package/runtime/cli/context.ts +105 -0
  35. package/runtime/cli/d1.ts +163 -0
  36. package/runtime/cli/dev.ts +392 -0
  37. package/runtime/cli/kv.ts +84 -0
  38. package/runtime/cli/queues.ts +109 -0
  39. package/runtime/cli/r2.ts +140 -0
  40. package/runtime/cli/traces.ts +251 -0
  41. package/runtime/cli.ts +102 -0
  42. package/runtime/config.ts +148 -0
  43. package/runtime/d1-migrate.ts +37 -0
  44. package/runtime/dashboard/api.ts +174 -0
  45. package/runtime/dashboard/app.tsx +220 -0
  46. package/runtime/dashboard/components/breadcrumb.tsx +16 -0
  47. package/runtime/dashboard/components/buttons.tsx +13 -0
  48. package/runtime/dashboard/components/code-block.tsx +5 -0
  49. package/runtime/dashboard/components/detail-field.tsx +8 -0
  50. package/runtime/dashboard/components/empty-state.tsx +8 -0
  51. package/runtime/dashboard/components/filter-input.tsx +11 -0
  52. package/runtime/dashboard/components/index.ts +16 -0
  53. package/runtime/dashboard/components/key-value-table.tsx +23 -0
  54. package/runtime/dashboard/components/modal.tsx +23 -0
  55. package/runtime/dashboard/components/page-header.tsx +11 -0
  56. package/runtime/dashboard/components/pill-button.tsx +14 -0
  57. package/runtime/dashboard/components/refresh-button.tsx +7 -0
  58. package/runtime/dashboard/components/service-info.tsx +45 -0
  59. package/runtime/dashboard/components/status-badge.tsx +7 -0
  60. package/runtime/dashboard/components/table-link.tsx +5 -0
  61. package/runtime/dashboard/components/table.tsx +26 -0
  62. package/runtime/dashboard/components.tsx +19 -0
  63. package/runtime/dashboard/index.html +23 -0
  64. package/runtime/dashboard/lib.ts +45 -0
  65. package/runtime/dashboard/rpc/client.ts +20 -0
  66. package/runtime/dashboard/rpc/handlers/ai.ts +71 -0
  67. package/runtime/dashboard/rpc/handlers/analytics-engine.ts +53 -0
  68. package/runtime/dashboard/rpc/handlers/cache.ts +24 -0
  69. package/runtime/dashboard/rpc/handlers/config.ts +137 -0
  70. package/runtime/dashboard/rpc/handlers/containers.ts +194 -0
  71. package/runtime/dashboard/rpc/handlers/d1.ts +84 -0
  72. package/runtime/dashboard/rpc/handlers/do.ts +117 -0
  73. package/runtime/dashboard/rpc/handlers/email.ts +82 -0
  74. package/runtime/dashboard/rpc/handlers/errors.ts +32 -0
  75. package/runtime/dashboard/rpc/handlers/generations.ts +60 -0
  76. package/runtime/dashboard/rpc/handlers/kv.ts +76 -0
  77. package/runtime/dashboard/rpc/handlers/overview.ts +94 -0
  78. package/runtime/dashboard/rpc/handlers/queue.ts +79 -0
  79. package/runtime/dashboard/rpc/handlers/r2.ts +72 -0
  80. package/runtime/dashboard/rpc/handlers/scheduled.ts +91 -0
  81. package/runtime/dashboard/rpc/handlers/traces.ts +64 -0
  82. package/runtime/dashboard/rpc/handlers/workers.ts +65 -0
  83. package/runtime/dashboard/rpc/handlers/workflows.ts +171 -0
  84. package/runtime/dashboard/rpc/hooks.ts +132 -0
  85. package/runtime/dashboard/rpc/server.ts +70 -0
  86. package/runtime/dashboard/rpc/types.ts +396 -0
  87. package/runtime/dashboard/sql-browser/data-browser-tab.tsx +122 -0
  88. package/runtime/dashboard/sql-browser/editable-cell.tsx +117 -0
  89. package/runtime/dashboard/sql-browser/filter-row.tsx +99 -0
  90. package/runtime/dashboard/sql-browser/history-panels.tsx +110 -0
  91. package/runtime/dashboard/sql-browser/hooks.ts +137 -0
  92. package/runtime/dashboard/sql-browser/index.ts +4 -0
  93. package/runtime/dashboard/sql-browser/insert-row-form.tsx +85 -0
  94. package/runtime/dashboard/sql-browser/modals.tsx +116 -0
  95. package/runtime/dashboard/sql-browser/schema-browser-tab.tsx +67 -0
  96. package/runtime/dashboard/sql-browser/sql-browser.tsx +52 -0
  97. package/runtime/dashboard/sql-browser/sql-console-tab.tsx +124 -0
  98. package/runtime/dashboard/sql-browser/table-data-view.tsx +566 -0
  99. package/runtime/dashboard/sql-browser/table-sidebar.tsx +38 -0
  100. package/runtime/dashboard/sql-browser/types.ts +61 -0
  101. package/runtime/dashboard/sql-browser/utils.ts +167 -0
  102. package/runtime/dashboard/style.css +177 -0
  103. package/runtime/dashboard/views/ai.tsx +152 -0
  104. package/runtime/dashboard/views/analytics-engine.tsx +169 -0
  105. package/runtime/dashboard/views/cache.tsx +93 -0
  106. package/runtime/dashboard/views/containers.tsx +197 -0
  107. package/runtime/dashboard/views/d1.tsx +81 -0
  108. package/runtime/dashboard/views/do.tsx +168 -0
  109. package/runtime/dashboard/views/email.tsx +235 -0
  110. package/runtime/dashboard/views/errors.tsx +558 -0
  111. package/runtime/dashboard/views/home.tsx +287 -0
  112. package/runtime/dashboard/views/kv.tsx +273 -0
  113. package/runtime/dashboard/views/queue.tsx +193 -0
  114. package/runtime/dashboard/views/r2.tsx +202 -0
  115. package/runtime/dashboard/views/scheduled.tsx +89 -0
  116. package/runtime/dashboard/views/trace-waterfall.tsx +410 -0
  117. package/runtime/dashboard/views/traces.tsx +768 -0
  118. package/runtime/dashboard/views/workers.tsx +55 -0
  119. package/runtime/dashboard/views/workflows.tsx +473 -0
  120. package/runtime/db.ts +258 -0
  121. package/runtime/env.ts +362 -0
  122. package/runtime/error-page/app.tsx +394 -0
  123. package/runtime/error-page/build.ts +269 -0
  124. package/runtime/error-page/index.html +16 -0
  125. package/runtime/error-page/style.css +31 -0
  126. package/runtime/execution-context.ts +18 -0
  127. package/runtime/file-watcher.ts +57 -0
  128. package/runtime/generation-manager.ts +230 -0
  129. package/runtime/generation.ts +411 -0
  130. package/runtime/plugin.ts +292 -0
  131. package/runtime/request-cf.ts +28 -0
  132. package/runtime/rpc-validate.ts +154 -0
  133. package/runtime/tracing/context.ts +40 -0
  134. package/runtime/tracing/db.ts +73 -0
  135. package/runtime/tracing/frames.ts +75 -0
  136. package/runtime/tracing/instrument.ts +186 -0
  137. package/runtime/tracing/span.ts +138 -0
  138. package/runtime/tracing/store.ts +499 -0
  139. package/runtime/tracing/types.ts +47 -0
  140. package/runtime/vite-plugin/config-plugin.ts +68 -0
  141. package/runtime/vite-plugin/dev-server-plugin.ts +493 -0
  142. package/runtime/vite-plugin/dist/index.mjs +52333 -0
  143. package/runtime/vite-plugin/globals-plugin.ts +94 -0
  144. package/runtime/vite-plugin/index.ts +43 -0
  145. package/runtime/vite-plugin/modules-plugin.ts +88 -0
  146. package/runtime/vite-plugin/react-router-plugin.ts +95 -0
  147. package/runtime/worker-registry.ts +52 -0
@@ -0,0 +1,117 @@
1
+ import type { HandlerContext, DoNamespace, DoInstance, DoDetail, OkResponse, D1Table, QueryResult } from "../types";
2
+ import { getAllConfigs, getDoNamespace } from "../types";
3
+ import { getDatabase, getDataDir } from "../../../db";
4
+ import { Database } from "bun:sqlite";
5
+ import { join } from "node:path";
6
+ import { existsSync } from "node:fs";
7
+
8
+ export const handlers = {
9
+ "do.listNamespaces"(_input: {}, ctx: HandlerContext): DoNamespace[] {
10
+ const db = getDatabase();
11
+ const rows = db.query<{ namespace: string; count: number }, []>(
12
+ "SELECT namespace, COUNT(*) as count FROM do_instances GROUP BY namespace ORDER BY namespace"
13
+ ).all();
14
+ const rowMap = new Map(rows.map(r => [r.namespace, r]));
15
+ for (const config of getAllConfigs(ctx)) {
16
+ for (const b of config.durable_objects?.bindings ?? []) {
17
+ if (!rowMap.has(b.class_name)) {
18
+ rows.push({ namespace: b.class_name, count: 0 });
19
+ }
20
+ }
21
+ }
22
+ rows.sort((a, b) => a.namespace.localeCompare(b.namespace));
23
+ return rows;
24
+ },
25
+
26
+ "do.listInstances"({ ns }: { ns: string }): DoInstance[] {
27
+ const db = getDatabase();
28
+ const instances = db.query<{ id: string; name: string | null }, [string]>(
29
+ "SELECT id, name FROM do_instances WHERE namespace = ? ORDER BY id"
30
+ ).all(ns);
31
+
32
+ const kvCounts = db.query<{ id: string; key_count: number }, [string]>(
33
+ "SELECT id, COUNT(*) as key_count FROM do_storage WHERE namespace = ? GROUP BY id"
34
+ ).all(ns);
35
+ const kvMap = new Map(kvCounts.map(r => [r.id, r.key_count]));
36
+
37
+ const alarms = db.query<{ id: string; alarm_time: number }, [string]>(
38
+ "SELECT id, alarm_time FROM do_alarms WHERE namespace = ?"
39
+ ).all(ns);
40
+ const alarmMap = new Map(alarms.map(a => [a.id, a.alarm_time]));
41
+
42
+ return instances.map(inst => ({
43
+ id: inst.id,
44
+ name: inst.name,
45
+ key_count: kvMap.get(inst.id) ?? 0,
46
+ alarm: alarmMap.get(inst.id) ?? null,
47
+ }));
48
+ },
49
+
50
+ "do.getInstance"({ ns, id }: { ns: string; id: string }, ctx: HandlerContext): DoDetail {
51
+ const db = getDatabase();
52
+ const entries = db.query<{ key: string; value: string }, [string, string]>(
53
+ "SELECT key, value FROM do_storage WHERE namespace = ? AND id = ? ORDER BY key"
54
+ ).all(ns, id);
55
+
56
+ const alarm = db.query<{ alarm_time: number }, [string, string]>(
57
+ "SELECT alarm_time FROM do_alarms WHERE namespace = ? AND id = ?"
58
+ ).get(ns, id);
59
+
60
+ const namespace = getDoNamespace(ctx, ns);
61
+ return { entries, alarm: alarm?.alarm_time ?? null, hasAlarmHandler: namespace?.hasAlarmHandler() ?? false };
62
+ },
63
+
64
+ "do.deleteEntry"({ ns, id, key }: { ns: string; id: string; key: string }): OkResponse {
65
+ const db = getDatabase();
66
+ db.prepare("DELETE FROM do_storage WHERE namespace = ? AND id = ? AND key = ?").run(ns, id, key);
67
+ return { ok: true };
68
+ },
69
+
70
+ "do.listSqlTables"({ ns, id }: { ns: string; id: string }): D1Table[] {
71
+ const dbPath = join(getDataDir(), "do-sql", ns, `${id}.sqlite`);
72
+ if (!existsSync(dbPath)) return [];
73
+
74
+ const dodb = new Database(dbPath);
75
+ try {
76
+ const tables = dodb.query<{ name: string; sql: string }, []>(
77
+ "SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
78
+ ).all();
79
+
80
+ return tables.map(t => {
81
+ const row = dodb.query<{ count: number }, []>(`SELECT COUNT(*) as count FROM "${t.name}"`).get();
82
+ return { name: t.name, sql: t.sql, rows: row?.count ?? 0 };
83
+ });
84
+ } finally {
85
+ dodb.close();
86
+ }
87
+ },
88
+
89
+ async "do.triggerAlarm"({ ns, id }: { ns: string; id: string }, ctx: HandlerContext): Promise<OkResponse> {
90
+ const namespace = getDoNamespace(ctx, ns);
91
+ if (!namespace) throw new Error(`Durable Object namespace "${ns}" not found (worker not loaded?)`);
92
+ await namespace.triggerAlarm(id);
93
+ return { ok: true };
94
+ },
95
+
96
+ "do.sqlQuery"({ ns, id, sql }: { ns: string; id: string; sql: string }): QueryResult {
97
+ if (!sql) throw new Error("Missing sql field");
98
+
99
+ const dbPath = join(getDataDir(), "do-sql", ns, `${id}.sqlite`);
100
+ if (!existsSync(dbPath)) throw new Error("SQL database not found for this instance");
101
+
102
+ const dodb = new Database(dbPath);
103
+ try {
104
+ const stmt = dodb.prepare(sql);
105
+ if (stmt.columnNames.length > 0) {
106
+ const rows = stmt.all() as Record<string, unknown>[];
107
+ return { columns: stmt.columnNames, rows, count: rows.length };
108
+ } else {
109
+ stmt.run();
110
+ const changes = dodb.query<{ c: number }, []>("SELECT changes() as c").get()?.c ?? 0;
111
+ return { columns: [], rows: [], count: changes, message: `${changes} row(s) affected` };
112
+ }
113
+ } finally {
114
+ dodb.close();
115
+ }
116
+ },
117
+ };
@@ -0,0 +1,82 @@
1
+ import type { HandlerContext, EmailRecord, OkResponse } from "../types";
2
+ import { getDatabase } from "../../../db";
3
+ import type { SQLQueryBindings } from "bun:sqlite";
4
+
5
+ export const handlers = {
6
+ "email.list"({ status, limit = 50 }: { status?: string; limit?: number }): EmailRecord[] {
7
+ const db = getDatabase();
8
+ let query = "SELECT id, binding, from_addr, to_addr, raw_size, status, reject_reason, created_at FROM email_messages";
9
+ const params: SQLQueryBindings[] = [];
10
+
11
+ if (status) {
12
+ query += " WHERE status = ?";
13
+ params.push(status);
14
+ }
15
+ query += " ORDER BY created_at DESC LIMIT ?";
16
+ params.push(limit);
17
+
18
+ return db.prepare(query).all(...params) as EmailRecord[];
19
+ },
20
+
21
+ "email.get"({ id }: { id: string }): { record: EmailRecord; raw: string } | null {
22
+ const db = getDatabase();
23
+ const row = db.query<Record<string, unknown>, [string]>(
24
+ "SELECT id, binding, from_addr, to_addr, raw, raw_size, status, reject_reason, created_at FROM email_messages WHERE id = ?",
25
+ ).get(id);
26
+ if (!row) return null;
27
+
28
+ let rawStr: string;
29
+ try {
30
+ rawStr = new TextDecoder().decode(row.raw as BufferSource);
31
+ } catch {
32
+ rawStr = "<binary content>";
33
+ }
34
+
35
+ const { raw: _raw, ...record } = row;
36
+ return { record: record as unknown as EmailRecord, raw: rawStr };
37
+ },
38
+
39
+ "email.delete"({ id }: { id: string }): OkResponse {
40
+ const db = getDatabase();
41
+ db.prepare("DELETE FROM email_messages WHERE id = ?").run(id);
42
+ return { ok: true };
43
+ },
44
+
45
+ async "email.trigger"(
46
+ { from, to, subject, body }: { from: string; to: string; subject?: string; body?: string },
47
+ ctx: HandlerContext,
48
+ ): Promise<OkResponse> {
49
+ // Build raw MIME content
50
+ const lines: string[] = [];
51
+ lines.push(`From: ${from}`);
52
+ lines.push(`To: ${to}`);
53
+ if (subject) lines.push(`Subject: ${subject}`);
54
+ lines.push(`Date: ${new Date().toUTCString()}`);
55
+ lines.push("MIME-Version: 1.0");
56
+ lines.push("Content-Type: text/plain; charset=utf-8");
57
+ lines.push("");
58
+ lines.push(body ?? "");
59
+ const rawContent = lines.join("\r\n");
60
+ const rawBytes = new TextEncoder().encode(rawContent);
61
+
62
+ const gen = ctx.manager?.active;
63
+ if (!gen) throw new Error("No active generation");
64
+ const res = await gen.callEmail(rawBytes, from, to);
65
+ if (!res.ok) {
66
+ const text = await res.text();
67
+ throw new Error(text || `Email handler failed with status ${res.status}`);
68
+ }
69
+ return { ok: true };
70
+ },
71
+
72
+ "email.stats"(_input: {}, _ctx: HandlerContext): { total: number; byStatus: Record<string, number> } {
73
+ const db = getDatabase();
74
+ const total = db.query<{ count: number }, []>("SELECT COUNT(*) as count FROM email_messages").get()?.count ?? 0;
75
+ const rows = db.query<{ status: string; count: number }, []>(
76
+ "SELECT status, COUNT(*) as count FROM email_messages GROUP BY status",
77
+ ).all();
78
+ const byStatus: Record<string, number> = {};
79
+ for (const r of rows) byStatus[r.status] = r.count;
80
+ return { total, byStatus };
81
+ },
82
+ };
@@ -0,0 +1,32 @@
1
+ import { getTraceStore } from "../../../tracing/store";
2
+
3
+ export const handlers = {
4
+ "errors.list"(input: { limit?: number; cursor?: string }) {
5
+ if (input.limit !== undefined && (typeof input.limit !== "number" || input.limit < 1)) {
6
+ throw new Error("limit must be a positive number");
7
+ }
8
+ return getTraceStore().listErrors({ limit: input.limit ?? 50, cursor: input.cursor });
9
+ },
10
+
11
+ "errors.get"(input: { id: string }) {
12
+ if (!input.id || typeof input.id !== "string") {
13
+ throw new Error("id is required and must be a string");
14
+ }
15
+ const error = getTraceStore().getError(input.id);
16
+ if (!error) throw new Error("Error not found");
17
+ return error;
18
+ },
19
+
20
+ "errors.delete"(input: { id: string }): { ok: true } {
21
+ if (!input.id || typeof input.id !== "string") {
22
+ throw new Error("id is required and must be a string");
23
+ }
24
+ getTraceStore().deleteError(input.id);
25
+ return { ok: true };
26
+ },
27
+
28
+ "errors.clear"(_input: {}): { ok: true } {
29
+ getTraceStore().clearErrors();
30
+ return { ok: true };
31
+ },
32
+ };
@@ -0,0 +1,60 @@
1
+ import type { HandlerContext, GenerationsData, GenerationInfo, OkResponse } from "../types";
2
+
3
+ export const handlers = {
4
+ "generations.list"(_input: {}, ctx: HandlerContext): GenerationsData {
5
+ if (!ctx.manager) throw new Error("Generation manager not available");
6
+
7
+ const result: GenerationsData = {
8
+ generations: ctx.manager.list(),
9
+ gracePeriodMs: ctx.manager.gracePeriodMs,
10
+ };
11
+
12
+ // Include per-worker data when registry is available (multi-worker mode)
13
+ if (ctx.registry) {
14
+ result.workers = [];
15
+ for (const [name, mgr] of ctx.registry.listManagers()) {
16
+ result.workers.push({
17
+ workerName: name,
18
+ generations: mgr.list(),
19
+ gracePeriodMs: mgr.gracePeriodMs,
20
+ });
21
+ }
22
+ }
23
+
24
+ return result;
25
+ },
26
+
27
+ async "generations.reload"(input: { workerName?: string }, ctx: HandlerContext): Promise<{ ok: true; generation: GenerationInfo }> {
28
+ let manager = ctx.manager;
29
+ if (input.workerName && ctx.registry) {
30
+ manager = ctx.registry.getManager(input.workerName) ?? null;
31
+ }
32
+ if (!manager) throw new Error("Generation manager not available");
33
+ const gen = await manager.reload();
34
+ return { ok: true, generation: gen.getInfo() };
35
+ },
36
+
37
+ "generations.drain"(input: { workerName?: string }, ctx: HandlerContext): { ok: true; stoppedGeneration: number } {
38
+ let manager = ctx.manager;
39
+ if (input.workerName && ctx.registry) {
40
+ manager = ctx.registry.getManager(input.workerName) ?? null;
41
+ }
42
+ if (!manager) throw new Error("Generation manager not available");
43
+ const gens = manager.list().filter(g => g.state === "draining");
44
+ if (gens.length === 0) throw new Error("No draining generations");
45
+ const oldest = gens.reduce((a, b) => a.createdAt < b.createdAt ? a : b);
46
+ manager.stop(oldest.id);
47
+ return { ok: true, stoppedGeneration: oldest.id };
48
+ },
49
+
50
+ "generations.config"({ gracePeriodMs, workerName }: { gracePeriodMs: number; workerName?: string }, ctx: HandlerContext): { ok: true; gracePeriodMs: number } {
51
+ let manager = ctx.manager;
52
+ if (workerName && ctx.registry) {
53
+ manager = ctx.registry.getManager(workerName) ?? null;
54
+ }
55
+ if (!manager) throw new Error("Generation manager not available");
56
+ if (typeof gracePeriodMs !== "number" || gracePeriodMs < 0) throw new Error("Invalid gracePeriodMs");
57
+ manager.setGracePeriod(gracePeriodMs);
58
+ return { ok: true, gracePeriodMs: manager.gracePeriodMs };
59
+ },
60
+ };
@@ -0,0 +1,76 @@
1
+ import type { HandlerContext, KvNamespace, Paginated, KvKey, KvValue, OkResponse } from "../types";
2
+ import { getAllConfigs } from "../types";
3
+ import { getDatabase } from "../../../db";
4
+ import type { SQLQueryBindings } from "bun:sqlite";
5
+
6
+ export const handlers = {
7
+ "kv.listNamespaces"(_input: {}, ctx: HandlerContext): KvNamespace[] {
8
+ const db = getDatabase();
9
+ const rows = db.query<{ namespace: string; count: number }, []>(
10
+ "SELECT namespace, COUNT(*) as count FROM kv GROUP BY namespace ORDER BY namespace"
11
+ ).all();
12
+ const rowMap = new Map(rows.map(r => [r.namespace, r]));
13
+ for (const config of getAllConfigs(ctx)) {
14
+ for (const ns of config.kv_namespaces ?? []) {
15
+ if (!rowMap.has(ns.id)) {
16
+ rows.push({ namespace: ns.id, count: 0 });
17
+ }
18
+ }
19
+ }
20
+ rows.sort((a, b) => a.namespace.localeCompare(b.namespace));
21
+ return rows;
22
+ },
23
+
24
+ "kv.listKeys"({ ns, limit = 50, cursor = "", prefix = "" }: { ns: string; limit?: number; cursor?: string; prefix?: string }): Paginated<KvKey> {
25
+ const db = getDatabase();
26
+ let query = "SELECT key, LENGTH(value) as size, metadata, expiration FROM kv WHERE namespace = ?";
27
+ const params: SQLQueryBindings[] = [ns];
28
+
29
+ if (prefix) { query += " AND key LIKE ?"; params.push(prefix + "%"); }
30
+ if (cursor) { query += " AND key > ?"; params.push(cursor); }
31
+ query += " ORDER BY key LIMIT ?";
32
+ params.push(limit + 1);
33
+
34
+ const rows = db.prepare(query).all(...params) as KvKey[];
35
+ const hasMore = rows.length > limit;
36
+ const items = rows.slice(0, limit);
37
+ const last = items[items.length - 1];
38
+ return { items, cursor: hasMore && last ? last.key : null };
39
+ },
40
+
41
+ "kv.getKey"({ ns, key }: { ns: string; key: string }): KvValue {
42
+ const db = getDatabase();
43
+ const row = db.query<{ value: Buffer; metadata: string | null; expiration: number | null }, [string, string]>(
44
+ "SELECT value, metadata, expiration FROM kv WHERE namespace = ? AND key = ?"
45
+ ).get(ns, key);
46
+ if (!row) throw new Error("Key not found");
47
+
48
+ let valueStr: string;
49
+ try { valueStr = new TextDecoder().decode(row.value); }
50
+ catch { valueStr = `<binary: ${row.value.length} bytes>`; }
51
+
52
+ return {
53
+ key,
54
+ value: valueStr,
55
+ metadata: row.metadata ? JSON.parse(row.metadata) : null,
56
+ expiration: row.expiration,
57
+ };
58
+ },
59
+
60
+ "kv.putKey"({ ns, key, value, metadata, expirationTtl }: { ns: string; key: string; value: string; metadata?: string; expirationTtl?: number }): OkResponse {
61
+ const db = getDatabase();
62
+ const encoded = new TextEncoder().encode(value);
63
+ const exp = expirationTtl ? Math.floor(Date.now() / 1000) + expirationTtl : null;
64
+ const meta = metadata?.trim() || null;
65
+ db.prepare(
66
+ "INSERT OR REPLACE INTO kv (namespace, key, value, metadata, expiration) VALUES (?, ?, ?, ?, ?)"
67
+ ).run(ns, key, encoded, meta, exp);
68
+ return { ok: true };
69
+ },
70
+
71
+ "kv.deleteKey"({ ns, key }: { ns: string; key: string }): OkResponse {
72
+ const db = getDatabase();
73
+ db.prepare("DELETE FROM kv WHERE namespace = ? AND key = ?").run(ns, key);
74
+ return { ok: true };
75
+ },
76
+ };
@@ -0,0 +1,94 @@
1
+ import type { HandlerContext, OverviewData } from "../types";
2
+ import { getAllConfigs } from "../types";
3
+ import { getDatabase, getDataDir } from "../../../db";
4
+ import { getTraceStore } from "../../../tracing/store";
5
+ import { join } from "node:path";
6
+ import { existsSync, readdirSync } from "node:fs";
7
+
8
+ export const handlers = {
9
+ "overview.get"(_input: {}, ctx: HandlerContext): OverviewData {
10
+ const db = getDatabase();
11
+
12
+ const d1Dir = join(getDataDir(), "d1");
13
+ let d1Count = 0;
14
+ if (existsSync(d1Dir)) {
15
+ d1Count = readdirSync(d1Dir).filter(f => f.endsWith(".sqlite")).length;
16
+ }
17
+
18
+ const dbContainers = new Set<string>();
19
+ const dbKv = new Set(db.query<{ namespace: string }, []>("SELECT DISTINCT namespace FROM kv").all().map(r => r.namespace));
20
+ const dbR2 = new Set(db.query<{ bucket: string }, []>("SELECT DISTINCT bucket FROM r2_objects").all().map(r => r.bucket));
21
+ const dbQueue = new Set(db.query<{ queue: string }, []>("SELECT DISTINCT queue FROM queue_messages").all().map(r => r.queue));
22
+ const dbDo = new Set(db.query<{ namespace: string }, []>("SELECT DISTINCT namespace FROM do_storage").all().map(r => r.namespace));
23
+ const dbWorkflows = new Set(db.query<{ workflow_name: string }, []>("SELECT DISTINCT workflow_name FROM workflow_instances").all().map(r => r.workflow_name));
24
+
25
+ for (const config of getAllConfigs(ctx)) {
26
+ for (const ns of config.kv_namespaces ?? []) dbKv.add(ns.id);
27
+ for (const b of config.r2_buckets ?? []) dbR2.add(b.bucket_name);
28
+ for (const p of config.queues?.producers ?? []) dbQueue.add(p.queue);
29
+ for (const b of config.durable_objects?.bindings ?? []) dbDo.add(b.class_name);
30
+ for (const w of config.workflows ?? []) dbWorkflows.add(w.binding);
31
+ for (const c of config.containers ?? []) dbContainers.add(c.class_name);
32
+ d1Count = Math.max(d1Count, (config.d1_databases ?? []).length);
33
+ }
34
+
35
+ const emailCount = db.query<{ count: number }, []>("SELECT COUNT(*) as count FROM email_messages").get()?.count ?? 0;
36
+ const aiCount = db.query<{ count: number }, []>("SELECT COUNT(*) as count FROM ai_requests").get()?.count ?? 0;
37
+ const analyticsEngineCount = db.query<{ count: number }, []>("SELECT COUNT(DISTINCT dataset) as count FROM analytics_engine").get()?.count ?? 0;
38
+
39
+ return {
40
+ kv: dbKv.size,
41
+ r2: dbR2.size,
42
+ queue: dbQueue.size,
43
+ do: dbDo.size,
44
+ workflows: dbWorkflows.size,
45
+ containers: dbContainers.size,
46
+ d1: d1Count,
47
+ cache: db.query<{ count: number }, []>("SELECT COUNT(DISTINCT cache_name) as count FROM cache_entries").get()?.count ?? 0,
48
+ errors: getTraceStore().getErrorCount(),
49
+ workerErrors: getTraceStore().getErrorCountsByWorker(),
50
+ scheduled: getAllConfigs(ctx).reduce((sum, cfg) => sum + (cfg.triggers?.crons?.length ?? 0), 0),
51
+ email: emailCount,
52
+ ai: aiCount,
53
+ analyticsEngine: analyticsEngineCount,
54
+ generations: ctx.manager ? ctx.manager.list() : [],
55
+ runtime: (() => {
56
+ const mem = process.memoryUsage();
57
+ const cpu = process.cpuUsage();
58
+ const envPassthrough: Record<string, string> = {};
59
+ for (const key of [
60
+ "NODE_ENV", "BUN_ENV", "SHELL", "TERM", "LANG", "HOME", "USER",
61
+ "PATH", "EDITOR", "TZ",
62
+ ]) {
63
+ if (process.env[key]) envPassthrough[key] = process.env[key]!;
64
+ }
65
+ return {
66
+ bunVersion: Bun.version,
67
+ platform: process.platform,
68
+ arch: process.arch,
69
+ pid: process.pid,
70
+ cwd: process.cwd(),
71
+ uptime: process.uptime(),
72
+ startedAt: Date.now() - process.uptime() * 1000,
73
+ memory: {
74
+ rss: mem.rss,
75
+ heapUsed: mem.heapUsed,
76
+ heapTotal: mem.heapTotal,
77
+ external: mem.external,
78
+ },
79
+ cpuUsage: {
80
+ user: cpu.user,
81
+ system: cpu.system,
82
+ },
83
+ env: envPassthrough,
84
+ };
85
+ })(),
86
+ ...(ctx.registry ? {
87
+ workers: Array.from(ctx.registry.listManagers()).map(([name, mgr]) => ({
88
+ workerName: name,
89
+ generations: mgr.list(),
90
+ })),
91
+ } : {}),
92
+ };
93
+ },
94
+ };
@@ -0,0 +1,79 @@
1
+ import type { HandlerContext, QueueInfo, QueueMessage, OkResponse } from "../types";
2
+ import { getAllConfigs } from "../types";
3
+ import { getDatabase } from "../../../db";
4
+ import { randomUUIDv7 } from "bun";
5
+ import type { SQLQueryBindings } from "bun:sqlite";
6
+
7
+ export const handlers = {
8
+ "queue.listQueues"(_input: {}, ctx: HandlerContext): QueueInfo[] {
9
+ const db = getDatabase();
10
+ const rows = db.query<{ queue: string; pending: number; acked: number; failed: number }, []>(
11
+ `SELECT queue,
12
+ SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending,
13
+ SUM(CASE WHEN status = 'acked' THEN 1 ELSE 0 END) as acked,
14
+ SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed
15
+ FROM queue_messages GROUP BY queue ORDER BY queue`
16
+ ).all();
17
+ const rowMap = new Map(rows.map(r => [r.queue, r]));
18
+ for (const config of getAllConfigs(ctx)) {
19
+ for (const p of config.queues?.producers ?? []) {
20
+ if (!rowMap.has(p.queue)) {
21
+ rows.push({ queue: p.queue, pending: 0, acked: 0, failed: 0 });
22
+ }
23
+ }
24
+ }
25
+ rows.sort((a, b) => a.queue.localeCompare(b.queue));
26
+ return rows;
27
+ },
28
+
29
+ "queue.listMessages"({ queue, limit = 50, status }: { queue: string; limit?: number; status?: string }): QueueMessage[] {
30
+ const db = getDatabase();
31
+ let query = "SELECT id, body, content_type, status, attempts, visible_at, created_at, completed_at FROM queue_messages WHERE queue = ?";
32
+ const params: SQLQueryBindings[] = [queue];
33
+
34
+ if (status) { query += " AND status = ?"; params.push(status); }
35
+ query += " ORDER BY created_at DESC LIMIT ?";
36
+ params.push(limit);
37
+
38
+ const rows = db.prepare(query).all(...params) as Record<string, unknown>[];
39
+ return rows.map(row => {
40
+ let bodyStr: string;
41
+ try { bodyStr = new TextDecoder().decode(row.body as BufferSource); }
42
+ catch { bodyStr = `<binary>`; }
43
+ return { ...row, body: bodyStr } as QueueMessage;
44
+ });
45
+ },
46
+
47
+ "queue.deleteMessage"({ queue, id }: { queue: string; id: string }): OkResponse {
48
+ const db = getDatabase();
49
+ db.prepare("DELETE FROM queue_messages WHERE queue = ? AND id = ?").run(queue, id);
50
+ return { ok: true };
51
+ },
52
+
53
+ "queue.publishMessage"({ queue, body, contentType = "json" }: { queue: string; body: string; contentType?: string }): OkResponse {
54
+ const db = getDatabase();
55
+ const now = Date.now();
56
+ let encoded: Uint8Array;
57
+ if (contentType === "json") {
58
+ // Validate JSON
59
+ JSON.parse(body);
60
+ encoded = new TextEncoder().encode(body);
61
+ } else {
62
+ encoded = new TextEncoder().encode(body);
63
+ }
64
+ db.run(
65
+ "INSERT INTO queue_messages (id, queue, body, content_type, attempts, status, visible_at, created_at) VALUES (?, ?, ?, ?, 0, 'pending', ?, ?)",
66
+ [randomUUIDv7(), queue, encoded, contentType, now, now],
67
+ );
68
+ return { ok: true };
69
+ },
70
+
71
+ "queue.requeueMessage"({ queue, id }: { queue: string; id: string }): OkResponse {
72
+ const db = getDatabase();
73
+ const now = Date.now();
74
+ db.prepare(
75
+ "UPDATE queue_messages SET status = 'pending', attempts = 0, visible_at = ?, completed_at = NULL WHERE queue = ? AND id = ?",
76
+ ).run(now, queue, id);
77
+ return { ok: true };
78
+ },
79
+ };
@@ -0,0 +1,72 @@
1
+ import type { HandlerContext, R2Bucket, R2Object, Paginated, OkResponse } from "../types";
2
+ import { getAllConfigs } from "../types";
3
+ import { getDatabase, getDataDir } from "../../../db";
4
+ import type { SQLQueryBindings } from "bun:sqlite";
5
+ import { join, dirname } from "node:path";
6
+ import { existsSync, unlinkSync, mkdirSync } from "node:fs";
7
+
8
+ export const handlers = {
9
+ "r2.listBuckets"(_input: {}, ctx: HandlerContext): R2Bucket[] {
10
+ const db = getDatabase();
11
+ const rows = db.query<{ bucket: string; count: number; total_size: number }, []>(
12
+ "SELECT bucket, COUNT(*) as count, COALESCE(SUM(size),0) as total_size FROM r2_objects GROUP BY bucket ORDER BY bucket"
13
+ ).all();
14
+ const rowMap = new Map(rows.map(r => [r.bucket, r]));
15
+ for (const config of getAllConfigs(ctx)) {
16
+ for (const b of config.r2_buckets ?? []) {
17
+ if (!rowMap.has(b.bucket_name)) {
18
+ rows.push({ bucket: b.bucket_name, count: 0, total_size: 0 });
19
+ }
20
+ }
21
+ }
22
+ rows.sort((a, b) => a.bucket.localeCompare(b.bucket));
23
+ return rows;
24
+ },
25
+
26
+ "r2.listObjects"({ bucket, limit = 50, cursor = "", prefix = "" }: { bucket: string; limit?: number; cursor?: string; prefix?: string }): Paginated<R2Object> {
27
+ const db = getDatabase();
28
+ let query = "SELECT key, size, etag, uploaded, http_metadata, custom_metadata FROM r2_objects WHERE bucket = ?";
29
+ const params: SQLQueryBindings[] = [bucket];
30
+
31
+ if (prefix) { query += " AND key LIKE ?"; params.push(prefix + "%"); }
32
+ if (cursor) { query += " AND key > ?"; params.push(cursor); }
33
+ query += " ORDER BY key LIMIT ?";
34
+ params.push(limit + 1);
35
+
36
+ const rows = db.prepare(query).all(...params) as R2Object[];
37
+ const hasMore = rows.length > limit;
38
+ const items = rows.slice(0, limit);
39
+ const last = items[items.length - 1];
40
+ return { items, cursor: hasMore && last ? last.key : null };
41
+ },
42
+
43
+ "r2.deleteObject"({ bucket, key }: { bucket: string; key: string }): OkResponse {
44
+ const db = getDatabase();
45
+ db.prepare("DELETE FROM r2_objects WHERE bucket = ? AND key = ?").run(bucket, key);
46
+ const filePath = join(getDataDir(), "r2", bucket, key);
47
+ if (existsSync(filePath)) unlinkSync(filePath);
48
+ return { ok: true };
49
+ },
50
+
51
+ async "r2.renameObject"({ bucket, oldKey, newKey }: { bucket: string; oldKey: string; newKey: string }): Promise<OkResponse> {
52
+ const db = getDatabase();
53
+ const oldPath = join(getDataDir(), "r2", bucket, oldKey);
54
+ const newPath = join(getDataDir(), "r2", bucket, newKey);
55
+
56
+ if (!existsSync(oldPath)) {
57
+ throw new Error(`Object "${oldKey}" not found in bucket "${bucket}"`);
58
+ }
59
+
60
+ mkdirSync(dirname(newPath), { recursive: true });
61
+ const data = await Bun.file(oldPath).arrayBuffer();
62
+ await Bun.write(newPath, data);
63
+
64
+ db.run(
65
+ "UPDATE r2_objects SET key = ? WHERE bucket = ? AND key = ?",
66
+ [newKey, bucket, oldKey],
67
+ );
68
+
69
+ unlinkSync(oldPath);
70
+ return { ok: true };
71
+ },
72
+ };