@stage-labs/metro 0.1.0-beta.45 → 0.1.0-beta.47

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.
Files changed (62) hide show
  1. package/dist/api.js +5 -5
  2. package/dist/bedrock-proxy.js +1 -1
  3. package/dist/cli.js +7 -7
  4. package/dist/plugin.js +1 -1
  5. package/dist/prompt.js +1 -1
  6. package/dist/runtime.js +1 -1
  7. package/dist/serve.js +35 -5
  8. package/package.json +1 -1
  9. package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +0 -1
  10. package/runtime/node_modules/@metro-labs/mcp/package.json +1 -5
  11. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-import.ts +2 -2
  12. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-grant.ts +1 -1
  13. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +2 -5
  14. package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +13 -26
  15. package/runtime/node_modules/@metro-labs/mcp/src/daemon/claude-files.ts +1 -3
  16. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +2 -4
  17. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connect-hint.ts +11 -10
  18. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +2 -2
  19. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +3 -5
  20. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +1 -4
  21. package/runtime/node_modules/@metro-labs/mcp/src/daemon/events.ts +2 -2
  22. package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +1 -2
  23. package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +4 -83
  24. package/runtime/node_modules/@metro-labs/mcp/src/daemon/is-record.ts +2 -0
  25. package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-cli-api.ts +2 -2
  26. package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-mode.ts +3 -39
  27. package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-owner.ts +14 -0
  28. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-client.ts +1 -4
  29. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-discovery.ts +1 -4
  30. package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +0 -33
  31. package/runtime/node_modules/@metro-labs/mcp/src/daemon/protocol.ts +3 -3
  32. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +2 -2
  33. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-config.ts +1 -6
  34. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +1 -42
  35. package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-auth.ts +2 -11
  36. package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-routes.ts +2 -2
  37. package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +8 -90
  38. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +1 -1
  39. package/runtime/node_modules/@metro-labs/mcp/src/daemon/version.ts +1 -0
  40. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-connectors.ts +1 -1
  41. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +1 -4
  42. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-relay.ts +1 -1
  43. package/runtime/node_modules/@metro-labs/mcp/src/db/file-admin.ts +20 -34
  44. package/runtime/node_modules/@metro-labs/mcp/src/db/file-source.ts +1 -3
  45. package/runtime/node_modules/@metro-labs/mcp/src/db/local-connectors.ts +11 -22
  46. package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +3 -13
  47. package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +3 -3
  48. package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +1 -1
  49. package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +2 -4
  50. package/runtime/node_modules/@metro-labs/mcp/src/mcp/event-store.ts +1 -1
  51. package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +1 -1
  52. package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +1 -21
  53. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-resolve.ts +1 -1
  54. package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +1 -1
  55. package/runtime/node_modules/@metro-labs/telegram/package.json +0 -2
  56. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +0 -1
  57. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +0 -1
  58. package/runtime/node_modules/@metro-labs/webhook/src/station.ts +2 -5
  59. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +0 -1
  60. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +0 -1
  61. package/runtime/runtime.json +1 -1
  62. package/runtime/node_modules/@metro-labs/mcp/src/db/token-carry.ts +0 -45
@@ -1,86 +1,7 @@
1
- import { execFileSync } from 'node:child_process';
2
- import { errMsg, log } from './log.js';
3
1
  import { Line } from '../stations/lines.js';
4
2
 
5
- const TTL_MS = 5_000;
6
- type Cache = { id: string; at: number } | null;
3
+ export const userSelf = (): Line =>
4
+ (process.env.METRO_FROM ?? 'metro://user') as Line;
7
5
 
