@chatroomcp/chatroom 0.1.3
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/LICENSE +202 -0
- package/README.md +50 -0
- package/assets/chatroom.svg +15 -0
- package/chatroom.config.example.json +24 -0
- package/dist/app/application.d.ts +18 -0
- package/dist/app/application.js +63 -0
- package/dist/app/event-bus.d.ts +23 -0
- package/dist/app/event-bus.js +12 -0
- package/dist/app/external-access-registry.d.ts +18 -0
- package/dist/app/external-access-registry.js +48 -0
- package/dist/app/lifecycle.d.ts +12 -0
- package/dist/app/lifecycle.js +41 -0
- package/dist/auth/auth-service.d.ts +88 -0
- package/dist/auth/auth-service.js +222 -0
- package/dist/auth/ingress-policy.d.ts +21 -0
- package/dist/auth/ingress-policy.js +57 -0
- package/dist/auth/passkey-service.d.ts +39 -0
- package/dist/auth/passkey-service.js +145 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +10 -0
- package/dist/cli/run-cli.d.ts +1 -0
- package/dist/cli/run-cli.js +42 -0
- package/dist/config/load-config.d.ts +7 -0
- package/dist/config/load-config.js +182 -0
- package/dist/config/platform-paths.d.ts +6 -0
- package/dist/config/platform-paths.js +34 -0
- package/dist/config/types.d.ts +25 -0
- package/dist/config/types.js +1 -0
- package/dist/core/auth/passkey-repository.d.ts +18 -0
- package/dist/core/auth/passkey-repository.js +1 -0
- package/dist/core/auth/repository.d.ts +59 -0
- package/dist/core/auth/repository.js +7 -0
- package/dist/core/auth/session-repository.d.ts +6 -0
- package/dist/core/auth/session-repository.js +1 -0
- package/dist/core/errors/chatroom-error.d.ts +23 -0
- package/dist/core/errors/chatroom-error.js +46 -0
- package/dist/core/operations/bounded-value.d.ts +6 -0
- package/dist/core/operations/bounded-value.js +41 -0
- package/dist/core/operations/redactor.d.ts +4 -0
- package/dist/core/operations/redactor.js +65 -0
- package/dist/core/operations/repository.d.ts +19 -0
- package/dist/core/operations/repository.js +1 -0
- package/dist/core/operations/types.d.ts +31 -0
- package/dist/core/operations/types.js +9 -0
- package/dist/core/runtime/child-environment.d.ts +1 -0
- package/dist/core/runtime/child-environment.js +39 -0
- package/dist/core/runtime/command-runner.d.ts +15 -0
- package/dist/core/runtime/command-runner.js +39 -0
- package/dist/core/runtime/identity.d.ts +2 -0
- package/dist/core/runtime/identity.js +9 -0
- package/dist/infrastructure/database/app-database.d.ts +6 -0
- package/dist/infrastructure/database/app-database.js +104 -0
- package/dist/infrastructure/database/oauth-repository.d.ts +20 -0
- package/dist/infrastructure/database/oauth-repository.js +140 -0
- package/dist/infrastructure/database/operation-repository.d.ts +16 -0
- package/dist/infrastructure/database/operation-repository.js +129 -0
- package/dist/infrastructure/database/passkey-repository.d.ts +11 -0
- package/dist/infrastructure/database/passkey-repository.js +57 -0
- package/dist/infrastructure/database/web-session-repository.d.ts +10 -0
- package/dist/infrastructure/database/web-session-repository.js +26 -0
- package/dist/infrastructure/http/http-server.d.ts +26 -0
- package/dist/infrastructure/http/http-server.js +166 -0
- package/dist/mcp/server/create-mcp-server.d.ts +3 -0
- package/dist/mcp/server/create-mcp-server.js +16 -0
- package/dist/mcp/server/tool-support.d.ts +105 -0
- package/dist/mcp/server/tool-support.js +124 -0
- package/dist/operations/operation-log.d.ts +23 -0
- package/dist/operations/operation-log.js +103 -0
- package/dist/plugins/cloud/api-client.d.ts +56 -0
- package/dist/plugins/cloud/api-client.js +122 -0
- package/dist/plugins/cloud/controller.d.ts +48 -0
- package/dist/plugins/cloud/controller.js +282 -0
- package/dist/plugins/cloud/device-proof.d.ts +9 -0
- package/dist/plugins/cloud/device-proof.js +17 -0
- package/dist/plugins/cloud/plugin.d.ts +4 -0
- package/dist/plugins/cloud/plugin.js +17 -0
- package/dist/plugins/cloud/state-store.d.ts +7 -0
- package/dist/plugins/cloud/state-store.js +74 -0
- package/dist/plugins/cloud/tunnel-client.d.ts +36 -0
- package/dist/plugins/cloud/tunnel-client.js +293 -0
- package/dist/plugins/cloud/types.d.ts +70 -0
- package/dist/plugins/cloud/types.js +32 -0
- package/dist/plugins/plugin-manager.d.ts +11 -0
- package/dist/plugins/plugin-manager.js +24 -0
- package/dist/plugins/process/backend.d.ts +19 -0
- package/dist/plugins/process/backend.js +1 -0
- package/dist/plugins/process/head-tail-buffer.d.ts +18 -0
- package/dist/plugins/process/head-tail-buffer.js +49 -0
- package/dist/plugins/process/infrastructure/pipe-process-backend.d.ts +8 -0
- package/dist/plugins/process/infrastructure/pipe-process-backend.js +61 -0
- package/dist/plugins/process/infrastructure/pty-process-backend.d.ts +8 -0
- package/dist/plugins/process/infrastructure/pty-process-backend.js +50 -0
- package/dist/plugins/process/mcp.d.ts +4 -0
- package/dist/plugins/process/mcp.js +69 -0
- package/dist/plugins/process/plugin.d.ts +4 -0
- package/dist/plugins/process/plugin.js +28 -0
- package/dist/plugins/process/process-supervisor.d.ts +36 -0
- package/dist/plugins/process/process-supervisor.js +232 -0
- package/dist/plugins/process/types.d.ts +34 -0
- package/dist/plugins/process/types.js +1 -0
- package/dist/plugins/types.d.ts +32 -0
- package/dist/plugins/types.js +17 -0
- package/dist/plugins/web/api-types.d.ts +10 -0
- package/dist/plugins/web/api-types.js +1 -0
- package/dist/plugins/web/http/api-router.d.ts +13 -0
- package/dist/plugins/web/http/api-router.js +146 -0
- package/dist/plugins/web/http/cloud-api-router.d.ts +4 -0
- package/dist/plugins/web/http/cloud-api-router.js +41 -0
- package/dist/plugins/web/http/process-api-router.d.ts +3 -0
- package/dist/plugins/web/http/process-api-router.js +16 -0
- package/dist/plugins/web/http/workspace-api-router.d.ts +3 -0
- package/dist/plugins/web/http/workspace-api-router.js +82 -0
- package/dist/plugins/web/plugin.d.ts +8 -0
- package/dist/plugins/web/plugin.js +17 -0
- package/dist/plugins/web/runtime.d.ts +36 -0
- package/dist/plugins/web/runtime.js +66 -0
- package/dist/plugins/workspace/domain/filesystem.d.ts +25 -0
- package/dist/plugins/workspace/domain/filesystem.js +1 -0
- package/dist/plugins/workspace/domain/git.d.ts +7 -0
- package/dist/plugins/workspace/domain/git.js +1 -0
- package/dist/plugins/workspace/domain/repository.d.ts +16 -0
- package/dist/plugins/workspace/domain/repository.js +1 -0
- package/dist/plugins/workspace/domain/review.d.ts +28 -0
- package/dist/plugins/workspace/domain/review.js +1 -0
- package/dist/plugins/workspace/domain/workspace.d.ts +23 -0
- package/dist/plugins/workspace/domain/workspace.js +1 -0
- package/dist/plugins/workspace/git/git-command-runner.d.ts +6 -0
- package/dist/plugins/workspace/git/git-command-runner.js +15 -0
- package/dist/plugins/workspace/git/git-service.d.ts +14 -0
- package/dist/plugins/workspace/git/git-service.js +107 -0
- package/dist/plugins/workspace/git/git-snapshot-service.d.ts +33 -0
- package/dist/plugins/workspace/git/git-snapshot-service.js +352 -0
- package/dist/plugins/workspace/infrastructure/workspace-fs.d.ts +38 -0
- package/dist/plugins/workspace/infrastructure/workspace-fs.js +340 -0
- package/dist/plugins/workspace/infrastructure/workspace-repository.d.ts +12 -0
- package/dist/plugins/workspace/infrastructure/workspace-repository.js +51 -0
- package/dist/plugins/workspace/mcp.d.ts +4 -0
- package/dist/plugins/workspace/mcp.js +168 -0
- package/dist/plugins/workspace/plugin.d.ts +10 -0
- package/dist/plugins/workspace/plugin.js +37 -0
- package/dist/plugins/workspace/workspace-helpers.d.ts +9 -0
- package/dist/plugins/workspace/workspace-helpers.js +67 -0
- package/dist/plugins/workspace/workspace-service.d.ts +46 -0
- package/dist/plugins/workspace/workspace-service.js +339 -0
- package/dist/plugins/workspace/worktree-review-service.d.ts +16 -0
- package/dist/plugins/workspace/worktree-review-service.js +51 -0
- package/dist/presentation/http/http-utils.d.ts +7 -0
- package/dist/presentation/http/http-utils.js +47 -0
- package/dist/presentation/http/oauth-router.d.ts +5 -0
- package/dist/presentation/http/oauth-router.js +163 -0
- package/dist/web/assets/chatroom-B5p-5n3y.svg +15 -0
- package/dist/web/assets/index-BF_mWl1D.js +26 -0
- package/dist/web/assets/index-JPZut9tc.css +1 -0
- package/dist/web/assets/materialdesignicons-webfont-B7mPwVP_.ttf +0 -0
- package/dist/web/assets/materialdesignicons-webfont-CSr8KVlo.eot +0 -0
- package/dist/web/assets/materialdesignicons-webfont-Dp5v-WZN.woff2 +0 -0
- package/dist/web/assets/materialdesignicons-webfont-PXm3-2wK.woff +0 -0
- package/dist/web/index.html +19 -0
- package/package.json +77 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { ChatRoomError } from "../errors/chatroom-error.js";
|
|
3
|
+
import { childEnvironment } from "./child-environment.js";
|
|
4
|
+
export class CommandRunner {
|
|
5
|
+
run(request) {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const child = spawn(request.command, request.args ?? [], {
|
|
8
|
+
cwd: request.cwd,
|
|
9
|
+
env: childEnvironment(request.env),
|
|
10
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
11
|
+
});
|
|
12
|
+
let stdout = "";
|
|
13
|
+
let stderr = "";
|
|
14
|
+
const timeout = request.timeoutMs
|
|
15
|
+
? setTimeout(() => child.kill("SIGKILL"), request.timeoutMs)
|
|
16
|
+
: null;
|
|
17
|
+
timeout?.unref();
|
|
18
|
+
child.stdout.setEncoding("utf8");
|
|
19
|
+
child.stderr.setEncoding("utf8");
|
|
20
|
+
child.stdout.on("data", (chunk) => {
|
|
21
|
+
stdout += chunk;
|
|
22
|
+
});
|
|
23
|
+
child.stderr.on("data", (chunk) => {
|
|
24
|
+
stderr += chunk;
|
|
25
|
+
});
|
|
26
|
+
child.once("error", reject);
|
|
27
|
+
child.once("close", (code) => {
|
|
28
|
+
if (timeout)
|
|
29
|
+
clearTimeout(timeout);
|
|
30
|
+
const exitCode = code ?? -1;
|
|
31
|
+
if (exitCode !== 0) {
|
|
32
|
+
reject(new ChatRoomError("PROCESS_FAILED", `${request.command} exited with code ${exitCode}`, { stdout, stderr, exitCode }));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
resolve({ stdout, stderr, exitCode });
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
export const CHATROOM_NAME = "ChatRoom";
|
|
4
|
+
const packageJsonPath = fileURLToPath(new URL("../../../package.json", import.meta.url));
|
|
5
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
6
|
+
if (typeof packageJson.version !== "string" || !packageJson.version) {
|
|
7
|
+
throw new Error("ChatRoom package version is missing");
|
|
8
|
+
}
|
|
9
|
+
export const CHATROOM_VERSION = packageJson.version;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { DatabaseSync } from "node:sqlite";
|
|
4
|
+
const SCHEMA = `
|
|
5
|
+
CREATE TABLE IF NOT EXISTS workspaces (
|
|
6
|
+
id TEXT PRIMARY KEY,
|
|
7
|
+
root TEXT NOT NULL UNIQUE,
|
|
8
|
+
source_root TEXT NOT NULL,
|
|
9
|
+
mode TEXT NOT NULL CHECK(mode IN ('checkout','worktree')),
|
|
10
|
+
created_at TEXT NOT NULL,
|
|
11
|
+
last_used_at TEXT NOT NULL
|
|
12
|
+
);
|
|
13
|
+
CREATE INDEX IF NOT EXISTS workspaces_last_used_idx ON workspaces(last_used_at DESC);
|
|
14
|
+
|
|
15
|
+
CREATE TABLE IF NOT EXISTS operations (
|
|
16
|
+
operation_id TEXT PRIMARY KEY,
|
|
17
|
+
plugin_id TEXT NOT NULL,
|
|
18
|
+
source TEXT NOT NULL,
|
|
19
|
+
action TEXT NOT NULL,
|
|
20
|
+
status TEXT NOT NULL,
|
|
21
|
+
workspace_id TEXT,
|
|
22
|
+
process_id TEXT,
|
|
23
|
+
input_json TEXT NOT NULL,
|
|
24
|
+
output_json TEXT NOT NULL,
|
|
25
|
+
error_json TEXT NOT NULL,
|
|
26
|
+
input_truncated INTEGER NOT NULL DEFAULT 0,
|
|
27
|
+
output_truncated INTEGER NOT NULL DEFAULT 0,
|
|
28
|
+
started_at TEXT NOT NULL,
|
|
29
|
+
finished_at TEXT,
|
|
30
|
+
duration_ms INTEGER
|
|
31
|
+
);
|
|
32
|
+
CREATE INDEX IF NOT EXISTS operations_started_idx ON operations(started_at DESC);
|
|
33
|
+
CREATE INDEX IF NOT EXISTS operations_plugin_idx ON operations(plugin_id, started_at DESC);
|
|
34
|
+
CREATE INDEX IF NOT EXISTS operations_workspace_idx ON operations(workspace_id, started_at DESC);
|
|
35
|
+
CREATE INDEX IF NOT EXISTS operations_status_idx ON operations(status, started_at DESC);
|
|
36
|
+
|
|
37
|
+
CREATE TABLE IF NOT EXISTS oauth_clients (
|
|
38
|
+
client_id TEXT PRIMARY KEY,
|
|
39
|
+
name TEXT NOT NULL,
|
|
40
|
+
redirect_uris_json TEXT NOT NULL,
|
|
41
|
+
created_at TEXT NOT NULL
|
|
42
|
+
);
|
|
43
|
+
CREATE TABLE IF NOT EXISTS oauth_codes (
|
|
44
|
+
code_hash TEXT PRIMARY KEY,
|
|
45
|
+
client_id TEXT NOT NULL REFERENCES oauth_clients(client_id) ON DELETE CASCADE,
|
|
46
|
+
redirect_uri TEXT NOT NULL,
|
|
47
|
+
code_challenge TEXT NOT NULL,
|
|
48
|
+
scope TEXT NOT NULL,
|
|
49
|
+
expires_at TEXT NOT NULL,
|
|
50
|
+
used_at TEXT
|
|
51
|
+
);
|
|
52
|
+
CREATE INDEX IF NOT EXISTS oauth_code_expiry_idx ON oauth_codes(expires_at);
|
|
53
|
+
CREATE TABLE IF NOT EXISTS oauth_tokens (
|
|
54
|
+
token_hash TEXT PRIMARY KEY,
|
|
55
|
+
client_id TEXT NOT NULL REFERENCES oauth_clients(client_id) ON DELETE CASCADE,
|
|
56
|
+
scope TEXT NOT NULL,
|
|
57
|
+
expires_at TEXT NOT NULL,
|
|
58
|
+
revoked_at TEXT,
|
|
59
|
+
created_at TEXT NOT NULL
|
|
60
|
+
);
|
|
61
|
+
CREATE INDEX IF NOT EXISTS oauth_token_expiry_idx ON oauth_tokens(expires_at);
|
|
62
|
+
CREATE TABLE IF NOT EXISTS oauth_refresh_tokens (
|
|
63
|
+
token_hash TEXT PRIMARY KEY,
|
|
64
|
+
client_id TEXT NOT NULL REFERENCES oauth_clients(client_id) ON DELETE CASCADE,
|
|
65
|
+
scope TEXT NOT NULL,
|
|
66
|
+
expires_at TEXT NOT NULL,
|
|
67
|
+
revoked_at TEXT,
|
|
68
|
+
created_at TEXT NOT NULL
|
|
69
|
+
);
|
|
70
|
+
CREATE INDEX IF NOT EXISTS oauth_refresh_expiry_idx ON oauth_refresh_tokens(expires_at);
|
|
71
|
+
|
|
72
|
+
CREATE TABLE IF NOT EXISTS web_sessions (
|
|
73
|
+
token_hash TEXT PRIMARY KEY,
|
|
74
|
+
expires_at TEXT NOT NULL,
|
|
75
|
+
created_at TEXT NOT NULL
|
|
76
|
+
);
|
|
77
|
+
CREATE INDEX IF NOT EXISTS web_session_expiry_idx ON web_sessions(expires_at);
|
|
78
|
+
|
|
79
|
+
CREATE TABLE IF NOT EXISTS passkeys (
|
|
80
|
+
credential_id TEXT PRIMARY KEY,
|
|
81
|
+
public_key BLOB NOT NULL,
|
|
82
|
+
counter INTEGER NOT NULL,
|
|
83
|
+
transports_json TEXT NOT NULL,
|
|
84
|
+
device_type TEXT NOT NULL CHECK(device_type IN ('singleDevice','multiDevice')),
|
|
85
|
+
backed_up INTEGER NOT NULL,
|
|
86
|
+
name TEXT NOT NULL,
|
|
87
|
+
created_at TEXT NOT NULL,
|
|
88
|
+
last_used_at TEXT NOT NULL
|
|
89
|
+
);
|
|
90
|
+
CREATE INDEX IF NOT EXISTS passkeys_last_used_idx ON passkeys(last_used_at DESC);
|
|
91
|
+
|
|
92
|
+
`;
|
|
93
|
+
export class AppDatabase {
|
|
94
|
+
raw;
|
|
95
|
+
constructor(databasePath) {
|
|
96
|
+
mkdirSync(path.dirname(databasePath), { recursive: true, mode: 0o700 });
|
|
97
|
+
this.raw = new DatabaseSync(databasePath);
|
|
98
|
+
this.raw.exec("PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000; PRAGMA foreign_keys = ON;");
|
|
99
|
+
this.raw.exec(SCHEMA);
|
|
100
|
+
}
|
|
101
|
+
close() {
|
|
102
|
+
this.raw.close();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { OAuthClientRecord, OAuthCodeRecord, OAuthStateRepository, OAuthTokenRecord } from "../../core/auth/repository.js";
|
|
2
|
+
import type { AppDatabase } from "./app-database.js";
|
|
3
|
+
export declare class OAuthRepository implements OAuthStateRepository {
|
|
4
|
+
private readonly database;
|
|
5
|
+
constructor(database: AppDatabase);
|
|
6
|
+
createClient(client: OAuthClientRecord): void;
|
|
7
|
+
getClient(clientId: string): OAuthClientRecord | null;
|
|
8
|
+
createCode(code: OAuthCodeRecord): void;
|
|
9
|
+
getCode(codeHash: string): OAuthCodeRecord | null;
|
|
10
|
+
consumeCode(codeHash: string, now: string): OAuthCodeRecord | null;
|
|
11
|
+
createToken(tokenHash: string, clientId: string, scope: string, expiresAt: string): void;
|
|
12
|
+
createRefreshToken(tokenHash: string, clientId: string, scope: string, expiresAt: string): void;
|
|
13
|
+
consumeRefreshToken(tokenHash: string, clientId?: string): {
|
|
14
|
+
clientId: string;
|
|
15
|
+
scope: string;
|
|
16
|
+
} | null;
|
|
17
|
+
tokenInfo(tokenHash: string): OAuthTokenRecord | null;
|
|
18
|
+
revokeToken(tokenHash: string): void;
|
|
19
|
+
prune(now: string): number;
|
|
20
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
export class OAuthRepository {
|
|
2
|
+
database;
|
|
3
|
+
constructor(database) {
|
|
4
|
+
this.database = database;
|
|
5
|
+
}
|
|
6
|
+
createClient(client) {
|
|
7
|
+
this.database.raw
|
|
8
|
+
.prepare("INSERT INTO oauth_clients(client_id,name,redirect_uris_json,created_at) VALUES(?,?,?,?)")
|
|
9
|
+
.run(client.clientId, client.name, JSON.stringify(client.redirectUris), client.createdAt);
|
|
10
|
+
}
|
|
11
|
+
getClient(clientId) {
|
|
12
|
+
const row = this.database.raw
|
|
13
|
+
.prepare("SELECT * FROM oauth_clients WHERE client_id=?")
|
|
14
|
+
.get(clientId);
|
|
15
|
+
return row ? clientFromRow(row) : null;
|
|
16
|
+
}
|
|
17
|
+
createCode(code) {
|
|
18
|
+
this.database.raw
|
|
19
|
+
.prepare("INSERT INTO oauth_codes(code_hash,client_id,redirect_uri,code_challenge,scope,expires_at,used_at) VALUES(?,?,?,?,?,?,NULL)")
|
|
20
|
+
.run(code.codeHash, code.clientId, code.redirectUri, code.codeChallenge, code.scope, code.expiresAt);
|
|
21
|
+
}
|
|
22
|
+
getCode(codeHash) {
|
|
23
|
+
const row = this.database.raw
|
|
24
|
+
.prepare("SELECT * FROM oauth_codes WHERE code_hash=? AND used_at IS NULL")
|
|
25
|
+
.get(codeHash);
|
|
26
|
+
return row ? codeFromRow(row) : null;
|
|
27
|
+
}
|
|
28
|
+
consumeCode(codeHash, now) {
|
|
29
|
+
this.database.raw.exec("BEGIN IMMEDIATE");
|
|
30
|
+
try {
|
|
31
|
+
const row = this.database.raw
|
|
32
|
+
.prepare("SELECT * FROM oauth_codes WHERE code_hash=? AND used_at IS NULL AND expires_at > ?")
|
|
33
|
+
.get(codeHash, now);
|
|
34
|
+
if (!row) {
|
|
35
|
+
this.database.raw.exec("COMMIT");
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const update = this.database.raw
|
|
39
|
+
.prepare("UPDATE oauth_codes SET used_at=? WHERE code_hash=? AND used_at IS NULL AND expires_at > ?")
|
|
40
|
+
.run(now, codeHash, now);
|
|
41
|
+
if (Number(update.changes) !== 1) {
|
|
42
|
+
this.database.raw.exec("ROLLBACK");
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
this.database.raw.exec("COMMIT");
|
|
46
|
+
return codeFromRow({ ...row, used_at: now });
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
this.database.raw.exec("ROLLBACK");
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
createToken(tokenHash, clientId, scope, expiresAt) {
|
|
54
|
+
this.database.raw
|
|
55
|
+
.prepare("INSERT INTO oauth_tokens(token_hash,client_id,scope,expires_at,revoked_at,created_at) VALUES(?,?,?,?,NULL,?)")
|
|
56
|
+
.run(tokenHash, clientId, scope, expiresAt, new Date().toISOString());
|
|
57
|
+
}
|
|
58
|
+
createRefreshToken(tokenHash, clientId, scope, expiresAt) {
|
|
59
|
+
this.database.raw
|
|
60
|
+
.prepare("INSERT INTO oauth_refresh_tokens(token_hash,client_id,scope,expires_at,revoked_at,created_at) VALUES(?,?,?,?,NULL,?)")
|
|
61
|
+
.run(tokenHash, clientId, scope, expiresAt, new Date().toISOString());
|
|
62
|
+
}
|
|
63
|
+
consumeRefreshToken(tokenHash, clientId) {
|
|
64
|
+
this.database.raw.exec("BEGIN IMMEDIATE");
|
|
65
|
+
try {
|
|
66
|
+
const now = new Date().toISOString();
|
|
67
|
+
const row = this.database.raw
|
|
68
|
+
.prepare(clientId
|
|
69
|
+
? "SELECT client_id,scope FROM oauth_refresh_tokens WHERE token_hash=? AND client_id=? AND revoked_at IS NULL AND expires_at > ?"
|
|
70
|
+
: "SELECT client_id,scope FROM oauth_refresh_tokens WHERE token_hash=? AND revoked_at IS NULL AND expires_at > ?")
|
|
71
|
+
.get(...(clientId ? [tokenHash, clientId, now] : [tokenHash, now]));
|
|
72
|
+
if (!row) {
|
|
73
|
+
this.database.raw.exec("COMMIT");
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const update = this.database.raw
|
|
77
|
+
.prepare("UPDATE oauth_refresh_tokens SET revoked_at=? WHERE token_hash=? AND revoked_at IS NULL")
|
|
78
|
+
.run(now, tokenHash);
|
|
79
|
+
if (Number(update.changes) !== 1) {
|
|
80
|
+
this.database.raw.exec("ROLLBACK");
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
this.database.raw.exec("COMMIT");
|
|
84
|
+
return { clientId: row.client_id, scope: row.scope };
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
this.database.raw.exec("ROLLBACK");
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
tokenInfo(tokenHash) {
|
|
92
|
+
const row = this.database.raw
|
|
93
|
+
.prepare("SELECT client_id,scope,expires_at FROM oauth_tokens WHERE token_hash=? AND revoked_at IS NULL AND expires_at > ?")
|
|
94
|
+
.get(tokenHash, new Date().toISOString());
|
|
95
|
+
return row
|
|
96
|
+
? { clientId: row.client_id, scope: row.scope, expiresAt: row.expires_at }
|
|
97
|
+
: null;
|
|
98
|
+
}
|
|
99
|
+
revokeToken(tokenHash) {
|
|
100
|
+
const now = new Date().toISOString();
|
|
101
|
+
this.database.raw
|
|
102
|
+
.prepare("UPDATE oauth_tokens SET revoked_at=? WHERE token_hash=?")
|
|
103
|
+
.run(now, tokenHash);
|
|
104
|
+
this.database.raw
|
|
105
|
+
.prepare("UPDATE oauth_refresh_tokens SET revoked_at=? WHERE token_hash=?")
|
|
106
|
+
.run(now, tokenHash);
|
|
107
|
+
}
|
|
108
|
+
prune(now) {
|
|
109
|
+
let deleted = 0;
|
|
110
|
+
deleted += Number(this.database.raw
|
|
111
|
+
.prepare("DELETE FROM oauth_codes WHERE expires_at <= ? OR used_at IS NOT NULL")
|
|
112
|
+
.run(now).changes);
|
|
113
|
+
deleted += Number(this.database.raw
|
|
114
|
+
.prepare("DELETE FROM oauth_tokens WHERE expires_at <= ? OR revoked_at IS NOT NULL")
|
|
115
|
+
.run(now).changes);
|
|
116
|
+
deleted += Number(this.database.raw
|
|
117
|
+
.prepare("DELETE FROM oauth_refresh_tokens WHERE expires_at <= ? OR revoked_at IS NOT NULL")
|
|
118
|
+
.run(now).changes);
|
|
119
|
+
return deleted;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function clientFromRow(row) {
|
|
123
|
+
return {
|
|
124
|
+
clientId: row.client_id,
|
|
125
|
+
name: row.name,
|
|
126
|
+
redirectUris: JSON.parse(row.redirect_uris_json),
|
|
127
|
+
createdAt: row.created_at,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function codeFromRow(row) {
|
|
131
|
+
return {
|
|
132
|
+
codeHash: row.code_hash,
|
|
133
|
+
clientId: row.client_id,
|
|
134
|
+
redirectUri: row.redirect_uri,
|
|
135
|
+
codeChallenge: row.code_challenge,
|
|
136
|
+
scope: row.scope,
|
|
137
|
+
expiresAt: row.expires_at,
|
|
138
|
+
usedAt: row.used_at,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Operation } from "../../core/operations/types.js";
|
|
2
|
+
import type { OperationQuery, OperationRepository as OperationRepositoryPort } from "../../core/operations/repository.js";
|
|
3
|
+
import type { AppDatabase } from "./app-database.js";
|
|
4
|
+
export declare class OperationRepository implements OperationRepositoryPort {
|
|
5
|
+
private readonly database;
|
|
6
|
+
constructor(database: AppDatabase);
|
|
7
|
+
insert(operation: Operation): void;
|
|
8
|
+
update(operation: Operation): void;
|
|
9
|
+
get(operationId: string): Operation | null;
|
|
10
|
+
list(query?: OperationQuery): Operation[];
|
|
11
|
+
reconcileRunning(finishedAt: string): number;
|
|
12
|
+
clearHistory(): {
|
|
13
|
+
deleted: number;
|
|
14
|
+
preserved: number;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
export class OperationRepository {
|
|
2
|
+
database;
|
|
3
|
+
constructor(database) {
|
|
4
|
+
this.database = database;
|
|
5
|
+
}
|
|
6
|
+
insert(operation) {
|
|
7
|
+
this.database.raw
|
|
8
|
+
.prepare(`
|
|
9
|
+
INSERT INTO operations(
|
|
10
|
+
operation_id, plugin_id, source, action, status, workspace_id, process_id,
|
|
11
|
+
input_json, output_json, error_json, input_truncated, output_truncated,
|
|
12
|
+
started_at, finished_at, duration_ms
|
|
13
|
+
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
14
|
+
`)
|
|
15
|
+
.run(...toRowValues(operation));
|
|
16
|
+
}
|
|
17
|
+
update(operation) {
|
|
18
|
+
this.database.raw
|
|
19
|
+
.prepare(`
|
|
20
|
+
UPDATE operations SET
|
|
21
|
+
plugin_id=?, source=?, action=?, status=?, workspace_id=?, process_id=?,
|
|
22
|
+
input_json=?, output_json=?, error_json=?, input_truncated=?, output_truncated=?,
|
|
23
|
+
started_at=?, finished_at=?, duration_ms=?
|
|
24
|
+
WHERE operation_id=?
|
|
25
|
+
`)
|
|
26
|
+
.run(...toRowValues(operation).slice(1), operation.operationId);
|
|
27
|
+
}
|
|
28
|
+
get(operationId) {
|
|
29
|
+
const row = this.database.raw
|
|
30
|
+
.prepare("SELECT * FROM operations WHERE operation_id=?")
|
|
31
|
+
.get(operationId);
|
|
32
|
+
return row ? fromRow(row) : null;
|
|
33
|
+
}
|
|
34
|
+
list(query = {}) {
|
|
35
|
+
const where = [];
|
|
36
|
+
const args = [];
|
|
37
|
+
if (query.pluginId) {
|
|
38
|
+
where.push("plugin_id=?");
|
|
39
|
+
args.push(query.pluginId);
|
|
40
|
+
}
|
|
41
|
+
if (query.status) {
|
|
42
|
+
where.push("status=?");
|
|
43
|
+
args.push(query.status);
|
|
44
|
+
}
|
|
45
|
+
if (query.workspaceId) {
|
|
46
|
+
where.push("workspace_id=?");
|
|
47
|
+
args.push(query.workspaceId);
|
|
48
|
+
}
|
|
49
|
+
const limit = Math.min(Math.max(query.limit ?? 100, 1), 500);
|
|
50
|
+
const offset = Math.max(query.offset ?? 0, 0);
|
|
51
|
+
const filter = where.length ? `WHERE ${where.join(" AND ")}` : "";
|
|
52
|
+
return this.database.raw
|
|
53
|
+
.prepare(`SELECT * FROM operations ${filter} ORDER BY started_at DESC LIMIT ? OFFSET ?`)
|
|
54
|
+
.all(...args, limit, offset).map(fromRow);
|
|
55
|
+
}
|
|
56
|
+
reconcileRunning(finishedAt) {
|
|
57
|
+
const result = this.database.raw
|
|
58
|
+
.prepare(`
|
|
59
|
+
UPDATE operations
|
|
60
|
+
SET status='cancelled', finished_at=?,
|
|
61
|
+
duration_ms=MAX(0, CAST((julianday(?) - julianday(started_at)) * 86400000 AS INTEGER)),
|
|
62
|
+
error_json=?
|
|
63
|
+
WHERE status='running'
|
|
64
|
+
`)
|
|
65
|
+
.run(finishedAt, finishedAt, JSON.stringify({
|
|
66
|
+
code: "INTERRUPTED",
|
|
67
|
+
message: "Runtime stopped before this operation completed",
|
|
68
|
+
}));
|
|
69
|
+
return Number(result.changes);
|
|
70
|
+
}
|
|
71
|
+
clearHistory() {
|
|
72
|
+
const before = this.database.raw
|
|
73
|
+
.prepare("SELECT COUNT(*) AS count FROM operations")
|
|
74
|
+
.get().count;
|
|
75
|
+
this.database.raw
|
|
76
|
+
.prepare("DELETE FROM operations WHERE status != 'running'")
|
|
77
|
+
.run();
|
|
78
|
+
const preserved = this.database.raw
|
|
79
|
+
.prepare("SELECT COUNT(*) AS count FROM operations")
|
|
80
|
+
.get().count;
|
|
81
|
+
return { deleted: before - preserved, preserved };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function toRowValues(operation) {
|
|
85
|
+
return [
|
|
86
|
+
operation.operationId,
|
|
87
|
+
operation.pluginId,
|
|
88
|
+
operation.source,
|
|
89
|
+
operation.action,
|
|
90
|
+
operation.status,
|
|
91
|
+
operation.workspaceId,
|
|
92
|
+
operation.processId,
|
|
93
|
+
JSON.stringify(operation.input),
|
|
94
|
+
JSON.stringify(operation.output),
|
|
95
|
+
JSON.stringify(operation.error),
|
|
96
|
+
operation.inputTruncated ? 1 : 0,
|
|
97
|
+
operation.outputTruncated ? 1 : 0,
|
|
98
|
+
operation.startedAt,
|
|
99
|
+
operation.finishedAt,
|
|
100
|
+
operation.durationMs,
|
|
101
|
+
];
|
|
102
|
+
}
|
|
103
|
+
function fromRow(row) {
|
|
104
|
+
return {
|
|
105
|
+
operationId: row.operation_id,
|
|
106
|
+
pluginId: row.plugin_id,
|
|
107
|
+
source: row.source,
|
|
108
|
+
action: row.action,
|
|
109
|
+
status: row.status,
|
|
110
|
+
workspaceId: row.workspace_id,
|
|
111
|
+
processId: row.process_id,
|
|
112
|
+
input: parse(row.input_json),
|
|
113
|
+
output: parse(row.output_json),
|
|
114
|
+
error: parse(row.error_json),
|
|
115
|
+
inputTruncated: Boolean(row.input_truncated),
|
|
116
|
+
outputTruncated: Boolean(row.output_truncated),
|
|
117
|
+
startedAt: row.started_at,
|
|
118
|
+
finishedAt: row.finished_at,
|
|
119
|
+
durationMs: row.duration_ms,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function parse(value) {
|
|
123
|
+
try {
|
|
124
|
+
return JSON.parse(value);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { PasskeyRecord, PasskeyRepository as PasskeyRepositoryPort } from "../../core/auth/passkey-repository.js";
|
|
2
|
+
import type { AppDatabase } from "./app-database.js";
|
|
3
|
+
export declare class PasskeyRepository implements PasskeyRepositoryPort {
|
|
4
|
+
private readonly database;
|
|
5
|
+
constructor(database: AppDatabase);
|
|
6
|
+
list(): PasskeyRecord[];
|
|
7
|
+
get(credentialId: string): PasskeyRecord | null;
|
|
8
|
+
upsert(record: PasskeyRecord): void;
|
|
9
|
+
remove(credentialId: string): void;
|
|
10
|
+
updateCounter(credentialId: string, counter: number): void;
|
|
11
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export class PasskeyRepository {
|
|
2
|
+
database;
|
|
3
|
+
constructor(database) {
|
|
4
|
+
this.database = database;
|
|
5
|
+
}
|
|
6
|
+
list() {
|
|
7
|
+
return this.database.raw
|
|
8
|
+
.prepare("SELECT * FROM passkeys ORDER BY last_used_at DESC, created_at DESC")
|
|
9
|
+
.all()
|
|
10
|
+
.map((row) => mapPasskey(row));
|
|
11
|
+
}
|
|
12
|
+
get(credentialId) {
|
|
13
|
+
const row = this.database.raw
|
|
14
|
+
.prepare("SELECT * FROM passkeys WHERE credential_id=?")
|
|
15
|
+
.get(credentialId);
|
|
16
|
+
return row ? mapPasskey(row) : null;
|
|
17
|
+
}
|
|
18
|
+
upsert(record) {
|
|
19
|
+
this.database.raw
|
|
20
|
+
.prepare(`INSERT INTO passkeys(
|
|
21
|
+
credential_id, public_key, counter, transports_json, device_type, backed_up,
|
|
22
|
+
name, created_at, last_used_at
|
|
23
|
+
) VALUES(?,?,?,?,?,?,?,?,?)
|
|
24
|
+
ON CONFLICT(credential_id) DO UPDATE SET
|
|
25
|
+
public_key=excluded.public_key,
|
|
26
|
+
counter=excluded.counter,
|
|
27
|
+
transports_json=excluded.transports_json,
|
|
28
|
+
device_type=excluded.device_type,
|
|
29
|
+
backed_up=excluded.backed_up,
|
|
30
|
+
name=excluded.name,
|
|
31
|
+
last_used_at=excluded.last_used_at`)
|
|
32
|
+
.run(record.credentialId, Buffer.from(record.publicKey), record.counter, JSON.stringify(record.transports), record.deviceType, record.backedUp ? 1 : 0, record.name, record.createdAt, record.lastUsedAt);
|
|
33
|
+
}
|
|
34
|
+
remove(credentialId) {
|
|
35
|
+
this.database.raw
|
|
36
|
+
.prepare("DELETE FROM passkeys WHERE credential_id=?")
|
|
37
|
+
.run(credentialId);
|
|
38
|
+
}
|
|
39
|
+
updateCounter(credentialId, counter) {
|
|
40
|
+
this.database.raw
|
|
41
|
+
.prepare("UPDATE passkeys SET counter=?, last_used_at=? WHERE credential_id=?")
|
|
42
|
+
.run(counter, new Date().toISOString(), credentialId);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function mapPasskey(row) {
|
|
46
|
+
return {
|
|
47
|
+
credentialId: row.credential_id,
|
|
48
|
+
publicKey: new Uint8Array(row.public_key),
|
|
49
|
+
counter: row.counter,
|
|
50
|
+
transports: JSON.parse(row.transports_json),
|
|
51
|
+
deviceType: row.device_type,
|
|
52
|
+
backedUp: Boolean(row.backed_up),
|
|
53
|
+
name: row.name,
|
|
54
|
+
createdAt: row.created_at,
|
|
55
|
+
lastUsedAt: row.last_used_at,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { WebSessionRepository as WebSessionRepositoryPort } from "../../core/auth/session-repository.js";
|
|
2
|
+
import type { AppDatabase } from "./app-database.js";
|
|
3
|
+
export declare class WebSessionRepository implements WebSessionRepositoryPort {
|
|
4
|
+
private readonly database;
|
|
5
|
+
constructor(database: AppDatabase);
|
|
6
|
+
create(tokenHash: string, expiresAt: string): void;
|
|
7
|
+
valid(tokenHash: string, now: string): boolean;
|
|
8
|
+
revoke(tokenHash: string): void;
|
|
9
|
+
prune(now: string): number;
|
|
10
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export class WebSessionRepository {
|
|
2
|
+
database;
|
|
3
|
+
constructor(database) {
|
|
4
|
+
this.database = database;
|
|
5
|
+
}
|
|
6
|
+
create(tokenHash, expiresAt) {
|
|
7
|
+
this.database.raw
|
|
8
|
+
.prepare("INSERT INTO web_sessions(token_hash,expires_at,created_at) VALUES(?,?,?)")
|
|
9
|
+
.run(tokenHash, expiresAt, new Date().toISOString());
|
|
10
|
+
}
|
|
11
|
+
valid(tokenHash, now) {
|
|
12
|
+
return Boolean(this.database.raw
|
|
13
|
+
.prepare("SELECT 1 AS ok FROM web_sessions WHERE token_hash=? AND expires_at > ?")
|
|
14
|
+
.get(tokenHash, now));
|
|
15
|
+
}
|
|
16
|
+
revoke(tokenHash) {
|
|
17
|
+
this.database.raw
|
|
18
|
+
.prepare("DELETE FROM web_sessions WHERE token_hash=?")
|
|
19
|
+
.run(tokenHash);
|
|
20
|
+
}
|
|
21
|
+
prune(now) {
|
|
22
|
+
return Number(this.database.raw
|
|
23
|
+
.prepare("DELETE FROM web_sessions WHERE expires_at <= ?")
|
|
24
|
+
.run(now).changes);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AddressInfo } from "node:net";
|
|
2
|
+
import type { McpHttpHandler } from "@modelcontextprotocol/server";
|
|
3
|
+
import type { ChatRoomConfig } from "../../config/types.js";
|
|
4
|
+
import type { WebRuntime } from "../../plugins/web/runtime.js";
|
|
5
|
+
import type { RuntimeEventBus } from "../../app/event-bus.js";
|
|
6
|
+
import type { ExternalAccessRegistry } from "../../app/external-access-registry.js";
|
|
7
|
+
import type { AuthService } from "../../auth/auth-service.js";
|
|
8
|
+
import type { PasskeyService } from "../../auth/passkey-service.js";
|
|
9
|
+
import type { CloudController } from "../../plugins/cloud/controller.js";
|
|
10
|
+
export declare class HttpServer {
|
|
11
|
+
private readonly config;
|
|
12
|
+
private readonly application;
|
|
13
|
+
private readonly eventBus;
|
|
14
|
+
private readonly auth;
|
|
15
|
+
private readonly passkeys;
|
|
16
|
+
private readonly mcp;
|
|
17
|
+
private readonly cloud;
|
|
18
|
+
private server;
|
|
19
|
+
private readonly ingress;
|
|
20
|
+
private readonly startedAt;
|
|
21
|
+
private mcpRequestCount;
|
|
22
|
+
constructor(config: ChatRoomConfig, application: WebRuntime, eventBus: RuntimeEventBus, auth: AuthService, passkeys: PasskeyService, mcp: McpHttpHandler, externalAccess: ExternalAccessRegistry, cloud: CloudController);
|
|
23
|
+
start(): Promise<void>;
|
|
24
|
+
address(): AddressInfo | null;
|
|
25
|
+
close(): Promise<void>;
|
|
26
|
+
}
|