trellis 3.1.35 → 3.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (287) hide show
  1. package/CHANGELOG.md +23 -5
  2. package/README.md +63 -156
  3. package/bin/trellis.mjs +2 -75
  4. package/dist/{better-sqlite-backend-ahx5p0br.js → better-sqlite-backend-2P4F6AOJ.js} +91 -29
  5. package/dist/boot-middleware-MHLHFCUC.js +9 -0
  6. package/dist/browser/index.d.ts +18 -0
  7. package/dist/browser/index.d.ts.map +1 -0
  8. package/dist/browser/index.js +95 -0
  9. package/dist/{index-53f3b8p8.js → chunk-2W3MHTMG.js} +552 -213
  10. package/dist/chunk-44FFQKQX.js +142 -0
  11. package/dist/chunk-4HYPLFJD.js +150 -0
  12. package/dist/chunk-4XIVMVHC.js +361 -0
  13. package/dist/{index-yp88he8n.js → chunk-6CTA6MCE.js} +88 -64
  14. package/dist/chunk-6GEZ6DN5.js +394 -0
  15. package/dist/chunk-7ZFRVCUE.js +211 -0
  16. package/dist/chunk-CAG4FULI.js +121 -0
  17. package/dist/chunk-FF36CQHZ.js +61 -0
  18. package/dist/{index-h32txmxe.js → chunk-FKQO5A3H.js} +6 -13
  19. package/dist/{index-nq520y6k.js → chunk-FWRS4CSC.js} +46 -32
  20. package/dist/chunk-GFZCJ4EH.js +108 -0
  21. package/dist/chunk-GLM4HGN4.js +55 -0
  22. package/dist/{index-4cdr7x2x.js → chunk-GU374KWZ.js} +182 -111
  23. package/dist/chunk-H6JB3PZ3.js +45 -0
  24. package/dist/chunk-IZX2PLIB.js +1240 -0
  25. package/dist/chunk-J2TXUFCZ.js +289 -0
  26. package/dist/chunk-J3WYZO3Q.js +211 -0
  27. package/dist/{index-xzym9w0m.js → chunk-JA7AIHRK.js} +12 -7
  28. package/dist/chunk-JHHEXEDM.js +162 -0
  29. package/dist/chunk-JUEMDWLU.js +233 -0
  30. package/dist/chunk-KFQGP6VL.js +33 -0
  31. package/dist/chunk-KIJITUZB.js +419 -0
  32. package/dist/{index-4wxa8xz4.js → chunk-KQ4A2D2P.js} +162 -23
  33. package/dist/chunk-KSU2GW22.js +958 -0
  34. package/dist/{index-rv1p47gp.js → chunk-L5N5PR2S.js} +3015 -2480
  35. package/dist/{index-h9e2efx4.js → chunk-LNUIGRDZ.js} +71 -55
  36. package/dist/{index-skhn0agf.js → chunk-MLC5BUYO.js} +19 -20
  37. package/dist/chunk-MPXUVGT3.js +104 -0
  38. package/dist/chunk-N6MYZT4O.js +68 -0
  39. package/dist/chunk-O2AV5WMN.js +48 -0
  40. package/dist/chunk-PZ4XYZ4J.js +143 -0
  41. package/dist/chunk-QB5ISE47.js +419 -0
  42. package/dist/chunk-RSWUFTO2.js +857 -0
  43. package/dist/chunk-S3XLFZ27.js +1095 -0
  44. package/dist/{index-w5wccer3.js → chunk-S6GBJ2ZB.js} +28 -12
  45. package/dist/{index-7pjz3tsy.js → chunk-SGMDTWJJ.js} +57 -15
  46. package/dist/chunk-SPSPV5BC.js +390 -0
  47. package/dist/chunk-TCKXSOBP.js +123 -0
  48. package/dist/chunk-UBGUDMUV.js +59 -0
  49. package/dist/{index-wt8rz4gn.js → chunk-UQISRW6T.js} +97 -26
  50. package/dist/chunk-USEINYUL.js +12 -0
  51. package/dist/{index-bmyt7k8n.js → chunk-UZRUP7QW.js} +12 -4
  52. package/dist/{index-xhcp6xrn.js → chunk-VBYUTRXX.js} +695 -514
  53. package/dist/{index-vkpkfwhq.js → chunk-VDAKEOML.js} +234 -194
  54. package/dist/chunk-WXSWA3MV.js +0 -0
  55. package/dist/{index-n9f2qyh5.js → chunk-X5FLTRUB.js} +55 -33
  56. package/dist/{index-h7zxhhhh.js → chunk-XMQ7G77X.js} +46 -38
  57. package/dist/{index-w7ng765c.js → chunk-YCM7ZYEZ.js} +102 -49
  58. package/dist/cli/deploy-cli.d.ts +18 -0
  59. package/dist/cli/deploy-cli.d.ts.map +1 -0
  60. package/dist/cli/errors.d.ts +6 -0
  61. package/dist/cli/errors.d.ts.map +1 -0
  62. package/dist/cli/examples.d.ts +23 -0
  63. package/dist/cli/examples.d.ts.map +1 -0
  64. package/dist/cli/index.d.ts +1 -1
  65. package/dist/cli/index.d.ts.map +1 -1
  66. package/dist/cli/index.js +2426 -6225
  67. package/dist/client/index.browser.d.ts +29 -0
  68. package/dist/client/index.browser.d.ts.map +1 -0
  69. package/dist/client/index.browser.js +64 -0
  70. package/dist/client/index.d.ts +6 -1
  71. package/dist/client/index.d.ts.map +1 -1
  72. package/dist/client/index.js +41 -21
  73. package/dist/client/live.d.ts +67 -0
  74. package/dist/client/live.d.ts.map +1 -0
  75. package/dist/client/reactive.d.ts +31 -0
  76. package/dist/client/reactive.d.ts.map +1 -0
  77. package/dist/client/sdk.browser.d.ts +104 -0
  78. package/dist/client/sdk.browser.d.ts.map +1 -0
  79. package/dist/client/sdk.browser.js +9 -0
  80. package/dist/client/sdk.d.ts +26 -2
  81. package/dist/client/sdk.d.ts.map +1 -1
  82. package/dist/client/sdk.js +9 -0
  83. package/dist/client/vcs-client.d.ts +129 -0
  84. package/dist/client/vcs-client.d.ts.map +1 -0
  85. package/dist/cms/index.js +171 -163
  86. package/dist/{config-8hczw0rs.js → config-PFTP67TR.js} +8 -9
  87. package/dist/core/computation/index.d.ts +5 -0
  88. package/dist/core/computation/index.d.ts.map +1 -0
  89. package/dist/core/computation/rollup.d.ts +18 -0
  90. package/dist/core/computation/rollup.d.ts.map +1 -0
  91. package/dist/core/index.d.ts +5 -0
  92. package/dist/core/index.d.ts.map +1 -1
  93. package/dist/core/index.js +171 -104
  94. package/dist/core/kernel/boot-middleware.d.ts +10 -0
  95. package/dist/core/kernel/boot-middleware.d.ts.map +1 -0
  96. package/dist/core/kernel/logic-middleware.d.ts +7 -1
  97. package/dist/core/kernel/logic-middleware.d.ts.map +1 -1
  98. package/dist/core/kernel/trellis-kernel.d.ts.map +1 -1
  99. package/dist/core/ontology/types.d.ts +5 -0
  100. package/dist/core/ontology/types.d.ts.map +1 -1
  101. package/dist/core/persist/better-sqlite-backend.d.ts +9 -2
  102. package/dist/core/persist/better-sqlite-backend.d.ts.map +1 -1
  103. package/dist/core/persist/factory.d.ts +6 -7
  104. package/dist/core/persist/factory.d.ts.map +1 -1
  105. package/dist/core/persist/factory.js +2 -3
  106. package/dist/core/persist/sqljs-backend.js +2 -3
  107. package/dist/db/index.d.ts +4 -1
  108. package/dist/db/index.d.ts.map +1 -1
  109. package/dist/db/index.js +67 -54
  110. package/dist/db/inspector.js +11 -19
  111. package/dist/db/trellis.css +1 -1
  112. package/dist/decisions/index.js +9 -10
  113. package/dist/deploy-LQ7GUGJ5.js +9 -0
  114. package/dist/deploy-YNVG5E4E.js +9 -0
  115. package/dist/deploy-cli-24BCHAXY.js +67 -0
  116. package/dist/deploy-cli-C35HTITO.js +67 -0
  117. package/dist/embeddings/auto-embed.d.ts +2 -2
  118. package/dist/embeddings/auto-embed.d.ts.map +1 -1
  119. package/dist/embeddings/index.js +20 -22
  120. package/dist/embeddings/search.d.ts +2 -1
  121. package/dist/embeddings/search.d.ts.map +1 -1
  122. package/dist/embeddings/store.d.ts +25 -4
  123. package/dist/embeddings/store.d.ts.map +1 -1
  124. package/dist/engine.d.ts +59 -19
  125. package/dist/engine.d.ts.map +1 -1
  126. package/dist/import-UZEKXU3B.js +10 -0
  127. package/dist/index.js +102 -93
  128. package/dist/launch-explorer-HZ4ZJA4X.js +95 -0
  129. package/dist/links/index.js +24 -26
  130. package/dist/mcp/server.d.ts.map +1 -1
  131. package/dist/plugins/agent-memory/index.js +316 -0
  132. package/dist/plugins/idea-garden/index.js +165 -0
  133. package/dist/plugins/plan-approval/index.js +579 -0
  134. package/dist/plugins/proactive-watcher/index.js +194 -0
  135. package/dist/{index-2r4jxwnb.js → query-XYLTJYSA.js} +14 -15
  136. package/dist/react/index.js +39 -70
  137. package/dist/react/realtime.js +11 -0
  138. package/dist/react/schema-hooks.js +87 -0
  139. package/dist/realtime/broadcast-channel-transport.d.ts +43 -0
  140. package/dist/realtime/broadcast-channel-transport.d.ts.map +1 -0
  141. package/dist/realtime/durable-object-relay-transport.d.ts +96 -0
  142. package/dist/realtime/durable-object-relay-transport.d.ts.map +1 -0
  143. package/dist/realtime/index.d.ts +44 -0
  144. package/dist/realtime/index.d.ts.map +1 -0
  145. package/dist/realtime/index.js +40 -0
  146. package/dist/realtime/memory-hub.d.ts +32 -0
  147. package/dist/realtime/memory-hub.d.ts.map +1 -0
  148. package/dist/realtime/persistent-channel.d.ts +105 -0
  149. package/dist/realtime/persistent-channel.d.ts.map +1 -0
  150. package/dist/realtime/presence.d.ts +73 -0
  151. package/dist/realtime/presence.d.ts.map +1 -0
  152. package/dist/realtime/relay-persistence.d.ts +37 -0
  153. package/dist/realtime/relay-persistence.d.ts.map +1 -0
  154. package/dist/realtime/relay-persistence.js +107 -0
  155. package/dist/realtime/relay-server.d.ts +103 -0
  156. package/dist/realtime/relay-server.d.ts.map +1 -0
  157. package/dist/realtime/room.d.ts +98 -0
  158. package/dist/realtime/room.d.ts.map +1 -0
  159. package/dist/realtime/text.d.ts +73 -0
  160. package/dist/realtime/text.d.ts.map +1 -0
  161. package/dist/realtime/types.d.ts +89 -0
  162. package/dist/realtime/types.d.ts.map +1 -0
  163. package/dist/realtime/websocket-relay-transport.d.ts +28 -0
  164. package/dist/realtime/websocket-relay-transport.d.ts.map +1 -0
  165. package/dist/realtime.bundle.js +1100 -0
  166. package/dist/{remote-manager-xvbg4230.js → remote-manager-ODE3T2KR.js} +25 -15
  167. package/dist/scaffold/seed.d.ts.map +1 -1
  168. package/dist/scaffold/write.d.ts +1 -1
  169. package/dist/scaffold/write.d.ts.map +1 -1
  170. package/dist/schema/define.d.ts +137 -0
  171. package/dist/schema/define.d.ts.map +1 -0
  172. package/dist/schema/entity-projection.d.ts +34 -0
  173. package/dist/schema/entity-projection.d.ts.map +1 -0
  174. package/dist/schema/eql.d.ts +46 -0
  175. package/dist/schema/eql.d.ts.map +1 -0
  176. package/dist/schema/index.d.ts +21 -0
  177. package/dist/schema/index.d.ts.map +1 -0
  178. package/dist/schema/index.js +46 -0
  179. package/dist/schema/kernel-resolve.d.ts +21 -0
  180. package/dist/schema/kernel-resolve.d.ts.map +1 -0
  181. package/dist/schema/mutations.d.ts +20 -0
  182. package/dist/schema/mutations.d.ts.map +1 -0
  183. package/dist/schema/resolve.d.ts +21 -0
  184. package/dist/schema/resolve.d.ts.map +1 -0
  185. package/dist/server/deploy-meta.d.ts +25 -0
  186. package/dist/server/deploy-meta.d.ts.map +1 -0
  187. package/dist/server/deploy.d.ts +7 -2
  188. package/dist/server/deploy.d.ts.map +1 -1
  189. package/dist/server/index.d.ts +7 -2
  190. package/dist/server/index.d.ts.map +1 -1
  191. package/dist/server/index.js +100 -58
  192. package/dist/server/node-adapter.js +5 -104
  193. package/dist/server/realtime.d.ts +14 -3
  194. package/dist/server/realtime.d.ts.map +1 -1
  195. package/dist/server/server.d.ts +8 -16
  196. package/dist/server/server.d.ts.map +1 -1
  197. package/dist/server/sprites.d.ts +18 -1
  198. package/dist/server/sprites.d.ts.map +1 -1
  199. package/dist/server/tenancy.d.ts +4 -0
  200. package/dist/server/tenancy.d.ts.map +1 -1
  201. package/dist/server/usage-meter.d.ts +53 -0
  202. package/dist/server/usage-meter.d.ts.map +1 -0
  203. package/dist/server-MB3OPSPI.js +18 -0
  204. package/dist/server-ZVWGLLRF.js +18 -0
  205. package/dist/sprites-KQGTGM6W.js +27 -0
  206. package/dist/{sprites-vc4qbrp1.js → sprites-RU3E4XQN.js} +6 -7
  207. package/dist/svelte/index.d.ts +17 -0
  208. package/dist/svelte/index.d.ts.map +1 -0
  209. package/dist/svelte/index.js +51 -0
  210. package/dist/svelte/schema-hooks.d.ts +45 -0
  211. package/dist/svelte/schema-hooks.d.ts.map +1 -0
  212. package/dist/svelte/schema-hooks.js +71 -0
  213. package/dist/svelte/stores.d.ts +78 -0
  214. package/dist/svelte/stores.d.ts.map +1 -0
  215. package/dist/sync/http-transport.d.ts +3 -3
  216. package/dist/sync/http-transport.d.ts.map +1 -1
  217. package/dist/sync/index.d.ts +15 -1
  218. package/dist/sync/index.d.ts.map +1 -1
  219. package/dist/sync/index.js +617 -0
  220. package/dist/sync/memory-room.d.ts +37 -0
  221. package/dist/sync/memory-room.d.ts.map +1 -0
  222. package/dist/sync/memory-transport.d.ts +3 -3
  223. package/dist/sync/memory-transport.d.ts.map +1 -1
  224. package/dist/sync/partykit-room.d.ts +40 -0
  225. package/dist/sync/partykit-room.d.ts.map +1 -0
  226. package/dist/sync/partykit-transport.d.ts +84 -0
  227. package/dist/sync/partykit-transport.d.ts.map +1 -0
  228. package/dist/sync/room-core.d.ts +53 -0
  229. package/dist/sync/room-core.d.ts.map +1 -0
  230. package/dist/sync/sync-engine.d.ts +40 -2
  231. package/dist/sync/sync-engine.d.ts.map +1 -1
  232. package/dist/sync/sync-room-server.d.ts +39 -0
  233. package/dist/sync/sync-room-server.d.ts.map +1 -0
  234. package/dist/sync/types.d.ts +70 -3
  235. package/dist/sync/types.d.ts.map +1 -1
  236. package/dist/sync/vcs-sync-peer.d.ts +63 -0
  237. package/dist/sync/vcs-sync-peer.d.ts.map +1 -0
  238. package/dist/sync/ws-transport.d.ts +2 -2
  239. package/dist/sync/ws-transport.d.ts.map +1 -1
  240. package/dist/tenancy-NBLLGCNJ.js +14 -0
  241. package/dist/tenancy-POMWQP6M.js +16 -0
  242. package/dist/ui/client.html +355 -67
  243. package/dist/ui/launch-explorer.d.ts +22 -0
  244. package/dist/ui/launch-explorer.d.ts.map +1 -0
  245. package/dist/ui/server.d.ts +1 -1
  246. package/dist/ui/server.d.ts.map +1 -1
  247. package/dist/vcs/blob-store.d.ts.map +1 -1
  248. package/dist/vcs/idb-op-log.d.ts +67 -0
  249. package/dist/vcs/idb-op-log.d.ts.map +1 -0
  250. package/dist/vcs/index.d.ts +1 -0
  251. package/dist/vcs/index.d.ts.map +1 -1
  252. package/dist/vcs/index.js +81 -72
  253. package/dist/vcs/issue.d.ts +22 -5
  254. package/dist/vcs/issue.d.ts.map +1 -1
  255. package/dist/vcs/lane-materialize.d.ts +5 -0
  256. package/dist/vcs/lane-materialize.d.ts.map +1 -1
  257. package/dist/vcs/op-log.d.ts +28 -2
  258. package/dist/vcs/op-log.d.ts.map +1 -1
  259. package/dist/vcs/ops.d.ts +10 -0
  260. package/dist/vcs/ops.d.ts.map +1 -1
  261. package/dist/vcs/types.d.ts +2 -0
  262. package/dist/vcs/types.d.ts.map +1 -1
  263. package/dist/{vm-config-6xhsj6b3.js → vm-config-JI4OXKDQ.js} +9 -10
  264. package/dist/vue/hooks.d.ts +64 -0
  265. package/dist/vue/hooks.d.ts.map +1 -0
  266. package/dist/vue/index.d.ts +19 -0
  267. package/dist/vue/index.d.ts.map +1 -0
  268. package/dist/vue/index.js +59 -0
  269. package/dist/vue/schema-hooks.d.ts +41 -0
  270. package/dist/vue/schema-hooks.d.ts.map +1 -0
  271. package/dist/vue/schema-hooks.js +64 -0
  272. package/package.json +130 -17
  273. package/dist/deploy-999q207z.js +0 -10
  274. package/dist/engine-y0724kjq.js +0 -8
  275. package/dist/import-s2b8e0ft.js +0 -11
  276. package/dist/index-a76rekgs.js +0 -67
  277. package/dist/index-c9h37r6h.js +0 -1
  278. package/dist/index-hr9qvv77.js +0 -860
  279. package/dist/index-k5b0xskw.js +0 -1
  280. package/dist/index-nzb3am4f.js +0 -80
  281. package/dist/index-wncptktd.js +0 -292
  282. package/dist/index-y6a4kj0p.js +0 -43
  283. package/dist/index-yhwjgfvj.js +0 -342
  284. package/dist/sdk-bepky0xs.js +0 -16
  285. package/dist/server-szdjx0nt.js +0 -13
  286. package/dist/sqlite-backend-0vsmc6qj.js +0 -8
  287. package/dist/tenancy-pjm32b4v.js +0 -14
