trellis 2.1.7 → 3.0.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/README.md +65 -706
- package/dist/cli/index.d.ts +16 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +3188 -232
- package/dist/client/config.d.ts +56 -0
- package/dist/client/config.d.ts.map +1 -0
- package/dist/client/index.d.ts +15 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +32 -0
- package/dist/client/sdk.d.ts +152 -0
- package/dist/client/sdk.d.ts.map +1 -0
- package/dist/config-8hczw0rs.js +20 -0
- package/dist/context/heat-map-manager.d.ts +100 -0
- package/dist/context/heat-map-manager.d.ts.map +1 -0
- package/dist/context/manager.d.ts +16 -0
- package/dist/context/manager.d.ts.map +1 -0
- package/dist/context/types.d.ts +20 -0
- package/dist/context/types.d.ts.map +1 -0
- package/dist/core/agents/harness.d.ts +58 -0
- package/dist/core/agents/harness.d.ts.map +1 -0
- package/dist/core/agents/index.d.ts +8 -0
- package/dist/core/agents/index.d.ts.map +1 -0
- package/dist/core/agents/types.d.ts +79 -0
- package/dist/core/agents/types.d.ts.map +1 -0
- package/dist/core/computation/expr-evaluator.d.ts +52 -0
- package/dist/core/computation/expr-evaluator.d.ts.map +1 -0
- package/dist/core/index.d.ts +25 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +105 -11
- package/dist/core/kernel/logic-middleware.d.ts +19 -0
- package/dist/core/kernel/logic-middleware.d.ts.map +1 -0
- package/dist/core/kernel/middleware.d.ts +28 -0
- package/dist/core/kernel/middleware.d.ts.map +1 -0
- package/dist/core/kernel/schema-middleware.d.ts +15 -0
- package/dist/core/kernel/schema-middleware.d.ts.map +1 -0
- package/dist/core/kernel/security-middleware.d.ts +24 -0
- package/dist/core/kernel/security-middleware.d.ts.map +1 -0
- package/dist/core/kernel/sync-provider.d.ts +59 -0
- package/dist/core/kernel/sync-provider.d.ts.map +1 -0
- package/dist/core/kernel/trellis-kernel.d.ts +217 -0
- package/dist/core/kernel/trellis-kernel.d.ts.map +1 -0
- package/dist/core/ontology/builtins.d.ts +16 -0
- package/dist/core/ontology/builtins.d.ts.map +1 -0
- package/dist/core/ontology/core-ontology.d.ts +20 -0
- package/dist/core/ontology/core-ontology.d.ts.map +1 -0
- package/dist/core/ontology/index.d.ts +12 -0
- package/dist/core/ontology/index.d.ts.map +1 -0
- package/dist/core/ontology/registry.d.ts +70 -0
- package/dist/core/ontology/registry.d.ts.map +1 -0
- package/dist/core/ontology/types.d.ts +201 -0
- package/dist/core/ontology/types.d.ts.map +1 -0
- package/dist/core/ontology/validator.d.ts +34 -0
- package/dist/core/ontology/validator.d.ts.map +1 -0
- package/dist/core/persist/backend.d.ts +62 -0
- package/dist/core/persist/backend.d.ts.map +1 -0
- package/dist/core/persist/better-sqlite-backend.d.ts +33 -0
- package/dist/core/persist/better-sqlite-backend.d.ts.map +1 -0
- package/dist/core/persist/sqlite-backend.d.ts +43 -0
- package/dist/core/persist/sqlite-backend.d.ts.map +1 -0
- package/dist/core/plugins/index.d.ts +8 -0
- package/dist/core/plugins/index.d.ts.map +1 -0
- package/dist/core/plugins/registry.d.ts +69 -0
- package/dist/core/plugins/registry.d.ts.map +1 -0
- package/dist/core/plugins/types.d.ts +87 -0
- package/dist/core/plugins/types.d.ts.map +1 -0
- package/dist/core/query/datalog.d.ts +52 -0
- package/dist/core/query/datalog.d.ts.map +1 -0
- package/dist/core/query/engine.d.ts +42 -0
- package/dist/core/query/engine.d.ts.map +1 -0
- package/dist/core/query/index.d.ts +12 -0
- package/dist/core/query/index.d.ts.map +1 -0
- package/dist/core/query/parser.d.ts +37 -0
- package/dist/core/query/parser.d.ts.map +1 -0
- package/dist/core/query/types.d.ts +135 -0
- package/dist/core/query/types.d.ts.map +1 -0
- package/dist/core/store/eav-store.d.ts +111 -0
- package/dist/core/store/eav-store.d.ts.map +1 -0
- package/dist/db/index.d.ts +18 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +85 -0
- package/dist/db/inspector.js +28 -0
- package/dist/db/trellis.css +1 -0
- package/dist/decisions/auto-capture.d.ts +31 -0
- package/dist/decisions/auto-capture.d.ts.map +1 -0
- package/dist/decisions/hooks.d.ts +48 -0
- package/dist/decisions/hooks.d.ts.map +1 -0
- package/dist/decisions/index.d.ts +36 -0
- package/dist/decisions/index.d.ts.map +1 -0
- package/dist/decisions/types.d.ts +73 -0
- package/dist/decisions/types.d.ts.map +1 -0
- package/dist/deploy-999q207z.js +10 -0
- package/dist/embeddings/auto-embed.d.ts +52 -0
- package/dist/embeddings/auto-embed.d.ts.map +1 -0
- package/dist/embeddings/chunker.d.ts +73 -0
- package/dist/embeddings/chunker.d.ts.map +1 -0
- package/dist/embeddings/index.d.ts +18 -0
- package/dist/embeddings/index.d.ts.map +1 -0
- package/dist/embeddings/model.d.ts +30 -0
- package/dist/embeddings/model.d.ts.map +1 -0
- package/dist/embeddings/search.d.ts +87 -0
- package/dist/embeddings/search.d.ts.map +1 -0
- package/dist/embeddings/store.d.ts +66 -0
- package/dist/embeddings/store.d.ts.map +1 -0
- package/dist/embeddings/types.d.ts +54 -0
- package/dist/embeddings/types.d.ts.map +1 -0
- package/dist/engine-y0724kjq.js +8 -0
- package/dist/engine.d.ts +218 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/evals/types.d.ts +29 -0
- package/dist/evals/types.d.ts.map +1 -0
- package/dist/garden/cluster.d.ts +57 -0
- package/dist/garden/cluster.d.ts.map +1 -0
- package/dist/garden/garden.d.ts +104 -0
- package/dist/garden/garden.d.ts.map +1 -0
- package/dist/garden/index.d.ts +15 -0
- package/dist/garden/index.d.ts.map +1 -0
- package/dist/git/git-exporter.d.ts +37 -0
- package/dist/git/git-exporter.d.ts.map +1 -0
- package/dist/git/git-importer.d.ts +36 -0
- package/dist/git/git-importer.d.ts.map +1 -0
- package/dist/git/git-reader.d.ts +63 -0
- package/dist/git/git-reader.d.ts.map +1 -0
- package/dist/git/index.d.ts +10 -0
- package/dist/git/index.d.ts.map +1 -0
- package/dist/identity/governance.d.ts +54 -0
- package/dist/identity/governance.d.ts.map +1 -0
- package/dist/identity/identity.d.ts +63 -0
- package/dist/identity/identity.d.ts.map +1 -0
- package/dist/identity/index.d.ts +10 -0
- package/dist/identity/index.d.ts.map +1 -0
- package/dist/identity/signing-middleware.d.ts +38 -0
- package/dist/identity/signing-middleware.d.ts.map +1 -0
- package/dist/import-s2b8e0ft.js +11 -0
- package/dist/{index-3ejh8k6v.js → index-0q7wbasy.js} +18 -4
- package/dist/index-0zk3fx2s.js +1004 -0
- package/dist/index-2r4jxwnb.js +32 -0
- package/dist/index-6n5dcebj.js +847 -0
- package/dist/index-7e27kvvj.js +292 -0
- package/dist/index-bmyt7k8n.js +90 -0
- package/dist/index-c9h37r6h.js +1 -0
- package/dist/{index-k5kf7sd0.js → index-hmdbnd4n.js} +1 -1
- package/dist/index-k5b0xskw.js +1 -0
- package/dist/index-n9f2qyh5.js +495 -0
- package/dist/{index-22jx9qsz.js → index-q31hfjja.js} +861 -51
- package/dist/index-skhn0agf.js +155 -0
- package/dist/{index-5m0g9r0y.js → index-w7ng765c.js} +4 -497
- package/dist/index-wt8rz4gn.js +132 -0
- package/dist/index-xzym9w0m.js +43 -0
- package/dist/index-y3d71wzd.js +77 -0
- package/dist/index-y6a4kj0p.js +43 -0
- package/dist/index-yhwjgfvj.js +342 -0
- package/dist/index-yp88he8n.js +316 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -9
- package/dist/links/index.d.ts +16 -0
- package/dist/links/index.d.ts.map +1 -0
- package/dist/links/lifecycle.d.ts +112 -0
- package/dist/links/lifecycle.d.ts.map +1 -0
- package/dist/links/parser.d.ts +56 -0
- package/dist/links/parser.d.ts.map +1 -0
- package/dist/links/ref-index.d.ts +55 -0
- package/dist/links/ref-index.d.ts.map +1 -0
- package/dist/links/resolver.d.ts +90 -0
- package/dist/links/resolver.d.ts.map +1 -0
- package/dist/links/types.d.ts +70 -0
- package/dist/links/types.d.ts.map +1 -0
- package/dist/llm/provider.d.ts +11 -0
- package/dist/llm/provider.d.ts.map +1 -0
- package/dist/llm/types.d.ts +74 -0
- package/dist/llm/types.d.ts.map +1 -0
- package/dist/mcp/docs.d.ts +18 -0
- package/dist/mcp/docs.d.ts.map +1 -0
- package/dist/mcp/index.d.ts +15 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/server.d.ts +26 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/orchestration/types.d.ts +22 -0
- package/dist/orchestration/types.d.ts.map +1 -0
- package/dist/plugins/agent-memory/graph-context-manager.d.ts +75 -0
- package/dist/plugins/agent-memory/graph-context-manager.d.ts.map +1 -0
- package/dist/plugins/agent-memory/index.d.ts +30 -0
- package/dist/plugins/agent-memory/index.d.ts.map +1 -0
- package/dist/plugins/agent-memory/ontology.d.ts +13 -0
- package/dist/plugins/agent-memory/ontology.d.ts.map +1 -0
- package/dist/plugins/agent-memory/plugin.d.ts +17 -0
- package/dist/plugins/agent-memory/plugin.d.ts.map +1 -0
- package/dist/plugins/brand/cache.d.ts +18 -0
- package/dist/plugins/brand/cache.d.ts.map +1 -0
- package/dist/plugins/brand/catalog-generator.d.ts +89 -0
- package/dist/plugins/brand/catalog-generator.d.ts.map +1 -0
- package/dist/plugins/brand/constraints.d.ts +55 -0
- package/dist/plugins/brand/constraints.d.ts.map +1 -0
- package/dist/plugins/brand/index.d.ts +44 -0
- package/dist/plugins/brand/index.d.ts.map +1 -0
- package/dist/plugins/brand/mcp-tools.d.ts +101 -0
- package/dist/plugins/brand/mcp-tools.d.ts.map +1 -0
- package/dist/plugins/brand/ontology.d.ts +13 -0
- package/dist/plugins/brand/ontology.d.ts.map +1 -0
- package/dist/plugins/brand/plugin.d.ts +21 -0
- package/dist/plugins/brand/plugin.d.ts.map +1 -0
- package/dist/plugins/brand/voice-tone.d.ts +24 -0
- package/dist/plugins/brand/voice-tone.d.ts.map +1 -0
- package/dist/plugins/idea-garden/api.d.ts +26 -0
- package/dist/plugins/idea-garden/api.d.ts.map +1 -0
- package/dist/plugins/idea-garden/index.d.ts +12 -0
- package/dist/plugins/idea-garden/index.d.ts.map +1 -0
- package/dist/plugins/idea-garden/plugin.d.ts +16 -0
- package/dist/plugins/idea-garden/plugin.d.ts.map +1 -0
- package/dist/plugins/idea-garden/types.d.ts +22 -0
- package/dist/plugins/idea-garden/types.d.ts.map +1 -0
- package/dist/plugins/plan-approval/index.d.ts +36 -0
- package/dist/plugins/plan-approval/index.d.ts.map +1 -0
- package/dist/plugins/plan-approval/ontology.d.ts +11 -0
- package/dist/plugins/plan-approval/ontology.d.ts.map +1 -0
- package/dist/plugins/plan-approval/plan-manager.d.ts +104 -0
- package/dist/plugins/plan-approval/plan-manager.d.ts.map +1 -0
- package/dist/plugins/plan-approval/plugin.d.ts +110 -0
- package/dist/plugins/plan-approval/plugin.d.ts.map +1 -0
- package/dist/plugins/proactive-watcher/index.d.ts +28 -0
- package/dist/plugins/proactive-watcher/index.d.ts.map +1 -0
- package/dist/plugins/proactive-watcher/ontology.d.ts +8 -0
- package/dist/plugins/proactive-watcher/ontology.d.ts.map +1 -0
- package/dist/plugins/proactive-watcher/plugin.d.ts +20 -0
- package/dist/plugins/proactive-watcher/plugin.d.ts.map +1 -0
- package/dist/plugins/proactive-watcher/watcher-manager.d.ts +36 -0
- package/dist/plugins/proactive-watcher/watcher-manager.d.ts.map +1 -0
- package/dist/plugins/sprite-tools/checkpoint-middleware.d.ts +43 -0
- package/dist/plugins/sprite-tools/checkpoint-middleware.d.ts.map +1 -0
- package/dist/plugins/sprite-tools/index.d.ts +40 -0
- package/dist/plugins/sprite-tools/index.d.ts.map +1 -0
- package/dist/plugins/sprite-tools/plugin.d.ts +69 -0
- package/dist/plugins/sprite-tools/plugin.d.ts.map +1 -0
- package/dist/react/index.js +189 -0
- package/dist/scaffold/index.d.ts +13 -0
- package/dist/scaffold/index.d.ts.map +1 -0
- package/dist/scaffold/infer.d.ts +42 -0
- package/dist/scaffold/infer.d.ts.map +1 -0
- package/dist/scaffold/profile.d.ts +51 -0
- package/dist/scaffold/profile.d.ts.map +1 -0
- package/dist/scaffold/seed.d.ts +27 -0
- package/dist/scaffold/seed.d.ts.map +1 -0
- package/dist/scaffold/write.d.ts +53 -0
- package/dist/scaffold/write.d.ts.map +1 -0
- package/dist/sdk-snn5gad3.js +15 -0
- package/dist/semantic/csharp-parser.d.ts +12 -0
- package/dist/semantic/csharp-parser.d.ts.map +1 -0
- package/dist/semantic/go-parser.d.ts +12 -0
- package/dist/semantic/go-parser.d.ts.map +1 -0
- package/dist/semantic/index.d.ts +22 -0
- package/dist/semantic/index.d.ts.map +1 -0
- package/dist/semantic/java-parser.d.ts +12 -0
- package/dist/semantic/java-parser.d.ts.map +1 -0
- package/dist/semantic/python-parser.d.ts +12 -0
- package/dist/semantic/python-parser.d.ts.map +1 -0
- package/dist/semantic/ruby-parser.d.ts +12 -0
- package/dist/semantic/ruby-parser.d.ts.map +1 -0
- package/dist/semantic/rust-parser.d.ts +12 -0
- package/dist/semantic/rust-parser.d.ts.map +1 -0
- package/dist/semantic/semantic-merge.d.ts +20 -0
- package/dist/semantic/semantic-merge.d.ts.map +1 -0
- package/dist/semantic/ts-parser.d.ts +13 -0
- package/dist/semantic/ts-parser.d.ts.map +1 -0
- package/dist/semantic/types.d.ts +130 -0
- package/dist/semantic/types.d.ts.map +1 -0
- package/dist/server/auth.d.ts +72 -0
- package/dist/server/auth.d.ts.map +1 -0
- package/dist/server/deploy.d.ts +44 -0
- package/dist/server/deploy.d.ts.map +1 -0
- package/dist/server/import.d.ts +40 -0
- package/dist/server/import.d.ts.map +1 -0
- package/dist/server/index.d.ts +26 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +90 -0
- package/dist/server/permissions.d.ts +84 -0
- package/dist/server/permissions.d.ts.map +1 -0
- package/dist/server/realtime.d.ts +78 -0
- package/dist/server/realtime.d.ts.map +1 -0
- package/dist/server/server.d.ts +43 -0
- package/dist/server/server.d.ts.map +1 -0
- package/dist/server/sprites.d.ts +26 -0
- package/dist/server/sprites.d.ts.map +1 -0
- package/dist/server/tenancy.d.ts +53 -0
- package/dist/server/tenancy.d.ts.map +1 -0
- package/dist/server/vm-config.d.ts +60 -0
- package/dist/server/vm-config.d.ts.map +1 -0
- package/dist/server-mrctdwzr.js +11 -0
- package/dist/sprites-vc4qbrp1.js +16 -0
- package/dist/streaming/types.d.ts +43 -0
- package/dist/streaming/types.d.ts.map +1 -0
- package/dist/sync/http-transport.d.ts +47 -0
- package/dist/sync/http-transport.d.ts.map +1 -0
- package/dist/sync/index.d.ts +22 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/memory-transport.d.ts +27 -0
- package/dist/sync/memory-transport.d.ts.map +1 -0
- package/dist/sync/multi-repo.d.ts +82 -0
- package/dist/sync/multi-repo.d.ts.map +1 -0
- package/dist/sync/reconciler.d.ts +48 -0
- package/dist/sync/reconciler.d.ts.map +1 -0
- package/dist/sync/sync-engine.d.ts +65 -0
- package/dist/sync/sync-engine.d.ts.map +1 -0
- package/dist/sync/types.d.ts +71 -0
- package/dist/sync/types.d.ts.map +1 -0
- package/dist/sync/ws-transport.d.ts +41 -0
- package/dist/sync/ws-transport.d.ts.map +1 -0
- package/dist/tenancy-7d1g4ayp.js +13 -0
- package/dist/ui/client.html +460 -664
- package/dist/ui/server.d.ts +42 -0
- package/dist/ui/server.d.ts.map +1 -0
- package/dist/vcs/blob-store.d.ts +49 -0
- package/dist/vcs/blob-store.d.ts.map +1 -0
- package/dist/vcs/branch.d.ts +35 -0
- package/dist/vcs/branch.d.ts.map +1 -0
- package/dist/vcs/checkpoint.d.ts +24 -0
- package/dist/vcs/checkpoint.d.ts.map +1 -0
- package/dist/vcs/decompose.d.ts +19 -0
- package/dist/vcs/decompose.d.ts.map +1 -0
- package/dist/vcs/diff.d.ts +65 -0
- package/dist/vcs/diff.d.ts.map +1 -0
- package/dist/vcs/engine-context.d.ts +21 -0
- package/dist/vcs/engine-context.d.ts.map +1 -0
- package/dist/vcs/index.d.ts +23 -0
- package/dist/vcs/index.d.ts.map +1 -0
- package/dist/vcs/index.js +2 -2
- package/dist/vcs/issue.d.ts +159 -0
- package/dist/vcs/issue.d.ts.map +1 -0
- package/dist/vcs/merge.d.ts +55 -0
- package/dist/vcs/merge.d.ts.map +1 -0
- package/dist/vcs/milestone.d.ts +30 -0
- package/dist/vcs/milestone.d.ts.map +1 -0
- package/dist/vcs/ops.d.ts +27 -0
- package/dist/vcs/ops.d.ts.map +1 -0
- package/dist/vcs/types.d.ts +95 -0
- package/dist/vcs/types.d.ts.map +1 -0
- package/dist/vcs/vcs-middleware.d.ts +14 -0
- package/dist/vcs/vcs-middleware.d.ts.map +1 -0
- package/dist/vm-config-6xhsj6b3.js +22 -0
- package/dist/watcher/fs-watcher.d.ts +51 -0
- package/dist/watcher/fs-watcher.d.ts.map +1 -0
- package/dist/watcher/index.d.ts +9 -0
- package/dist/watcher/index.d.ts.map +1 -0
- package/dist/watcher/ingestion.d.ts +28 -0
- package/dist/watcher/ingestion.d.ts.map +1 -0
- package/package.json +57 -7
- package/dist/index-hybgxe40.js +0 -1174
|
@@ -0,0 +1,847 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
parseSimple
|
|
4
|
+
} from "./index-n9f2qyh5.js";
|
|
5
|
+
import {
|
|
6
|
+
QueryEngine
|
|
7
|
+
} from "./index-yp88he8n.js";
|
|
8
|
+
|
|
9
|
+
// src/server/server.ts
|
|
10
|
+
import { existsSync, readFileSync } from "fs";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
|
|
13
|
+
// src/server/auth.ts
|
|
14
|
+
var ANONYMOUS = {
|
|
15
|
+
userId: null,
|
|
16
|
+
tenantId: null,
|
|
17
|
+
roles: [],
|
|
18
|
+
claims: {},
|
|
19
|
+
authenticated: false
|
|
20
|
+
};
|
|
21
|
+
function base64UrlEncode(input) {
|
|
22
|
+
return btoa(String.fromCharCode(...input)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
23
|
+
}
|
|
24
|
+
function base64UrlDecode(input) {
|
|
25
|
+
const padded = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
26
|
+
const padLen = (4 - padded.length % 4) % 4;
|
|
27
|
+
const base64 = padded + "=".repeat(padLen);
|
|
28
|
+
const binary = atob(base64);
|
|
29
|
+
return Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
30
|
+
}
|
|
31
|
+
async function hmacKey(secret) {
|
|
32
|
+
return crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"]);
|
|
33
|
+
}
|
|
34
|
+
async function signJwt(payload, secret, expiresInSeconds = 86400) {
|
|
35
|
+
const header = { alg: "HS256", typ: "JWT" };
|
|
36
|
+
const now = Math.floor(Date.now() / 1000);
|
|
37
|
+
const claims = { iat: now, exp: now + expiresInSeconds, ...payload };
|
|
38
|
+
const enc = new TextEncoder;
|
|
39
|
+
const headerB64 = base64UrlEncode(enc.encode(JSON.stringify(header)));
|
|
40
|
+
const payloadB64 = base64UrlEncode(enc.encode(JSON.stringify(claims)));
|
|
41
|
+
const signing = `${headerB64}.${payloadB64}`;
|
|
42
|
+
const key = await hmacKey(secret);
|
|
43
|
+
const sig = await crypto.subtle.sign("HMAC", key, enc.encode(signing));
|
|
44
|
+
const sigB64 = base64UrlEncode(new Uint8Array(sig));
|
|
45
|
+
return `${signing}.${sigB64}`;
|
|
46
|
+
}
|
|
47
|
+
async function verifyJwt(token, secret) {
|
|
48
|
+
const parts = token.split(".");
|
|
49
|
+
if (parts.length !== 3)
|
|
50
|
+
return null;
|
|
51
|
+
const [headerB64, payloadB64, sigB64] = parts;
|
|
52
|
+
const enc = new TextEncoder;
|
|
53
|
+
const signing = `${headerB64}.${payloadB64}`;
|
|
54
|
+
try {
|
|
55
|
+
const key = await hmacKey(secret);
|
|
56
|
+
const sigRaw = base64UrlDecode(sigB64);
|
|
57
|
+
const sigBuf = sigRaw.buffer.slice(sigRaw.byteOffset, sigRaw.byteOffset + sigRaw.byteLength);
|
|
58
|
+
const valid = await crypto.subtle.verify("HMAC", key, sigBuf, enc.encode(signing));
|
|
59
|
+
if (!valid)
|
|
60
|
+
return null;
|
|
61
|
+
const claims = JSON.parse(new TextDecoder().decode(base64UrlDecode(payloadB64)));
|
|
62
|
+
const now = Math.floor(Date.now() / 1000);
|
|
63
|
+
if (typeof claims.exp === "number" && claims.exp < now)
|
|
64
|
+
return null;
|
|
65
|
+
return claims;
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function resolveAuth(authHeader, config) {
|
|
71
|
+
if (!authHeader) {
|
|
72
|
+
return config.allowPublic === false ? { ...ANONYMOUS, authenticated: false } : ANONYMOUS;
|
|
73
|
+
}
|
|
74
|
+
const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : authHeader;
|
|
75
|
+
if (config.apiKey && token === config.apiKey) {
|
|
76
|
+
return {
|
|
77
|
+
userId: "service",
|
|
78
|
+
tenantId: null,
|
|
79
|
+
roles: ["admin"],
|
|
80
|
+
claims: { sub: "service" },
|
|
81
|
+
authenticated: true
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (config.jwtSecret) {
|
|
85
|
+
const claims = await verifyJwt(token, config.jwtSecret);
|
|
86
|
+
if (claims) {
|
|
87
|
+
return {
|
|
88
|
+
userId: claims.sub ?? null,
|
|
89
|
+
tenantId: claims.tenantId ?? null,
|
|
90
|
+
roles: Array.isArray(claims.roles) ? claims.roles : typeof claims.role === "string" ? [claims.role] : [],
|
|
91
|
+
claims,
|
|
92
|
+
authenticated: true
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return ANONYMOUS;
|
|
97
|
+
}
|
|
98
|
+
var GOOGLE_PROVIDER = {
|
|
99
|
+
name: "google",
|
|
100
|
+
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
101
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
102
|
+
userInfoUrl: "https://www.googleapis.com/oauth2/v3/userinfo",
|
|
103
|
+
scopes: ["openid", "email", "profile"]
|
|
104
|
+
};
|
|
105
|
+
var GITHUB_PROVIDER = {
|
|
106
|
+
name: "github",
|
|
107
|
+
authUrl: "https://github.com/login/oauth/authorize",
|
|
108
|
+
tokenUrl: "https://github.com/login/oauth/access_token",
|
|
109
|
+
userInfoUrl: "https://api.github.com/user",
|
|
110
|
+
scopes: ["read:user", "user:email"]
|
|
111
|
+
};
|
|
112
|
+
function buildOAuthUrl(provider, redirectUri, state) {
|
|
113
|
+
const params = new URLSearchParams({
|
|
114
|
+
client_id: provider.clientId,
|
|
115
|
+
redirect_uri: redirectUri,
|
|
116
|
+
response_type: "code",
|
|
117
|
+
scope: provider.scopes.join(" "),
|
|
118
|
+
state
|
|
119
|
+
});
|
|
120
|
+
return `${provider.authUrl}?${params}`;
|
|
121
|
+
}
|
|
122
|
+
async function exchangeOAuthCode(provider, code, redirectUri) {
|
|
123
|
+
const tokenRes = await fetch(provider.tokenUrl, {
|
|
124
|
+
method: "POST",
|
|
125
|
+
headers: {
|
|
126
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
127
|
+
Accept: "application/json"
|
|
128
|
+
},
|
|
129
|
+
body: new URLSearchParams({
|
|
130
|
+
client_id: provider.clientId,
|
|
131
|
+
client_secret: provider.clientSecret,
|
|
132
|
+
code,
|
|
133
|
+
redirect_uri: redirectUri,
|
|
134
|
+
grant_type: "authorization_code"
|
|
135
|
+
})
|
|
136
|
+
});
|
|
137
|
+
if (!tokenRes.ok) {
|
|
138
|
+
throw new Error(`OAuth token exchange failed: ${tokenRes.status}`);
|
|
139
|
+
}
|
|
140
|
+
const tokenData = await tokenRes.json();
|
|
141
|
+
const accessToken = tokenData.access_token;
|
|
142
|
+
const userRes = await fetch(provider.userInfoUrl, {
|
|
143
|
+
headers: {
|
|
144
|
+
Authorization: `Bearer ${accessToken}`,
|
|
145
|
+
Accept: "application/json"
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
if (!userRes.ok) {
|
|
149
|
+
throw new Error(`OAuth user info fetch failed: ${userRes.status}`);
|
|
150
|
+
}
|
|
151
|
+
const user = await userRes.json();
|
|
152
|
+
return {
|
|
153
|
+
id: String(user.id ?? user.sub ?? ""),
|
|
154
|
+
email: String(user.email ?? ""),
|
|
155
|
+
name: String(user.name ?? user.login ?? ""),
|
|
156
|
+
avatarUrl: user.picture ?? user.avatar_url ?? undefined
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/server/permissions.ts
|
|
161
|
+
class PermissionRegistry {
|
|
162
|
+
rules = new Map;
|
|
163
|
+
defaultRule = "authenticated";
|
|
164
|
+
register(entityType, permissions) {
|
|
165
|
+
this.rules.set(entityType, permissions);
|
|
166
|
+
}
|
|
167
|
+
setDefault(rule) {
|
|
168
|
+
this.defaultRule = rule;
|
|
169
|
+
}
|
|
170
|
+
getRule(entityType, op) {
|
|
171
|
+
const def = this.rules.get(entityType);
|
|
172
|
+
return def?.[op] ?? this.defaultRule;
|
|
173
|
+
}
|
|
174
|
+
check(auth, entityType, op, entity = null) {
|
|
175
|
+
const rule = this.getRule(entityType, op);
|
|
176
|
+
return evaluateRule(rule, auth, entity);
|
|
177
|
+
}
|
|
178
|
+
assert(auth, entityType, op, entity = null) {
|
|
179
|
+
if (!this.check(auth, entityType, op, entity)) {
|
|
180
|
+
throw new PermissionError(auth, entityType, op);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function evaluateRule(rule, auth, entity) {
|
|
185
|
+
if (rule === "public") {
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
if (rule === "authenticated") {
|
|
189
|
+
return auth.authenticated;
|
|
190
|
+
}
|
|
191
|
+
if (rule === "own") {
|
|
192
|
+
if (!auth.authenticated || !auth.userId)
|
|
193
|
+
return false;
|
|
194
|
+
const ownerFact = entity?.facts.find((f) => f.a === "ownerId" || f.a === "createdBy");
|
|
195
|
+
return ownerFact?.v === auth.userId;
|
|
196
|
+
}
|
|
197
|
+
if (typeof rule === "object") {
|
|
198
|
+
if ("role" in rule) {
|
|
199
|
+
return auth.roles.includes(rule.role);
|
|
200
|
+
}
|
|
201
|
+
if ("roles" in rule) {
|
|
202
|
+
return rule.roles.some((r) => auth.roles.includes(r));
|
|
203
|
+
}
|
|
204
|
+
if ("fn" in rule) {
|
|
205
|
+
try {
|
|
206
|
+
return rule.fn(auth, entity);
|
|
207
|
+
} catch {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
class PermissionError extends Error {
|
|
216
|
+
auth;
|
|
217
|
+
entityType;
|
|
218
|
+
op;
|
|
219
|
+
constructor(auth, entityType, op) {
|
|
220
|
+
const who = auth.authenticated ? `user:${auth.userId}` : "anonymous";
|
|
221
|
+
super(`Permission denied: ${who} cannot ${op} ${entityType}`);
|
|
222
|
+
this.auth = auth;
|
|
223
|
+
this.entityType = entityType;
|
|
224
|
+
this.op = op;
|
|
225
|
+
this.name = "PermissionError";
|
|
226
|
+
}
|
|
227
|
+
toResponse() {
|
|
228
|
+
return {
|
|
229
|
+
error: "Forbidden",
|
|
230
|
+
message: this.message,
|
|
231
|
+
code: 403
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
var PUBLIC_READ = {
|
|
236
|
+
read: "public",
|
|
237
|
+
create: "authenticated",
|
|
238
|
+
update: "authenticated",
|
|
239
|
+
delete: "authenticated"
|
|
240
|
+
};
|
|
241
|
+
var FULLY_PUBLIC = {
|
|
242
|
+
read: "public",
|
|
243
|
+
create: "public",
|
|
244
|
+
update: "public",
|
|
245
|
+
delete: "public"
|
|
246
|
+
};
|
|
247
|
+
var OWNER_ONLY = {
|
|
248
|
+
read: "own",
|
|
249
|
+
create: "authenticated",
|
|
250
|
+
update: "own",
|
|
251
|
+
delete: "own"
|
|
252
|
+
};
|
|
253
|
+
var ADMIN_ONLY = {
|
|
254
|
+
read: { role: "admin" },
|
|
255
|
+
create: { role: "admin" },
|
|
256
|
+
update: { role: "admin" },
|
|
257
|
+
delete: { role: "admin" }
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// src/server/realtime.ts
|
|
261
|
+
class SubscriptionManager {
|
|
262
|
+
clients = new Map;
|
|
263
|
+
pool;
|
|
264
|
+
permissions;
|
|
265
|
+
constructor(pool, permissions = null) {
|
|
266
|
+
this.pool = pool;
|
|
267
|
+
this.permissions = permissions;
|
|
268
|
+
}
|
|
269
|
+
addClient(clientId, ws, auth, tenantId) {
|
|
270
|
+
this.clients.set(clientId, {
|
|
271
|
+
id: clientId,
|
|
272
|
+
ws,
|
|
273
|
+
subscriptions: new Map,
|
|
274
|
+
auth,
|
|
275
|
+
tenantId
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
removeClient(clientId) {
|
|
279
|
+
this.clients.delete(clientId);
|
|
280
|
+
}
|
|
281
|
+
async handleMessage(clientId, raw) {
|
|
282
|
+
const client = this.clients.get(clientId);
|
|
283
|
+
if (!client)
|
|
284
|
+
return;
|
|
285
|
+
let msg;
|
|
286
|
+
try {
|
|
287
|
+
msg = JSON.parse(raw);
|
|
288
|
+
} catch {
|
|
289
|
+
this._send(client, { type: "error", id: "", message: "Invalid JSON" });
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (msg.type === "ping") {
|
|
293
|
+
this._send(client, { type: "pong" });
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (msg.type === "subscribe") {
|
|
297
|
+
await this._handleSubscribe(client, msg.id, msg.query, msg.tenantId);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (msg.type === "unsubscribe") {
|
|
301
|
+
client.subscriptions.delete(msg.id);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
async notify(tenantId) {
|
|
306
|
+
const tid = tenantId ?? null;
|
|
307
|
+
const dead = [];
|
|
308
|
+
for (const [clientId, client] of this.clients) {
|
|
309
|
+
if (client.ws.readyState !== 1) {
|
|
310
|
+
dead.push(clientId);
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (client.tenantId !== tid)
|
|
314
|
+
continue;
|
|
315
|
+
for (const [subId, sub] of client.subscriptions) {
|
|
316
|
+
if (sub.tenantId !== tid)
|
|
317
|
+
continue;
|
|
318
|
+
await this._pushUpdate(client, sub);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
for (const id of dead)
|
|
322
|
+
this.clients.delete(id);
|
|
323
|
+
}
|
|
324
|
+
get clientCount() {
|
|
325
|
+
return this.clients.size;
|
|
326
|
+
}
|
|
327
|
+
async _handleSubscribe(client, subId, queryStr, tenantId) {
|
|
328
|
+
const tid = tenantId ?? client.tenantId ?? null;
|
|
329
|
+
let parsedQuery;
|
|
330
|
+
try {
|
|
331
|
+
parsedQuery = parseSimple(queryStr);
|
|
332
|
+
} catch (err) {
|
|
333
|
+
this._send(client, {
|
|
334
|
+
type: "error",
|
|
335
|
+
id: subId,
|
|
336
|
+
message: `Invalid query: ${err instanceof Error ? err.message : String(err)}`
|
|
337
|
+
});
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const kernel = this.pool.get(tid);
|
|
341
|
+
const engine = new QueryEngine(kernel.getStore());
|
|
342
|
+
let result;
|
|
343
|
+
try {
|
|
344
|
+
const qr = engine.execute(parsedQuery);
|
|
345
|
+
result = qr.bindings;
|
|
346
|
+
} catch (err) {
|
|
347
|
+
this._send(client, {
|
|
348
|
+
type: "error",
|
|
349
|
+
id: subId,
|
|
350
|
+
message: `Query failed: ${err instanceof Error ? err.message : String(err)}`
|
|
351
|
+
});
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const sub = {
|
|
355
|
+
id: subId,
|
|
356
|
+
query: queryStr,
|
|
357
|
+
tenantId: tid,
|
|
358
|
+
auth: client.auth,
|
|
359
|
+
lastResult: result
|
|
360
|
+
};
|
|
361
|
+
client.subscriptions.set(subId, sub);
|
|
362
|
+
this._send(client, { type: "subscribed", id: subId });
|
|
363
|
+
this._send(client, {
|
|
364
|
+
type: "data",
|
|
365
|
+
id: subId,
|
|
366
|
+
result,
|
|
367
|
+
diff: { added: result, updated: [], removed: [] }
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
async _pushUpdate(client, sub) {
|
|
371
|
+
const kernel = this.pool.get(sub.tenantId);
|
|
372
|
+
const engine = new QueryEngine(kernel.getStore());
|
|
373
|
+
let newResult;
|
|
374
|
+
try {
|
|
375
|
+
const parsed = parseSimple(sub.query);
|
|
376
|
+
const qr = engine.execute(parsed);
|
|
377
|
+
newResult = qr.bindings;
|
|
378
|
+
} catch {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const diff = computeDiff(sub.lastResult, newResult);
|
|
382
|
+
if (diff.added.length === 0 && diff.updated.length === 0 && diff.removed.length === 0) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
sub.lastResult = newResult;
|
|
386
|
+
this._send(client, { type: "data", id: sub.id, result: newResult, diff });
|
|
387
|
+
}
|
|
388
|
+
_send(client, payload) {
|
|
389
|
+
try {
|
|
390
|
+
client.ws.send(JSON.stringify(payload));
|
|
391
|
+
} catch {}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
function entityId(row) {
|
|
395
|
+
return String(row["?e"] ?? row.id ?? row.e ?? JSON.stringify(row));
|
|
396
|
+
}
|
|
397
|
+
function computeDiff(prev, next) {
|
|
398
|
+
const prevMap = new Map(prev.map((r) => [entityId(r), r]));
|
|
399
|
+
const nextMap = new Map(next.map((r) => [entityId(r), r]));
|
|
400
|
+
const added = [];
|
|
401
|
+
const updated = [];
|
|
402
|
+
const removed = [];
|
|
403
|
+
for (const [id, row] of nextMap) {
|
|
404
|
+
if (!prevMap.has(id)) {
|
|
405
|
+
added.push(row);
|
|
406
|
+
} else if (JSON.stringify(prevMap.get(id)) !== JSON.stringify(row)) {
|
|
407
|
+
updated.push(row);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
for (const [id, row] of prevMap) {
|
|
411
|
+
if (!nextMap.has(id))
|
|
412
|
+
removed.push(row);
|
|
413
|
+
}
|
|
414
|
+
return { added, updated, removed };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// src/server/server.ts
|
|
418
|
+
function startServer(opts) {
|
|
419
|
+
const { pool, permissions, config } = opts;
|
|
420
|
+
const port = opts.port ?? config.port ?? 3000;
|
|
421
|
+
const authConfig = {
|
|
422
|
+
jwtSecret: config.jwtSecret,
|
|
423
|
+
apiKey: config.apiKey,
|
|
424
|
+
allowPublic: true
|
|
425
|
+
};
|
|
426
|
+
const subs = new SubscriptionManager(pool, permissions ?? null);
|
|
427
|
+
const server = Bun.serve({
|
|
428
|
+
port,
|
|
429
|
+
async fetch(req, server2) {
|
|
430
|
+
const url = new URL(req.url);
|
|
431
|
+
const path = url.pathname;
|
|
432
|
+
if (req.headers.get("upgrade") === "websocket") {
|
|
433
|
+
const upgraded = server2.upgrade(req);
|
|
434
|
+
if (upgraded)
|
|
435
|
+
return;
|
|
436
|
+
return new Response("WebSocket upgrade failed", { status: 400 });
|
|
437
|
+
}
|
|
438
|
+
const auth = await resolveAuth(req.headers.get("authorization"), authConfig);
|
|
439
|
+
const tenantId = auth.tenantId ?? url.searchParams.get("tenantId") ?? null;
|
|
440
|
+
try {
|
|
441
|
+
return await route(req, url, path, auth, tenantId, {
|
|
442
|
+
pool,
|
|
443
|
+
permissions: permissions ?? null,
|
|
444
|
+
subs,
|
|
445
|
+
authConfig,
|
|
446
|
+
config,
|
|
447
|
+
oauthProviders: opts.oauthProviders ?? {}
|
|
448
|
+
});
|
|
449
|
+
} catch (err) {
|
|
450
|
+
if (err instanceof PermissionError) {
|
|
451
|
+
return json(err.toResponse(), 403);
|
|
452
|
+
}
|
|
453
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
454
|
+
return json({ error: "Internal Server Error", message: msg }, 500);
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
websocket: {
|
|
458
|
+
async open(ws) {
|
|
459
|
+
const id = crypto.randomUUID();
|
|
460
|
+
ws.__clientId = id;
|
|
461
|
+
subs.addClient(id, ws, {
|
|
462
|
+
userId: null,
|
|
463
|
+
tenantId: null,
|
|
464
|
+
roles: [],
|
|
465
|
+
claims: {},
|
|
466
|
+
authenticated: false
|
|
467
|
+
}, null);
|
|
468
|
+
},
|
|
469
|
+
async message(ws, raw) {
|
|
470
|
+
const id = ws.__clientId;
|
|
471
|
+
await subs.handleMessage(id, typeof raw === "string" ? raw : new TextDecoder().decode(raw));
|
|
472
|
+
},
|
|
473
|
+
close(ws) {
|
|
474
|
+
const id = ws.__clientId;
|
|
475
|
+
subs.removeClient(id);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
return server;
|
|
480
|
+
}
|
|
481
|
+
async function route(req, url, path, auth, tenantId, ctx) {
|
|
482
|
+
const method = req.method.toUpperCase();
|
|
483
|
+
if (method === "GET" && path === "/__trellis/inspector.js") {
|
|
484
|
+
const candidates = [
|
|
485
|
+
join(import.meta.dir, "db", "inspector.js"),
|
|
486
|
+
join(import.meta.dir, "inspector.js")
|
|
487
|
+
];
|
|
488
|
+
for (const p of candidates) {
|
|
489
|
+
if (existsSync(p)) {
|
|
490
|
+
return new Response(readFileSync(p, "utf-8"), {
|
|
491
|
+
headers: { "Content-Type": "application/javascript; charset=utf-8" }
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return new Response("/* Trellis DB Inspector: run `bun run build:inspector` first */", {
|
|
496
|
+
headers: { "Content-Type": "application/javascript" }
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
if (method === "GET" && path === "/__trellis/trellis.css") {
|
|
500
|
+
const candidates = [
|
|
501
|
+
join(import.meta.dir, "db", "trellis.css"),
|
|
502
|
+
join(import.meta.dir, "trellis.css")
|
|
503
|
+
];
|
|
504
|
+
for (const p of candidates) {
|
|
505
|
+
if (existsSync(p)) {
|
|
506
|
+
return new Response(readFileSync(p, "utf-8"), {
|
|
507
|
+
headers: { "Content-Type": "text/css; charset=utf-8" }
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return new Response("/* Trellis CSS not found */", {
|
|
512
|
+
headers: { "Content-Type": "text/css" }
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
if (method === "GET" && (path === "/" || path === "/inspector")) {
|
|
516
|
+
const html = `<!DOCTYPE html>
|
|
517
|
+
<html lang="en">
|
|
518
|
+
<head>
|
|
519
|
+
<meta charset="UTF-8">
|
|
520
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
521
|
+
<title>Trellis DB Inspector</title>
|
|
522
|
+
<link rel="stylesheet" href="/__trellis/trellis.css">
|
|
523
|
+
</head>
|
|
524
|
+
<body>
|
|
525
|
+
<script src="/__trellis/inspector.js"></script>
|
|
526
|
+
</body>
|
|
527
|
+
</html>`;
|
|
528
|
+
return new Response(html, {
|
|
529
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
if (method === "GET" && path === "/health") {
|
|
533
|
+
const kernel = ctx.pool.get(tenantId);
|
|
534
|
+
const ops = kernel.readAllOps().length;
|
|
535
|
+
return json({
|
|
536
|
+
status: "ok",
|
|
537
|
+
ops,
|
|
538
|
+
tenants: ctx.pool.activeTenants().length
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
if (path === "/entities" || path === "/entities/") {
|
|
542
|
+
if (method === "POST")
|
|
543
|
+
return handleCreate(req, auth, tenantId, ctx);
|
|
544
|
+
if (method === "GET")
|
|
545
|
+
return handleList(url, auth, tenantId, ctx);
|
|
546
|
+
}
|
|
547
|
+
const entityMatch = path.match(/^\/entities\/([^/]+)$/);
|
|
548
|
+
if (entityMatch) {
|
|
549
|
+
const id = decodeURIComponent(entityMatch[1]);
|
|
550
|
+
if (method === "GET")
|
|
551
|
+
return handleRead(id, auth, tenantId, ctx);
|
|
552
|
+
if (method === "PUT" || method === "PATCH")
|
|
553
|
+
return handleUpdate(req, id, auth, tenantId, ctx);
|
|
554
|
+
if (method === "DELETE")
|
|
555
|
+
return handleDelete(id, auth, tenantId, ctx);
|
|
556
|
+
}
|
|
557
|
+
if (method === "POST" && path === "/query") {
|
|
558
|
+
return handleQuery(req, auth, tenantId, ctx);
|
|
559
|
+
}
|
|
560
|
+
if (method === "POST" && path === "/upload") {
|
|
561
|
+
return handleUpload(req, auth, tenantId, ctx);
|
|
562
|
+
}
|
|
563
|
+
const fileMatch = path.match(/^\/files\/([^/]+)$/);
|
|
564
|
+
if (method === "GET" && fileMatch) {
|
|
565
|
+
return handleFileDownload(fileMatch[1], ctx, tenantId);
|
|
566
|
+
}
|
|
567
|
+
if (method === "POST" && path === "/auth/register") {
|
|
568
|
+
return handleRegister(req, tenantId, ctx);
|
|
569
|
+
}
|
|
570
|
+
if (method === "POST" && path === "/auth/login") {
|
|
571
|
+
return handleLogin(req, tenantId, ctx);
|
|
572
|
+
}
|
|
573
|
+
const oauthMatch = path.match(/^\/auth\/oauth\/([^/]+)(\/callback)?$/);
|
|
574
|
+
if (oauthMatch) {
|
|
575
|
+
const providerName = oauthMatch[1];
|
|
576
|
+
const isCallback = !!oauthMatch[2];
|
|
577
|
+
if (isCallback)
|
|
578
|
+
return handleOAuthCallback(url, providerName, tenantId, ctx);
|
|
579
|
+
return handleOAuthRedirect(providerName, url, ctx);
|
|
580
|
+
}
|
|
581
|
+
return json({ error: "Not Found" }, 404);
|
|
582
|
+
}
|
|
583
|
+
async function handleCreate(req, auth, tenantId, ctx) {
|
|
584
|
+
const body = await req.json();
|
|
585
|
+
if (!body.type)
|
|
586
|
+
return json({ error: "type is required" }, 400);
|
|
587
|
+
ctx.permissions?.assert(auth, body.type, "create");
|
|
588
|
+
const kernel = ctx.pool.get(tenantId);
|
|
589
|
+
const entityId2 = `${body.type.toLowerCase()}:${crypto.randomUUID()}`;
|
|
590
|
+
const attrs = {
|
|
591
|
+
...body.attributes
|
|
592
|
+
};
|
|
593
|
+
if (auth.userId)
|
|
594
|
+
attrs.createdBy = auth.userId;
|
|
595
|
+
if (auth.tenantId)
|
|
596
|
+
attrs.tenantId = auth.tenantId;
|
|
597
|
+
const result = await kernel.createEntity(entityId2, body.type, attrs, body.links);
|
|
598
|
+
await ctx.subs.notify(tenantId);
|
|
599
|
+
return json({ id: entityId2, op: result.op.hash }, 201);
|
|
600
|
+
}
|
|
601
|
+
async function handleRead(id, auth, tenantId, ctx) {
|
|
602
|
+
const kernel = ctx.pool.get(tenantId);
|
|
603
|
+
const entity = kernel.getEntity(id);
|
|
604
|
+
if (!entity)
|
|
605
|
+
return json({ error: "Not Found" }, 404);
|
|
606
|
+
ctx.permissions?.assert(auth, entity.type, "read", entity);
|
|
607
|
+
return json(entityToJson(entity));
|
|
608
|
+
}
|
|
609
|
+
async function handleUpdate(req, id, auth, tenantId, ctx) {
|
|
610
|
+
const kernel = ctx.pool.get(tenantId);
|
|
611
|
+
const entity = kernel.getEntity(id);
|
|
612
|
+
if (!entity)
|
|
613
|
+
return json({ error: "Not Found" }, 404);
|
|
614
|
+
ctx.permissions?.assert(auth, entity.type, "update", entity);
|
|
615
|
+
const updates = await req.json();
|
|
616
|
+
await kernel.updateEntity(id, updates);
|
|
617
|
+
await ctx.subs.notify(tenantId);
|
|
618
|
+
return json({ id, updated: true });
|
|
619
|
+
}
|
|
620
|
+
async function handleDelete(id, auth, tenantId, ctx) {
|
|
621
|
+
const kernel = ctx.pool.get(tenantId);
|
|
622
|
+
const entity = kernel.getEntity(id);
|
|
623
|
+
if (!entity)
|
|
624
|
+
return json({ error: "Not Found" }, 404);
|
|
625
|
+
ctx.permissions?.assert(auth, entity.type, "delete", entity);
|
|
626
|
+
await kernel.deleteEntity(id);
|
|
627
|
+
await ctx.subs.notify(tenantId);
|
|
628
|
+
return json({ id, deleted: true });
|
|
629
|
+
}
|
|
630
|
+
async function handleList(url, auth, tenantId, ctx) {
|
|
631
|
+
const type = url.searchParams.get("type") ?? undefined;
|
|
632
|
+
const limit = parseInt(url.searchParams.get("limit") ?? "100");
|
|
633
|
+
const offset = parseInt(url.searchParams.get("offset") ?? "0");
|
|
634
|
+
const kernel = ctx.pool.get(tenantId);
|
|
635
|
+
let entities = kernel.listEntities(type);
|
|
636
|
+
if (ctx.permissions && type) {
|
|
637
|
+
entities = entities.filter((e) => ctx.permissions.check(auth, e.type, "read", e));
|
|
638
|
+
}
|
|
639
|
+
const page = entities.slice(offset, offset + limit);
|
|
640
|
+
return json({
|
|
641
|
+
data: page.map(entityToJson),
|
|
642
|
+
total: entities.length,
|
|
643
|
+
limit,
|
|
644
|
+
offset
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
async function handleQuery(req, auth, tenantId, ctx) {
|
|
648
|
+
const body = await req.json();
|
|
649
|
+
if (!body.query)
|
|
650
|
+
return json({ error: "query is required" }, 400);
|
|
651
|
+
let parsed;
|
|
652
|
+
try {
|
|
653
|
+
parsed = parseSimple(body.query);
|
|
654
|
+
} catch (err) {
|
|
655
|
+
return json({ error: "Invalid query", message: String(err) }, 400);
|
|
656
|
+
}
|
|
657
|
+
const kernel = ctx.pool.get(tenantId);
|
|
658
|
+
const engine = new QueryEngine(kernel.getStore());
|
|
659
|
+
const result = engine.execute(parsed);
|
|
660
|
+
return json({
|
|
661
|
+
bindings: result.bindings,
|
|
662
|
+
executionTime: result.executionTime
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
async function handleUpload(req, auth, tenantId, ctx) {
|
|
666
|
+
if (!auth.authenticated && ctx.config.apiKey) {
|
|
667
|
+
return json({ error: "Unauthorized" }, 401);
|
|
668
|
+
}
|
|
669
|
+
const contentType = req.headers.get("content-type") ?? "application/octet-stream";
|
|
670
|
+
const buffer = new Uint8Array(await req.arrayBuffer());
|
|
671
|
+
const hashBuf = await crypto.subtle.digest("SHA-256", buffer);
|
|
672
|
+
const hash = `blob:${Array.from(new Uint8Array(hashBuf)).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
673
|
+
const kernel = ctx.pool.get(tenantId);
|
|
674
|
+
const backend = kernel.getBackend();
|
|
675
|
+
if (!backend.hasBlob(hash)) {
|
|
676
|
+
backend.putBlob(hash, buffer);
|
|
677
|
+
}
|
|
678
|
+
return json({ hash, size: buffer.length, contentType }, 201);
|
|
679
|
+
}
|
|
680
|
+
async function handleFileDownload(hash, ctx, tenantId) {
|
|
681
|
+
const kernel = ctx.pool.get(tenantId);
|
|
682
|
+
const backend = kernel.getBackend();
|
|
683
|
+
const blob = backend.getBlob(hash);
|
|
684
|
+
if (!blob)
|
|
685
|
+
return json({ error: "Not Found" }, 404);
|
|
686
|
+
const cleanBuf = blob.buffer.slice(blob.byteOffset, blob.byteOffset + blob.byteLength);
|
|
687
|
+
return new Response(cleanBuf, {
|
|
688
|
+
headers: { "Content-Type": "application/octet-stream" }
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
async function handleRegister(req, tenantId, ctx) {
|
|
692
|
+
if (!ctx.config.jwtSecret) {
|
|
693
|
+
return json({ error: "Auth not configured (no jwtSecret)" }, 501);
|
|
694
|
+
}
|
|
695
|
+
const body = await req.json();
|
|
696
|
+
if (!body.email || !body.password) {
|
|
697
|
+
return json({ error: "email and password are required" }, 400);
|
|
698
|
+
}
|
|
699
|
+
const kernel = ctx.pool.get(tenantId);
|
|
700
|
+
const existing = kernel.listEntities("User", { email: body.email });
|
|
701
|
+
if (existing.length > 0) {
|
|
702
|
+
return json({ error: "Email already registered" }, 409);
|
|
703
|
+
}
|
|
704
|
+
const userId = `user:${crypto.randomUUID()}`;
|
|
705
|
+
const pwHash = await hashPassword(body.password);
|
|
706
|
+
await kernel.createEntity(userId, "User", {
|
|
707
|
+
email: body.email,
|
|
708
|
+
name: body.name ?? "",
|
|
709
|
+
passwordHash: pwHash,
|
|
710
|
+
role: "user",
|
|
711
|
+
...tenantId ? { tenantId } : {}
|
|
712
|
+
});
|
|
713
|
+
const token = await signJwt({ sub: userId, email: body.email, roles: ["user"], tenantId }, ctx.config.jwtSecret);
|
|
714
|
+
return json({ token, userId }, 201);
|
|
715
|
+
}
|
|
716
|
+
async function handleLogin(req, tenantId, ctx) {
|
|
717
|
+
if (!ctx.config.jwtSecret) {
|
|
718
|
+
return json({ error: "Auth not configured (no jwtSecret)" }, 501);
|
|
719
|
+
}
|
|
720
|
+
const body = await req.json();
|
|
721
|
+
if (!body.email || !body.password) {
|
|
722
|
+
return json({ error: "email and password are required" }, 400);
|
|
723
|
+
}
|
|
724
|
+
const kernel = ctx.pool.get(tenantId);
|
|
725
|
+
const users = kernel.listEntities("User", { email: body.email });
|
|
726
|
+
if (users.length === 0) {
|
|
727
|
+
return json({ error: "Invalid credentials" }, 401);
|
|
728
|
+
}
|
|
729
|
+
const user = users[0];
|
|
730
|
+
const pwHashFact = user.facts.find((f) => f.a === "passwordHash");
|
|
731
|
+
const roleFact = user.facts.find((f) => f.a === "role");
|
|
732
|
+
if (!pwHashFact || !await verifyPassword(body.password, String(pwHashFact.v))) {
|
|
733
|
+
return json({ error: "Invalid credentials" }, 401);
|
|
734
|
+
}
|
|
735
|
+
const role = String(roleFact?.v ?? "user");
|
|
736
|
+
const token = await signJwt({ sub: user.id, email: body.email, roles: [role], tenantId }, ctx.config.jwtSecret);
|
|
737
|
+
return json({ token, userId: user.id });
|
|
738
|
+
}
|
|
739
|
+
function handleOAuthRedirect(providerName, url, ctx) {
|
|
740
|
+
const provider = ctx.oauthProviders[providerName] ?? getBuiltinProvider(providerName, ctx);
|
|
741
|
+
if (!provider)
|
|
742
|
+
return json({ error: `Unknown provider: ${providerName}` }, 400);
|
|
743
|
+
const redirectUri = `${url.origin}/auth/oauth/${providerName}/callback`;
|
|
744
|
+
const state = crypto.randomUUID();
|
|
745
|
+
const authUrl = buildOAuthUrl(provider, redirectUri, state);
|
|
746
|
+
return Response.redirect(authUrl, 302);
|
|
747
|
+
}
|
|
748
|
+
async function handleOAuthCallback(url, providerName, tenantId, ctx) {
|
|
749
|
+
if (!ctx.config.jwtSecret) {
|
|
750
|
+
return json({ error: "Auth not configured (no jwtSecret)" }, 501);
|
|
751
|
+
}
|
|
752
|
+
const code = url.searchParams.get("code");
|
|
753
|
+
if (!code)
|
|
754
|
+
return json({ error: "Missing code" }, 400);
|
|
755
|
+
const provider = ctx.oauthProviders[providerName] ?? getBuiltinProvider(providerName, ctx);
|
|
756
|
+
if (!provider)
|
|
757
|
+
return json({ error: `Unknown provider: ${providerName}` }, 400);
|
|
758
|
+
const redirectUri = `${url.origin}/auth/oauth/${providerName}/callback`;
|
|
759
|
+
let profile;
|
|
760
|
+
try {
|
|
761
|
+
profile = await exchangeOAuthCode(provider, code, redirectUri);
|
|
762
|
+
} catch (err) {
|
|
763
|
+
return json({ error: "OAuth exchange failed", message: String(err) }, 400);
|
|
764
|
+
}
|
|
765
|
+
const kernel = ctx.pool.get(tenantId);
|
|
766
|
+
const oauthId = `oauth:${providerName}:${profile.id}`;
|
|
767
|
+
let users = kernel.listEntities("User", { oauthId });
|
|
768
|
+
let userId;
|
|
769
|
+
if (users.length === 0) {
|
|
770
|
+
userId = `user:${crypto.randomUUID()}`;
|
|
771
|
+
await kernel.createEntity(userId, "User", {
|
|
772
|
+
email: profile.email,
|
|
773
|
+
name: profile.name,
|
|
774
|
+
avatarUrl: profile.avatarUrl ?? "",
|
|
775
|
+
oauthId,
|
|
776
|
+
oauthProvider: providerName,
|
|
777
|
+
role: "user",
|
|
778
|
+
...tenantId ? { tenantId } : {}
|
|
779
|
+
});
|
|
780
|
+
} else {
|
|
781
|
+
userId = users[0].id;
|
|
782
|
+
}
|
|
783
|
+
const token = await signJwt({ sub: userId, email: profile.email, roles: ["user"], tenantId }, ctx.config.jwtSecret);
|
|
784
|
+
return json({ token, userId });
|
|
785
|
+
}
|
|
786
|
+
function getBuiltinProvider(name, ctx) {
|
|
787
|
+
if (name === "google") {
|
|
788
|
+
const cid = process.env.GOOGLE_CLIENT_ID;
|
|
789
|
+
const csec = process.env.GOOGLE_CLIENT_SECRET;
|
|
790
|
+
if (!cid || !csec)
|
|
791
|
+
return null;
|
|
792
|
+
return { ...GOOGLE_PROVIDER, clientId: cid, clientSecret: csec };
|
|
793
|
+
}
|
|
794
|
+
if (name === "github") {
|
|
795
|
+
const cid = process.env.GITHUB_CLIENT_ID;
|
|
796
|
+
const csec = process.env.GITHUB_CLIENT_SECRET;
|
|
797
|
+
if (!cid || !csec)
|
|
798
|
+
return null;
|
|
799
|
+
return { ...GITHUB_PROVIDER, clientId: cid, clientSecret: csec };
|
|
800
|
+
}
|
|
801
|
+
return null;
|
|
802
|
+
}
|
|
803
|
+
async function hashPassword(password) {
|
|
804
|
+
const salt = crypto.randomUUID().replace(/-/g, "");
|
|
805
|
+
const enc = new TextEncoder;
|
|
806
|
+
const keyMat = await crypto.subtle.importKey("raw", enc.encode(password), "PBKDF2", false, ["deriveBits"]);
|
|
807
|
+
const bits = await crypto.subtle.deriveBits({
|
|
808
|
+
name: "PBKDF2",
|
|
809
|
+
salt: enc.encode(salt),
|
|
810
|
+
iterations: 1e5,
|
|
811
|
+
hash: "SHA-256"
|
|
812
|
+
}, keyMat, 256);
|
|
813
|
+
const hash = Array.from(new Uint8Array(bits)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
814
|
+
return `${salt}:${hash}`;
|
|
815
|
+
}
|
|
816
|
+
async function verifyPassword(password, stored) {
|
|
817
|
+
const [salt, expectedHash] = stored.split(":");
|
|
818
|
+
if (!salt || !expectedHash)
|
|
819
|
+
return false;
|
|
820
|
+
const enc = new TextEncoder;
|
|
821
|
+
const keyMat = await crypto.subtle.importKey("raw", enc.encode(password), "PBKDF2", false, ["deriveBits"]);
|
|
822
|
+
const bits = await crypto.subtle.deriveBits({
|
|
823
|
+
name: "PBKDF2",
|
|
824
|
+
salt: enc.encode(salt),
|
|
825
|
+
iterations: 1e5,
|
|
826
|
+
hash: "SHA-256"
|
|
827
|
+
}, keyMat, 256);
|
|
828
|
+
const hash = Array.from(new Uint8Array(bits)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
829
|
+
return hash === expectedHash;
|
|
830
|
+
}
|
|
831
|
+
function entityToJson(entity) {
|
|
832
|
+
const attrs = {};
|
|
833
|
+
for (const f of entity.facts) {
|
|
834
|
+
if (f.a !== "type") {
|
|
835
|
+
attrs[f.a] = f.v;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return { id: entity.id, type: entity.type, ...attrs };
|
|
839
|
+
}
|
|
840
|
+
function json(data, status = 200) {
|
|
841
|
+
return new Response(JSON.stringify(data), {
|
|
842
|
+
status,
|
|
843
|
+
headers: { "Content-Type": "application/json" }
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
export { ANONYMOUS, signJwt, verifyJwt, resolveAuth, GOOGLE_PROVIDER, GITHUB_PROVIDER, buildOAuthUrl, exchangeOAuthCode, PermissionRegistry, PermissionError, PUBLIC_READ, FULLY_PUBLIC, OWNER_ONLY, ADMIN_ONLY, SubscriptionManager, startServer };
|