8
- function memo(loader: () => string): () => string {
9
- let cache: Cache = null;
10
- return () => {
11
- if (cache && Date.now() - cache.at < TTL_MS) return cache.id;
12
- const id = loader();
13
- cache = { id, at: Date.now() };
14
- return id;
15
- };
16
- }
17
-
18
- const claudeAccountId = memo(() => {
19
- let raw: string;
20
- try {
21
- raw = execFileSync('claude', ['auth', 'status', '--json'], {
22
- encoding: 'utf8',
23
- stdio: ['ignore', 'pipe', 'pipe'],
24
- });
25
- } catch (e) {
26
- throw new Error(
27
- `metro: failed to run 'claude auth status --json' — is Claude Code installed? (${errMsg(e)})`,
28
- );
29
- }
30
- let p: { loggedIn?: boolean; orgId?: string };
31
- try {
32
- p = JSON.parse(raw) as { loggedIn?: boolean; orgId?: string };
33
- } catch {
34
- throw new Error(
35
- `metro: 'claude auth status --json' returned non-JSON: ${raw.slice(0, 200)}`,
36
- );
37
- }
38
- if (!p.loggedIn || !p.orgId)
39
- throw new Error(
40
- "metro: Claude Code is not logged in — run 'claude auth login'",
41
- );
42
- return p.orgId;
43
- });
44
-
45
- export const claudeUserId = (): string =>
46
- process.env.METRO_USER_ID ?? claudeAccountId();
47
-
48
- export const claudeSessionId = (): string | null =>
49
- process.env.METRO_USER_SESSION_ID ??
50
- process.env.CLAUDE_CODE_SESSION_ID ??
51
- null;
52
-
53
- export function userSelf(): Line {
54
- const explicit = process.env.METRO_FROM;
55
- if (explicit) return explicit as Line;
56
- if (!process.env.CLAUDECODE) return 'metro://user' as Line;
57
- try {
58
- return Line.user('claude', claudeUserId());
59
- } catch (e) {
60
- log.warn(
61
- { reason: errMsg(e) },
62
- 'could not resolve a Claude account id; relaying as metro://user',
63
- );
64
- return 'metro://user' as Line;
65
- }
66
- }
67
-
68
- export function daemonSelf(): Line {
69
- const explicit = process.env.METRO_FROM ?? process.env.METRO_SELF_URI;
70
- return (explicit ?? 'metro://user') as Line;
71
- }
72
-
73
- export function selfLine(): Line | null {
74
- if (!process.env.CLAUDECODE) return null;
75
- const s = claudeSessionId();
76
- if (!s) return null;
77
- try {
78
- return Line.claude(claudeUserId(), s);
79
- } catch (e) {
80
- log.warn(
81
- { reason: errMsg(e) },
82
- 'could not resolve a Claude account id; relaying with no self line',
83
- );
84
- return null;
85
- }
86
- }
6
+ export const daemonSelf = (): Line =>
7
+ (process.env.METRO_FROM ?? process.env.METRO_SELF_URI ?? 'metro://user') as Line;
@@ -0,0 +1,2 @@
1
+ export const isRecord = (v: unknown): v is Record<string, unknown> =>
2
+ typeof v === 'object' && v !== null && !Array.isArray(v);
@@ -14,7 +14,7 @@ export interface LocalCliDeps {
14
14
  connectorSummaries: (agentId: string) => Promise<ConnectorSummary[]>;
15
15
  }
16
16
 
