@agentcairn/mcp-server 0.0.0-stage → 0.1.0
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 +23 -2
- package/dist/authn.d.ts +38 -0
- package/dist/authn.js +86 -0
- package/dist/authn.js.map +1 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +25 -0
- package/dist/bin.js.map +1 -0
- package/dist/context.d.ts +49 -0
- package/dist/context.js +79 -0
- package/dist/context.js.map +1 -0
- package/dist/dashboard.d.ts +29 -0
- package/dist/dashboard.js +365 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/events.d.ts +120 -0
- package/dist/events.js +161 -0
- package/dist/events.js.map +1 -0
- package/dist/http.d.ts +35 -0
- package/dist/http.js +348 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/security.d.ts +49 -0
- package/dist/security.js +143 -0
- package/dist/security.js.map +1 -0
- package/dist/serve.d.ts +2 -0
- package/dist/serve.js +60 -0
- package/dist/serve.js.map +1 -0
- package/dist/server.d.ts +10 -0
- package/dist/server.js +14 -0
- package/dist/server.js.map +1 -0
- package/dist/tools.d.ts +8 -0
- package/dist/tools.js +199 -0
- package/dist/tools.js.map +1 -0
- package/dist/util.d.ts +13 -0
- package/dist/util.js +49 -0
- package/dist/util.js.map +1 -0
- package/package.json +55 -4
- package/ui/css/app.css +361 -0
- package/ui/favicon.svg +1 -0
- package/ui/index.html +17 -0
- package/ui/js/api.js +37 -0
- package/ui/js/app.js +190 -0
- package/ui/js/charts.js +133 -0
- package/ui/js/dom.js +247 -0
- package/ui/js/drawer.js +39 -0
- package/ui/js/json.js +37 -0
- package/ui/js/store.js +77 -0
- package/ui/js/views/access.js +129 -0
- package/ui/js/views/auth.js +102 -0
- package/ui/js/views/eventDrawer.js +106 -0
- package/ui/js/views/graph.js +344 -0
- package/ui/js/views/live.js +203 -0
- package/ui/js/views/memoryDrawer.js +37 -0
- package/ui/js/views/project.js +94 -0
- package/ui/js/views/projects.js +77 -0
- package/ui/js/views/query.js +107 -0
- package/ui/js/views/system.js +73 -0
package/README.md
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @agentcairn/mcp-server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The server of [Agent Cairn](https://github.com/itsmemac/agentcairn): shared project memory for AI coding agents, stored in Neo4j.
|
|
4
|
+
|
|
5
|
+
It exposes:
|
|
6
|
+
|
|
7
|
+
- an **MCP endpoint** (`POST /mcp`, stateless Streamable HTTP) with the tools `cairn_briefing`, `cairn_recall`, `cairn_remember`, `cairn_checkpoint`, `cairn_areas`, `cairn_forget` and `cairn_status`;
|
|
8
|
+
- a **JSON API** (`/api/*`) used by the `cairn` CLI and Claude Code hooks;
|
|
9
|
+
- a **dashboard** at `/ui/`: live request feed, projects, memory graph, read-only Cypher console, token management;
|
|
10
|
+
- `GET /healthz` for container health checks.
|
|
11
|
+
|
|
12
|
+
## Run it
|
|
13
|
+
|
|
14
|
+
Most people run the Docker image (`ghcr.io/itsmemac/agentcairn`); see the [self-hosting guide](https://github.com/itsmemac/agentcairn/blob/main/docs/self-hosting.md). To run from source or npm:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
NEO4J_URI=bolt://localhost:7687 NEO4J_PASSWORD=... npx -p @agentcairn/mcp-server cairn-mcp-http
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The first start prints a one-time setup code in its log; open `http://localhost:8787/ui/` and use it to create the admin account.
|
|
21
|
+
|
|
22
|
+
There is also a local **stdio mode** (`cairn-mcp`) that talks to Neo4j directly and reads the working directory for the project, for single-machine use.
|
|
23
|
+
|
|
24
|
+
All settings are listed in [docs/configuration.md](https://github.com/itsmemac/agentcairn/blob/main/docs/configuration.md). MIT licensed.
|
package/dist/authn.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { IncomingMessage } from 'node:http';
|
|
2
|
+
import { type Brain } from '@agentcairn/core';
|
|
3
|
+
export type AuthResult = {
|
|
4
|
+
ok: true;
|
|
5
|
+
kind: 'env' | 'managed' | 'open';
|
|
6
|
+
name: string;
|
|
7
|
+
} | {
|
|
8
|
+
ok: false;
|
|
9
|
+
reason: 'missing' | 'invalid' | 'no_tokens';
|
|
10
|
+
};
|
|
11
|
+
export interface AuthOptions {
|
|
12
|
+
/** A static token from the environment (CAIRN_TOKEN). */
|
|
13
|
+
envToken?: string;
|
|
14
|
+
/** Explicit opt-in to no authentication at all. */
|
|
15
|
+
noAuth?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Checks bearer tokens for the machine-facing endpoints: the static
|
|
19
|
+
* environment token, or tokens created in the dashboard (stored hashed).
|
|
20
|
+
* Lookups are cached briefly so a busy agent does not hit the database on
|
|
21
|
+
* every call; `invalidate()` makes revocations take effect at once.
|
|
22
|
+
*/
|
|
23
|
+
export declare class Authenticator {
|
|
24
|
+
private readonly brain;
|
|
25
|
+
private readonly options;
|
|
26
|
+
private readonly clock;
|
|
27
|
+
private readonly log;
|
|
28
|
+
private readonly good;
|
|
29
|
+
private readonly bad;
|
|
30
|
+
private readonly touched;
|
|
31
|
+
private managed;
|
|
32
|
+
constructor(brain: Brain, options?: AuthOptions, clock?: () => number, log?: (m: string) => void);
|
|
33
|
+
invalidate(): void;
|
|
34
|
+
private hasManagedTokens;
|
|
35
|
+
check(req: IncomingMessage): Promise<AuthResult>;
|
|
36
|
+
private touch;
|
|
37
|
+
}
|
|
38
|
+
export declare const authFailureMessage: (reason: "missing" | "invalid" | "no_tokens") => string;
|
package/dist/authn.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { countActiveTokens, findApiToken, touchApiToken } from '@agentcairn/core';
|
|
3
|
+
const digest = (s) => createHash('sha256').update(s).digest();
|
|
4
|
+
const POSITIVE_MS = 30_000;
|
|
5
|
+
const NEGATIVE_MS = 5_000;
|
|
6
|
+
const TOUCH_MS = 60_000;
|
|
7
|
+
/**
|
|
8
|
+
* Checks bearer tokens for the machine-facing endpoints: the static
|
|
9
|
+
* environment token, or tokens created in the dashboard (stored hashed).
|
|
10
|
+
* Lookups are cached briefly so a busy agent does not hit the database on
|
|
11
|
+
* every call; `invalidate()` makes revocations take effect at once.
|
|
12
|
+
*/
|
|
13
|
+
export class Authenticator {
|
|
14
|
+
brain;
|
|
15
|
+
options;
|
|
16
|
+
clock;
|
|
17
|
+
log;
|
|
18
|
+
good = new Map();
|
|
19
|
+
bad = new Map();
|
|
20
|
+
touched = new Map();
|
|
21
|
+
managed;
|
|
22
|
+
constructor(brain, options = {}, clock = Date.now, log = () => undefined) {
|
|
23
|
+
this.brain = brain;
|
|
24
|
+
this.options = options;
|
|
25
|
+
this.clock = clock;
|
|
26
|
+
this.log = log;
|
|
27
|
+
}
|
|
28
|
+
invalidate() {
|
|
29
|
+
this.good.clear();
|
|
30
|
+
this.bad.clear();
|
|
31
|
+
this.managed = undefined;
|
|
32
|
+
}
|
|
33
|
+
async hasManagedTokens() {
|
|
34
|
+
const now = this.clock();
|
|
35
|
+
if (this.managed && this.managed.until > now)
|
|
36
|
+
return this.managed.value;
|
|
37
|
+
const value = (await countActiveTokens(this.brain)) > 0;
|
|
38
|
+
this.managed = { value, until: now + NEGATIVE_MS };
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
async check(req) {
|
|
42
|
+
if (this.options.noAuth)
|
|
43
|
+
return { ok: true, kind: 'open', name: 'open' };
|
|
44
|
+
const header = req.headers.authorization ?? '';
|
|
45
|
+
const bearer = header.startsWith('Bearer ') ? header.slice(7).trim() : '';
|
|
46
|
+
if (bearer && this.options.envToken) {
|
|
47
|
+
const a = digest(bearer);
|
|
48
|
+
const b = digest(this.options.envToken);
|
|
49
|
+
if (timingSafeEqual(a, b))
|
|
50
|
+
return { ok: true, kind: 'env', name: 'env' };
|
|
51
|
+
}
|
|
52
|
+
if (bearer) {
|
|
53
|
+
const key = createHash('sha256').update(bearer).digest('hex');
|
|
54
|
+
const now = this.clock();
|
|
55
|
+
const cached = this.good.get(key);
|
|
56
|
+
if (cached && cached.until > now) {
|
|
57
|
+
this.touch(cached.id, now);
|
|
58
|
+
return { ok: true, kind: 'managed', name: cached.name };
|
|
59
|
+
}
|
|
60
|
+
if ((this.bad.get(key) ?? 0) > now)
|
|
61
|
+
return { ok: false, reason: 'invalid' };
|
|
62
|
+
const found = await findApiToken(this.brain, bearer);
|
|
63
|
+
if (found) {
|
|
64
|
+
this.good.set(key, { ...found, until: now + POSITIVE_MS });
|
|
65
|
+
this.touch(found.id, now);
|
|
66
|
+
return { ok: true, kind: 'managed', name: found.name };
|
|
67
|
+
}
|
|
68
|
+
if (this.bad.size > 500)
|
|
69
|
+
this.bad.clear();
|
|
70
|
+
this.bad.set(key, now + NEGATIVE_MS);
|
|
71
|
+
}
|
|
72
|
+
if (!this.options.envToken && !(await this.hasManagedTokens()))
|
|
73
|
+
return { ok: false, reason: 'no_tokens' };
|
|
74
|
+
return { ok: false, reason: bearer ? 'invalid' : 'missing' };
|
|
75
|
+
}
|
|
76
|
+
touch(id, now) {
|
|
77
|
+
if ((this.touched.get(id) ?? 0) + TOUCH_MS > now)
|
|
78
|
+
return;
|
|
79
|
+
this.touched.set(id, now);
|
|
80
|
+
void touchApiToken(this.brain, id).catch(error => this.log(`could not record token use: ${error.message}`));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export const authFailureMessage = (reason) => reason === 'no_tokens'
|
|
84
|
+
? 'No access tokens exist yet. Open the dashboard (/ui/) and create one.'
|
|
85
|
+
: 'Missing or invalid bearer token.';
|
|
86
|
+
//# sourceMappingURL=authn.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authn.js","sourceRoot":"","sources":["../src/authn.ts"],"names":[],"mappings":"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"}
|
package/dist/bin.d.ts
ADDED
package/dist/bin.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { loadConfig, loadEnvironment, openBrain } from '@agentcairn/core';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { createBrainServer } from './server.js';
|
|
5
|
+
async function main() {
|
|
6
|
+
const cwd = process.cwd();
|
|
7
|
+
loadEnvironment(cwd);
|
|
8
|
+
const brain = openBrain(loadConfig());
|
|
9
|
+
const server = createBrainServer(brain, cwd);
|
|
10
|
+
const shutdown = async () => {
|
|
11
|
+
await server.close().catch(() => undefined);
|
|
12
|
+
await brain.close().catch(() => undefined);
|
|
13
|
+
process.exit(0);
|
|
14
|
+
};
|
|
15
|
+
process.on('SIGINT', shutdown);
|
|
16
|
+
process.on('SIGTERM', shutdown);
|
|
17
|
+
// stdout carries the protocol: log to stderr only.
|
|
18
|
+
await server.connect(new StdioServerTransport());
|
|
19
|
+
console.error(`cairn: ready (folder ${cwd})`);
|
|
20
|
+
}
|
|
21
|
+
main().catch(error => {
|
|
22
|
+
console.error('cairn: failed to start:', error);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
});
|
|
25
|
+
//# sourceMappingURL=bin.js.map
|
package/dist/bin.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACzE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAA;AAChF,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAE/C,KAAK,UAAU,IAAI;IACjB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,CAAA;IACzB,eAAe,CAAC,GAAG,CAAC,CAAA;IACpB,MAAM,KAAK,GAAG,SAAS,CAAC,UAAU,EAAE,CAAC,CAAA;IACrC,MAAM,MAAM,GAAG,iBAAiB,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;IAE5C,MAAM,QAAQ,GAAG,KAAK,IAAI,EAAE;QAC1B,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;QAC3C,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;QAC1C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC,CAAA;IACD,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;IAC9B,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAA;IAE/B,mDAAmD;IACnD,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,oBAAoB,EAAE,CAAC,CAAA;IAChD,OAAO,CAAC,KAAK,CAAC,wBAAwB,GAAG,GAAG,CAAC,CAAA;AAC/C,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,yBAAyB,EAAE,KAAK,CAAC,CAAA;IAC/C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACjB,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type Brain, type ProjectHint, type ResolvedProject } from '@agentcairn/core';
|
|
2
|
+
/**
|
|
3
|
+
* Where the project comes from. A local (stdio) server reads the folder the
|
|
4
|
+
* agent works in; a remote server cannot, so the client says who it is.
|
|
5
|
+
*/
|
|
6
|
+
export type Scope = {
|
|
7
|
+
cwd: string;
|
|
8
|
+
} | {
|
|
9
|
+
hint: ProjectHint;
|
|
10
|
+
};
|
|
11
|
+
export interface Provenance {
|
|
12
|
+
commitSha?: string;
|
|
13
|
+
author?: string;
|
|
14
|
+
agent?: string;
|
|
15
|
+
machine?: string;
|
|
16
|
+
}
|
|
17
|
+
/** Everything a tool call needs: the brain, which project, and who is asking. */
|
|
18
|
+
export declare class BrainContext {
|
|
19
|
+
readonly brain: Brain;
|
|
20
|
+
readonly scope: Scope;
|
|
21
|
+
private readonly clientName;
|
|
22
|
+
/** Told which project each call resolved to (the dashboard shows it). */
|
|
23
|
+
private readonly onProject;
|
|
24
|
+
private schema;
|
|
25
|
+
constructor(brain: Brain, scope: Scope, clientName?: () => string | undefined,
|
|
26
|
+
/** Told which project each call resolved to (the dashboard shows it). */
|
|
27
|
+
onProject?: (projectId: string) => void);
|
|
28
|
+
/** The folder, when the server can see one (stdio mode). */
|
|
29
|
+
get cwd(): string | undefined;
|
|
30
|
+
/** Creates the schema once; a failed attempt is retried on the next call. */
|
|
31
|
+
ready(): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Identifies the project. Writes also create a project that was only known
|
|
34
|
+
* by its git remote (an explicit id is never auto-created here: `cairn init`
|
|
35
|
+
* does that on purpose).
|
|
36
|
+
*/
|
|
37
|
+
project(options: {
|
|
38
|
+
write: boolean;
|
|
39
|
+
}): Promise<ResolvedProject>;
|
|
40
|
+
/**
|
|
41
|
+
* Who wrote a memory. Locally the server reads git itself; remotely the
|
|
42
|
+
* client's hook supplies the commit and author. Values that do not look like
|
|
43
|
+
* a commit are ignored, so an agent guessing cannot corrupt staleness.
|
|
44
|
+
*/
|
|
45
|
+
provenance(given?: {
|
|
46
|
+
commitSha?: string;
|
|
47
|
+
author?: string;
|
|
48
|
+
}): Promise<Provenance>;
|
|
49
|
+
}
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { hostname } from 'node:os';
|
|
2
|
+
import { RemoteConflictError, attachRemote, ensureProject, ensureSchema, getRepoState, resolveProject, resolveProjectFromHint, } from '@agentcairn/core';
|
|
3
|
+
const SHA = /^[0-9a-f]{7,40}$/i;
|
|
4
|
+
/** Everything a tool call needs: the brain, which project, and who is asking. */
|
|
5
|
+
export class BrainContext {
|
|
6
|
+
brain;
|
|
7
|
+
scope;
|
|
8
|
+
clientName;
|
|
9
|
+
onProject;
|
|
10
|
+
schema;
|
|
11
|
+
constructor(brain, scope, clientName = () => undefined,
|
|
12
|
+
/** Told which project each call resolved to (the dashboard shows it). */
|
|
13
|
+
onProject = () => undefined) {
|
|
14
|
+
this.brain = brain;
|
|
15
|
+
this.scope = scope;
|
|
16
|
+
this.clientName = clientName;
|
|
17
|
+
this.onProject = onProject;
|
|
18
|
+
}
|
|
19
|
+
/** The folder, when the server can see one (stdio mode). */
|
|
20
|
+
get cwd() {
|
|
21
|
+
return 'cwd' in this.scope ? this.scope.cwd : undefined;
|
|
22
|
+
}
|
|
23
|
+
/** Creates the schema once; a failed attempt is retried on the next call. */
|
|
24
|
+
ready() {
|
|
25
|
+
this.schema ??= ensureSchema(this.brain).catch(error => {
|
|
26
|
+
this.schema = undefined;
|
|
27
|
+
throw error;
|
|
28
|
+
});
|
|
29
|
+
return this.schema;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Identifies the project. Writes also create a project that was only known
|
|
33
|
+
* by its git remote (an explicit id is never auto-created here: `cairn init`
|
|
34
|
+
* does that on purpose).
|
|
35
|
+
*/
|
|
36
|
+
async project(options) {
|
|
37
|
+
await this.ready();
|
|
38
|
+
const project = 'cwd' in this.scope
|
|
39
|
+
? await resolveProject(this.brain, this.scope.cwd)
|
|
40
|
+
: await resolveProjectFromHint(this.brain, this.scope.hint);
|
|
41
|
+
this.onProject(project.projectId);
|
|
42
|
+
if (options.write && project.isNew && project.source === 'remote') {
|
|
43
|
+
await ensureProject(this.brain, project.projectId);
|
|
44
|
+
if (project.remoteHash) {
|
|
45
|
+
try {
|
|
46
|
+
await attachRemote(this.brain, project.projectId, project.remoteHash);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (!(error instanceof RemoteConflictError))
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { ...project, isNew: false };
|
|
54
|
+
}
|
|
55
|
+
return project;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Who wrote a memory. Locally the server reads git itself; remotely the
|
|
59
|
+
* client's hook supplies the commit and author. Values that do not look like
|
|
60
|
+
* a commit are ignored, so an agent guessing cannot corrupt staleness.
|
|
61
|
+
*/
|
|
62
|
+
async provenance(given = {}) {
|
|
63
|
+
const given_ = {
|
|
64
|
+
commitSha: given.commitSha && SHA.test(given.commitSha) ? given.commitSha : undefined,
|
|
65
|
+
author: given.author?.trim().slice(0, 100) || undefined,
|
|
66
|
+
};
|
|
67
|
+
if (this.cwd) {
|
|
68
|
+
const repo = await getRepoState(this.cwd);
|
|
69
|
+
return {
|
|
70
|
+
commitSha: given_.commitSha ?? repo.head ?? undefined,
|
|
71
|
+
author: given_.author ?? repo.user ?? undefined,
|
|
72
|
+
agent: this.clientName(),
|
|
73
|
+
machine: hostname(),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return { ...given_, agent: this.clientName() };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type IncomingMessage, type ServerResponse } from 'node:http';
|
|
2
|
+
import { type Brain } from '@agentcairn/core';
|
|
3
|
+
import type { Authenticator } from './authn.js';
|
|
4
|
+
import type { EventLog } from './events.js';
|
|
5
|
+
import { type SessionStore, type SetupGate } from './security.js';
|
|
6
|
+
export interface DashboardDeps {
|
|
7
|
+
brain: Brain;
|
|
8
|
+
events: EventLog;
|
|
9
|
+
auth: Authenticator;
|
|
10
|
+
sessions: SessionStore;
|
|
11
|
+
setup: SetupGate;
|
|
12
|
+
version: string;
|
|
13
|
+
port: number;
|
|
14
|
+
uiDir: string;
|
|
15
|
+
trustProxy: boolean;
|
|
16
|
+
secureCookies: boolean;
|
|
17
|
+
maxBodyBytes: number;
|
|
18
|
+
capture: 'full' | 'meta';
|
|
19
|
+
authMode: {
|
|
20
|
+
envToken: boolean;
|
|
21
|
+
noAuth: boolean;
|
|
22
|
+
};
|
|
23
|
+
log: (message: string) => void;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The web dashboard: static UI under /ui/, JSON under /admin/api/. Returns a
|
|
27
|
+
* handler that answers true when it took the request.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createDashboard(deps: DashboardDeps): (req: IncomingMessage, res: ServerResponse, path: string) => Promise<boolean>;
|