@@ -1,17 +1,21 @@
1
- // @bun
2
1
  import {
3
- parseSimple
4
- } from "./index-n9f2qyh5.js";
2
+ DEFAULT_TENANT
3
+ } from "./chunk-JHHEXEDM.js";
4
+ import {
5
+ rel
6
+ } from "./chunk-44FFQKQX.js";
5
7
  import {
6
- QueryEngine
7
- } from "./index-yp88he8n.js";
8
+ entityRecordToPlain,
9
+ hydrateBindings,
10
+ resolveRelations
11
+ } from "./chunk-JUEMDWLU.js";
8
12
  import {
9
- __require
10
- } from "./index-a76rekgs.js";
13
+ parseSimple
14
+ } from "./chunk-X5FLTRUB.js";
11
15
 
12
16
  // src/server/server.ts
13
- import { existsSync, readFileSync } from "fs";
14
- import { dirname, join } from "path";
17
+ import { existsSync as existsSync2, readFileSync } from "fs";
18
+ import { dirname, join as join2 } from "path";
15
19
  import { fileURLToPath } from "url";
16
20
 
17
21
  // src/server/auth.ts
@@ -33,13 +37,19 @@ function base64UrlDecode(input) {
33
37
  return Uint8Array.from(binary, (c) => c.charCodeAt(0));
34
38
  }
