@stage-labs/metro 0.1.0-beta.324 → 0.1.0-beta.326

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 (176) hide show
  1. package/dist/agent.js +1 -1
  2. package/dist/cli.js +1 -1
  3. package/dist/lock.js +15 -3
  4. package/dist/root-release-io.js +1 -2
  5. package/dist/root-release.js +2 -3
  6. package/dist/route.js +1 -1
  7. package/dist/service.js +4 -4
  8. package/dist/version.js +1 -1
  9. package/package.json +1 -1
  10. package/runtime/node_modules/@metro-labs/core/src/automation-store.ts +1 -1
  11. package/runtime/node_modules/@metro-labs/core/src/automation-types.ts +2 -2
  12. package/runtime/node_modules/@metro-labs/core/src/lines.ts +2 -8
  13. package/runtime/node_modules/@metro-labs/core/src/protocol.ts +0 -6
  14. package/runtime/node_modules/@metro-labs/core/src/skill-source.ts +1 -1
  15. package/runtime/node_modules/@metro-labs/core/src/stations/account-status.ts +1 -1
  16. package/runtime/node_modules/@metro-labs/core/src/stations/attachments.ts +5 -9
  17. package/runtime/node_modules/@metro-labs/core/src/stations/channel-directory.ts +1 -1
  18. package/runtime/node_modules/@metro-labs/core/src/stations/mail-text.ts +4 -4
  19. package/runtime/node_modules/@metro-labs/core/src/stations/mail.ts +3 -2
  20. package/runtime/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +1 -1
  21. package/runtime/node_modules/@metro-labs/core/src/stations/oauth.ts +1 -1
  22. package/runtime/node_modules/@metro-labs/core/src/stations/profile.ts +2 -2
  23. package/runtime/node_modules/@metro-labs/core/src/stations/types.ts +2 -2
  24. package/runtime/node_modules/@metro-labs/core/src/train-error.ts +4 -4
  25. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/files.ts +2 -6
  26. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules.ts +6 -6
  27. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/user.ts +0 -24
  28. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/view.ts +3 -3
  29. package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +3 -4
  30. package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +4 -9
  31. package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +1 -6
  32. package/runtime/node_modules/@metro-labs/daemon/src/channels/inbound.ts +3 -3
  33. package/runtime/node_modules/@metro-labs/daemon/src/channels/pending.ts +2 -2
  34. package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +1 -1
  35. package/runtime/node_modules/@metro-labs/daemon/src/claude/skills.ts +1 -1
  36. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-grant.ts +3 -2
  37. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-serve.ts +1 -1
  38. package/runtime/node_modules/@metro-labs/daemon/src/files/upload-api.ts +3 -7
  39. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +2 -2
  40. package/runtime/node_modules/@metro-labs/daemon/src/mcp/accounts.ts +10 -19
  41. package/runtime/node_modules/@metro-labs/daemon/src/mcp/call-tools.ts +1 -5
  42. package/runtime/node_modules/@metro-labs/daemon/src/mcp/event-store.ts +3 -4
  43. package/runtime/node_modules/@metro-labs/daemon/src/mcp/group-tools.ts +1 -2
  44. package/runtime/node_modules/@metro-labs/daemon/src/mcp/index.ts +2 -4
  45. package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-gate.ts +5 -6
  46. package/runtime/node_modules/@metro-labs/daemon/src/mcp/server-approvals.ts +2 -5
  47. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session.ts +1 -5
  48. package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-catalog.ts +2 -4
  49. package/runtime/node_modules/@metro-labs/daemon/src/mcp-server/announced.ts +1 -1
  50. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/helper-script.ts +0 -1
  51. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/install.ts +2 -0
  52. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/tailscale-guard.ts +26 -0
  53. package/runtime/node_modules/@metro-labs/daemon/src/monitor/api.ts +1 -3
  54. package/runtime/node_modules/@metro-labs/daemon/src/routes/gmail-attach.ts +3 -3
  55. package/runtime/node_modules/@metro-labs/daemon/src/routes/http.ts +6 -21
  56. package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +1 -5
  57. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-resolve.ts +6 -6
  58. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach.ts +0 -1
  59. package/runtime/node_modules/@metro-labs/daemon/src/stations/materialize.ts +12 -61
  60. package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor.ts +13 -25
  61. package/runtime/node_modules/@metro-labs/daemon/src/stations/threema-callbacks.ts +5 -5
  62. package/runtime/node_modules/@metro-labs/daemon/src/stations/webhook-endpoints.ts +2 -7
  63. package/runtime/node_modules/@metro-labs/daemon/src/voice/call.ts +2 -6
  64. package/runtime/node_modules/@metro-labs/daemon/src/voice/calls.ts +12 -12
  65. package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +5 -23
  66. package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +1 -7
  67. package/runtime/node_modules/@metro-labs/discord-bot/src/channels.ts +2 -2
  68. package/runtime/node_modules/@metro-labs/discord-bot/src/group.ts +10 -35
  69. package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +5 -11
  70. package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +6 -34
  71. package/runtime/node_modules/@metro-labs/discord-bot/src/profile.ts +11 -14
  72. package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +1 -3
  73. package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +2 -3
  74. package/runtime/node_modules/@metro-labs/gmail/src/accounts.ts +3 -3
  75. package/runtime/node_modules/@metro-labs/gmail/src/auth.ts +8 -8
  76. package/runtime/node_modules/@metro-labs/gmail/src/managed.ts +3 -2
  77. package/runtime/node_modules/@metro-labs/http/src/agent-signature.ts +1 -1
  78. package/runtime/node_modules/@metro-labs/http/src/api-http.ts +3 -4
  79. package/runtime/node_modules/@metro-labs/http/src/connector-discovery.ts +0 -113
  80. package/runtime/node_modules/@metro-labs/http/src/connector-input.ts +1 -1
  81. package/runtime/node_modules/@metro-labs/http/src/connector-mcp.ts +12 -13
  82. package/runtime/node_modules/@metro-labs/http/src/connector-oauth.ts +1 -1
  83. package/runtime/node_modules/@metro-labs/http/src/connector-policy.ts +1 -6
  84. package/runtime/node_modules/@metro-labs/http/src/connector-public-url.ts +4 -2
  85. package/runtime/node_modules/@metro-labs/http/src/connector-tools.ts +1 -11
  86. package/runtime/node_modules/@metro-labs/http/src/connector-url.ts +0 -6
  87. package/runtime/node_modules/@metro-labs/http/src/mcp-server-protocol.ts +1 -1
  88. package/runtime/node_modules/@metro-labs/http/src/mcp-server-tools.ts +0 -2
  89. package/runtime/node_modules/@metro-labs/http/src/mode-api.ts +1 -1
  90. package/runtime/node_modules/@metro-labs/http/src/return-to.ts +2 -2
  91. package/runtime/node_modules/@metro-labs/http/src/workos-token.ts +6 -7
  92. package/runtime/node_modules/@metro-labs/inference/package.json +1 -0
  93. package/runtime/node_modules/@metro-labs/inference/src/attempts.ts +3 -3
  94. package/runtime/node_modules/@metro-labs/inference/src/bedrock.ts +1 -1
  95. package/runtime/node_modules/@metro-labs/inference/src/codex-translate.ts +2 -2
  96. package/runtime/node_modules/@metro-labs/inference/src/delivered.ts +1 -1
  97. package/runtime/node_modules/@metro-labs/inference/src/effort.ts +0 -6
  98. package/runtime/node_modules/@metro-labs/inference/src/fallback.ts +7 -5
  99. package/runtime/node_modules/@metro-labs/inference/src/forward.ts +5 -5
  100. package/runtime/node_modules/@metro-labs/inference/src/measure.ts +2 -3
  101. package/runtime/node_modules/@metro-labs/inference/src/named.ts +3 -3
  102. package/runtime/node_modules/@metro-labs/inference/src/probes.ts +98 -0
  103. package/runtime/node_modules/@metro-labs/inference/src/usage.ts +5 -5
  104. package/runtime/node_modules/@metro-labs/outlook/src/accounts.ts +6 -8
  105. package/runtime/node_modules/@metro-labs/outlook/src/auth.ts +2 -1
  106. package/runtime/node_modules/@metro-labs/outlook/src/inbound.ts +3 -5
  107. package/runtime/node_modules/@metro-labs/outlook/src/me.ts +6 -6
  108. package/runtime/node_modules/@metro-labs/outlook/src/outbound.ts +10 -13
  109. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +7 -26
  110. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +1 -1
  111. package/runtime/node_modules/@metro-labs/telegram/src/login.ts +3 -6
  112. package/runtime/node_modules/@metro-labs/telegram/src/profile.ts +3 -2
  113. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +6 -20
  114. package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +1 -7
  115. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +0 -2
  116. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +1 -16
  117. package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +1 -8
  118. package/runtime/node_modules/@metro-labs/telegram-bot/src/profile.ts +2 -1
  119. package/runtime/node_modules/@metro-labs/threema/src/actions.ts +1 -8
  120. package/runtime/node_modules/@metro-labs/threema/src/chats.ts +4 -3
  121. package/runtime/node_modules/@metro-labs/threema/src/format.ts +15 -17
  122. package/runtime/node_modules/@metro-labs/threema/src/groups.ts +2 -4
  123. package/runtime/node_modules/@metro-labs/threema/src/inbound.ts +2 -1
  124. package/runtime/node_modules/@metro-labs/threema/src/messages.ts +2 -1
  125. package/runtime/node_modules/@metro-labs/threema/src/outbound.ts +7 -7
  126. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +1 -3
  127. package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +0 -2
  128. package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +1 -1
  129. package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +1 -1
  130. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +7 -0
  131. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +4 -10
  132. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +4 -5
  133. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +14 -26
  134. package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +1 -7
  135. package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +17 -58
  136. package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +1 -6
  137. package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +10 -31
  138. package/runtime/node_modules/@metro-labs/xmtp/src/frames.ts +12 -14
  139. package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +2 -3
  140. package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +3 -9
  141. package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +8 -9
  142. package/runtime/node_modules/@metro-labs/xmtp/src/names.ts +4 -2
  143. package/runtime/node_modules/@metro-labs/xmtp/src/network.ts +2 -3
  144. package/runtime/node_modules/@metro-labs/xmtp/src/profile.ts +2 -1
  145. package/runtime/node_modules/@metro-labs/xmtp/src/read-attachments.ts +2 -2
  146. package/runtime/node_modules/@metro-labs/xmtp/src/sender.ts +2 -1
  147. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +23 -46
  148. package/runtime/node_modules/@metro-labs/xmtp/src/wallet.ts +5 -4
  149. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +12 -15
  150. package/runtime/runtime.json +1 -1
  151. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/automation-store.ts +1 -1
  152. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/automation-types.ts +2 -2
  153. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/lines.ts +2 -8
  154. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/protocol.ts +0 -6
  155. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/skill-source.ts +1 -1
  156. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/account-status.ts +1 -1
  157. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/attachments.ts +5 -9
  158. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/channel-directory.ts +1 -1
  159. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/mail-text.ts +4 -4
  160. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/mail.ts +3 -2
  161. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +1 -1
  162. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/oauth.ts +1 -1
  163. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/profile.ts +2 -2
  164. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/types.ts +2 -2
  165. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/train-error.ts +4 -4
  166. package/runtime/sdk-runner/runner.json +1 -1
  167. package/runtime/sdk-runner/src/approvals.ts +2 -2
  168. package/runtime/sdk-runner/src/automation-command.ts +2 -2
  169. package/runtime/sdk-runner/src/calls.ts +0 -6
  170. package/runtime/sdk-runner/src/errors.ts +1 -1
  171. package/runtime/sdk-runner/src/link.ts +1 -1
  172. package/runtime/sdk-runner/src/runner.ts +2 -2
  173. package/runtime/sdk-runner/src/session-watch.ts +5 -34
  174. package/runtime/sdk-runner/src/task-state.ts +1 -1
  175. package/runtime/sdk-runner/src/tool-proxy.ts +1 -1
  176. package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +0 -40