17
- export function keyOf(req: IncomingMessage): string {
17
+ function keyOf(req: IncomingMessage): string {
18
18
  const header = req.headers.authorization ?? '';
19
19
  if (header.toLowerCase().startsWith('bearer ')) return header.slice(7).trim();
20
20
  const query = new URLSearchParams((req.url ?? '').split('?')[1] ?? '');
@@ -29,7 +29,7 @@ export function keyIdentity(req: IncomingMessage): AgentIdentity | null {
29
29
 
30
30
  async function answer(path: string, key: string, agentId: string, deps: LocalCliDeps): Promise<unknown> {
31
31
  const agent = deps.agentName(agentId) ?? '';
32
- if (path === `${PREFIX}/session`) return { email: 'this machine', subject: 'this machine', agent };
32
+ if (path === `${PREFIX}/session`) return { subject: 'this machine', agent };
33
33
  if (path === `${PREFIX}/connectors`) return { agent, connectors: await deps.connectorSummaries(agentId) };
34
34
  return { json: relayServersJson(await deps.connectorEntries(agentId), publicBaseOrDefault(), key), agent };
35
35
  }
@@ -5,7 +5,6 @@ import { AttachSessions } from './attach-session.js';
5
5
  import type { AgentApiDeps } from './agent-api.js';
6
6
  import { ATTACHABLE, type AccountApiDeps } from './account-api.js';
7
7
  import type { AgentConnectorApiDeps } from './agent-connector-api.js';
8
- import type { ProjectApiDeps } from './project-api.js';
9
8
  import type { SessionApis } from './session-apis.js';
10
9
  import type { ModeInfo } from './mode-api.js';
11
10
  import type { ImportApiDeps } from './import-api.js';
@@ -33,7 +32,7 @@ import {
33
32
  } from '../db/local-connectors.js';
34
33
  import {
35
34
  assertLocalOwner,
36
- claimLocalOwner,
35
+ ownerSignIn,
37
36
  connectorIdsOfLocalAgent,
38
37
  LOCAL_PROJECT_ID,
39
38
  localAttachAccount,
@@ -46,9 +45,7 @@ import {
46
45
  localOwner,
47
46
  localResetAgentKey,
48
47
  } from '../db/file-admin.js';
49
- import { normalizeAddress } from '../db/users.js';
50
48
  import { listAgentFiles, readAgentFile } from '../db/file-source.js';
51
- import type { Project } from '../db/projects.js';
52
49
  import type { StationName } from '../db/schema.js';
53
50
 
54
51
  export interface LocalModeDeps {
@@ -66,16 +63,6 @@ const notHere = (what: string): Promise<never> =>
66
63
  new ApiError(`${what} are managed on metro.box, not on a local daemon`, 400),
67
64
  );
68
65
 
69
- function localProject(): Project {
70
- return {
71
- id: LOCAL_PROJECT_ID,
72
- name: hostname(),
73
- isDefault: true,
74
- owner: true,
75
- role: 'admin',
76
- };
77
- }
78
-
79
66
  function attachSessions(deps: LocalModeDeps): AttachSessions {
80
67
  return new AttachSessions({
81
68
  authorize: async (owner) => {
@@ -124,28 +111,6 @@ function agentApi(deps: LocalModeDeps): AgentApiDeps {
124
111
  };
125
112
  }
126
113
 
127
- const projectApi: ProjectApiDeps = {
128
- listProjects: (subject) => {
129
- const owner = localOwner();
130
- const mine = owner !== null && owner === normalizeAddress(subject);
131
- return Promise.resolve(mine ? [localProject()] : []);
132
- },
133
- createProject: () => notHere('projects'),
134
- renameProject: () => notHere('projects'),
135
- deleteProject: () => notHere('projects'),
136
- listMembers: (subject, id) => {
137
- const owner = localOwner();
138
- if (id !== LOCAL_PROJECT_ID || owner === null || owner !== normalizeAddress(subject))
139
- return Promise.reject(new ApiError('no such project', 404));
140
- return Promise.resolve([
141
- { id: 'owner', address: owner, email: null, role: 'admin', owner: true },
142
- ]);
143
- },
144
- addMember: () => notHere('members'),
145
- setMemberRole: () => notHere('members'),
146
- removeMember: () => notHere('members'),
147
- };
148
-
149
114
  function connectorIdsOfLocalAgents(ids: string[]): Promise<Map<string, string[]>> {
150
115
  return Promise.resolve(new Map(ids.map((id) => [id, connectorIdsOfLocalAgent(id) ?? []] as const)));
151
116
  }
@@ -211,7 +176,7 @@ function importApi(deps: LocalModeDeps): ImportApiDeps {
211
176
  };
212
177
  }
213
178
 
214
- export function localModeInfo(): ModeInfo {
179
+ function localModeInfo(): ModeInfo {
215
180
  return { mode: 'local', owner: localOwner(), project: LOCAL_PROJECT_ID };
216
181
  }
217
182
 
@@ -225,8 +190,7 @@ export function localSessionApis(deps: LocalModeDeps): SessionApis {
225
190
  relayApi,
226
191
  localCli,
227
192
  claudeApi: { authorize: (subject) => { assertLocalOwner(subject); } },
228
- projectApi,
229
- siwe: { ensureUser: claimLocalOwner },
193
+ siwe: { ensureUser: ownerSignIn },
230
194
  mode: localModeInfo,
231
195
  };
232
196
  }
@@ -0,0 +1,14 @@
1
+ import { localOwner, setLocalOwner } from '../db/file-admin.js';
2
+ import { log } from './log.js';
3
+
4
+ export function applyLocalOwner(): string | null {
5
+ const wanted = process.env.METRO_OWNER?.trim() ?? '';
6
+ if (wanted !== '') {
7
+ const address = setLocalOwner(wanted);
8
+ log.info({ owner: address }, 'local daemon: owner set from METRO_OWNER');
9
+ return address;
10
+ }
11
+ const stored = localOwner();
12
+ if (stored === null) log.warn('local daemon: no owner set, so no wallet can sign in; restart with metro serve --owner <address>');
13
+ return stored;
14
+ }
@@ -4,6 +4,7 @@ import {
4
4
  type OAuthTokens,
5
5
  } from './connector-verify.js';
6
6
  import type { OAuthServer } from './oauth-discovery.js';
7
+ import { isRecord } from './is-record.js';
7
8
 
8
9
  const TOKEN_TIMEOUT_MS = 15_000;
9
10
  const CLIENT_NAME = 'Metro';
@@ -17,10 +18,6 @@ function refused(message: string): ConnectorVerifyError {
17
18
  return new ConnectorVerifyError(message, 400);
18
19
  }
19
20
 
20
- function isRecord(value: unknown): value is Record<string, unknown> {
21
- return typeof value === 'object' && value !== null && !Array.isArray(value);
22
- }
23
-
24
21
  export function newVerifier(): string {
25
22
  return randomBytes(32).toString('base64url');
26
23
  }
@@ -1,4 +1,5 @@
1
1
  import { ConnectorVerifyError, parseConnectorUrl } from './connector-verify.js';
2
+ import { isRecord } from './is-record.js';
2
3
 
3
4
  const DISCOVERY_TIMEOUT_MS = 10_000;
4
5
 
@@ -14,10 +15,6 @@ function refused(message: string): ConnectorVerifyError {
14
15
  return new ConnectorVerifyError(message, 400);
15
16
  }
16
17
 
17
- function isRecord(value: unknown): value is Record<string, unknown> {
18
- return typeof value === 'object' && value !== null && !Array.isArray(value);
19
- }
20
-
21
18
  const str = (value: unknown): string =>
22
19
  typeof value === 'string' ? value : '';
23
20
 
@@ -1,7 +1,6 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
  import {
3
3
  closeSync,
4
- existsSync,
5
4
  mkdirSync,
6
5
  openSync,
7
6
  readFileSync,
@@ -20,10 +19,6 @@ mkdirSync(STATE_DIR, { recursive: true });
20
19
  export const configDir = (): string =>
21
20
  process.env.METRO_CONFIG_DIR ??
22
21
  join(process.env.XDG_CONFIG_HOME ?? join(homedir(), '.config'), 'metro');
23
- export const CONFIG_DIR = configDir();
24
- export const CONFIG_ENV_FILE = join(CONFIG_DIR, '.env');
25
-
26
- export const TRAINS_ENV_FILE = join(homedir(), '.metro', '.env');
27
22
 
28
23
  export function trainsDir(): string {
29
24
  const env = process.env.METRO_TRAINS_DIR?.trim();
@@ -34,34 +29,6 @@ export function trainsDir(): string {
34
29
  export const isLocalMode = (): boolean =>
35
30
  process.env.METRO_MODE?.trim() === 'local';
36
31
 
37
- export const envSources = (): readonly string[] => [
38
- join(process.cwd(), '.env'),
39
- TRAINS_ENV_FILE,
40
- CONFIG_ENV_FILE,
41
- ];
42
-
43
- const LINE_RE = /^\s*([A-Za-z_]\w*)\s*=\s*(.*?)\s*$/;
44
- const QUOTED_RE = /^(['"])(.*)\1$/;
45
-
46
- export function readDotenv(path: string): Record<string, string> {
47
- if (!existsSync(path)) return {};
48
- const out: Record<string, string> = {};
49
- for (const line of readFileSync(path, 'utf8').split('\n')) {
50
- const m = LINE_RE.exec(line);
51
- if (m?.[1] !== undefined && m[2] !== undefined)
52
- out[m[1]] = m[2].replace(QUOTED_RE, '$2');
53
- }
54
- return out;
55
- }
56
-
57
- export function loadMetroEnv(): void {
58
- for (const path of envSources()) {
59
- for (const [k, v] of Object.entries(readDotenv(path))) {
60
- process.env[k] ??= v;
61
- }
62
- }
63
- }
64
-
65
32
  function readLockPid(lockFile: string): number {
66
33
  try {
67
34
  return Number(readFileSync(lockFile, 'utf8').trim());
@@ -11,7 +11,7 @@ export {
11
11
  type TrainErrorInfo,
12
12
  } from './train-error.js';
13
13
 
14
- export const CALL_TIMEOUT_MS = 60_000;
14
+ const CALL_TIMEOUT_MS = 60_000;
15
15
 
16
16
  export interface Pending {
17
17
  resolve: (r: TrainCallResponse) => void;
@@ -24,7 +24,7 @@ export interface CallTarget {
24
24
  proc: ({ stdin?: unknown } & Record<string, unknown>) | null;
25
25
  }
26
26
 
27
- export const STDOUT_LINE_MAX = 4 * 1024 * 1024;
27
+ const STDOUT_LINE_MAX = 4 * 1024 * 1024;
28
28
 
29
29
  export type TrainEvent = {
30
30
  station?: string;
@@ -149,7 +149,7 @@ export function readCalls(
149
149
  });
150
150
  }
151
151
 
152
- export function encodeCall(id: string, action: string, args: unknown): string {
152
+ function encodeCall(id: string, action: string, args: unknown): string {
153
153
  return JSON.stringify({ op: 'call', id, action, args }) + '\n';
154
154
  }
155
155
 
@@ -9,7 +9,7 @@ import {
9
9
  sendJson,
10
10
  } from './api-http.js';
11
11
  import { AGENT_CODE_RE, takeAgentCode } from './agent-pair.js';
12
- import { sessionTtlFromEnv } from './session-config.js';
12
+ import { SESSION_TTL_SEC } from './session-config.js';
13
13
  import { signRunToken } from './session.js';
14
14
  import type { LoadedAgent } from '../db/materialize.js';
15
15
  import type { RuntimeLease } from '../db/runtimes.js';
@@ -80,7 +80,7 @@ async function handleClaim(
80
80
  const token = signRunToken(
81
81
  { subject: taken.subject, agentId: lease.agentId, runtimeId: lease.runtimeId },
82
82
  secret,
83
- { ttlSec: sessionTtlFromEnv() },
83
+ { ttlSec: SESSION_TTL_SEC },
84
84
  );
85
85
  log.info(
86
86
  { agent: lease.agentId, runtime: lease.runtimeId, label: lease.label },
@@ -1,11 +1,6 @@
1
- const DEFAULT_SESSION_TTL_SEC = 30 * 24 * 3600;
1
+ export const SESSION_TTL_SEC = 30 * 24 * 3600;
2
2
  const PREVIEW = /^([a-z0-9-]+--)?metro-ui\.netlify\.app$/;
3
3
 
4
- export function sessionTtlFromEnv(): number {
5
- const ttl = Number(process.env.METRO_SESSION_TTL_SEC);
6
- return Number.isFinite(ttl) && ttl > 0 ? ttl : DEFAULT_SESSION_TTL_SEC;
7
- }
8
-
9
4
  export function allowedWebHost(host: string): boolean {
10
5
  if (host === 'metro.box' || host === 'localhost' || host === '127.0.0.1')
11
6
  return true;
@@ -1,4 +1,4 @@
1
- import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
1
+ import { createHmac, timingSafeEqual } from 'node:crypto';
2
2
  import { ID_RE } from '../db/ids.js';
3
3
 
4
4
  export class SessionError extends Error {}
@@ -53,47 +53,6 @@ function verify(token: string, secret: string): Record<string, unknown> {
53
53
 
54
54
  const nowSec = (now?: number): number => Math.floor((now ?? Date.now()) / 1000);
55
55
 
56
- export function newNonce(): string {
57
- return b64url(randomBytes(16));
58
- }
59
-
60
- interface StateClaims {
61
- return_to: string;
62
- nonce: string;
63
- }
64
-
65
- export function signState(
66
- claims: StateClaims,
67
- secret: string,
68
- opts: { ttlSec?: number; now?: number } = {},
69
- ): string {
70
- const iat = nowSec(opts.now);
71
- return sign(
72
- {
73
- typ: 'state',
74
- return_to: claims.return_to,
75
- nonce: claims.nonce,
76
- iat,
77
- exp: iat + (opts.ttlSec ?? 600),
78
- },
79
- secret,
80
- );
81
- }
82
-
83
- export function verifyState(
84
- token: string,
85
- secret: string,
86
- now?: number,
87
- ): StateClaims {
88
- const p = verify(token, secret);
89
- if (p.typ !== 'state') throw new SessionError('wrong token type');
90
- if (typeof p.exp !== 'number' || p.exp < nowSec(now))
91
- throw new SessionError('state expired');
92
- if (typeof p.return_to !== 'string' || typeof p.nonce !== 'string')
93
- throw new SessionError('malformed state');
94
- return { return_to: p.return_to, nonce: p.nonce };
95
- }
96
-
97
56
  interface SessionClaims {
98
57
  subject: string;
99
58
  agentIds: string[];
@@ -10,16 +10,8 @@ export interface SiweLogin {
10
10
  signature: string;
11
11
  }
12
12
 
13
- export interface SiweVerifyArgs {
14
- address: `0x${string}`;
15
- message: string;
16
- signature: Hex;
17
- }
18
-
19
13
  export interface SiweDeps {
20
14
  takeNonce: (nonce: string) => boolean;
21
- now?: number;
22
- verify?: (args: SiweVerifyArgs) => Promise<boolean>;
23
15
  }
24
16
 
25
17
  const HEX_SIGNATURE = /^0x[0-9a-fA-F]{130}$/;
@@ -66,14 +58,13 @@ export async function verifySiweLogin(
66
58
  if (!HEX_SIGNATURE.test(input.signature))
67
59
  throw new SiweError('that is not an Ethereum signature', 400);
68
60
  const { address, domain, nonce, parsed } = fieldsOrThrow(input.message);
69
- const time = new Date(deps.now ?? Date.now());
61
+ const time = new Date();
70
62
  if (!validateSiweMessage({ message: parsed, domain, nonce, time }))
71
63
  throw new SiweError('this sign-in message is expired or does not match', 400);
72
64
  if (!deps.takeNonce(nonce))
73
65
  throw new SiweError('that sign-in has expired or was already used', 400);
74
- const verify = deps.verify ?? verifyMessage;
75
66
  const signature = input.signature as Hex;
76
- if (!(await verify({ address, message: input.message, signature })))
67
+ if (!(await verifyMessage({ address, message: input.message, signature })))
77
68
  throw new SiweError(
78
69
  'the signature does not match the address; smart-contract wallets are not supported yet',
79
70
  401,
@@ -10,7 +10,7 @@ import {
10
10
  import { mintNonce, takeNonce } from './siwe-nonces.js';
11
11
  import { verifySiweLogin } from './siwe-auth.js';
12
12
  import { signSession } from './session.js';
13
- import { sessionTtlFromEnv } from './session-config.js';
13
+ import { SESSION_TTL_SEC } from './session-config.js';
14
14
  import { ensureUserWithProject } from '../db/projects.js';
15
15
 
16
16
  const NONCE_PATH = '/auth/siwe/nonce';
@@ -39,7 +39,7 @@ async function handleVerify(
39
39
  const userId = await deps.ensureUser(address);
40
40
  log.info({ userId, address }, 'siwe: session issued');
41
41
  const session = signSession({ subject: address, agentIds: [] }, secret, {
42
- ttlSec: sessionTtlFromEnv(),
42
+ ttlSec: SESSION_TTL_SEC,
43
43
  });
44
44
  sendJson(req, res, 200, { session, address });
45
45
  }
@@ -1,31 +1,15 @@
1
- import { spawn, spawnSync, type ChildProcess } from 'node:child_process';
1
+ import { spawn, type ChildProcess } from 'node:child_process';
2
2
  import { timingSafeEqual } from 'node:crypto';
3
3
  import { Resolver } from 'node:dns/promises';
4
4
  import { homedir } from 'node:os';
5
5
  import { join } from 'node:path';
6
- import { STATE_DIR } from './paths.js';
7
6
  import { errMsg, log } from './log.js';
8
7
  import { readJson } from './secure-fs.js';
9
8
 
10
- const FILE = join(STATE_DIR, 'tunnel.json');
11
- const LEGACY_WEBHOOKS_FILE = join(STATE_DIR, 'webhooks.json');
12
9
  const RESTART_DELAY_MS = 2_000;
13
10
 
14
- interface NamedTunnel {
15
- name: string;
16
- hostname: string;
17
- }
18
-
19
- interface QuickTunnel {
20
- quick: true;
21
- }
22
-
23
- export type TunnelConfig = NamedTunnel | QuickTunnel;
24
-
25
- const isQuick = (cfg: TunnelConfig): cfg is QuickTunnel => 'quick' in cfg;
26
-
27
- export const tunnelConfigFromEnv = (): TunnelConfig | null =>
28
- process.env.METRO_TUNNEL?.trim() === 'quick' ? { quick: true } : null;
11
+ export const quickTunnelWanted = (): boolean =>
12
+ process.env.METRO_TUNNEL?.trim() === 'quick';
29
13
 
30
14
  const QUICK_URL_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
31
15
 
@@ -61,17 +45,11 @@ async function untilResolvable(host: string, resolves: Resolves): Promise<boolea
61
45
  return false;
62
46
  }
63
47
 
64
- export function configuredTunnelHost(): string | null {
65
- const cfg = loadTunnelConfig();
66
- return cfg !== null && !isQuick(cfg) ? cfg.hostname : null;
67
- }
68
-
69
48
  export interface Endpoint {
70
49
  id: string;
71
50
  webhookId?: string;
72
51
  label: string;
73
52
  secret?: string;
74
- session?: string;
75
53
  createdAt: string;
76
54
  }
77
55
  interface AccountRecord {
@@ -79,7 +57,6 @@ interface AccountRecord {
79
57
  webhookId?: unknown;
80
58
  label?: unknown;
81
59
  secret?: unknown;
82
- session?: unknown;
83
60
  createdAt?: unknown;
84
61
  }
85
62
 
@@ -101,7 +78,6 @@ function toEndpoint(raw: AccountRecord): Endpoint | null {
101
78
  webhookId: str(raw.webhookId),
102
79
  label: str(raw.label) ?? id,
103
80
  secret: str(raw.secret),
104
- session: str(raw.session),
105
81
  createdAt: str(raw.createdAt) ?? '',
106
82
  };
107
83
  }
@@ -125,76 +101,21 @@ export function tokenMatches(secret: string, given: string): boolean {
125
101
  return want.length === got.length && timingSafeEqual(want, got);
126
102
  }
127
103
 
128
- export function warnOnLegacyWebhooks(): void {
129
- const legacy = readJson<{ endpoints?: unknown[] }>(LEGACY_WEBHOOKS_FILE, {});
130
- const count = Array.isArray(legacy.endpoints) ? legacy.endpoints.length : 0;
131
- if (count > 0)
132
- log.warn(
133
- { file: LEGACY_WEBHOOKS_FILE, endpoints: count },
134
- 'webhook: webhooks.json is no longer read — endpoints live in the accounts table; these endpoints are inactive',
135
- );
136
- }
137
-
138
- export const loadTunnelConfig = (): TunnelConfig | null =>
139
- readJson<TunnelConfig | null>(FILE, null, {
140
- warn: 'tunnel.json: malformed, ignoring',
141
- });
142
-
143
- function fetchTunnelToken(name: string): string | null {
144
- const r = spawnSync('cloudflared', ['tunnel', 'token', name], {
145
- encoding: 'utf8',
146
- });
147
- if (r.status !== 0) return null;
148
- const token = r.stdout.trim();
149
- return token.length > 0 ? token : null;
150
- }
151
-
152
- interface Plan {
153
- args: string[];
154
- env: NodeJS.ProcessEnv;
155
- mode: string;
156
- }
157
-
158
104
  export class Tunnel {
159
105
  private child: ChildProcess | null = null;
160
106
  private closed = false;
161
- private token: string | null | undefined = undefined;
162
107
 
163
108
  constructor(
164
- private cfg: TunnelConfig,
165
109
  private port: number,
166
110
  private onUrl: (url: string) => void = () => undefined,
167
111
  private resolves: Resolves = resolvesAtCloudflare,
168
112
  ) {}
169
113
 
170
- get hostname(): string {
171
- if (!isQuick(this.cfg)) return this.cfg.hostname;
172
- return liveUrl === null ? '' : new URL(liveUrl).host;
173
- }
174
-
175
114
  private origin(): string {
176
115
  return `http://127.0.0.1:${String(this.port)}`;
177
116
  }
178
117
 
179
- private plan(): Plan {
180
- if (isQuick(this.cfg))
181
- return {
182
- args: ['--no-autoupdate', 'tunnel', '--url', this.origin()],
183
- env: process.env,
184
- mode: 'quick',
185
- };
186
- if (this.token === undefined) this.token = fetchTunnelToken(this.cfg.name);
187
- const args = ['--no-autoupdate', 'tunnel', 'run', '--url', this.origin()];
188
- if (!this.token) args.push(this.cfg.name);
189
- return {
190
- args,
191
- env: this.token ? { ...process.env, TUNNEL_TOKEN: this.token } : process.env,
192
- mode: this.token ? 'token' : 'named',
193
- };
194
- }
195
-
196
118
  private noticeUrl(text: string): void {
197
- if (!isQuick(this.cfg)) return;
198
119
  const url = quickTunnelUrlIn(text);
199
120
  if (url === null || url === liveUrl) return;
200
121
  liveUrl = url;
@@ -213,15 +134,12 @@ export class Tunnel {
213
134
 
214
135
  start(): void {
215
136
  if (this.closed) return;
216
- const plan = this.plan();
217
- log.info(
218
- { tunnel: isQuick(this.cfg) ? 'quick' : this.cfg.name, port: this.port, mode: plan.mode },
219
- 'cloudflared tunnel starting',
137
+ log.info({ port: this.port }, 'cloudflared quick tunnel starting');
138
+ const child = spawn(
139
+ 'cloudflared',
140
+ ['--no-autoupdate', 'tunnel', '--url', this.origin()],
141
+ { stdio: ['ignore', 'pipe', 'pipe'] },
220
142
  );
221
- const child = spawn('cloudflared', plan.args, {
222
- stdio: ['ignore', 'pipe', 'pipe'],
223
- env: plan.env,
224
- });
225
143
  this.child = child;
226
144
  child.stderr?.on('data', (d: Buffer | string) => {
227
145
  const text = (typeof d === 'string' ? d : d.toString('utf8')).trim();
@@ -38,7 +38,7 @@ const VAGUE_MIMES = new Set([
38
38
 
39
39
  const mib = (n: number): string => `${Math.round(n / (1024 * 1024))} MiB`;
40
40
 
41
- export const uploadLimit = (): string =>
41
+ const uploadLimit = (): string =>
42
42
  `${mib(MAX_UPLOAD_BYTES)} (${MAX_UPLOAD_BYTES} bytes)`;
43
43
 
44
44
  const tooLarge = (): string =>
@@ -0,0 +1 @@
1
+ export const METRO_VERSION = process.env.npm_package_version ?? '0.1.0-beta.15';
@@ -56,7 +56,7 @@ export async function assertRenameFreeOfClash(
56
56
  if (clash !== null) throw nameClash(name, clash);
57
57
  }
58
58
 
59
- export async function connectorIdsOfAgent(agentId: string): Promise<string[]> {
59
+ async function connectorIdsOfAgent(agentId: string): Promise<string[]> {
60
60
  const rows = await getDb()
61
61
  .select({ connectorId: agentConnectors.connectorId })
62
62
  .from(agentConnectors)