@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.
- package/dist/agent.js +1 -1
- package/dist/cli.js +1 -1
- package/dist/lock.js +15 -3
- package/dist/root-release-io.js +1 -2
- package/dist/root-release.js +2 -3
- package/dist/route.js +1 -1
- package/dist/service.js +4 -4
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/runtime/node_modules/@metro-labs/core/src/automation-store.ts +1 -1
- package/runtime/node_modules/@metro-labs/core/src/automation-types.ts +2 -2
- package/runtime/node_modules/@metro-labs/core/src/lines.ts +2 -8
- package/runtime/node_modules/@metro-labs/core/src/protocol.ts +0 -6
- package/runtime/node_modules/@metro-labs/core/src/skill-source.ts +1 -1
- package/runtime/node_modules/@metro-labs/core/src/stations/account-status.ts +1 -1
- package/runtime/node_modules/@metro-labs/core/src/stations/attachments.ts +5 -9
- package/runtime/node_modules/@metro-labs/core/src/stations/channel-directory.ts +1 -1
- package/runtime/node_modules/@metro-labs/core/src/stations/mail-text.ts +4 -4
- package/runtime/node_modules/@metro-labs/core/src/stations/mail.ts +3 -2
- package/runtime/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +1 -1
- package/runtime/node_modules/@metro-labs/core/src/stations/oauth.ts +1 -1
- package/runtime/node_modules/@metro-labs/core/src/stations/profile.ts +2 -2
- package/runtime/node_modules/@metro-labs/core/src/stations/types.ts +2 -2
- package/runtime/node_modules/@metro-labs/core/src/train-error.ts +4 -4
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/files.ts +2 -6
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules.ts +6 -6
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/user.ts +0 -24
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/view.ts +3 -3
- package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +3 -4
- package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +4 -9
- package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +1 -6
- package/runtime/node_modules/@metro-labs/daemon/src/channels/inbound.ts +3 -3
- package/runtime/node_modules/@metro-labs/daemon/src/channels/pending.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/claude/skills.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/files/attach-grant.ts +3 -2
- package/runtime/node_modules/@metro-labs/daemon/src/files/attach-serve.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/files/upload-api.ts +3 -7
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/accounts.ts +10 -19
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/call-tools.ts +1 -5
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/event-store.ts +3 -4
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/group-tools.ts +1 -2
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/index.ts +2 -4
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-gate.ts +5 -6
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/server-approvals.ts +2 -5
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session.ts +1 -5
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-catalog.ts +2 -4
- package/runtime/node_modules/@metro-labs/daemon/src/mcp-server/announced.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/metro-user/helper-script.ts +0 -1
- package/runtime/node_modules/@metro-labs/daemon/src/metro-user/install.ts +2 -0
- package/runtime/node_modules/@metro-labs/daemon/src/metro-user/tailscale-guard.ts +26 -0
- package/runtime/node_modules/@metro-labs/daemon/src/monitor/api.ts +1 -3
- package/runtime/node_modules/@metro-labs/daemon/src/routes/gmail-attach.ts +3 -3
- package/runtime/node_modules/@metro-labs/daemon/src/routes/http.ts +6 -21
- package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +1 -5
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-resolve.ts +6 -6
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach.ts +0 -1
- package/runtime/node_modules/@metro-labs/daemon/src/stations/materialize.ts +12 -61
- package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor.ts +13 -25
- package/runtime/node_modules/@metro-labs/daemon/src/stations/threema-callbacks.ts +5 -5
- package/runtime/node_modules/@metro-labs/daemon/src/stations/webhook-endpoints.ts +2 -7
- package/runtime/node_modules/@metro-labs/daemon/src/voice/call.ts +2 -6
- package/runtime/node_modules/@metro-labs/daemon/src/voice/calls.ts +12 -12
- package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +5 -23
- package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +1 -7
- package/runtime/node_modules/@metro-labs/discord-bot/src/channels.ts +2 -2
- package/runtime/node_modules/@metro-labs/discord-bot/src/group.ts +10 -35
- package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +5 -11
- package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +6 -34
- package/runtime/node_modules/@metro-labs/discord-bot/src/profile.ts +11 -14
- package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +1 -3
- package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +2 -3
- package/runtime/node_modules/@metro-labs/gmail/src/accounts.ts +3 -3
- package/runtime/node_modules/@metro-labs/gmail/src/auth.ts +8 -8
- package/runtime/node_modules/@metro-labs/gmail/src/managed.ts +3 -2
- package/runtime/node_modules/@metro-labs/http/src/agent-signature.ts +1 -1
- package/runtime/node_modules/@metro-labs/http/src/api-http.ts +3 -4
- package/runtime/node_modules/@metro-labs/http/src/connector-discovery.ts +0 -113
- package/runtime/node_modules/@metro-labs/http/src/connector-input.ts +1 -1
- package/runtime/node_modules/@metro-labs/http/src/connector-mcp.ts +12 -13
- package/runtime/node_modules/@metro-labs/http/src/connector-oauth.ts +1 -1
- package/runtime/node_modules/@metro-labs/http/src/connector-policy.ts +1 -6
- package/runtime/node_modules/@metro-labs/http/src/connector-public-url.ts +4 -2
- package/runtime/node_modules/@metro-labs/http/src/connector-tools.ts +1 -11
- package/runtime/node_modules/@metro-labs/http/src/connector-url.ts +0 -6
- package/runtime/node_modules/@metro-labs/http/src/mcp-server-protocol.ts +1 -1
- package/runtime/node_modules/@metro-labs/http/src/mcp-server-tools.ts +0 -2
- package/runtime/node_modules/@metro-labs/http/src/mode-api.ts +1 -1
- package/runtime/node_modules/@metro-labs/http/src/return-to.ts +2 -2
- package/runtime/node_modules/@metro-labs/http/src/workos-token.ts +6 -7
- package/runtime/node_modules/@metro-labs/inference/package.json +1 -0
- package/runtime/node_modules/@metro-labs/inference/src/attempts.ts +3 -3
- package/runtime/node_modules/@metro-labs/inference/src/bedrock.ts +1 -1
- package/runtime/node_modules/@metro-labs/inference/src/codex-translate.ts +2 -2
- package/runtime/node_modules/@metro-labs/inference/src/delivered.ts +1 -1
- package/runtime/node_modules/@metro-labs/inference/src/effort.ts +0 -6
- package/runtime/node_modules/@metro-labs/inference/src/fallback.ts +7 -5
- package/runtime/node_modules/@metro-labs/inference/src/forward.ts +5 -5
- package/runtime/node_modules/@metro-labs/inference/src/measure.ts +2 -3
- package/runtime/node_modules/@metro-labs/inference/src/named.ts +3 -3
- package/runtime/node_modules/@metro-labs/inference/src/probes.ts +98 -0
- package/runtime/node_modules/@metro-labs/inference/src/usage.ts +5 -5
- package/runtime/node_modules/@metro-labs/outlook/src/accounts.ts +6 -8
- package/runtime/node_modules/@metro-labs/outlook/src/auth.ts +2 -1
- package/runtime/node_modules/@metro-labs/outlook/src/inbound.ts +3 -5
- package/runtime/node_modules/@metro-labs/outlook/src/me.ts +6 -6
- package/runtime/node_modules/@metro-labs/outlook/src/outbound.ts +10 -13
- package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +7 -26
- package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +1 -1
- package/runtime/node_modules/@metro-labs/telegram/src/login.ts +3 -6
- package/runtime/node_modules/@metro-labs/telegram/src/profile.ts +3 -2
- package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +6 -20
- package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +1 -7
- package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +0 -2
- package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +1 -16
- package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +1 -8
- package/runtime/node_modules/@metro-labs/telegram-bot/src/profile.ts +2 -1
- package/runtime/node_modules/@metro-labs/threema/src/actions.ts +1 -8
- package/runtime/node_modules/@metro-labs/threema/src/chats.ts +4 -3
- package/runtime/node_modules/@metro-labs/threema/src/format.ts +15 -17
- package/runtime/node_modules/@metro-labs/threema/src/groups.ts +2 -4
- package/runtime/node_modules/@metro-labs/threema/src/inbound.ts +2 -1
- package/runtime/node_modules/@metro-labs/threema/src/messages.ts +2 -1
- package/runtime/node_modules/@metro-labs/threema/src/outbound.ts +7 -7
- package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +1 -3
- package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +0 -2
- package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +1 -1
- package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +1 -1
- package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +7 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +4 -10
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +4 -5
- package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +14 -26
- package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +1 -7
- package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +17 -58
- package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +1 -6
- package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +10 -31
- package/runtime/node_modules/@metro-labs/xmtp/src/frames.ts +12 -14
- package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +2 -3
- package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +3 -9
- package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +8 -9
- package/runtime/node_modules/@metro-labs/xmtp/src/names.ts +4 -2
- package/runtime/node_modules/@metro-labs/xmtp/src/network.ts +2 -3
- package/runtime/node_modules/@metro-labs/xmtp/src/profile.ts +2 -1
- package/runtime/node_modules/@metro-labs/xmtp/src/read-attachments.ts +2 -2
- package/runtime/node_modules/@metro-labs/xmtp/src/sender.ts +2 -1
- package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +23 -46
- package/runtime/node_modules/@metro-labs/xmtp/src/wallet.ts +5 -4
- package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +12 -15
- package/runtime/runtime.json +1 -1
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/automation-store.ts +1 -1
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/automation-types.ts +2 -2
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/lines.ts +2 -8
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/protocol.ts +0 -6
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/skill-source.ts +1 -1
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/account-status.ts +1 -1
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/attachments.ts +5 -9
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/channel-directory.ts +1 -1
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/mail-text.ts +4 -4
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/mail.ts +3 -2
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +1 -1
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/oauth.ts +1 -1
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/profile.ts +2 -2
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/types.ts +2 -2
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/train-error.ts +4 -4
- package/runtime/sdk-runner/runner.json +1 -1
- package/runtime/sdk-runner/src/approvals.ts +2 -2
- package/runtime/sdk-runner/src/automation-command.ts +2 -2
- package/runtime/sdk-runner/src/calls.ts +0 -6
- package/runtime/sdk-runner/src/errors.ts +1 -1
- package/runtime/sdk-runner/src/link.ts +1 -1
- package/runtime/sdk-runner/src/runner.ts +2 -2
- package/runtime/sdk-runner/src/session-watch.ts +5 -34
- package/runtime/sdk-runner/src/task-state.ts +1 -1
- package/runtime/sdk-runner/src/tool-proxy.ts +1 -1
- 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',
|
|
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
|
-
|
|
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 =
|
|
22
|
-
const clientSecret =
|
|
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 =
|
|
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 =
|
|
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: ${
|
|
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 =
|
|
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 (
|
|
29
|
-
return value
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
50
|
+
throw refused(RESOURCE_REFUSED);
|
|
49
51
|
}
|
|
50
|
-
throw refused(
|
|
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,
|
|
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
|
-
|
|
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
|
{
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
const PREVIEW = /^([a-z0-9-]+--)?metro-ui\.netlify\.app$/;
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 !== '
|
|
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
|
|
95
|
+
return isRecord(parsed) ? parsed : null;
|
|
97
96
|
} catch {
|
|
98
97
|
return null;
|
|
99
98
|
}
|
|
@@ -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
|
-
|
|
6
|
+
type AttemptOutcome = 'ok' | 'rate_limited' | 'refused' | 'provider_error' | 'cancelled';
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
type AttemptKind = 'main' | 'subagent' | 'compaction' | 'chore';
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
interface AttemptTokens {
|
|
11
11
|
input: number;
|
|
12
12
|
output: number;
|
|
13
13
|
cacheRead: number;
|
|
@@ -13,12 +13,12 @@ const HASH_LEN = 8;
|
|
|
13
13
|
|
|
14
14
|
type Item = Record<string, unknown>;
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
interface TranslateOptions {
|
|
17
17
|
promptCacheKey: string;
|
|
18
18
|
names?: ToolNames;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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
|
}
|