@@ -1,3 +1,4 @@
1
+ import { errMsg } from '@metro-labs/core/log';
1
2
  import { makeAccountStore, resolveAccountId, type Die } from '@metro-labs/core/stations/account-store';
2
3
  import { threadOfLine } from '@metro-labs/core/stations/mail';
3
4
  import { TokenKeeper, type FetchLike } from '@metro-labs/core/stations/oauth';
@@ -43,7 +44,7 @@ export const { loadAccounts } = makeAccountStore<AccountConfig>({
43
44
  });
44
45
 
45
46
  const signedOut = (err: unknown): never => {
46
- throw new TrainError('gmail_signed_out', err instanceof Error ? err.message : String(err), { retryable: false });
47
+ throw new TrainError('gmail_signed_out', errMsg(err), { retryable: false });
47
48
  };
48
49
 
49
50
  export class Account {
@@ -63,10 +64,9 @@ export class Account {
63
64
  const client = { clientId: cfg.clientId ?? '', clientSecret: cfg.clientSecret ?? '' };
64
65
  this.keeper = new TokenKeeper(this.state, async (refreshToken) => {
65
66
  checkManagedBinding(cfg);
66
- const tokens = await (cfg.managed === true
67
+ return (cfg.managed === true
67
68
  ? renewManaged(refreshToken, this.state.refreshGrant ?? '', cfg.managedHost ?? '', this.fetchImpl)
68
69
  : refreshTokens(client, refreshToken, this.fetchImpl, Date.now(), cfg.sendEnabled)).catch(signedOut);
69
- return tokens;
70
70
  }, () => {
71
71
  this.save();
72
72
  });
@@ -1,3 +1,5 @@
1
+ import { errMsg } from '@metro-labs/core/log';
2
+ import { str } from '@metro-labs/core/stations/mail';
1
3
  import { postForm, SignInError, tokensOf, type FetchLike, type OAuthBody, type Tokens } from '@metro-labs/core/stations/oauth';
2
4
  import { apiBase, authorizeBase, redirectUri, scopesFor, tokenUrl } from './config.js';
3
5
 
@@ -15,11 +17,9 @@ export interface GmailClient {
15
17
 
16
18
  const CLIENT_ID_RE = /^[\w.-]+\.apps\.googleusercontent\.com$/;
17
19
 
18
- const text = (v: unknown): string => (typeof v === 'string' ? v.trim() : '');
19
-
20
20
  export function clientOf(rawId: unknown, rawSecret: unknown): GmailClient {
21
- const clientId = text(rawId);
22
- const clientSecret = text(rawSecret);
21
+ const clientId = str(rawId);
22
+ const clientSecret = str(rawSecret);
23
23
  if (!CLIENT_ID_RE.test(clientId))
24
24
  throw new SignInError('Paste the client ID of your Google OAuth client. It ends with .apps.googleusercontent.com.');
25
25
  if (clientSecret === '' || /\s/.test(clientSecret)) throw new SignInError('Paste the client secret of your Google OAuth client.');
@@ -27,13 +27,13 @@ export function clientOf(rawId: unknown, rawSecret: unknown): GmailClient {
27
27
  }
28
28
 
29
29
  export function failureOf(body: OAuthBody): string {
30
- const error = text(body.error);
30
+ const error = str(body.error);
31
31
  if (error === 'access_denied') return 'The sign-in was declined, so nothing was connected.';
32
32
  if (error === 'invalid_client' || error === 'unauthorized_client')
33
33
  return 'Google does not accept this client ID and secret. Check both on the Clients page of your Google Cloud project.';
34
34
  if (error === 'redirect_uri_mismatch') return `Your Google OAuth client must list ${redirectUri()} as an authorized redirect URI.`;
35
35
  if (error === 'admin_policy_enforced') return 'Your Google Workspace administrator blocks this app. Trust it in the Admin console under API controls, then start again.';
36
- const detail = text(body.error_description);
36
+ const detail = str(body.error_description);
37
37
  return detail === '' ? `Google refused the sign-in (${error === '' ? 'no reason given' : error}).` : `Google refused the sign-in: ${detail}`;
38
38
  }
39
39
 
@@ -101,14 +101,14 @@ export async function verifyMailbox(accessToken: string, fetchImpl: FetchLike, w
101
101
  headers: { authorization: `Bearer ${accessToken}`, accept: 'application/json' },
102
102
  });
103
103
  } catch (err) {
104
- throw new SignInError(`Metro could not reach Gmail: ${err instanceof Error ? err.message : String(err)}`);
104
+ throw new SignInError(`Metro could not reach Gmail: ${errMsg(err)}`);
105
105
  }
106
106
  if (!res.ok) {
107
107
  const hint = res.status === 403 ? ' Check that the Gmail API is enabled in your Google Cloud project.' : '';
108
108
  throw new SignInError(`Gmail refused the new sign-in (${await googleMessage(res)}), so nothing was connected.${hint}`);
109
109
  }
110
110
  const body = (await res.json().catch(() => ({}))) as Record<string, unknown>;
111
- const email = text(body.emailAddress).toLowerCase();
111
+ const email = str(body.emailAddress).toLowerCase();
112
112
  if (email === '') throw new SignInError('Gmail did not say which mailbox this is, so nothing was connected.');
113
113
  if (wanted !== null && wanted !== email) throw new SignInError(wrongMailbox(email, wanted));
114
114
  return email;
@@ -1,3 +1,4 @@
1
+ import { isRecord } from '@metro-labs/core/is-record';
1
2
  import { SignInError, type FetchLike, type Tokens } from '@metro-labs/core/stations/oauth';
2
3
 
3
4
  const BASE = 'https://api.metro.box/api/gmail';
@@ -25,8 +26,8 @@ export async function managedRequest(path: 'start' | 'exchange' | 'cancel' | 're
25
26
  throw new SignInError(`Metro could not complete managed Gmail sign-in. ${retry}`);
26
27
  }
27
28
  const value: unknown = await response.json().catch(() => null);
28
- if (value === null || typeof value !== 'object' || Array.isArray(value)) throw new SignInError('Metro returned an invalid Gmail answer.');
29
- return value as Record<string, unknown>;
29
+ if (!isRecord(value)) throw new SignInError('Metro returned an invalid Gmail answer.');
30
+ return value;
30
31
  }
31
32
 
32
33
  export function managedTokens(value: Record<string, unknown>): ManagedTokens {
@@ -1,7 +1,7 @@
1
1
  import { createHash, createPublicKey, randomBytes, sign, verify, type KeyObject } from 'node:crypto';
2
2
  import { strongEd25519 } from './ed25519.js';
3
3
 
4
- export const AGENT_SCHEME = 'MetroAgent';
4
+ const AGENT_SCHEME = 'MetroAgent';
5
5
  export const AGENT_CLOCK_SKEW_MS = 5 * 60_000;
6
6
 
7
7
  const VERSION = 'metro-agent-request/v1';
@@ -1,10 +1,11 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
2
  import { ApiError } from './api-error.js';
3
+ import { isRecord } from '@metro-labs/core/is-record';
3
4
  import { errMsg, log } from '@metro-labs/core/log';
4
5
 
5
6
  const BODY_MAX = 4 * 1024;
6
7
 
7
- export type Role = 'admin' | 'member';
8
+ type Role = 'admin' | 'member';
8
9
 
9
10
  export interface ApiSession {
10
11
  subject: string;
@@ -151,9 +152,7 @@ export function sessionRoute(
151
152
  }
152
153
 
153
154
  export function bodyField(body: unknown, key: string): unknown {
154
- return typeof body === 'object' && body !== null && !Array.isArray(body)
155
- ? (body as Record<string, unknown>)[key]
156
- : undefined;
155
+ return isRecord(body) ? body[key] : undefined;
157
156
  }
158
157
 
159
158
  export const stringOf = (value: unknown): string => (typeof value === 'string' ? value : '');
@@ -1,10 +1,3 @@
1
- import { isRecord } from '@metro-labs/core/is-record';
2
- import type { FetchLike } from '@metro-labs/core/stations/oauth';
3
- import { stringOf } from './api-http.js';
4
- import { parseConnectorUrl, refused } from './connector-url.js';
5
-
6
- const DISCOVERY_TIMEOUT_MS = 10_000;
7
-
8
1
  export interface OAuthServer {
9
2
  issuer: string;
10
3
  authorizationEndpoint: string;
@@ -14,30 +7,6 @@ export interface OAuthServer {
14
7
  scopes: string[];
15
8
  }
16
9
 
17
- interface ResourceMetadata {
18
- issuer: URL;
19
- scopes: string[];
20
- }
21
-
22
- async function getJson(url: URL, fetchImpl: FetchLike): Promise<Record<string, unknown> | null> {
23
- let res: Response;
24
- try {
25
- res = await fetchImpl(url.toString(), {
26
- redirect: 'manual',
27
- signal: AbortSignal.timeout(DISCOVERY_TIMEOUT_MS),
28
- headers: { accept: 'application/json' },
29
- });
30
- } catch {
31
- return null;
32
- }
33
- if (!res.ok) {
34
- await res.body?.cancel().catch(() => undefined);
35
- return null;
36
- }
37
- const body: unknown = await res.json().catch(() => null);
38
- return isRecord(body) ? body : null;
39
- }
40
-
41
10
  function wellKnown(base: URL, suffix: string): URL {
42
11
  const url = new URL(base.toString());
43
12
  url.pathname = `/.well-known/${suffix}`;
@@ -56,77 +25,6 @@ export function resourceMetadataUrls(resource: URL): URL[] {
56
25
  return urls;
57
26
  }
58
27
 
59
- export async function advertisesOAuth(resource: URL, fetchImpl: FetchLike = fetch): Promise<boolean> {
60
- for (const candidate of resourceMetadataUrls(resource)) {
61
- const body = await getJson(candidate, fetchImpl);
62
- const servers: unknown = body?.authorization_servers;
63
- const first: unknown = Array.isArray(servers) ? servers[0] : undefined;
64
- if (typeof first === 'string' && first !== '') return true;
65
- }
66
- return false;
67
- }
68
-
69
- function scopesOf(body: Record<string, unknown> | null): string[] {
70
- const raw: unknown = body?.scopes_supported;
71
- if (!Array.isArray(raw)) return [];
72
- const out: string[] = [];
73
- for (const entry of raw) {
74
- const scope = stringOf(entry).trim();
75
- if (scope !== '' && !/\s/.test(scope) && !out.includes(scope)) out.push(scope);
76
- }
77
- return out;
78
- }
79
-
80
- async function resourceMetadata(resource: URL, fetchImpl: FetchLike): Promise<ResourceMetadata> {
81
- for (const candidate of resourceMetadataUrls(resource)) {
82
- const body = await getJson(candidate, fetchImpl);
83
- const servers: unknown = body?.authorization_servers;
84
- const first: unknown = Array.isArray(servers) ? servers[0] : undefined;
85
- if (typeof first === 'string' && first !== '')
86
- return { issuer: parseConnectorUrl(first), scopes: scopesOf(body) };
87
- }
88
- return { issuer: parseConnectorUrl(resource.origin), scopes: [] };
89
- }
90
-
91
- const webAddress = (text: string): boolean => {
92
- try {
93
- parseConnectorUrl(text);
94
- return true;
95
- } catch {
96
- return false;
97
- }
98
- };
99
-
100
- function endpointOf(raw: unknown, issuer: URL): string {
101
- const text = stringOf(raw);
102
- if (text !== '' && !webAddress(text)) throw refused(`${issuer.hostname} advertises a sign-in address Metro will not open.`);
103
- return text;
104
- }
105
-
106
- function toServer(
107
- body: Record<string, unknown>,
108
- issuer: URL,
109
- scopes: string[],
110
- ): OAuthServer {
111
- const authorizationEndpoint = endpointOf(body.authorization_endpoint, issuer);
112
- const tokenEndpoint = endpointOf(body.token_endpoint, issuer);
113
- if (authorizationEndpoint === '' || tokenEndpoint === '')
114
- throw refused(
115
- `${issuer.hostname} advertises OAuth but not where to sign in.`,
116
- );
117
- const methods = body.code_challenge_methods_supported;
118
- const offered = stringOf(body.registration_endpoint);
119
- const registration = webAddress(offered) ? offered : '';
120
- return {
121
- issuer: stringOf(body.issuer) === '' ? issuer.origin : stringOf(body.issuer),
122
- authorizationEndpoint,
123
- tokenEndpoint,
124
- registrationEndpoint: registration === '' ? null : registration,
125
- supportsS256: Array.isArray(methods) ? methods.includes('S256') : false,
126
- scopes,
127
- };
128
- }
129
-
130
28
  export function authServerMetadataUrls(issuer: URL): URL[] {
131
29
  const path = issuer.pathname.replace(/\/$/, '');
132
30
  const suffixes = ['oauth-authorization-server', 'openid-configuration'];
@@ -137,14 +35,3 @@ export function authServerMetadataUrls(issuer: URL): URL[] {
137
35
  );
138
36
  return [...aware, new URL(`${path}/.well-known/openid-configuration`, issuer.origin), ...urls];
139
37
  }
140
-
141
- export async function discoverOAuth(resource: URL, fetchImpl: FetchLike = fetch): Promise<OAuthServer> {
142
- const { issuer, scopes } = await resourceMetadata(resource, fetchImpl);
143
- for (const candidate of authServerMetadataUrls(issuer)) {
144
- const body = await getJson(candidate, fetchImpl);
145
- if (body !== null) return toServer(body, issuer, scopes);
146
- }
147
- throw refused(
148
- `${resource.hostname} needs authorization, but publishes no OAuth metadata Metro can use.`,
149
- );
150
- }
@@ -10,7 +10,7 @@ const CLIENT_ID_RE = /^[\x21-\x7e]{1,256}$/;
10
10
  const CLIENT_SECRET_RE = /^[\x21-\x7e]{1,1024}$/;
11
11
  const DEFAULT_HEADER = 'Authorization';
12
12
 
13
- export class ConnectorError extends ApiError {}
13
+ class ConnectorError extends ApiError {}
14
14
 
15
15
  function hasControlChar(value: string): boolean {
16
16
  for (const ch of value) {
@@ -10,7 +10,7 @@ export interface OAuthTokens {
10
10
  expiresAt?: number;
11
11
  }
12
12
 
13
- export interface OAuthAuth extends OAuthTokens {
13
+ interface OAuthAuth extends OAuthTokens {
14
14
  kind: 'oauth';
15
15
  issuer: string;
16
16
  tokenEndpoint: string;
@@ -30,10 +30,6 @@ export interface VerifiedServer {
30
30
  server: string;
31
31
  }
32
32
 
33
- export interface VerifiedRecord extends VerifiedServer {
34
- at: string;
35
- }
36
-
37
33
  const PROBE_TIMEOUT_MS = 10_000;
38
34
 
39
35
  export const INITIALIZE = {
@@ -49,12 +45,21 @@ export function authHeaders(auth: ConnectorAuth): Record<string, string> {
49
45
  return {};
50
46
  }
51
47
 
52
- export function payloadOf(text: string, contentType: string): string {
48
+ function payloadOf(text: string, contentType: string): string {
53
49
  if (!contentType.includes('text/event-stream')) return text;
54
50
  for (const line of text.replace(/\r\n/g, '\n').split('\n')) if (line.startsWith('data:')) return line.slice(5).trim();
55
51
  return '';
56
52
  }
57
53
 
54
+ export function resultOf(text: string, contentType: string): Record<string, unknown> | null {
55
+ try {
56
+ const parsed: unknown = JSON.parse(payloadOf(text, contentType));
57
+ return isRecord(parsed) && isRecord(parsed.result) ? parsed.result : null;
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
58
63
  export async function mcpPost(
59
64
  url: URL,
60
65
  auth: ConnectorAuth,
@@ -77,13 +82,7 @@ export async function mcpPost(
77
82
  }
78
83
 
79
84
  function serverNameOf(url: URL, text: string, contentType: string): string {
80
- let parsed: unknown;
81
- try {
82
- parsed = JSON.parse(payloadOf(text, contentType));
83
- } catch {
84
- throw refused(`${url.hostname} answered, but it does not speak MCP.`);
85
- }
86
- const result = isRecord(parsed) && isRecord(parsed.result) ? parsed.result : null;
85
+ const result = resultOf(text, contentType);
87
86
  if (result === null || typeof result.protocolVersion !== 'string') throw refused(`${url.hostname} answered, but it does not speak MCP.`);
88
87
  const info = isRecord(result.serverInfo) ? result.serverInfo : {};
89
88
  const title = typeof info.title === 'string' ? info.title : '';
@@ -22,7 +22,7 @@ export function newVerifier(): string {
22
22
  return randomBytes(32).toString('base64url');
23
23
  }
24
24
 
25
- export function challengeOf(verifier: string): string {
25
+ function challengeOf(verifier: string): string {
26
26
  return createHash('sha256').update(verifier).digest('base64url');
27
27
  }
28
28
 
@@ -1,6 +1,6 @@
1
1
  import { isRecord } from '@metro-labs/core/is-record';
2
2
 
3
- export type ConnectorDecision = 'allow' | 'ask' | 'deny';
3
+ type ConnectorDecision = 'allow' | 'ask' | 'deny';
4
4
 
5
5
  type ToolGroup = 'read' | 'write';
6
6
 
@@ -45,11 +45,6 @@ function toolPolicyOf(value: unknown): Record<string, ConnectorDecision> {
45
45
  return out;
46
46
  }
47
47
 
48
- export function connectorPolicyLayersOf(value: unknown): ConnectorPolicyLayers {
49
- if (!isRecord(value) || Object.keys(value).some((key) => !['organization', 'agent'].includes(key))) throw new Error('Invalid connector policy layers.');
50
- return { organization: connectorPolicyOf(value.organization), agent: connectorPolicyOf(value.agent) };
51
- }
52
-
53
48
  const strictest = (...values: (ConnectorDecision | undefined)[]): ConnectorDecision => values.reduce<ConnectorDecision>((held, next) => next !== undefined && RANK[next] > RANK[held] ? next : held, 'allow');
54
49
  const toolRule = (policy: ConnectorPolicy, name: string): ConnectorDecision | undefined => policy.tools !== undefined && Object.hasOwn(policy.tools, name) ? policy.tools[name] : undefined;
55
50
  const toolGroup = (groups: Record<string, ToolGroup>, name: string): ToolGroup => Object.hasOwn(groups, name) && groups[name] === 'read' ? 'read' : 'write';
@@ -40,14 +40,16 @@ export function parsePublicConnectorUrl(raw: unknown): URL {
40
40
  return url;
41
41
  }
42
42
 
43
+ const RESOURCE_REFUSED = 'The public OAuth resource must be a public HTTPS identifier without a query, user information or a fragment. Do not enter credentials.';
44
+
43
45
  export function publicOAuthResource(raw: unknown): string {
44
46
  try {
45
47
  const url = parsePublicConnectorUrl(raw);
46
48
  if (typeof raw === 'string' && !raw.includes('?') && url.search === '') return raw;
47
49
  } catch {
48
- throw refused('The public OAuth resource must be a public HTTPS identifier without a query, user information or a fragment. Do not enter credentials.');
50
+ throw refused(RESOURCE_REFUSED);
49
51
  }
50
- throw refused('The public OAuth resource must be a public HTTPS identifier without a query, user information or a fragment. Do not enter credentials.');
52
+ throw refused(RESOURCE_REFUSED);
51
53
  }
52
54
 
53
55
  export function optionalOAuthResource(raw: unknown): string | undefined {
@@ -1,7 +1,7 @@
1
1
  import { isRecord } from '@metro-labs/core/is-record';
2
2
  import { errMsg, log } from '@metro-labs/core/log';
3
3
  import type { FetchLike } from '@metro-labs/core/stations/oauth';
4
- import { authHeaders, INITIALIZE, mcpPost, payloadOf, type ConnectorAuth } from './connector-mcp.js';
4
+ import { authHeaders, INITIALIZE, mcpPost, resultOf, type ConnectorAuth } from './connector-mcp.js';
5
5
  import { ConnectorUnauthorized, refused } from './connector-url.js';
6
6
 
7
7
  export interface RemoteTool {
@@ -18,16 +18,6 @@ const TIMEOUT_MS = 15_000;
18
18
  const MAX_PAGES = 100;
19
19
  const MAX_TOOLS = 2000;
20
20
 
21
- function resultOf(text: string, contentType: string): Record<string, unknown> | null {
22
- let parsed: unknown = null;
23
- try {
24
- parsed = JSON.parse(payloadOf(text, contentType));
25
- } catch {
26
- return null;
27
- }
28
- return isRecord(parsed) && isRecord(parsed.result) ? parsed.result : null;
29
- }
30
-
31
21
  async function rpc(url: URL, auth: ConnectorAuth, session: Map<string, string>, body: unknown, fetchImpl?: FetchLike): Promise<Record<string, unknown> | null> {
32
22
  const res = await mcpPost(url, auth, body, { timeoutMs: TIMEOUT_MS, headers: Object.fromEntries(session), fetch: fetchImpl });
33
23
  const id = res.headers.get('mcp-session-id');
@@ -29,9 +29,3 @@ export function parseConnectorUrl(raw: unknown): URL {
29
29
  refuseShape(url);
30
30
  return url;
31
31
  }
32
-
33
- export function connectorUrlText(url: URL): string {
34
- const text = url.toString();
35
- if (url.pathname !== '/' || url.search !== '') return text;
36
- return text.endsWith('/') ? text.slice(0, -1) : text;
37
- }
@@ -4,7 +4,7 @@ export const MCP_PATHS = { token: '/v1/token', approvals: '/v1/approvals', answe
4
4
  export const MCP_FIND_DEFAULT = 5;
5
5
  export const MCP_FIND_MAX = 20;
6
6
  export const MCP_CALLS_MAX = 10;
7
- export const MCP_QUERY_MAX = 500;
7
+ const MCP_QUERY_MAX = 500;
8
8
  export const MCP_APPROVAL_WAIT_MS = 25_000;
9
9
  export const MCP_APPROVAL_ID_RE = /^[a-km-z]{5}$/;
10
10
  export const TOOL_SEARCH = 'tool_search';
@@ -96,8 +96,6 @@ const toolCallDescription = (place: ApprovalPlace): string =>
96
96
  '- vendor_error: the connector answered with its own error, shown as it came.',
97
97
  ].join('\n');
98
98
 
99
- export const TOOL_CALL_DESCRIPTION = toolCallDescription('session');
100
-
101
99
  export function mcpServerTools(connectors: ConnectorLabel[] | null, place: ApprovalPlace = 'session'): McpServerTool[] {
102
100
  return [
103
101
  {
@@ -3,7 +3,7 @@ import { cors, sendJson } from './api-http.js';
3
3
 
4
4
  const PATH = '/api/mode';
5
5
 
6
- export type DaemonMode = 'hosted' | 'local';
6
+ type DaemonMode = 'hosted' | 'local';
7
7
 
8
8
  export interface ModeInfo {
9
9
  mode: DaemonMode;
@@ -1,12 +1,12 @@
1
1
  const PREVIEW = /^([a-z0-9-]+--)?metro-ui\.netlify\.app$/;
2
2
 
3
- export function allowedWebHost(host: string): boolean {
3
+ function allowedWebHost(host: string): boolean {
4
4
  if (host === 'metro.box' || host === 'localhost' || host === '127.0.0.1')
5
5
  return true;
6
6
  return PREVIEW.test(host);
7
7
  }
8
8
 
9
- export const APP_RETURN = 'metro://auth';
9
+ const APP_RETURN = 'metro://auth';
10
10
 
11
11
  export const isAppReturn = (returnTo: string): boolean => returnTo === APP_RETURN;
12
12
 
@@ -1,10 +1,11 @@
1
1
  import type { IncomingMessage } from 'node:http';
2
2
  import { createPublicKey, verify, type JsonWebKey, type KeyObject } from 'node:crypto';
3
+ import { isRecord } from '@metro-labs/core/is-record';
3
4
 
4
5
  const WORKOS_API = 'https://api.workos.com';
5
6
  const ORG_RE = /^org_[A-Za-z0-9]{10,64}$/;
6
7
  export const isOrganizationId = (value: string): boolean => ORG_RE.test(value);
7
- export const DEFAULT_CLIENT_ID = 'client_01M2TJJJ6RM9CT081QB4XZK3G2';
8
+ const DEFAULT_CLIENT_ID = 'client_01M2TJJJ6RM9CT081QB4XZK3G2';
8
9
  const SKEW_S = 60;
9
10
  const REFETCH_COOLDOWN_MS = 60_000;
10
11
  const FETCH_MS = 10_000;
@@ -34,14 +35,12 @@ export const jwksUrl = (id: string, base = WORKOS_API): string => `${base}/sso/j
34
35
  function jwksKeys(text: string): Map<string, KeyObject> {
35
36
  const keys = new Map<string, KeyObject>();
36
37
  const parsed: unknown = JSON.parse(text);
37
- const list = typeof parsed === 'object' && parsed !== null ? (parsed as { keys?: unknown }).keys : undefined;
38
+ const list = isRecord(parsed) ? parsed.keys : undefined;
38
39
  if (!Array.isArray(list)) return keys;
39
40
  for (const jwk of list) {
40
- if (typeof jwk !== 'object' || jwk === null) continue;
41
- const { kid, kty } = jwk as { kid?: unknown; kty?: unknown };
42
- if (typeof kid !== 'string' || kty !== 'RSA') continue;
41
+ if (!isRecord(jwk) || typeof jwk.kid !== 'string' || jwk.kty !== 'RSA') continue;
43
42
  try {
44
- keys.set(kid, createPublicKey({ key: jwk as JsonWebKey, format: 'jwk' }));
43
+ keys.set(jwk.kid, createPublicKey({ key: jwk as JsonWebKey, format: 'jwk' }));
45
44
  } catch {
46
45
  continue;
47
46
  }
@@ -93,7 +92,7 @@ const b64url = (text: string): Buffer => Buffer.from(text.replace(/-/g, '+').rep
93
92
  function jsonPart(text: string): Record<string, unknown> | null {
94
93
  try {
95
94
  const parsed: unknown = JSON.parse(b64url(text).toString('utf8'));
96
- return typeof parsed === 'object' && parsed !== null ? (parsed as Record<string, unknown>) : null;
95
+ return isRecord(parsed) ? parsed : null;
97
96
  } catch {
98
97
  return null;
99
98
  }
@@ -19,6 +19,7 @@
19
19
  "./frames": "./src/frames.ts",
20
20
  "./gateway": "./src/gateway.ts",
21
21
  "./model-config": "./src/model-config.ts",
22
+ "./probes": "./src/probes.ts",
22
23
  "./served": "./src/served.ts",
23
24
  "./tool-search": "./src/tool-search.ts",
24
25
  "./usage": "./src/usage.ts"
@@ -3,11 +3,11 @@ import type { OutgoingHttpHeader, OutgoingHttpHeaders, ServerResponse } from 'no
3
3
  import type { RequestKind } from './effort.js';
4
4
  import type { Route } from './model-config.js';
5
5
 
6
- export type AttemptOutcome = 'ok' | 'rate_limited' | 'refused' | 'provider_error' | 'cancelled';
6
+ type AttemptOutcome = 'ok' | 'rate_limited' | 'refused' | 'provider_error' | 'cancelled';
7
7
 
8
- export type AttemptKind = 'main' | 'subagent' | 'compaction' | 'chore';
8
+ type AttemptKind = 'main' | 'subagent' | 'compaction' | 'chore';
9
9
 
10
- export interface AttemptTokens {
10
+ interface AttemptTokens {
11
11
  input: number;
12
12
  output: number;
13
13
  cacheRead: number;
@@ -27,7 +27,7 @@ export function forgetAdaptations(learned: Adaptations): void {
27
27
  learned.dropCacheTtl = false;
28
28
  }
29
29
 
30
- export interface BedrockUpstream {
30
+ interface BedrockUpstream {
31
31
  settings: Connection;
32
32
  base: string;
33
33
  learned: Adaptations;
@@ -13,12 +13,12 @@ const HASH_LEN = 8;
13
13
 
14
14
  type Item = Record<string, unknown>;
15
15
 
16
- export interface TranslateOptions {
16
+ interface TranslateOptions {
17
17
  promptCacheKey: string;
18
18
  names?: ToolNames;
19
19
  }
20
20
 
21
- export interface ResponsesRequest {
21
+ interface ResponsesRequest {
22
22
  model: string;
23
23
  instructions?: string;
24
24
  input: Item[];
@@ -2,7 +2,7 @@ import { readEffort, type RouteEffort } from '@metro-labs/core/effort';
2
2
  import { isRecord } from '@metro-labs/core/is-record';
3
3
  import { emptySetup, isProvider, type AuthKind, type ModelSetup, type Account, type Chain, type CodexAccess, type Connection, type Fallback, type Provider, type RouteSource } from './model-config.js';
4
4
 
5
- export interface DeliveredRoute {
5
+ interface DeliveredRoute {
6
6
  primary: Fallback | null;
7
7
  fallbacks: Fallback[];
8
8
  helpers: Chain | null;
@@ -86,12 +86,6 @@ export function withEffort(body: Body, effort: string): Body {
86
86
  return { ...top, messages: body.messages.map((message: unknown) => (isRecord(message) ? withOwnEffort(message, effort) : message)) };
87
87
  }
88
88
 
89
- export function withoutEffort(body: Body): Body {
90
- const config = Object.fromEntries(Object.entries(record(body.output_config)).filter(([key]) => key !== 'effort'));
91
- if (Object.keys(config).length > 0) return { ...body, output_config: config };
92
- return Object.fromEntries(Object.entries(body).filter(([key]) => key !== 'output_config'));
93
- }
94
-
95
89
  export function effortToApply(body: Body): string | null {
96
90
  if (chore(body)) return null;
97
91
  const effort = effortOf(body);
@@ -42,7 +42,7 @@ function applies(window: UsageWindow, route: Route, now: number): boolean {
42
42
  return scope === null || route.model.toLowerCase().split(/[^a-z0-9]+/).includes(scope);
43
43
  }
44
44
 
45
- export function windowsFor(route: Route, now = Date.now()): UsageWindow[] {
45
+ function windowsFor(route: Route, now = Date.now()): UsageWindow[] {
46
46
  return (usageOf(route.connection.id)?.windows ?? []).filter((w) => applies(w, route, now));
47
47
  }
48
48
 
@@ -131,20 +131,22 @@ function primaryOf(setup: ModelSetup): Route | null {
131
131
  return { connection: primary.connection, model };
132
132
  }
133
133
 
134
- function rowOf(route: Route, now: number): Omit<ChainRow, 'active'> {
135
- const reason = notReady(route);
134
+ type Readiness = (route: Route) => string | null;
135
+
136
+ function rowOf(route: Route, now: number, ready: Readiness): Omit<ChainRow, 'active'> {
137
+ const reason = ready(route);
136
138
  const used = windowsFor(route, now).reduce<number | null>((most, w) => Math.max(most ?? 0, w.used ?? 0), null);
137
139
  return { connection: route.connection.id, model: route.model, used, hold: reason === null ? holdOf(route, now) : { reason, until: null } };
138
140
  }
139
141
 
140
- export function chainStatus(setup: ModelSetup, now = Date.now()): ChainRow[] {
142
+ export function chainStatus(setup: ModelSetup, now = Date.now(), ready: Readiness = notReady): ChainRow[] {
141
143
  const primary = primaryOf(setup);
142
144
  if (primary === null) return [];
143
145
  const listed = setup.fallbacks.flatMap((f) => {
144
146
  const conn = connectionOf(setup, f.connection);
145
147
  return conn === null ? [] : [{ connection: conn, model: f.model }];
146
148
  });
147
- const rows = unique([primary, ...listed]).map((route) => rowOf(route, now));
149
+ const rows = unique([primary, ...listed]).map((route) => rowOf(route, now, ready));
148
150
  const active = Math.max(0, rows.findIndex((row) => row.hold === null));
149
151
  return rows.map((row, at) => ({ ...row, active: at === active }));
150
152
  }