35
39
  async function hmacKey(secret) {
36
- return crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"]);
40
+ return crypto.subtle.importKey(
41
+ "raw",
42
+ new TextEncoder().encode(secret),
43
+ { name: "HMAC", hash: "SHA-256" },
44
+ false,
45
+ ["sign", "verify"]
46
+ );
37
47
  }
38
48
  async function signJwt(payload, secret, expiresInSeconds = 86400) {
39
49
  const header = { alg: "HS256", typ: "JWT" };
40
- const now = Math.floor(Date.now() / 1000);
50
+ const now = Math.floor(Date.now() / 1e3);
41
51
  const claims = { iat: now, exp: now + expiresInSeconds, ...payload };
42
- const enc = new TextEncoder;
52
+ const enc = new TextEncoder();
43
53
  const headerB64 = base64UrlEncode(enc.encode(JSON.stringify(header)));
44
54
  const payloadB64 = base64UrlEncode(enc.encode(JSON.stringify(claims)));
45
55
  const signing = `${headerB64}.${payloadB64}`;
@@ -50,22 +60,29 @@ async function signJwt(payload, secret, expiresInSeconds = 86400) {
50
60
  }
51
61
  async function verifyJwt(token, secret) {
52
62
  const parts = token.split(".");
53
- if (parts.length !== 3)
54
- return null;
63
+ if (parts.length !== 3) return null;
55
64
  const [headerB64, payloadB64, sigB64] = parts;
56
- const enc = new TextEncoder;
65
+ const enc = new TextEncoder();
57
66
  const signing = `${headerB64}.${payloadB64}`;
58
67
  try {
59
68
  const key = await hmacKey(secret);
60
69
  const sigRaw = base64UrlDecode(sigB64);
61
- const sigBuf = sigRaw.buffer.slice(sigRaw.byteOffset, sigRaw.byteOffset + sigRaw.byteLength);
62
- const valid = await crypto.subtle.verify("HMAC", key, sigBuf, enc.encode(signing));
63
- if (!valid)
64
- return null;
65
- const claims = JSON.parse(new TextDecoder().decode(base64UrlDecode(payloadB64)));
66
- const now = Math.floor(Date.now() / 1000);
67
- if (typeof claims.exp === "number" && claims.exp < now)
68
- return null;
70
+ const sigBuf = sigRaw.buffer.slice(
71
+ sigRaw.byteOffset,
72
+ sigRaw.byteOffset + sigRaw.byteLength
73
+ );
74
+ const valid = await crypto.subtle.verify(
75
+ "HMAC",
76
+ key,
77
+ sigBuf,
78
+ enc.encode(signing)
79
+ );
80
+ if (!valid) return null;
81
+ const claims = JSON.parse(
82
+ new TextDecoder().decode(base64UrlDecode(payloadB64))
83
+ );
84
+ const now = Math.floor(Date.now() / 1e3);
85
+ if (typeof claims.exp === "number" && claims.exp < now) return null;
69
86
  return claims;
70
87
  } catch {
71
88
  return null;
@@ -157,34 +174,50 @@ async function exchangeOAuthCode(provider, code, redirectUri) {
157
174
  id: String(user.id ?? user.sub ?? ""),
158
175
  email: String(user.email ?? ""),
159
176
  name: String(user.name ?? user.login ?? ""),
160
- avatarUrl: user.picture ?? user.avatar_url ?? undefined
177
+ avatarUrl: user.picture ?? user.avatar_url ?? void 0
161
178
  };
162
179
  }
163
180
 
164
181
  // src/server/permissions.ts
165
- class PermissionRegistry {
166
- rules = new Map;
182
+ var PermissionRegistry = class {
183
+ rules = /* @__PURE__ */ new Map();
167
184
  defaultRule = "authenticated";
185
+ /**
186
+ * Register permissions for an entity type.
187
+ */
168
188
  register(entityType, permissions) {
169
189
  this.rules.set(entityType, permissions);
170
190
  }
191
+ /**
192
+ * Set the fallback rule used when an entity type has no declared permissions.
193
+ */
171
194
  setDefault(rule) {
172
195
  this.defaultRule = rule;
173
196
  }
197
+ /**
198
+ * Get the permission rule for a specific operation on an entity type.
199
+ * Falls back to the default rule if not declared.
200
+ */
174
201
  getRule(entityType, op) {
175
202
  const def = this.rules.get(entityType);
176
203
  return def?.[op] ?? this.defaultRule;
177
204
  }
205
+ /**
206
+ * Check whether an auth context is allowed to perform an operation.
207
+ */
178
208
  check(auth, entityType, op, entity = null) {
179
209
  const rule = this.getRule(entityType, op);
180
210
  return evaluateRule(rule, auth, entity);
181
211
  }
212
+ /**
213
+ * Assert access — throws a PermissionError if denied.
214
+ */
182
215
  assert(auth, entityType, op, entity = null) {
183
216
  if (!this.check(auth, entityType, op, entity)) {
184
217
  throw new PermissionError(auth, entityType, op);
185
218
  }
186
219
  }
187
- }
220
+ };
188
221
  function evaluateRule(rule, auth, entity) {
189
222
  if (rule === "public") {
190
223
  return true;
@@ -193,9 +226,10 @@ function evaluateRule(rule, auth, entity) {
193
226
  return auth.authenticated;
194
227
  }
195
228
  if (rule === "own") {
196
- if (!auth.authenticated || !auth.userId)
197
- return false;
198
- const ownerFact = entity?.facts.find((f) => f.a === "ownerId" || f.a === "createdBy");
229
+ if (!auth.authenticated || !auth.userId) return false;
230
+ const ownerFact = entity?.facts.find(
231
+ (f) => f.a === "ownerId" || f.a === "createdBy"
232
+ );
199
233
  return ownerFact?.v === auth.userId;
200
234
  }
201
235
  if (typeof rule === "object") {
@@ -215,11 +249,7 @@ function evaluateRule(rule, auth, entity) {
215
249
  }
216
250
  return false;
217
251
  }
218
-
219
- class PermissionError extends Error {
220
- auth;
221
- entityType;
222
- op;
252
+ var PermissionError = class extends Error {
223
253
  constructor(auth, entityType, op) {
224
254
  const who = auth.authenticated ? `user:${auth.userId}` : "anonymous";
225
255
  super(`Permission denied: ${who} cannot ${op} ${entityType}`);
@@ -235,7 +265,7 @@ class PermissionError extends Error {
235
265
  code: 403
236
266
  };
237
267
  }
238
- }
268
+ };
239
269
  var PUBLIC_READ = {
240
270
  read: "public",
241
271
  create: "authenticated",
@@ -261,20 +291,212 @@ var ADMIN_ONLY = {
261
291
  delete: { role: "admin" }
262
292
  };
263
293
 
294
+ // src/schema/kernel-resolve.ts
295
+ import { z } from "zod";
296
+ function ontologyTypeName(id) {
297
+ return id.includes(":") ? id.split(":").pop() : id;
298
+ }
299
+ function findOntologyByTypeName(kernel, typeName) {
300
+ return kernel.listOntologies().find((s) => {
301
+ const short = ontologyTypeName(s["@id"]);
302
+ return short === typeName || short.toLowerCase() === typeName.toLowerCase() || s.label === typeName;
303
+ });
304
+ }
305
+ function schemaHandleFromOntology(def, typeName) {
306
+ const name = typeName ?? ontologyTypeName(def["@id"]);
307
+ const relations = {};
308
+ for (const field of def.fields) {
309
+ if (field.valueType === "relation" && field.relation?.targetSchema) {
310
+ const target = ontologyTypeName(field.relation.targetSchema);
311
+ relations[field.name] = rel(
312
+ target,
313
+ field.relation.cardinality ?? "one"
314
+ );
315
+ }
316
+ }
317
+ return {
318
+ type: name,
319
+ zod: z.object({}),
320
+ relations,
321
+ computed: {},
322
+ definition: def,
323
+ toOntologySchema: () => {
324
+ throw new Error("schemaHandleFromOntology: legacy adapter not available");
325
+ }
326
+ };
327
+ }
328
+ function createSchemaLookup(kernel) {
329
+ const cache = /* @__PURE__ */ new Map();
330
+ return (typeName) => {
331
+ if (cache.has(typeName)) return cache.get(typeName);
332
+ const def = findOntologyByTypeName(kernel, typeName);
333
+ if (!def) return null;
334
+ const handle = schemaHandleFromOntology(def, typeName);
335
+ cache.set(typeName, handle);
336
+ return handle;
337
+ };
338
+ }
339
+ function createKernelResolveClient(kernel) {
340
+ return {
341
+ read: async (id) => {
342
+ const entity = kernel.getEntity(id);
343
+ return entity ? entityRecordToPlain(entity) : null;
344
+ },
345
+ query: async (q) => {
346
+ const parsed = parseSimple(q);
347
+ const qr = await kernel.query(parsed);
348
+ return {
349
+ bindings: hydrateBindings(
350
+ kernel,
351
+ qr.bindings
352
+ ),
353
+ executionTime: qr.executionTime
354
+ };
355
+ }
356
+ };
357
+ }
358
+ async function hydrateAndResolve(kernel, bindings, entityType, resolve) {
359
+ let entities = hydrateBindings(kernel, bindings);
360
+ if (!entityType || !resolve || Object.keys(resolve).length === 0) {
361
+ return entities;
362
+ }
363
+ const def = findOntologyByTypeName(kernel, entityType);
364
+ if (!def) return entities;
365
+ const schema = schemaHandleFromOntology(def, entityType);
366
+ const client = createKernelResolveClient(kernel);
367
+ const lookup = createSchemaLookup(kernel);
368
+ return resolveRelations(client, schema, entities, resolve, {
369
+ schemaLookup: lookup
370
+ });
371
+ }
372
+
373
+ // src/server/usage-meter.ts
374
+ import { existsSync, readdirSync, statSync } from "fs";
375
+ import { join } from "path";
376
+ var EMPTY_BUCKET = () => ({
377
+ graph_io: 0,
378
+ storage_bytes: 0,
379
+ egress_bytes: 0
380
+ });
381
+ function resolveUsageTenantId(tenantId) {
382
+ return tenantId ?? DEFAULT_TENANT;
383
+ }
384
+ function dayKey(date) {
385
+ return date.toISOString().slice(0, 10);
386
+ }
387
+ function dirSize(dir) {
388
+ let total = 0;
389
+ for (const ent of readdirSync(dir, { withFileTypes: true })) {
390
+ const p = join(dir, ent.name);
391
+ if (ent.isDirectory()) total += dirSize(p);
392
+ else if (ent.isFile()) total += statSync(p).size;
393
+ }
394
+ return total;
395
+ }
396
+ function sampleTenantStorage(pool, tenantId) {
397
+ let total = 0;
398
+ const sqlitePath = pool.dbFilePath(tenantId);
399
+ if (existsSync(sqlitePath)) {
400
+ total += statSync(sqlitePath).size;
401
+ }
402
+ const blobDir = join(pool.dataPath(), "blobs");
403
+ if (existsSync(blobDir)) {
404
+ total += dirSize(blobDir);
405
+ }
406
+ return total;
407
+ }
408
+ function verifyAdminKey(req) {
409
+ const expected = process.env.TURTLEDB_ADMIN_KEY;
410
+ if (!expected) return false;
411
+ const auth = req.headers.get("authorization");
412
+ if (auth?.startsWith("Bearer ")) {
413
+ return auth.slice("Bearer ".length) === expected;
414
+ }
415
+ return req.headers.get("x-turtledb-admin-key") === expected;
416
+ }
417
+ var UsageMeter = class {
418
+ buckets = /* @__PURE__ */ new Map();
419
+ storageSampledAt = /* @__PURE__ */ new Map();
420
+ now;
421
+ constructor(opts = {}) {
422
+ this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
423
+ }
424
+ recordGraphIo(tenantId, count = 1) {
425
+ if (count <= 0) return;
426
+ const bucket = this._bucket(tenantId);
427
+ bucket.graph_io += count;
428
+ }
429
+ recordEgress(tenantId, bytes) {
430
+ if (bytes <= 0) return;
431
+ const bucket = this._bucket(tenantId);
432
+ bucket.egress_bytes += bytes;
433
+ }
434
+ /** Set storage gauge for the current day (from sampler, not incremental). */
435
+ recordStorage(tenantId, bytes) {
436
+ const bucket = this._bucket(tenantId);
437
+ bucket.storage_bytes = bytes;
438
+ this.storageSampledAt.set(`${tenantId}:${this._today()}`, this.now().toISOString());
439
+ }
440
+ sampleStorage(pool, tenantId) {
441
+ const id = resolveUsageTenantId(tenantId);
442
+ const bytes = sampleTenantStorage(pool, id);
443
+ this.recordStorage(id, bytes);
444
+ return bytes;
445
+ }
446
+ getUsage(tenantId, day) {
447
+ const d = day ?? this._today();
448
+ const tenantBuckets = this.buckets.get(tenantId);
449
+ const meters = tenantBuckets?.get(d) ?? EMPTY_BUCKET();
450
+ const sampledAt = this.storageSampledAt.get(`${tenantId}:${d}`);
451
+ return {
452
+ tenantId,
453
+ day: d,
454
+ meters: { ...meters },
455
+ ...sampledAt ? { storageSampledAt: sampledAt } : {}
456
+ };
457
+ }
458
+ /** All day keys recorded for a tenant (sorted ascending). */
459
+ listDays(tenantId) {
460
+ const tenantBuckets = this.buckets.get(tenantId);
461
+ if (!tenantBuckets) return [];
462
+ return Array.from(tenantBuckets.keys()).sort();
463
+ }
464
+ _today() {
465
+ return dayKey(this.now());
466
+ }
467
+ _bucket(tenantId) {
468
+ const id = resolveUsageTenantId(tenantId);
469
+ const d = this._today();
470
+ if (!this.buckets.has(id)) {
471
+ this.buckets.set(id, /* @__PURE__ */ new Map());
472
+ }
473
+ const tenantBuckets = this.buckets.get(id);
474
+ if (!tenantBuckets.has(d)) {
475
+ tenantBuckets.set(d, EMPTY_BUCKET());
476
+ }
477
+ return tenantBuckets.get(d);
478
+ }
479
+ };
480
+
264
481
  // src/server/realtime.ts
265
- class SubscriptionManager {
266
- clients = new Map;
482
+ var SubscriptionManager = class {
483
+ clients = /* @__PURE__ */ new Map();
267
484
  pool;
268
485
  permissions;
269
- constructor(pool, permissions = null) {
486
+ meter;
487
+ constructor(pool, permissions = null, meter = null) {
270
488
  this.pool = pool;
271
489
  this.permissions = permissions;
490
+ this.meter = meter;
272
491
  }
492
+ // -------------------------------------------------------------------------
493
+ // Client lifecycle
494
+ // -------------------------------------------------------------------------
273
495
  addClient(clientId, ws, auth, tenantId) {
274
496
  this.clients.set(clientId, {
275
497
  id: clientId,
276
498
  ws,
277
- subscriptions: new Map,
499
+ subscriptions: /* @__PURE__ */ new Map(),
278
500
  auth,
279
501
  tenantId
280
502
  });
@@ -282,10 +504,12 @@ class SubscriptionManager {
282
504
  removeClient(clientId) {
283
505
  this.clients.delete(clientId);
284
506
  }
507
+ // -------------------------------------------------------------------------
508
+ // Message handling
509
+ // -------------------------------------------------------------------------
285
510
  async handleMessage(clientId, raw) {
286
511
  const client = this.clients.get(clientId);
287
- if (!client)
288
- return;
512
+ if (!client) return;
289
513
  let msg;
290
514
  try {
291
515
  msg = JSON.parse(raw);
@@ -298,7 +522,11 @@ class SubscriptionManager {
298
522
  return;
299
523
  }
300
524
  if (msg.type === "subscribe") {
301
- await this._handleSubscribe(client, msg.id, msg.query, msg.tenantId);
525
+ await this._handleSubscribe(client, msg.id, msg.query, {
526
+ tenantId: msg.tenantId,
527
+ entityType: msg.entityType,
528
+ resolve: msg.resolve
529
+ });
302
530
  return;
303
531
  }
304
532
  if (msg.type === "unsubscribe") {
@@ -306,6 +534,13 @@ class SubscriptionManager {
306
534
  return;
307
535
  }
308
536
  }
537
+ // -------------------------------------------------------------------------
538
+ // Notify — called after every mutation
539
+ // -------------------------------------------------------------------------
540
+ /**
541
+ * Re-evaluate all subscriptions for a given tenant and push diffs.
542
+ * Called after every write op lands.
543
+ */
309
544
  async notify(tenantId) {
310
545
  const tid = tenantId ?? null;
311
546
  const dead = [];
@@ -314,22 +549,22 @@ class SubscriptionManager {
314
549
  dead.push(clientId);
315
550
  continue;
316
551
  }
317
- if (client.tenantId !== tid)
318
- continue;
552
+ if (client.tenantId !== tid) continue;
319
553
  for (const [subId, sub] of client.subscriptions) {
320
- if (sub.tenantId !== tid)
321
- continue;
554
+ if (sub.tenantId !== tid) continue;
322
555
  await this._pushUpdate(client, sub);
323
556
  }
324
557
  }
325
- for (const id of dead)
326
- this.clients.delete(id);
558
+ for (const id of dead) this.clients.delete(id);
327
559
  }
328
560
  get clientCount() {
329
561
  return this.clients.size;
330
562
  }
331
- async _handleSubscribe(client, subId, queryStr, tenantId) {
332
- const tid = tenantId ?? client.tenantId ?? null;
563
+ // -------------------------------------------------------------------------
564
+ // Private
565
+ // -------------------------------------------------------------------------
566
+ async _handleSubscribe(client, subId, queryStr, opts = {}) {
567
+ const tid = opts.tenantId ?? client.tenantId ?? null;
333
568
  let parsedQuery;
334
569
  try {
335
570
  parsedQuery = parseSimple(queryStr);
@@ -341,12 +576,17 @@ class SubscriptionManager {
341
576
  });
342
577
  return;
343
578
  }
344
- const kernel = this.pool.get(tid);
345
- const engine = new QueryEngine(kernel.getStore());
579
+ const kernel = await this.pool.preload(tid);
346
580
  let result;
347
581
  try {
348
- const qr = engine.execute(parsedQuery);
349
- result = qr.bindings;
582
+ const qr = await kernel.query(parsedQuery);
583
+ this._recordGraphIo(tid);
584
+ result = await hydrateAndResolve(
585
+ kernel,
586
+ qr.bindings,
587
+ opts.entityType,
588
+ opts.resolve
589
+ );
350
590
  } catch (err) {
351
591
  this._send(client, {
352
592
  type: "error",
@@ -355,12 +595,15 @@ class SubscriptionManager {
355
595
  });
356
596
  return;
357
597
  }
598
+ const resolved = Boolean(opts.entityType) && Boolean(opts.resolve && Object.keys(opts.resolve).length > 0);
358
599
  const sub = {
359
600
  id: subId,
360
601
  query: queryStr,
361
602
  tenantId: tid,
362
603
  auth: client.auth,
363
- lastResult: result
604
+ lastResult: result,
605
+ entityType: opts.entityType,
606
+ resolve: opts.resolve
364
607
  };
365
608
  client.subscriptions.set(subId, sub);
366
609
  this._send(client, { type: "subscribed", id: subId });
@@ -368,17 +611,23 @@ class SubscriptionManager {
368
611
  type: "data",
369
612
  id: subId,
370
613
  result,
371
- diff: { added: result, updated: [], removed: [] }
614
+ diff: { added: result, updated: [], removed: [] },
615
+ ...resolved ? { resolved: true } : {}
372
616
  });
373
617
  }
374
618
  async _pushUpdate(client, sub) {
375
- const kernel = this.pool.get(sub.tenantId);
376
- const engine = new QueryEngine(kernel.getStore());
619
+ const kernel = await this.pool.preload(sub.tenantId);
377
620
  let newResult;
378
621
  try {
379
622
  const parsed = parseSimple(sub.query);
380
- const qr = engine.execute(parsed);
381
- newResult = qr.bindings;
623
+ const qr = await kernel.query(parsed);
624
+ this._recordGraphIo(sub.tenantId);
625
+ newResult = await hydrateAndResolve(
626
+ kernel,
627
+ qr.bindings,
628
+ sub.entityType,
629
+ sub.resolve
630
+ );
382
631
  } catch {
383
632
  return;
384
633
  }
@@ -387,14 +636,30 @@ class SubscriptionManager {
387
636
  return;
388
637
  }
389
638
  sub.lastResult = newResult;
390
- this._send(client, { type: "data", id: sub.id, result: newResult, diff });
639
+ const resolved = Boolean(sub.entityType) && Boolean(sub.resolve && Object.keys(sub.resolve).length > 0);
640
+ this._send(client, {
641
+ type: "data",
642
+ id: sub.id,
643
+ result: newResult,
644
+ diff,
645
+ ...resolved ? { resolved: true } : {}
646
+ });
647
+ }
648
+ _recordGraphIo(tenantId) {
649
+ this.meter?.recordGraphIo(resolveUsageTenantId(tenantId));
391
650
  }
392
651
  _send(client, payload) {
652
+ const data = JSON.stringify(payload);
653
+ if (this.meter) {
654
+ const tid = resolveUsageTenantId(client.tenantId ?? DEFAULT_TENANT);
655
+ this.meter.recordEgress(tid, new TextEncoder().encode(data).length);
656
+ }
393
657
  try {
394
- client.ws.send(JSON.stringify(payload));
395
- } catch {}
658
+ client.ws.send(data);
659
+ } catch {
660
+ }
396
661
  }
397
- }
662
+ };
398
663
  function entityId(row) {
399
664
  return String(row["?e"] ?? row.id ?? row.e ?? JSON.stringify(row));
400
665
  }
@@ -412,47 +677,41 @@ function computeDiff(prev, next) {
412
677
  }
413
678
  }
414
679
  for (const [id, row] of prevMap) {
415
- if (!nextMap.has(id))
416
- removed.push(row);
680
+ if (!nextMap.has(id)) removed.push(row);
417
681
  }
418
682
  return { added, updated, removed };
419
683
  }
420
684
 
421
685
  // src/server/server.ts
422
686
  var __moduleDir = import.meta.dir ?? dirname(fileURLToPath(import.meta.url));
423
- function startServer(opts) {
424
- return startServerBun(opts);
425
- }
426
- async function startServerCrossRuntime(opts) {
427
- if (typeof globalThis.Bun !== "undefined") {
428
- const bunServer = startServerBun(opts);
429
- return {
430
- port: bunServer.port ?? opts.port ?? opts.config.port ?? 3000,
431
- hostname: bunServer.hostname,
432
- stop: (closeActive) => bunServer.stop(closeActive)
433
- };
434
- }
687
+ async function startServer(opts) {
435
688
  return startServerNode(opts);
436
689
  }
690
+ var startServerCrossRuntime = startServer;
437
691
  function buildServerContext(opts) {
438
692
  const { pool, permissions, config } = opts;
439
- const port = opts.port ?? config.port ?? 3000;
693
+ const port = opts.port ?? config.port ?? 3e3;
440
694
  const authConfig = {
441
695
  jwtSecret: config.jwtSecret,
442
696
  apiKey: config.apiKey,
443
697
  allowPublic: true
444
698
  };
445
- const subs = new SubscriptionManager(pool, permissions ?? null);
699
+ const meter = new UsageMeter();
700
+ const subs = new SubscriptionManager(pool, permissions ?? null, meter);
446
701
  const handleHttp = async (req) => {
447
702
  const url = new URL(req.url);
448
703
  const path = url.pathname;
449
- const auth = await resolveAuth(req.headers.get("authorization"), authConfig);
704
+ const auth = await resolveAuth(
705
+ req.headers.get("authorization"),
706
+ authConfig
707
+ );
450
708
  const tenantId = auth.tenantId ?? url.searchParams.get("tenantId") ?? null;
451
709
  try {
452
710
  return await route(req, url, path, auth, tenantId, {
453
711
  pool,
454
712
  permissions: permissions ?? null,
455
713
  subs,
714
+ meter,
456
715
  authConfig,
457
716
  config,
458
717
  oauthProviders: opts.oauthProviders ?? {}
@@ -467,42 +726,6 @@ function buildServerContext(opts) {
467
726
  };
468
727
  return { port, authConfig, subs, handleHttp };
469
728
  }
470
- function startServerBun(opts) {
471
- const { port, subs, handleHttp } = buildServerContext(opts);
472
- return Bun.serve({
473
- port,
474
- async fetch(req, server) {
475
- if (req.headers.get("upgrade") === "websocket") {
476
- const upgraded = server.upgrade(req, { data: undefined });
477
- if (upgraded)
478
- return;
479
- return new Response("WebSocket upgrade failed", { status: 400 });
480
- }
481
- return handleHttp(req);
482
- },
483
- websocket: {
484
- async open(ws) {
485
- const id = crypto.randomUUID();
486
- ws.__clientId = id;
487
- subs.addClient(id, ws, {
488
- userId: null,
489
- tenantId: null,
490
- roles: [],
491
- claims: {},
492
- authenticated: false
493
- }, null);
494
- },
495
- async message(ws, raw) {
496
- const id = ws.__clientId;
497
- await subs.handleMessage(id, typeof raw === "string" ? raw : new TextDecoder().decode(raw));
498
- },
499
- close(ws) {
500
- const id = ws.__clientId;
501
- subs.removeClient(id);
502
- }
503
- }
504
- });
505
- }
506
729
  async function startServerNode(opts) {
507
730
  const { port, subs, handleHttp } = buildServerContext(opts);
508
731
  const { startNodeServer } = await import("./server/node-adapter.js");
@@ -513,17 +736,25 @@ async function startServerNode(opts) {
513
736
  open(ws) {
514
737
  const id = crypto.randomUUID();
515
738
  ws.__clientId = id;
516
- subs.addClient(id, ws, {
517
- userId: null,
518
- tenantId: null,
519
- roles: [],
520
- claims: {},
521
- authenticated: false
522
- }, null);
739
+ subs.addClient(
740
+ id,
741
+ ws,
742
+ {
743
+ userId: null,
744
+ tenantId: null,
745
+ roles: [],
746
+ claims: {},
747
+ authenticated: false
748
+ },
749
+ null
750
+ );
523
751
  },
524
752
  async message(ws, raw) {
525
753
  const id = ws.__clientId;
526
- await subs.handleMessage(id, typeof raw === "string" ? raw : raw.toString());
754
+ await subs.handleMessage(
755
+ id,
756
+ typeof raw === "string" ? raw : raw.toString()
757
+ );
527
758
  },
528
759
  close(ws) {
529
760
  const id = ws.__clientId;
@@ -532,31 +763,39 @@ async function startServerNode(opts) {
532
763
  }
533
764
  });
534
765
  }
766
+ function recordGraphIo(ctx, tenantId) {
767
+ ctx.meter.recordGraphIo(resolveUsageTenantId(tenantId));
768
+ }
535
769
  async function route(req, url, path, auth, tenantId, ctx) {
536
770
  const method = req.method.toUpperCase();
537
771
  if (method === "GET" && path === "/__trellis/inspector.js") {
538
772
  const candidates = [
539
- join(__moduleDir, "db", "inspector.js"),
540
- join(__moduleDir, "inspector.js")
773
+ join2(__moduleDir, "db", "inspector.js"),
774
+ // chunk lives in dist/, inspector in dist/db/
775
+ join2(__moduleDir, "inspector.js")
776
+ // chunk lives in dist/db/, inspector alongside
541
777
  ];
542
778
  for (const p of candidates) {
543
- if (existsSync(p)) {
779
+ if (existsSync2(p)) {
544
780
  return new Response(readFileSync(p, "utf-8"), {
545
781
  headers: { "Content-Type": "application/javascript; charset=utf-8" }
546
782
  });
547
783
  }
548
784
  }
549
- return new Response("/* Trellis DB Inspector: run `bun run build:inspector` first */", {
550
- headers: { "Content-Type": "application/javascript" }
551
- });
785
+ return new Response(
786
+ "/* Trellis DB Inspector: run `bun run build:inspector` first */",
787
+ {
788
+ headers: { "Content-Type": "application/javascript" }
789
+ }
790
+ );
552
791
  }
553
792
  if (method === "GET" && path === "/__trellis/trellis.css") {
554
793
  const candidates = [
555
- join(__moduleDir, "db", "trellis.css"),
556
- join(__moduleDir, "trellis.css")
794
+ join2(__moduleDir, "db", "trellis.css"),
795
+ join2(__moduleDir, "trellis.css")
557
796
  ];
558
797
  for (const p of candidates) {
559
- if (existsSync(p)) {
798
+ if (existsSync2(p)) {
560
799
  return new Response(readFileSync(p, "utf-8"), {
561
800
  headers: { "Content-Type": "text/css; charset=utf-8" }
562
801
  });
@@ -584,7 +823,7 @@ async function route(req, url, path, auth, tenantId, ctx) {
584
823
  });
585
824
  }
586
825
  if (method === "GET" && path === "/health") {
587
- const kernel = ctx.pool.get(tenantId);
826
+ const kernel = await ctx.pool.preload(tenantId);
588
827
  const ops = kernel.readAllOps().length;
589
828
  return json({
590
829
  status: "ok",
@@ -592,25 +831,27 @@ async function route(req, url, path, auth, tenantId, ctx) {
592
831
  tenants: ctx.pool.activeTenants().length
593
832
  });
594
833
  }
834
+ if (method === "GET" && path === "/admin/usage") {
835
+ return handleAdminUsage(req, url, ctx);
836
+ }
595
837
  if (path === "/entities" || path === "/entities/") {
596
- if (method === "POST")
597
- return handleCreate(req, auth, tenantId, ctx);
598
- if (method === "GET")
599
- return handleList(url, auth, tenantId, ctx);
838
+ if (method === "POST") return handleCreate(req, auth, tenantId, ctx);
839
+ if (method === "GET") return handleList(url, auth, tenantId, ctx);
600
840
  }
601
841
  const entityMatch = path.match(/^\/entities\/([^/]+)$/);
602
842
  if (entityMatch) {
603
843
  const id = decodeURIComponent(entityMatch[1]);
604
- if (method === "GET")
605
- return handleRead(id, auth, tenantId, ctx);
844
+ if (method === "GET") return handleRead(id, auth, tenantId, ctx);
606
845
  if (method === "PUT" || method === "PATCH")
607
846
  return handleUpdate(req, id, auth, tenantId, ctx);
608
- if (method === "DELETE")
609
- return handleDelete(id, auth, tenantId, ctx);
847
+ if (method === "DELETE") return handleDelete(id, auth, tenantId, ctx);
610
848
  }
611
849
  if (method === "POST" && path === "/query") {
612
850
  return handleQuery(req, auth, tenantId, ctx);
613
851
  }
852
+ if (method === "POST" && (path === "/ontologies" || path === "/ontologies/")) {
853
+ return handleCreateOntology(req, auth, tenantId, ctx);
854
+ }
614
855
  if (method === "POST" && path === "/upload") {
615
856
  return handleUpload(req, auth, tenantId, ctx);
616
857
  }
@@ -636,59 +877,63 @@ async function route(req, url, path, auth, tenantId, ctx) {
636
877
  }
637
878
  async function handleCreate(req, auth, tenantId, ctx) {
638
879
  const body = await req.json();
639
- if (!body.type)
640
- return json({ error: "type is required" }, 400);
880
+ if (!body.type) return json({ error: "type is required" }, 400);
641
881
  ctx.permissions?.assert(auth, body.type, "create");
642
- const kernel = ctx.pool.get(tenantId);
882
+ const kernel = await ctx.pool.preload(tenantId);
643
883
  const entityId2 = `${body.type.toLowerCase()}:${crypto.randomUUID()}`;
644
884
  const attrs = {
645
885
  ...body.attributes
646
886
  };
647
- if (auth.userId)
648
- attrs.createdBy = auth.userId;
649
- if (auth.tenantId)
650
- attrs.tenantId = auth.tenantId;
651
- const result = await kernel.createEntity(entityId2, body.type, attrs, body.links);
887
+ if (auth.userId) attrs.createdBy = auth.userId;
888
+ if (auth.tenantId) attrs.tenantId = auth.tenantId;
889
+ const result = await kernel.createEntity(
890
+ entityId2,
891
+ body.type,
892
+ attrs,
893
+ body.links
894
+ );
895
+ recordGraphIo(ctx, tenantId);
652
896
  await ctx.subs.notify(tenantId);
653
897
  return json({ id: entityId2, op: result.op.hash }, 201);
654
898
  }
655
899
  async function handleRead(id, auth, tenantId, ctx) {
656
- const kernel = ctx.pool.get(tenantId);
900
+ const kernel = await ctx.pool.preload(tenantId);
657
901
  const entity = kernel.getEntity(id);
658
- if (!entity)
659
- return json({ error: "Not Found" }, 404);
902
+ if (!entity) return json({ error: "Not Found" }, 404);
660
903
  ctx.permissions?.assert(auth, entity.type, "read", entity);
661
904
  return json(entityToJson(entity));
662
905
  }
663
906
  async function handleUpdate(req, id, auth, tenantId, ctx) {
664
- const kernel = ctx.pool.get(tenantId);
907
+ const kernel = await ctx.pool.preload(tenantId);
665
908
  const entity = kernel.getEntity(id);
666
- if (!entity)
667
- return json({ error: "Not Found" }, 404);
909
+ if (!entity) return json({ error: "Not Found" }, 404);
668
910
  ctx.permissions?.assert(auth, entity.type, "update", entity);
669
911
  const updates = await req.json();
670
912
  await kernel.updateEntity(id, updates);
913
+ recordGraphIo(ctx, tenantId);
671
914
  await ctx.subs.notify(tenantId);
672
915
  return json({ id, updated: true });
673
916
  }
674
917
  async function handleDelete(id, auth, tenantId, ctx) {
675
- const kernel = ctx.pool.get(tenantId);
918
+ const kernel = await ctx.pool.preload(tenantId);
676
919
  const entity = kernel.getEntity(id);
677
- if (!entity)
678
- return json({ error: "Not Found" }, 404);
920
+ if (!entity) return json({ error: "Not Found" }, 404);
679
921
  ctx.permissions?.assert(auth, entity.type, "delete", entity);
680
922
  await kernel.deleteEntity(id);
923
+ recordGraphIo(ctx, tenantId);
681
924
  await ctx.subs.notify(tenantId);
682
925
  return json({ id, deleted: true });
683
926
  }
684
927
  async function handleList(url, auth, tenantId, ctx) {
685
- const type = url.searchParams.get("type") ?? undefined;
928
+ const type = url.searchParams.get("type") ?? void 0;
686
929
  const limit = parseInt(url.searchParams.get("limit") ?? "100");
687
930
  const offset = parseInt(url.searchParams.get("offset") ?? "0");
688
- const kernel = ctx.pool.get(tenantId);
931
+ const kernel = await ctx.pool.preload(tenantId);
689
932
  let entities = kernel.listEntities(type);
690
933
  if (ctx.permissions && type) {
691
- entities = entities.filter((e) => ctx.permissions.check(auth, e.type, "read", e));
934
+ entities = entities.filter(
935
+ (e) => ctx.permissions.check(auth, e.type, "read", e)
936
+ );
692
937
  }
693
938
  const page = entities.slice(offset, offset + limit);
694
939
  return json({
@@ -700,22 +945,53 @@ async function handleList(url, auth, tenantId, ctx) {
700
945
  }
701
946
  async function handleQuery(req, auth, tenantId, ctx) {
702
947
  const body = await req.json();
703
- if (!body.query)
704
- return json({ error: "query is required" }, 400);
948
+ if (!body.query) return json({ error: "query is required" }, 400);
705
949
  let parsed;
706
950
  try {
707
951
  parsed = parseSimple(body.query);
708
952
  } catch (err) {
709
953
  return json({ error: "Invalid query", message: String(err) }, 400);
710
954
  }
711
- const kernel = ctx.pool.get(tenantId);
712
- const engine = new QueryEngine(kernel.getStore());
713
- const result = engine.execute(parsed);
955
+ const kernel = await ctx.pool.preload(tenantId);
956
+ const result = await kernel.query(parsed);
957
+ recordGraphIo(ctx, tenantId);
958
+ const bindings = hydrateBindings(
959
+ kernel,
960
+ result.bindings
961
+ );
714
962
  return json({
715
- bindings: result.bindings,
963
+ bindings,
716
964
  executionTime: result.executionTime
717
965
  });
718
966
  }
967
+ async function handleCreateOntology(req, auth, tenantId, ctx) {
968
+ const schema = await req.json();
969
+ if (!schema || !schema["@id"] || !Array.isArray(schema.fields)) {
970
+ return json(
971
+ { error: "A SchemaDefinition (with @id and fields) is required" },
972
+ 400
973
+ );
974
+ }
975
+ if ((schema.tier ?? "user") === "core") {
976
+ return json({ error: "Core ontologies are immutable" }, 403);
977
+ }
978
+ const kernel = await ctx.pool.preload(tenantId);
979
+ const exists = kernel.listOntologies().some((ont) => ont["@id"] === schema["@id"]);
980
+ if (exists) {
981
+ return json({ id: schema["@id"], registered: false, existed: true }, 200);
982
+ }
983
+ try {
984
+ kernel.createOntology(schema);
985
+ recordGraphIo(ctx, tenantId);
986
+ } catch (err) {
987
+ const msg = err instanceof Error ? err.message : String(err);
988
+ if (msg.includes("already exists")) {
989
+ return json({ id: schema["@id"], registered: false, existed: true }, 200);
990
+ }
991
+ return json({ error: "Could not register schema", message: msg }, 409);
992
+ }
993
+ return json({ id: schema["@id"], registered: true }, 201);
994
+ }
719
995
  async function handleUpload(req, auth, tenantId, ctx) {
720
996
  if (!auth.authenticated && ctx.config.apiKey) {
721
997
  return json({ error: "Unauthorized" }, 401);
@@ -724,7 +1000,7 @@ async function handleUpload(req, auth, tenantId, ctx) {
724
1000
  const buffer = new Uint8Array(await req.arrayBuffer());
725
1001
  const hashBuf = await crypto.subtle.digest("SHA-256", buffer);
726
1002
  const hash = `blob:${Array.from(new Uint8Array(hashBuf)).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
727
- const kernel = ctx.pool.get(tenantId);
1003
+ const kernel = await ctx.pool.preload(tenantId);
728
1004
  const backend = kernel.getBackend();
729
1005
  if (!backend.hasBlob(hash)) {
730
1006
  backend.putBlob(hash, buffer);
@@ -732,12 +1008,15 @@ async function handleUpload(req, auth, tenantId, ctx) {
732
1008
  return json({ hash, size: buffer.length, contentType }, 201);
733
1009
  }
734
1010
  async function handleFileDownload(hash, ctx, tenantId) {
735
- const kernel = ctx.pool.get(tenantId);
1011
+ const kernel = await ctx.pool.preload(tenantId);
736
1012
  const backend = kernel.getBackend();
737
1013
  const blob = backend.getBlob(hash);
738
- if (!blob)
739
- return json({ error: "Not Found" }, 404);
740
- const cleanBuf = blob.buffer.slice(blob.byteOffset, blob.byteOffset + blob.byteLength);
1014
+ if (!blob) return json({ error: "Not Found" }, 404);
1015
+ const cleanBuf = blob.buffer.slice(
1016
+ blob.byteOffset,
1017
+ blob.byteOffset + blob.byteLength
1018
+ );
1019
+ ctx.meter.recordEgress(resolveUsageTenantId(tenantId), blob.byteLength);
741
1020
  return new Response(cleanBuf, {
742
1021
  headers: { "Content-Type": "application/octet-stream" }
743
1022
  });
@@ -750,7 +1029,7 @@ async function handleRegister(req, tenantId, ctx) {
750
1029
  if (!body.email || !body.password) {
751
1030
  return json({ error: "email and password are required" }, 400);
752
1031
  }
753
- const kernel = ctx.pool.get(tenantId);
1032
+ const kernel = await ctx.pool.preload(tenantId);
754
1033
  const existing = kernel.listEntities("User", { email: body.email });
755
1034
  if (existing.length > 0) {
756
1035
  return json({ error: "Email already registered" }, 409);
@@ -764,7 +1043,10 @@ async function handleRegister(req, tenantId, ctx) {
764
1043
  role: "user",
765
1044
  ...tenantId ? { tenantId } : {}
766
1045
  });
767
- const token = await signJwt({ sub: userId, email: body.email, roles: ["user"], tenantId }, ctx.config.jwtSecret);
1046
+ const token = await signJwt(
1047
+ { sub: userId, email: body.email, roles: ["user"], tenantId },
1048
+ ctx.config.jwtSecret
1049
+ );
768
1050
  return json({ token, userId }, 201);
769
1051
  }
770
1052
  async function handleLogin(req, tenantId, ctx) {
@@ -775,7 +1057,7 @@ async function handleLogin(req, tenantId, ctx) {
775
1057
  if (!body.email || !body.password) {
776
1058
  return json({ error: "email and password are required" }, 400);
777
1059
  }
778
- const kernel = ctx.pool.get(tenantId);
1060
+ const kernel = await ctx.pool.preload(tenantId);
779
1061
  const users = kernel.listEntities("User", { email: body.email });
780
1062
  if (users.length === 0) {
781
1063
  return json({ error: "Invalid credentials" }, 401);
@@ -787,7 +1069,10 @@ async function handleLogin(req, tenantId, ctx) {
787
1069
  return json({ error: "Invalid credentials" }, 401);
788
1070
  }
789
1071
  const role = String(roleFact?.v ?? "user");
790
- const token = await signJwt({ sub: user.id, email: body.email, roles: [role], tenantId }, ctx.config.jwtSecret);
1072
+ const token = await signJwt(
1073
+ { sub: user.id, email: body.email, roles: [role], tenantId },
1074
+ ctx.config.jwtSecret
1075
+ );
791
1076
  return json({ token, userId: user.id });
792
1077
  }
793
1078
  function handleOAuthRedirect(providerName, url, ctx) {
@@ -804,8 +1089,7 @@ async function handleOAuthCallback(url, providerName, tenantId, ctx) {
804
1089
  return json({ error: "Auth not configured (no jwtSecret)" }, 501);
805
1090
  }
806
1091
  const code = url.searchParams.get("code");
807
- if (!code)
808
- return json({ error: "Missing code" }, 400);
1092
+ if (!code) return json({ error: "Missing code" }, 400);
809
1093
  const provider = ctx.oauthProviders[providerName] ?? getBuiltinProvider(providerName, ctx);
810
1094
  if (!provider)
811
1095
  return json({ error: `Unknown provider: ${providerName}` }, 400);
@@ -816,7 +1100,7 @@ async function handleOAuthCallback(url, providerName, tenantId, ctx) {
816
1100
  } catch (err) {
817
1101
  return json({ error: "OAuth exchange failed", message: String(err) }, 400);
818
1102
  }
819
- const kernel = ctx.pool.get(tenantId);
1103
+ const kernel = await ctx.pool.preload(tenantId);
820
1104
  const oauthId = `oauth:${providerName}:${profile.id}`;
821
1105
  let users = kernel.listEntities("User", { oauthId });
822
1106
  let userId;
@@ -834,62 +1118,94 @@ async function handleOAuthCallback(url, providerName, tenantId, ctx) {
834
1118
  } else {
835
1119
  userId = users[0].id;
836
1120
  }
837
- const token = await signJwt({ sub: userId, email: profile.email, roles: ["user"], tenantId }, ctx.config.jwtSecret);
1121
+ const token = await signJwt(
1122
+ { sub: userId, email: profile.email, roles: ["user"], tenantId },
1123
+ ctx.config.jwtSecret
1124
+ );
838
1125
  return json({ token, userId });
839
1126
  }
840
1127
  function getBuiltinProvider(name, ctx) {
841
1128
  if (name === "google") {
842
1129
  const cid = process.env.GOOGLE_CLIENT_ID;
843
1130
  const csec = process.env.GOOGLE_CLIENT_SECRET;
844
- if (!cid || !csec)
845
- return null;
1131
+ if (!cid || !csec) return null;
846
1132
  return { ...GOOGLE_PROVIDER, clientId: cid, clientSecret: csec };
847
1133
  }
848
1134
  if (name === "github") {
849
1135
  const cid = process.env.GITHUB_CLIENT_ID;
850
1136
  const csec = process.env.GITHUB_CLIENT_SECRET;
851
- if (!cid || !csec)
852
- return null;
1137
+ if (!cid || !csec) return null;
853
1138
  return { ...GITHUB_PROVIDER, clientId: cid, clientSecret: csec };
854
1139
  }
855
1140
  return null;
856
1141
  }
857
1142
  async function hashPassword(password) {
858
1143
  const salt = crypto.randomUUID().replace(/-/g, "");
859
- const enc = new TextEncoder;
860
- const keyMat = await crypto.subtle.importKey("raw", enc.encode(password), "PBKDF2", false, ["deriveBits"]);
861
- const bits = await crypto.subtle.deriveBits({
862
- name: "PBKDF2",
863
- salt: enc.encode(salt),
864
- iterations: 1e5,
865
- hash: "SHA-256"
866
- }, keyMat, 256);
1144
+ const enc = new TextEncoder();
1145
+ const keyMat = await crypto.subtle.importKey(
1146
+ "raw",
1147
+ enc.encode(password),
1148
+ "PBKDF2",
1149
+ false,
1150
+ ["deriveBits"]
1151
+ );
1152
+ const bits = await crypto.subtle.deriveBits(
1153
+ {
1154
+ name: "PBKDF2",
1155
+ salt: enc.encode(salt),
1156
+ iterations: 1e5,
1157
+ hash: "SHA-256"
1158
+ },
1159
+ keyMat,
1160
+ 256
1161
+ );
867
1162
  const hash = Array.from(new Uint8Array(bits)).map((b) => b.toString(16).padStart(2, "0")).join("");
868
1163
  return `${salt}:${hash}`;
869
1164
  }
870
1165
  async function verifyPassword(password, stored) {
871
1166
  const [salt, expectedHash] = stored.split(":");
872
- if (!salt || !expectedHash)
873
- return false;
874
- const enc = new TextEncoder;
875
- const keyMat = await crypto.subtle.importKey("raw", enc.encode(password), "PBKDF2", false, ["deriveBits"]);
876
- const bits = await crypto.subtle.deriveBits({
877
- name: "PBKDF2",
878
- salt: enc.encode(salt),
879
- iterations: 1e5,
880
- hash: "SHA-256"
881
- }, keyMat, 256);
1167
+ if (!salt || !expectedHash) return false;
1168
+ const enc = new TextEncoder();
1169
+ const keyMat = await crypto.subtle.importKey(
1170
+ "raw",
1171
+ enc.encode(password),
1172
+ "PBKDF2",
1173
+ false,
1174
+ ["deriveBits"]
1175
+ );
1176
+ const bits = await crypto.subtle.deriveBits(
1177
+ {
1178
+ name: "PBKDF2",
1179
+ salt: enc.encode(salt),
1180
+ iterations: 1e5,
1181
+ hash: "SHA-256"
1182
+ },
1183
+ keyMat,
1184
+ 256
1185
+ );
882
1186
  const hash = Array.from(new Uint8Array(bits)).map((b) => b.toString(16).padStart(2, "0")).join("");
883
1187
  return hash === expectedHash;
884
1188
  }
885
- function entityToJson(entity) {
886
- const attrs = {};
887
- for (const f of entity.facts) {
888
- if (f.a !== "type") {
889
- attrs[f.a] = f.v;
890
- }
1189
+ function handleAdminUsage(req, url, ctx) {
1190
+ if (!process.env.TURTLEDB_ADMIN_KEY) {
1191
+ return json({ error: "Admin usage not configured" }, 501);
891
1192
  }
892
- return { id: entity.id, type: entity.type, ...attrs };
1193
+ if (!verifyAdminKey(req)) {
1194
+ return json({ error: "Unauthorized" }, 401);
1195
+ }
1196
+ const tenant = url.searchParams.get("tenant");
1197
+ if (!tenant) {
1198
+ return json({ error: "tenant query param is required" }, 400);
1199
+ }
1200
+ const refresh = url.searchParams.get("refresh") === "1";
1201
+ if (refresh) {
1202
+ ctx.meter.sampleStorage(ctx.pool, tenant);
1203
+ }
1204
+ const day = url.searchParams.get("day") ?? void 0;
1205
+ return json(ctx.meter.getUsage(tenant, day));
1206
+ }
1207
+ function entityToJson(entity) {
1208
+ return entityRecordToPlain(entity);
893
1209
  }
894
1210
  function json(data, status = 200) {
895
1211
  return new Response(JSON.stringify(data), {
@@ -898,4 +1214,27 @@ function json(data, status = 200) {
898
1214
  });
899
1215
  }
900
1216
 
901
- export { ANONYMOUS, signJwt, verifyJwt, resolveAuth, GOOGLE_PROVIDER, GITHUB_PROVIDER, buildOAuthUrl, exchangeOAuthCode, PermissionRegistry, PermissionError, PUBLIC_READ, FULLY_PUBLIC, OWNER_ONLY, ADMIN_ONLY, SubscriptionManager, startServer, startServerCrossRuntime };
1217
+ export {
1218
+ ANONYMOUS,
1219
+ signJwt,
1220
+ verifyJwt,
1221
+ resolveAuth,
1222
+ GOOGLE_PROVIDER,
1223
+ GITHUB_PROVIDER,
1224
+ buildOAuthUrl,
1225
+ exchangeOAuthCode,
1226
+ PermissionRegistry,
1227
+ PermissionError,
1228
+ PUBLIC_READ,
1229
+ FULLY_PUBLIC,
1230
+ OWNER_ONLY,
1231
+ ADMIN_ONLY,
1232
+ resolveUsageTenantId,
1233
+ dayKey,
1234
+ sampleTenantStorage,
1235
+ verifyAdminKey,
1236
+ UsageMeter,
1237
+ SubscriptionManager,
1238
+ startServer,
1239
+ startServerCrossRuntime
1240
+ };