@wolpertingerlabs/drawlatch 1.0.0-alpha.2 → 1.0.0-alpha.29
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/CONNECTIONS.md +3 -0
- package/README.md +395 -465
- package/bin/drawlatch.js +1018 -49
- package/dist/auth/auth.d.ts +10 -0
- package/dist/auth/auth.js +146 -0
- package/dist/auth/env-writer.d.ts +6 -0
- package/dist/auth/env-writer.js +52 -0
- package/dist/auth/password.d.ts +8 -0
- package/dist/auth/password.js +28 -0
- package/dist/auth/sessions.d.ts +13 -0
- package/dist/auth/sessions.js +85 -0
- package/dist/cli/generate-keys.d.ts +4 -4
- package/dist/cli/generate-keys.js +30 -25
- package/dist/connections/{anthropic.json → ai/anthropic.json} +19 -1
- package/dist/connections/{devin.json → ai/devin.json} +7 -1
- package/dist/connections/{google-ai.json → ai/google-ai.json} +7 -1
- package/dist/connections/{openai.json → ai/openai.json} +7 -1
- package/dist/connections/{openrouter.json → ai/openrouter.json} +7 -1
- package/dist/connections/developer-tools/github.json +138 -0
- package/dist/connections/{hex.json → developer-tools/hex.json} +7 -1
- package/dist/connections/developer-tools/linear.json +75 -0
- package/dist/connections/{lichess.json → gaming/lichess.json} +7 -1
- package/dist/connections/messaging/agentmail.json +21 -0
- package/dist/connections/messaging/discord-bot.json +114 -0
- package/dist/connections/{discord-oauth.json → messaging/discord-oauth.json} +7 -1
- package/dist/connections/messaging/slack.json +75 -0
- package/dist/connections/messaging/telegram.json +66 -0
- package/dist/connections/{google.json → productivity/google.json} +7 -1
- package/dist/connections/productivity/notion.json +75 -0
- package/dist/connections/productivity/stripe.json +74 -0
- package/dist/connections/productivity/trello.json +113 -0
- package/dist/connections/{bluesky.json → social-media/bluesky.json} +41 -0
- package/dist/connections/social-media/mastodon.json +65 -0
- package/dist/connections/social-media/reddit.json +80 -0
- package/dist/connections/{twitch.json → social-media/twitch.json} +40 -0
- package/dist/connections/social-media/x.json +67 -0
- package/dist/mcp/server.js +544 -31
- package/dist/remote/admin-mutations.d.ts +37 -0
- package/dist/remote/admin-mutations.js +251 -0
- package/dist/remote/admin-types.d.ts +149 -0
- package/dist/remote/admin-types.js +11 -0
- package/dist/remote/admin.d.ts +37 -0
- package/dist/remote/admin.js +316 -0
- package/dist/remote/caller-bootstrap.d.ts +71 -0
- package/dist/remote/caller-bootstrap.js +141 -0
- package/dist/remote/ingestors/base-ingestor.d.ts +19 -3
- package/dist/remote/ingestors/base-ingestor.js +27 -6
- package/dist/remote/ingestors/discord/discord-gateway.d.ts +2 -2
- package/dist/remote/ingestors/discord/discord-gateway.js +29 -6
- package/dist/remote/ingestors/e2e/setup.d.ts +69 -0
- package/dist/remote/ingestors/e2e/setup.js +147 -0
- package/dist/remote/ingestors/manager.d.ts +110 -10
- package/dist/remote/ingestors/manager.js +449 -42
- package/dist/remote/ingestors/poll/poll-ingestor.d.ts +4 -2
- package/dist/remote/ingestors/poll/poll-ingestor.js +26 -5
- package/dist/remote/ingestors/registry.d.ts +2 -2
- package/dist/remote/ingestors/registry.js +2 -2
- package/dist/remote/ingestors/slack/socket-mode.d.ts +2 -2
- package/dist/remote/ingestors/slack/socket-mode.js +28 -6
- package/dist/remote/ingestors/types.d.ts +25 -0
- package/dist/remote/ingestors/webhook/base-webhook-ingestor.d.ts +46 -7
- package/dist/remote/ingestors/webhook/base-webhook-ingestor.js +115 -10
- package/dist/remote/ingestors/webhook/github-webhook-ingestor.d.ts +30 -0
- package/dist/remote/ingestors/webhook/github-webhook-ingestor.js +73 -2
- package/dist/remote/ingestors/webhook/lifecycle-types.d.ts +63 -0
- package/dist/remote/ingestors/webhook/lifecycle-types.js +12 -0
- package/dist/remote/ingestors/webhook/stripe-webhook-ingestor.js +2 -2
- package/dist/remote/ingestors/webhook/trello-webhook-ingestor.d.ts +17 -5
- package/dist/remote/ingestors/webhook/trello-webhook-ingestor.js +32 -26
- package/dist/remote/ingestors/webhook/webhook-lifecycle-manager.d.ts +46 -0
- package/dist/remote/ingestors/webhook/webhook-lifecycle-manager.js +261 -0
- package/dist/remote/server.d.ts +25 -33
- package/dist/remote/server.js +681 -195
- package/dist/remote/tool-dispatch.d.ts +84 -0
- package/dist/remote/tool-dispatch.js +910 -0
- package/dist/remote/triggers/rule-engine.d.ts +40 -0
- package/dist/remote/triggers/rule-engine.js +228 -0
- package/dist/remote/triggers/types.d.ts +69 -0
- package/dist/remote/triggers/types.js +10 -0
- package/dist/remote/tunnel-state.d.ts +14 -0
- package/dist/remote/tunnel-state.js +20 -0
- package/dist/remote/tunnel.d.ts +53 -0
- package/dist/remote/tunnel.js +149 -0
- package/dist/shared/config.d.ts +97 -25
- package/dist/shared/config.js +48 -35
- package/dist/shared/connections.d.ts +21 -5
- package/dist/shared/connections.js +56 -14
- package/dist/shared/crypto/index.d.ts +1 -0
- package/dist/shared/crypto/index.js +1 -0
- package/dist/shared/crypto/key-manager.d.ts +67 -0
- package/dist/shared/crypto/key-manager.js +152 -0
- package/dist/shared/env-utils.d.ts +42 -0
- package/dist/shared/env-utils.js +150 -0
- package/dist/shared/listener-config.d.ts +157 -0
- package/dist/shared/listener-config.js +10 -0
- package/dist/shared/migrations.d.ts +40 -0
- package/dist/shared/migrations.js +122 -0
- package/dist/shared/protocol/handshake.js +14 -1
- package/dist/shared/protocol/index.d.ts +2 -0
- package/dist/shared/protocol/index.js +2 -0
- package/dist/shared/protocol/sync-client.d.ts +52 -0
- package/dist/shared/protocol/sync-client.js +99 -0
- package/dist/shared/protocol/sync.d.ts +71 -0
- package/dist/shared/protocol/sync.js +176 -0
- package/frontend/dist/assets/index-D4k5QKBP.css +1 -0
- package/frontend/dist/assets/index-IzwnSOmu.js +267 -0
- package/frontend/dist/index.html +15 -0
- package/package.json +66 -13
- package/dist/connections/discord-bot.json +0 -24
- package/dist/connections/github.json +0 -25
- package/dist/connections/linear.json +0 -29
- package/dist/connections/mastodon.json +0 -25
- package/dist/connections/notion.json +0 -33
- package/dist/connections/reddit.json +0 -28
- package/dist/connections/slack.json +0 -23
- package/dist/connections/stripe.json +0 -25
- package/dist/connections/telegram.json +0 -26
- package/dist/connections/trello.json +0 -25
- package/dist/connections/x.json +0 -27
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
2
|
+
export declare const SESSION_COOKIE_NAME: string;
|
|
3
|
+
export declare const SESSION_TTL_MS: number;
|
|
4
|
+
export declare function isPasswordConfigured(): boolean;
|
|
5
|
+
export declare function loginHandler(req: Request, res: Response): Promise<void>;
|
|
6
|
+
export declare function logoutHandler(req: Request, res: Response): void;
|
|
7
|
+
export declare function checkAuthHandler(req: Request, res: Response): void;
|
|
8
|
+
export declare function changePasswordHandler(req: Request, res: Response): Promise<void>;
|
|
9
|
+
export declare function requireAuth(req: Request, res: Response, next: NextFunction): void;
|
|
10
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { getSession, createSession, deleteSession, extendSession, cleanupExpiredSessions, deleteAllSessionsExcept, } from './sessions.js';
|
|
3
|
+
import { verifyPassword, hashPassword, generateSalt } from './password.js';
|
|
4
|
+
import { updateEnvFile } from './env-writer.js';
|
|
5
|
+
export const SESSION_COOKIE_NAME = process.env.SESSION_COOKIE_NAME ?? 'drawlatch_session';
|
|
6
|
+
export const SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
7
|
+
const NO_PASSWORD_MESSAGE = 'Server misconfigured: no password set. Run: drawlatch set-password';
|
|
8
|
+
// ── Password helpers ────────────────────────────────────────────────
|
|
9
|
+
export function isPasswordConfigured() {
|
|
10
|
+
return !!process.env.AUTH_PASSWORD_HASH;
|
|
11
|
+
}
|
|
12
|
+
async function verifyConfiguredPassword(password) {
|
|
13
|
+
const storedHash = process.env.AUTH_PASSWORD_HASH;
|
|
14
|
+
if (!storedHash)
|
|
15
|
+
return false;
|
|
16
|
+
const salt = process.env.AUTH_PASSWORD_SALT ?? '';
|
|
17
|
+
return verifyPassword(password, storedHash, salt);
|
|
18
|
+
}
|
|
19
|
+
// ── Session helpers ─────────────────────────────────────────────────
|
|
20
|
+
function setSessionCookie(res, token) {
|
|
21
|
+
res.cookie(SESSION_COOKIE_NAME, token, {
|
|
22
|
+
httpOnly: true,
|
|
23
|
+
sameSite: 'strict',
|
|
24
|
+
// No `secure` flag — the daemon serves the dashboard over HTTP on
|
|
25
|
+
// loopback/LAN. Setting `secure: true` would prevent the cookie from
|
|
26
|
+
// being sent.
|
|
27
|
+
maxAge: SESSION_TTL_MS,
|
|
28
|
+
path: '/',
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/** Roll the session: extend server-side expiry and refresh the cookie. */
|
|
32
|
+
function rollSession(token, res) {
|
|
33
|
+
extendSession(token, Date.now() + SESSION_TTL_MS);
|
|
34
|
+
setSessionCookie(res, token);
|
|
35
|
+
}
|
|
36
|
+
// Best-effort startup cleanup. Skipped when no password is configured —
|
|
37
|
+
// the dashboard is locked in that case anyway, but the import is still
|
|
38
|
+
// performed by tests, where an empty data dir is fine.
|
|
39
|
+
try {
|
|
40
|
+
cleanupExpiredSessions();
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// sessions file may not exist yet on first run; cleanup is best-effort.
|
|
44
|
+
}
|
|
45
|
+
// ── Handlers ────────────────────────────────────────────────────────
|
|
46
|
+
export async function loginHandler(req, res) {
|
|
47
|
+
if (!isPasswordConfigured()) {
|
|
48
|
+
res.status(503).json({ error: NO_PASSWORD_MESSAGE });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const { password } = req.body ?? {};
|
|
52
|
+
if (typeof password !== 'string' || password.length === 0) {
|
|
53
|
+
res.status(400).json({ error: 'Password is required' });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const valid = await verifyConfiguredPassword(password);
|
|
57
|
+
if (!valid) {
|
|
58
|
+
res.status(401).json({ error: 'Invalid password' });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const token = randomBytes(32).toString('hex');
|
|
62
|
+
const forwarded = req.headers['x-forwarded-for'];
|
|
63
|
+
const firstForwarded = typeof forwarded === 'string' ? forwarded.split(',')[0]?.trim() : undefined;
|
|
64
|
+
const ip = firstForwarded ?? req.ip;
|
|
65
|
+
createSession(token, Date.now() + SESSION_TTL_MS, ip);
|
|
66
|
+
setSessionCookie(res, token);
|
|
67
|
+
res.json({ ok: true });
|
|
68
|
+
}
|
|
69
|
+
export function logoutHandler(req, res) {
|
|
70
|
+
const token = req.cookies[SESSION_COOKIE_NAME];
|
|
71
|
+
if (token)
|
|
72
|
+
deleteSession(token);
|
|
73
|
+
res.clearCookie(SESSION_COOKIE_NAME, { path: '/' });
|
|
74
|
+
res.json({ ok: true });
|
|
75
|
+
}
|
|
76
|
+
export function checkAuthHandler(req, res) {
|
|
77
|
+
if (!isPasswordConfigured()) {
|
|
78
|
+
res.status(503).json({
|
|
79
|
+
authenticated: false,
|
|
80
|
+
error: NO_PASSWORD_MESSAGE,
|
|
81
|
+
});
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const token = req.cookies[SESSION_COOKIE_NAME];
|
|
85
|
+
if (!token) {
|
|
86
|
+
res.status(401).json({ authenticated: false });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const entry = getSession(token);
|
|
90
|
+
if (!entry || Date.now() > entry.expires_at) {
|
|
91
|
+
if (entry)
|
|
92
|
+
deleteSession(token);
|
|
93
|
+
res.status(401).json({ authenticated: false });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
rollSession(token, res);
|
|
97
|
+
res.json({ authenticated: true });
|
|
98
|
+
}
|
|
99
|
+
export async function changePasswordHandler(req, res) {
|
|
100
|
+
const { currentPassword, newPassword } = req.body ?? {};
|
|
101
|
+
if (typeof currentPassword !== 'string' || typeof newPassword !== 'string') {
|
|
102
|
+
res.status(400).json({ error: 'Both currentPassword and newPassword are required.' });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (newPassword.length < 8) {
|
|
106
|
+
res.status(400).json({ error: 'New password must be at least 8 characters.' });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const valid = await verifyConfiguredPassword(currentPassword);
|
|
110
|
+
if (!valid) {
|
|
111
|
+
res.status(401).json({ error: 'Current password is incorrect.' });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const salt = generateSalt();
|
|
115
|
+
const hash = await hashPassword(newPassword, salt);
|
|
116
|
+
updateEnvFile({ AUTH_PASSWORD_HASH: hash, AUTH_PASSWORD_SALT: salt });
|
|
117
|
+
process.env.AUTH_PASSWORD_HASH = hash;
|
|
118
|
+
process.env.AUTH_PASSWORD_SALT = salt;
|
|
119
|
+
// Invalidate every session except the caller's, so other browsers/devices
|
|
120
|
+
// are forced to re-authenticate with the new password.
|
|
121
|
+
const currentToken = req.cookies[SESSION_COOKIE_NAME];
|
|
122
|
+
deleteAllSessionsExcept(currentToken);
|
|
123
|
+
res.json({ ok: true });
|
|
124
|
+
}
|
|
125
|
+
// ── Middleware ───────────────────────────────────────────────────────
|
|
126
|
+
export function requireAuth(req, res, next) {
|
|
127
|
+
if (!isPasswordConfigured()) {
|
|
128
|
+
res.status(503).json({ error: NO_PASSWORD_MESSAGE });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const token = req.cookies[SESSION_COOKIE_NAME];
|
|
132
|
+
if (!token) {
|
|
133
|
+
res.status(401).json({ error: 'Not authenticated' });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const entry = getSession(token);
|
|
137
|
+
if (!entry || Date.now() > entry.expires_at) {
|
|
138
|
+
if (entry)
|
|
139
|
+
deleteSession(token);
|
|
140
|
+
res.status(401).json({ error: 'Session expired' });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
rollSession(token, res);
|
|
144
|
+
next();
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read the current .env file, update or add the specified key/value pairs,
|
|
3
|
+
* and write back with mode 0o600. Keys in `keysToRemove` are deleted entirely.
|
|
4
|
+
*/
|
|
5
|
+
export declare function updateEnvFile(updates: Record<string, string>, keysToRemove?: string[]): void;
|
|
6
|
+
//# sourceMappingURL=env-writer.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { getConfigDir, getEnvFilePath } from '../shared/config.js';
|
|
3
|
+
/** Ensure the daemon's config dir and .env file exist before reading/writing. */
|
|
4
|
+
function ensureEnvFile() {
|
|
5
|
+
mkdirSync(getConfigDir(), { recursive: true, mode: 0o700 });
|
|
6
|
+
const envFile = getEnvFilePath();
|
|
7
|
+
if (!existsSync(envFile)) {
|
|
8
|
+
writeFileSync(envFile, '', { mode: 0o600 });
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Read the current .env file, update or add the specified key/value pairs,
|
|
13
|
+
* and write back with mode 0o600. Keys in `keysToRemove` are deleted entirely.
|
|
14
|
+
*/
|
|
15
|
+
export function updateEnvFile(updates, keysToRemove = []) {
|
|
16
|
+
ensureEnvFile();
|
|
17
|
+
const envFile = getEnvFilePath();
|
|
18
|
+
const content = readFileSync(envFile, 'utf-8');
|
|
19
|
+
const lines = content.split('\n');
|
|
20
|
+
const updatedKeys = new Set();
|
|
21
|
+
const result = [];
|
|
22
|
+
for (const line of lines) {
|
|
23
|
+
const trimmed = line.trim();
|
|
24
|
+
const shouldRemove = keysToRemove.some((key) => {
|
|
25
|
+
return trimmed.startsWith(`${key}=`) || trimmed === `${key}=`;
|
|
26
|
+
});
|
|
27
|
+
if (shouldRemove)
|
|
28
|
+
continue;
|
|
29
|
+
let replaced = false;
|
|
30
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
31
|
+
if (trimmed.startsWith(`${key}=`) || trimmed === `${key}=`) {
|
|
32
|
+
result.push(`${key}=${value}`);
|
|
33
|
+
updatedKeys.add(key);
|
|
34
|
+
replaced = true;
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (!replaced)
|
|
39
|
+
result.push(line);
|
|
40
|
+
}
|
|
41
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
42
|
+
if (!updatedKeys.has(key)) {
|
|
43
|
+
let insertIdx = result.length;
|
|
44
|
+
while (insertIdx > 0 && result[insertIdx - 1].trim() === '') {
|
|
45
|
+
insertIdx--;
|
|
46
|
+
}
|
|
47
|
+
result.splice(insertIdx, 0, `${key}=${value}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
writeFileSync(envFile, result.join('\n'), { mode: 0o600 });
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=env-writer.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function generateSalt(): string;
|
|
2
|
+
export declare function hashPassword(password: string, salt: string): Promise<string>;
|
|
3
|
+
/**
|
|
4
|
+
* Verify a password against a stored hash and salt.
|
|
5
|
+
* Uses timing-safe comparison to prevent timing attacks.
|
|
6
|
+
*/
|
|
7
|
+
export declare function verifyPassword(password: string, storedHash: string, salt: string): Promise<boolean>;
|
|
8
|
+
//# sourceMappingURL=password.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { scrypt, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
const KEY_LENGTH = 64;
|
|
3
|
+
const SALT_LENGTH = 16;
|
|
4
|
+
export function generateSalt() {
|
|
5
|
+
return randomBytes(SALT_LENGTH).toString('hex');
|
|
6
|
+
}
|
|
7
|
+
export function hashPassword(password, salt) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
scrypt(password, salt, KEY_LENGTH, (err, derivedKey) => {
|
|
10
|
+
if (err)
|
|
11
|
+
return reject(err);
|
|
12
|
+
resolve(derivedKey.toString('hex'));
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Verify a password against a stored hash and salt.
|
|
18
|
+
* Uses timing-safe comparison to prevent timing attacks.
|
|
19
|
+
*/
|
|
20
|
+
export async function verifyPassword(password, storedHash, salt) {
|
|
21
|
+
const derivedKey = await hashPassword(password, salt);
|
|
22
|
+
const hashBuffer = Buffer.from(storedHash, 'hex');
|
|
23
|
+
const derivedBuffer = Buffer.from(derivedKey, 'hex');
|
|
24
|
+
if (hashBuffer.length !== derivedBuffer.length)
|
|
25
|
+
return false;
|
|
26
|
+
return timingSafeEqual(hashBuffer, derivedBuffer);
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=password.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface SessionData {
|
|
2
|
+
expires_at: number;
|
|
3
|
+
created_at: number;
|
|
4
|
+
ip?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function getSession(token: string): SessionData | undefined;
|
|
7
|
+
export declare function createSession(token: string, expiresAt: number, ip?: string): void;
|
|
8
|
+
export declare function extendSession(token: string, newExpiresAt: number): void;
|
|
9
|
+
export declare function deleteSession(token: string): void;
|
|
10
|
+
/** Used after change-password to log out every other session. */
|
|
11
|
+
export declare function deleteAllSessionsExcept(exceptToken?: string): void;
|
|
12
|
+
export declare function cleanupExpiredSessions(): number;
|
|
13
|
+
//# sourceMappingURL=sessions.d.ts.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { getConfigDir } from '../shared/config.js';
|
|
4
|
+
/** File-backed session store at ~/.drawlatch/data/sessions.json (mode 0o600). */
|
|
5
|
+
function getSessionsFile() {
|
|
6
|
+
return join(getConfigDir(), 'data', 'sessions.json');
|
|
7
|
+
}
|
|
8
|
+
function loadSessions() {
|
|
9
|
+
const file = getSessionsFile();
|
|
10
|
+
if (!existsSync(file)) {
|
|
11
|
+
const initial = {
|
|
12
|
+
sessions: {},
|
|
13
|
+
metadata: { last_cleanup: Date.now(), version: 1 },
|
|
14
|
+
};
|
|
15
|
+
saveSessions(initial);
|
|
16
|
+
return initial;
|
|
17
|
+
}
|
|
18
|
+
return JSON.parse(readFileSync(file, 'utf-8'));
|
|
19
|
+
}
|
|
20
|
+
function saveSessions(data) {
|
|
21
|
+
const file = getSessionsFile();
|
|
22
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
23
|
+
writeFileSync(file, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
24
|
+
// writeFileSync only applies mode on file creation; chmod on every write
|
|
25
|
+
// ensures stricter perms even if the file was created externally.
|
|
26
|
+
try {
|
|
27
|
+
chmodSync(file, 0o600);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// best effort
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function getSession(token) {
|
|
34
|
+
return loadSessions().sessions[token];
|
|
35
|
+
}
|
|
36
|
+
export function createSession(token, expiresAt, ip) {
|
|
37
|
+
const data = loadSessions();
|
|
38
|
+
data.sessions[token] = {
|
|
39
|
+
expires_at: expiresAt,
|
|
40
|
+
created_at: Date.now(),
|
|
41
|
+
ip,
|
|
42
|
+
};
|
|
43
|
+
saveSessions(data);
|
|
44
|
+
}
|
|
45
|
+
export function extendSession(token, newExpiresAt) {
|
|
46
|
+
const data = loadSessions();
|
|
47
|
+
if (!(token in data.sessions))
|
|
48
|
+
return;
|
|
49
|
+
data.sessions[token].expires_at = newExpiresAt;
|
|
50
|
+
saveSessions(data);
|
|
51
|
+
}
|
|
52
|
+
export function deleteSession(token) {
|
|
53
|
+
const data = loadSessions();
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- token-keyed session store
|
|
55
|
+
delete data.sessions[token];
|
|
56
|
+
saveSessions(data);
|
|
57
|
+
}
|
|
58
|
+
/** Used after change-password to log out every other session. */
|
|
59
|
+
export function deleteAllSessionsExcept(exceptToken) {
|
|
60
|
+
const data = loadSessions();
|
|
61
|
+
for (const token of Object.keys(data.sessions)) {
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- token-keyed session store
|
|
63
|
+
if (token !== exceptToken)
|
|
64
|
+
delete data.sessions[token];
|
|
65
|
+
}
|
|
66
|
+
saveSessions(data);
|
|
67
|
+
}
|
|
68
|
+
export function cleanupExpiredSessions() {
|
|
69
|
+
const data = loadSessions();
|
|
70
|
+
const now = Date.now();
|
|
71
|
+
let removed = 0;
|
|
72
|
+
for (const [token, session] of Object.entries(data.sessions)) {
|
|
73
|
+
if (now > session.expires_at) {
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- token-keyed session store
|
|
75
|
+
delete data.sessions[token];
|
|
76
|
+
removed++;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (removed > 0) {
|
|
80
|
+
data.metadata.last_cleanup = now;
|
|
81
|
+
saveSessions(data);
|
|
82
|
+
}
|
|
83
|
+
return removed;
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=sessions.js.map
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
* Key generation CLI.
|
|
4
4
|
*
|
|
5
5
|
* Generates Ed25519 (signing) + X25519 (key exchange) keypairs for either
|
|
6
|
-
*
|
|
6
|
+
* a caller identity or the server, and saves them with correct
|
|
7
7
|
* file permissions (0600 for private keys, 0644 for public keys).
|
|
8
8
|
*
|
|
9
9
|
* Usage:
|
|
10
|
-
* npx tsx src/cli/generate-keys.ts
|
|
11
|
-
* npx tsx src/cli/generate-keys.ts
|
|
12
|
-
* npx tsx src/cli/generate-keys.ts --dir /path
|
|
10
|
+
* npx tsx src/cli/generate-keys.ts caller [alias] # Generate caller keys
|
|
11
|
+
* npx tsx src/cli/generate-keys.ts server # Generate server keys
|
|
12
|
+
* npx tsx src/cli/generate-keys.ts --dir /path # Custom directory
|
|
13
13
|
*/
|
|
14
14
|
export {};
|
|
15
15
|
//# sourceMappingURL=generate-keys.d.ts.map
|
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
* Key generation CLI.
|
|
4
4
|
*
|
|
5
5
|
* Generates Ed25519 (signing) + X25519 (key exchange) keypairs for either
|
|
6
|
-
*
|
|
6
|
+
* a caller identity or the server, and saves them with correct
|
|
7
7
|
* file permissions (0600 for private keys, 0644 for public keys).
|
|
8
8
|
*
|
|
9
9
|
* Usage:
|
|
10
|
-
* npx tsx src/cli/generate-keys.ts
|
|
11
|
-
* npx tsx src/cli/generate-keys.ts
|
|
12
|
-
* npx tsx src/cli/generate-keys.ts --dir /path
|
|
10
|
+
* npx tsx src/cli/generate-keys.ts caller [alias] # Generate caller keys
|
|
11
|
+
* npx tsx src/cli/generate-keys.ts server # Generate server keys
|
|
12
|
+
* npx tsx src/cli/generate-keys.ts --dir /path # Custom directory
|
|
13
13
|
*/
|
|
14
14
|
import { generateKeyBundle, saveKeyBundle, loadKeyBundle, fingerprint, extractPublicKeys, } from '../shared/crypto/index.js';
|
|
15
|
-
import {
|
|
15
|
+
import { getCallerKeysDir, getServerKeysDir, getConfigDir } from '../shared/config.js';
|
|
16
16
|
import fs from 'node:fs';
|
|
17
17
|
import path from 'node:path';
|
|
18
18
|
function usage() {
|
|
@@ -20,12 +20,13 @@ function usage() {
|
|
|
20
20
|
Drawlatch key generation
|
|
21
21
|
|
|
22
22
|
Usage:
|
|
23
|
-
generate-keys
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
generate-keys
|
|
27
|
-
|
|
28
|
-
generate-keys
|
|
23
|
+
generate-keys caller [alias] Generate caller keypair
|
|
24
|
+
Alias defaults to "default" if omitted.
|
|
25
|
+
Keys are stored in keys/callers/<alias>/
|
|
26
|
+
generate-keys server Generate server keypair
|
|
27
|
+
Keys are stored in keys/server/
|
|
28
|
+
generate-keys --dir <path> Generate keypair in a custom directory
|
|
29
|
+
generate-keys show <path> Show fingerprint of an existing keypair
|
|
29
30
|
|
|
30
31
|
Keys are saved as PEM files:
|
|
31
32
|
<dir>/signing.pub.pem Ed25519 public key (safe to share)
|
|
@@ -71,27 +72,31 @@ if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
|
71
72
|
}
|
|
72
73
|
// Ensure base config directory exists
|
|
73
74
|
fs.mkdirSync(getConfigDir(), { recursive: true, mode: 0o700 });
|
|
74
|
-
if (args[0] === 'local') {
|
|
75
|
+
if (args[0] === 'caller' || args[0] === 'local') {
|
|
76
|
+
if (args[0] === 'local') {
|
|
77
|
+
console.error('Note: "local" is deprecated, use "caller" instead.');
|
|
78
|
+
}
|
|
75
79
|
const alias = args[1] && !args[1].startsWith('-') ? args[1] : 'default';
|
|
76
|
-
const
|
|
77
|
-
const targetDir = path.join(
|
|
78
|
-
// Check for legacy flat key layout (PEM files directly in keys/
|
|
79
|
-
const legacyKeyPath = path.join(
|
|
80
|
+
const callersDir = getCallerKeysDir();
|
|
81
|
+
const targetDir = path.join(callersDir, alias);
|
|
82
|
+
// Check for legacy flat key layout (PEM files directly in keys/callers/)
|
|
83
|
+
const legacyKeyPath = path.join(callersDir, 'signing.key.pem');
|
|
80
84
|
if (fs.existsSync(legacyKeyPath)) {
|
|
81
|
-
console.error(`\n⚠️ Legacy key layout detected: PEM files found directly in ${
|
|
82
|
-
`
|
|
85
|
+
console.error(`\n⚠️ Legacy key layout detected: PEM files found directly in ${callersDir}\n` +
|
|
86
|
+
` Caller keys are now stored per-alias: keys/callers/<alias>/\n` +
|
|
83
87
|
` To migrate, move your existing keys:\n\n` +
|
|
84
88
|
` mkdir -p ${targetDir}\n` +
|
|
85
|
-
` mv ${
|
|
86
|
-
` mv ${
|
|
87
|
-
` Then update localKeysDir in your proxy.config.json to point to:\n` +
|
|
88
|
-
` ${targetDir}\n`);
|
|
89
|
+
` mv ${callersDir}/signing.* ${targetDir}/\n` +
|
|
90
|
+
` mv ${callersDir}/exchange.* ${targetDir}/\n`);
|
|
89
91
|
process.exit(1);
|
|
90
92
|
}
|
|
91
|
-
generateAndSave(targetDir, `
|
|
93
|
+
generateAndSave(targetDir, `caller — alias "${alias}"`);
|
|
92
94
|
}
|
|
93
|
-
else if (args[0] === 'remote') {
|
|
94
|
-
|
|
95
|
+
else if (args[0] === 'server' || args[0] === 'remote') {
|
|
96
|
+
if (args[0] === 'remote') {
|
|
97
|
+
console.error('Note: "remote" is deprecated, use "server" instead.');
|
|
98
|
+
}
|
|
99
|
+
generateAndSave(getServerKeysDir(), 'server');
|
|
95
100
|
}
|
|
96
101
|
else if (args[0] === '--dir' && args[1]) {
|
|
97
102
|
generateAndSave(args[1], 'custom');
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "Anthropic API",
|
|
3
|
+
"stability": "beta",
|
|
4
|
+
"category": "ai",
|
|
3
5
|
"description": "Anthropic Claude API — messages, models, and more. Auth is handled automatically via the ANTHROPIC_API_KEY environment variable. Uses x-api-key header (not Bearer token). The anthropic-version header is set to 2023-06-01.",
|
|
4
6
|
"docsUrl": "https://docs.anthropic.com/en/api",
|
|
5
7
|
"headers": {
|
|
@@ -12,5 +14,21 @@
|
|
|
12
14
|
},
|
|
13
15
|
"allowedEndpoints": [
|
|
14
16
|
"https://api.anthropic.com/**"
|
|
15
|
-
]
|
|
17
|
+
],
|
|
18
|
+
"testConnection": {
|
|
19
|
+
"method": "POST",
|
|
20
|
+
"url": "https://api.anthropic.com/v1/messages",
|
|
21
|
+
"body": {
|
|
22
|
+
"model": "claude-3-haiku-20240307",
|
|
23
|
+
"max_tokens": 1,
|
|
24
|
+
"messages": [
|
|
25
|
+
{
|
|
26
|
+
"role": "user",
|
|
27
|
+
"content": "hi"
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
"description": "Sends a minimal message to verify API key",
|
|
32
|
+
"expectedStatus": [200]
|
|
33
|
+
}
|
|
16
34
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "Devin API",
|
|
3
|
+
"stability": "dev",
|
|
4
|
+
"category": "ai",
|
|
3
5
|
"description": "Devin AI API — sessions, knowledge, and machine management. Auth is handled automatically via the DEVIN_API_KEY environment variable.",
|
|
4
6
|
"docsUrl": "https://docs.devin.ai/api-reference/overview",
|
|
5
7
|
"headers": {
|
|
@@ -11,5 +13,9 @@
|
|
|
11
13
|
},
|
|
12
14
|
"allowedEndpoints": [
|
|
13
15
|
"https://api.devin.ai/**"
|
|
14
|
-
]
|
|
16
|
+
],
|
|
17
|
+
"testConnection": {
|
|
18
|
+
"url": "https://api.devin.ai/v1/sessions",
|
|
19
|
+
"description": "Lists Devin sessions to verify API key"
|
|
20
|
+
}
|
|
15
21
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "Google AI (Gemini) API",
|
|
3
|
+
"stability": "stable",
|
|
4
|
+
"category": "ai",
|
|
3
5
|
"description": "Google AI Gemini API — chat completions, embeddings, image generation, token counting, and more. Auth is handled automatically via the GOOGLE_AI_API_KEY environment variable. Uses x-goog-api-key header (not Bearer token). This is separate from the 'google' connection which covers Google Workspace APIs (Sheets, Drive, etc.).",
|
|
4
6
|
"docsUrl": "https://ai.google.dev/api",
|
|
5
7
|
"headers": {
|
|
@@ -11,5 +13,9 @@
|
|
|
11
13
|
},
|
|
12
14
|
"allowedEndpoints": [
|
|
13
15
|
"https://generativelanguage.googleapis.com/**"
|
|
14
|
-
]
|
|
16
|
+
],
|
|
17
|
+
"testConnection": {
|
|
18
|
+
"url": "https://generativelanguage.googleapis.com/v1beta/models",
|
|
19
|
+
"description": "Lists available Gemini models"
|
|
20
|
+
}
|
|
15
21
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "OpenAI API",
|
|
3
|
+
"stability": "beta",
|
|
4
|
+
"category": "ai",
|
|
3
5
|
"description": "OpenAI API — chat completions, embeddings, images, audio, files, fine-tuning, and more. Auth is handled automatically via the OPENAI_API_KEY environment variable.",
|
|
4
6
|
"docsUrl": "https://platform.openai.com/docs/api-reference",
|
|
5
7
|
"openApiUrl": "https://raw.githubusercontent.com/openai/openai-openapi/master/openapi.yaml",
|
|
@@ -12,5 +14,9 @@
|
|
|
12
14
|
},
|
|
13
15
|
"allowedEndpoints": [
|
|
14
16
|
"https://api.openai.com/**"
|
|
15
|
-
]
|
|
17
|
+
],
|
|
18
|
+
"testConnection": {
|
|
19
|
+
"url": "https://api.openai.com/v1/models",
|
|
20
|
+
"description": "Lists available models"
|
|
21
|
+
}
|
|
16
22
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "OpenRouter API",
|
|
3
|
+
"stability": "stable",
|
|
4
|
+
"category": "ai",
|
|
3
5
|
"description": "OpenRouter unified LLM API — chat completions, models, generation stats, and more. Auth is handled automatically via the OPENROUTER_API_KEY environment variable.",
|
|
4
6
|
"docsUrl": "https://openrouter.ai/docs/api-reference",
|
|
5
7
|
"openApiUrl": "https://openrouter.ai/openapi.json",
|
|
@@ -12,5 +14,9 @@
|
|
|
12
14
|
},
|
|
13
15
|
"allowedEndpoints": [
|
|
14
16
|
"https://openrouter.ai/api/**"
|
|
15
|
-
]
|
|
17
|
+
],
|
|
18
|
+
"testConnection": {
|
|
19
|
+
"url": "https://openrouter.ai/api/v1/models",
|
|
20
|
+
"description": "Lists available models"
|
|
21
|
+
}
|
|
16
22
|
}
|