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.
- package/CHANGELOG.md +23 -5
- package/README.md +63 -156
- package/bin/trellis.mjs +2 -75
- package/dist/{better-sqlite-backend-ahx5p0br.js → better-sqlite-backend-2P4F6AOJ.js} +91 -29
- package/dist/boot-middleware-MHLHFCUC.js +9 -0
- package/dist/browser/index.d.ts +18 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +95 -0
- package/dist/{index-53f3b8p8.js → chunk-2W3MHTMG.js} +552 -213
- package/dist/chunk-44FFQKQX.js +142 -0
- package/dist/chunk-4HYPLFJD.js +150 -0
- package/dist/chunk-4XIVMVHC.js +361 -0
- package/dist/{index-yp88he8n.js → chunk-6CTA6MCE.js} +88 -64
- package/dist/chunk-6GEZ6DN5.js +394 -0
- package/dist/chunk-7ZFRVCUE.js +211 -0
- package/dist/chunk-CAG4FULI.js +121 -0
- package/dist/chunk-FF36CQHZ.js +61 -0
- package/dist/{index-h32txmxe.js → chunk-FKQO5A3H.js} +6 -13
- package/dist/{index-nq520y6k.js → chunk-FWRS4CSC.js} +46 -32
- package/dist/chunk-GFZCJ4EH.js +108 -0
- package/dist/chunk-GLM4HGN4.js +55 -0
- package/dist/{index-4cdr7x2x.js → chunk-GU374KWZ.js} +182 -111
- package/dist/chunk-H6JB3PZ3.js +45 -0
- package/dist/chunk-IZX2PLIB.js +1240 -0
- package/dist/chunk-J2TXUFCZ.js +289 -0
- package/dist/chunk-J3WYZO3Q.js +211 -0
- package/dist/{index-xzym9w0m.js → chunk-JA7AIHRK.js} +12 -7
- package/dist/chunk-JHHEXEDM.js +162 -0
- package/dist/chunk-JUEMDWLU.js +233 -0
- package/dist/chunk-KFQGP6VL.js +33 -0
- package/dist/chunk-KIJITUZB.js +419 -0
- package/dist/{index-4wxa8xz4.js → chunk-KQ4A2D2P.js} +162 -23
- package/dist/chunk-KSU2GW22.js +958 -0
- package/dist/{index-rv1p47gp.js → chunk-L5N5PR2S.js} +3015 -2480
- package/dist/{index-h9e2efx4.js → chunk-LNUIGRDZ.js} +71 -55
- package/dist/{index-skhn0agf.js → chunk-MLC5BUYO.js} +19 -20
- package/dist/chunk-MPXUVGT3.js +104 -0
- package/dist/chunk-N6MYZT4O.js +68 -0
- package/dist/chunk-O2AV5WMN.js +48 -0
- package/dist/chunk-PZ4XYZ4J.js +143 -0
- package/dist/chunk-QB5ISE47.js +419 -0
- package/dist/chunk-RSWUFTO2.js +857 -0
- package/dist/chunk-S3XLFZ27.js +1095 -0
- package/dist/{index-w5wccer3.js → chunk-S6GBJ2ZB.js} +28 -12
- package/dist/{index-7pjz3tsy.js → chunk-SGMDTWJJ.js} +57 -15
- package/dist/chunk-SPSPV5BC.js +390 -0
- package/dist/chunk-TCKXSOBP.js +123 -0
- package/dist/chunk-UBGUDMUV.js +59 -0
- package/dist/{index-wt8rz4gn.js → chunk-UQISRW6T.js} +97 -26
- package/dist/chunk-USEINYUL.js +12 -0
- package/dist/{index-bmyt7k8n.js → chunk-UZRUP7QW.js} +12 -4
- package/dist/{index-xhcp6xrn.js → chunk-VBYUTRXX.js} +695 -514
- package/dist/{index-vkpkfwhq.js → chunk-VDAKEOML.js} +234 -194
- package/dist/chunk-WXSWA3MV.js +0 -0
- package/dist/{index-n9f2qyh5.js → chunk-X5FLTRUB.js} +55 -33
- package/dist/{index-h7zxhhhh.js → chunk-XMQ7G77X.js} +46 -38
- package/dist/{index-w7ng765c.js → chunk-YCM7ZYEZ.js} +102 -49
- package/dist/cli/deploy-cli.d.ts +18 -0
- package/dist/cli/deploy-cli.d.ts.map +1 -0
- package/dist/cli/errors.d.ts +6 -0
- package/dist/cli/errors.d.ts.map +1 -0
- package/dist/cli/examples.d.ts +23 -0
- package/dist/cli/examples.d.ts.map +1 -0
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2426 -6225
- package/dist/client/index.browser.d.ts +29 -0
- package/dist/client/index.browser.d.ts.map +1 -0
- package/dist/client/index.browser.js +64 -0
- package/dist/client/index.d.ts +6 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +41 -21
- package/dist/client/live.d.ts +67 -0
- package/dist/client/live.d.ts.map +1 -0
- package/dist/client/reactive.d.ts +31 -0
- package/dist/client/reactive.d.ts.map +1 -0
- package/dist/client/sdk.browser.d.ts +104 -0
- package/dist/client/sdk.browser.d.ts.map +1 -0
- package/dist/client/sdk.browser.js +9 -0
- package/dist/client/sdk.d.ts +26 -2
- package/dist/client/sdk.d.ts.map +1 -1
- package/dist/client/sdk.js +9 -0
- package/dist/client/vcs-client.d.ts +129 -0
- package/dist/client/vcs-client.d.ts.map +1 -0
- package/dist/cms/index.js +171 -163
- package/dist/{config-8hczw0rs.js → config-PFTP67TR.js} +8 -9
- package/dist/core/computation/index.d.ts +5 -0
- package/dist/core/computation/index.d.ts.map +1 -0
- package/dist/core/computation/rollup.d.ts +18 -0
- package/dist/core/computation/rollup.d.ts.map +1 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +171 -104
- package/dist/core/kernel/boot-middleware.d.ts +10 -0
- package/dist/core/kernel/boot-middleware.d.ts.map +1 -0
- package/dist/core/kernel/logic-middleware.d.ts +7 -1
- package/dist/core/kernel/logic-middleware.d.ts.map +1 -1
- package/dist/core/kernel/trellis-kernel.d.ts.map +1 -1
- package/dist/core/ontology/types.d.ts +5 -0
- package/dist/core/ontology/types.d.ts.map +1 -1
- package/dist/core/persist/better-sqlite-backend.d.ts +9 -2
- package/dist/core/persist/better-sqlite-backend.d.ts.map +1 -1
- package/dist/core/persist/factory.d.ts +6 -7
- package/dist/core/persist/factory.d.ts.map +1 -1
- package/dist/core/persist/factory.js +2 -3
- package/dist/core/persist/sqljs-backend.js +2 -3
- package/dist/db/index.d.ts +4 -1
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +67 -54
- package/dist/db/inspector.js +11 -19
- package/dist/db/trellis.css +1 -1
- package/dist/decisions/index.js +9 -10
- package/dist/deploy-LQ7GUGJ5.js +9 -0
- package/dist/deploy-YNVG5E4E.js +9 -0
- package/dist/deploy-cli-24BCHAXY.js +67 -0
- package/dist/deploy-cli-C35HTITO.js +67 -0
- package/dist/embeddings/auto-embed.d.ts +2 -2
- package/dist/embeddings/auto-embed.d.ts.map +1 -1
- package/dist/embeddings/index.js +20 -22
- package/dist/embeddings/search.d.ts +2 -1
- package/dist/embeddings/search.d.ts.map +1 -1
- package/dist/embeddings/store.d.ts +25 -4
- package/dist/embeddings/store.d.ts.map +1 -1
- package/dist/engine.d.ts +59 -19
- package/dist/engine.d.ts.map +1 -1
- package/dist/import-UZEKXU3B.js +10 -0
- package/dist/index.js +102 -93
- package/dist/launch-explorer-HZ4ZJA4X.js +95 -0
- package/dist/links/index.js +24 -26
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/plugins/agent-memory/index.js +316 -0
- package/dist/plugins/idea-garden/index.js +165 -0
- package/dist/plugins/plan-approval/index.js +579 -0
- package/dist/plugins/proactive-watcher/index.js +194 -0
- package/dist/{index-2r4jxwnb.js → query-XYLTJYSA.js} +14 -15
- package/dist/react/index.js +39 -70
- package/dist/react/realtime.js +11 -0
- package/dist/react/schema-hooks.js +87 -0
- package/dist/realtime/broadcast-channel-transport.d.ts +43 -0
- package/dist/realtime/broadcast-channel-transport.d.ts.map +1 -0
- package/dist/realtime/durable-object-relay-transport.d.ts +96 -0
- package/dist/realtime/durable-object-relay-transport.d.ts.map +1 -0
- package/dist/realtime/index.d.ts +44 -0
- package/dist/realtime/index.d.ts.map +1 -0
- package/dist/realtime/index.js +40 -0
- package/dist/realtime/memory-hub.d.ts +32 -0
- package/dist/realtime/memory-hub.d.ts.map +1 -0
- package/dist/realtime/persistent-channel.d.ts +105 -0
- package/dist/realtime/persistent-channel.d.ts.map +1 -0
- package/dist/realtime/presence.d.ts +73 -0
- package/dist/realtime/presence.d.ts.map +1 -0
- package/dist/realtime/relay-persistence.d.ts +37 -0
- package/dist/realtime/relay-persistence.d.ts.map +1 -0
- package/dist/realtime/relay-persistence.js +107 -0
- package/dist/realtime/relay-server.d.ts +103 -0
- package/dist/realtime/relay-server.d.ts.map +1 -0
- package/dist/realtime/room.d.ts +98 -0
- package/dist/realtime/room.d.ts.map +1 -0
- package/dist/realtime/text.d.ts +73 -0
- package/dist/realtime/text.d.ts.map +1 -0
- package/dist/realtime/types.d.ts +89 -0
- package/dist/realtime/types.d.ts.map +1 -0
- package/dist/realtime/websocket-relay-transport.d.ts +28 -0
- package/dist/realtime/websocket-relay-transport.d.ts.map +1 -0
- package/dist/realtime.bundle.js +1100 -0
- package/dist/{remote-manager-xvbg4230.js → remote-manager-ODE3T2KR.js} +25 -15
- package/dist/scaffold/seed.d.ts.map +1 -1
- package/dist/scaffold/write.d.ts +1 -1
- package/dist/scaffold/write.d.ts.map +1 -1
- package/dist/schema/define.d.ts +137 -0
- package/dist/schema/define.d.ts.map +1 -0
- package/dist/schema/entity-projection.d.ts +34 -0
- package/dist/schema/entity-projection.d.ts.map +1 -0
- package/dist/schema/eql.d.ts +46 -0
- package/dist/schema/eql.d.ts.map +1 -0
- package/dist/schema/index.d.ts +21 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +46 -0
- package/dist/schema/kernel-resolve.d.ts +21 -0
- package/dist/schema/kernel-resolve.d.ts.map +1 -0
- package/dist/schema/mutations.d.ts +20 -0
- package/dist/schema/mutations.d.ts.map +1 -0
- package/dist/schema/resolve.d.ts +21 -0
- package/dist/schema/resolve.d.ts.map +1 -0
- package/dist/server/deploy-meta.d.ts +25 -0
- package/dist/server/deploy-meta.d.ts.map +1 -0
- package/dist/server/deploy.d.ts +7 -2
- package/dist/server/deploy.d.ts.map +1 -1
- package/dist/server/index.d.ts +7 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +100 -58
- package/dist/server/node-adapter.js +5 -104
- package/dist/server/realtime.d.ts +14 -3
- package/dist/server/realtime.d.ts.map +1 -1
- package/dist/server/server.d.ts +8 -16
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/sprites.d.ts +18 -1
- package/dist/server/sprites.d.ts.map +1 -1
- package/dist/server/tenancy.d.ts +4 -0
- package/dist/server/tenancy.d.ts.map +1 -1
- package/dist/server/usage-meter.d.ts +53 -0
- package/dist/server/usage-meter.d.ts.map +1 -0
- package/dist/server-MB3OPSPI.js +18 -0
- package/dist/server-ZVWGLLRF.js +18 -0
- package/dist/sprites-KQGTGM6W.js +27 -0
- package/dist/{sprites-vc4qbrp1.js → sprites-RU3E4XQN.js} +6 -7
- package/dist/svelte/index.d.ts +17 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +51 -0
- package/dist/svelte/schema-hooks.d.ts +45 -0
- package/dist/svelte/schema-hooks.d.ts.map +1 -0
- package/dist/svelte/schema-hooks.js +71 -0
- package/dist/svelte/stores.d.ts +78 -0
- package/dist/svelte/stores.d.ts.map +1 -0
- package/dist/sync/http-transport.d.ts +3 -3
- package/dist/sync/http-transport.d.ts.map +1 -1
- package/dist/sync/index.d.ts +15 -1
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +617 -0
- package/dist/sync/memory-room.d.ts +37 -0
- package/dist/sync/memory-room.d.ts.map +1 -0
- package/dist/sync/memory-transport.d.ts +3 -3
- package/dist/sync/memory-transport.d.ts.map +1 -1
- package/dist/sync/partykit-room.d.ts +40 -0
- package/dist/sync/partykit-room.d.ts.map +1 -0
- package/dist/sync/partykit-transport.d.ts +84 -0
- package/dist/sync/partykit-transport.d.ts.map +1 -0
- package/dist/sync/room-core.d.ts +53 -0
- package/dist/sync/room-core.d.ts.map +1 -0
- package/dist/sync/sync-engine.d.ts +40 -2
- package/dist/sync/sync-engine.d.ts.map +1 -1
- package/dist/sync/sync-room-server.d.ts +39 -0
- package/dist/sync/sync-room-server.d.ts.map +1 -0
- package/dist/sync/types.d.ts +70 -3
- package/dist/sync/types.d.ts.map +1 -1
- package/dist/sync/vcs-sync-peer.d.ts +63 -0
- package/dist/sync/vcs-sync-peer.d.ts.map +1 -0
- package/dist/sync/ws-transport.d.ts +2 -2
- package/dist/sync/ws-transport.d.ts.map +1 -1
- package/dist/tenancy-NBLLGCNJ.js +14 -0
- package/dist/tenancy-POMWQP6M.js +16 -0
- package/dist/ui/client.html +355 -67
- package/dist/ui/launch-explorer.d.ts +22 -0
- package/dist/ui/launch-explorer.d.ts.map +1 -0
- package/dist/ui/server.d.ts +1 -1
- package/dist/ui/server.d.ts.map +1 -1
- package/dist/vcs/blob-store.d.ts.map +1 -1
- package/dist/vcs/idb-op-log.d.ts +67 -0
- package/dist/vcs/idb-op-log.d.ts.map +1 -0
- package/dist/vcs/index.d.ts +1 -0
- package/dist/vcs/index.d.ts.map +1 -1
- package/dist/vcs/index.js +81 -72
- package/dist/vcs/issue.d.ts +22 -5
- package/dist/vcs/issue.d.ts.map +1 -1
- package/dist/vcs/lane-materialize.d.ts +5 -0
- package/dist/vcs/lane-materialize.d.ts.map +1 -1
- package/dist/vcs/op-log.d.ts +28 -2
- package/dist/vcs/op-log.d.ts.map +1 -1
- package/dist/vcs/ops.d.ts +10 -0
- package/dist/vcs/ops.d.ts.map +1 -1
- package/dist/vcs/types.d.ts +2 -0
- package/dist/vcs/types.d.ts.map +1 -1
- package/dist/{vm-config-6xhsj6b3.js → vm-config-JI4OXKDQ.js} +9 -10
- package/dist/vue/hooks.d.ts +64 -0
- package/dist/vue/hooks.d.ts.map +1 -0
- package/dist/vue/index.d.ts +19 -0
- package/dist/vue/index.d.ts.map +1 -0
- package/dist/vue/index.js +59 -0
- package/dist/vue/schema-hooks.d.ts +41 -0
- package/dist/vue/schema-hooks.d.ts.map +1 -0
- package/dist/vue/schema-hooks.js +64 -0
- package/package.json +130 -17
- package/dist/deploy-999q207z.js +0 -10
- package/dist/engine-y0724kjq.js +0 -8
- package/dist/import-s2b8e0ft.js +0 -11
- package/dist/index-a76rekgs.js +0 -67
- package/dist/index-c9h37r6h.js +0 -1
- package/dist/index-hr9qvv77.js +0 -860
- package/dist/index-k5b0xskw.js +0 -1
- package/dist/index-nzb3am4f.js +0 -80
- package/dist/index-wncptktd.js +0 -292
- package/dist/index-y6a4kj0p.js +0 -43
- package/dist/index-yhwjgfvj.js +0 -342
- package/dist/sdk-bepky0xs.js +0 -16
- package/dist/server-szdjx0nt.js +0 -13
- package/dist/sqlite-backend-0vsmc6qj.js +0 -8
- package/dist/tenancy-pjm32b4v.js +0 -14
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
// @bun
|
|
2
1
|
import {
|
|
3
|
-
|
|
4
|
-
} from "./
|
|
2
|
+
DEFAULT_TENANT
|
|
3
|
+
} from "./chunk-JHHEXEDM.js";
|
|
4
|
+
import {
|
|
5
|
+
rel
|
|
6
|
+
} from "./chunk-44FFQKQX.js";
|
|
5
7
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
entityRecordToPlain,
|
|
9
|
+
hydrateBindings,
|
|
10
|
+
resolveRelations
|
|
11
|
+
} from "./chunk-JUEMDWLU.js";
|
|
8
12
|
import {
|
|
9
|
-
|
|
10
|
-
} from "./
|
|
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(
|
|
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() /
|
|
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(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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 ??
|
|
177
|
+
avatarUrl: user.picture ?? user.avatar_url ?? void 0
|
|
161
178
|
};
|
|
162
179
|
}
|
|
163
180
|
|
|
164
181
|
// src/server/permissions.ts
|
|
165
|
-
|
|
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
|
-
|
|
198
|
-
|
|
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
|
-
|
|
266
|
-
clients = new Map;
|
|
482
|
+
var SubscriptionManager = class {
|
|
483
|
+
clients = /* @__PURE__ */ new Map();
|
|
267
484
|
pool;
|
|
268
485
|
permissions;
|
|
269
|
-
|
|
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,
|
|
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
|
-
|
|
332
|
-
|
|
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.
|
|
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 =
|
|
349
|
-
|
|
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.
|
|
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 =
|
|
381
|
-
|
|
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
|
-
|
|
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(
|
|
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 ??
|
|
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
|
|
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(
|
|
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(
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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(
|
|
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
|
-
|
|
540
|
-
|
|
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 (
|
|
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(
|
|
550
|
-
|
|
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
|
-
|
|
556
|
-
|
|
794
|
+
join2(__moduleDir, "db", "trellis.css"),
|
|
795
|
+
join2(__moduleDir, "trellis.css")
|
|
557
796
|
];
|
|
558
797
|
for (const p of candidates) {
|
|
559
|
-
if (
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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") ??
|
|
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.
|
|
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(
|
|
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.
|
|
712
|
-
const
|
|
713
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
740
|
-
|
|
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.
|
|
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(
|
|
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.
|
|
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(
|
|
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.
|
|
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(
|
|
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(
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
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
|
-
|
|
874
|
-
const
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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
|
|
886
|
-
|
|
887
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
+
};
|