@stage-labs/metro 0.1.0-beta.2 → 0.1.0-beta.21
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/api.js +103 -0
- package/dist/cli.js +134 -565
- package/dist/control.js +102 -0
- package/dist/prompt.js +10 -0
- package/dist/runtime.js +94 -0
- package/dist/store.js +70 -0
- package/dist/update.js +33 -0
- package/dist/version.js +97 -0
- package/package.json +34 -22
- package/runtime/node_modules/@metro-labs/discord/package.json +28 -0
- package/runtime/node_modules/@metro-labs/discord/src/accounts.ts +119 -0
- package/runtime/node_modules/@metro-labs/discord/src/actions.ts +388 -0
- package/runtime/node_modules/@metro-labs/discord/src/api-base.ts +1 -0
- package/runtime/node_modules/@metro-labs/discord/src/attachments.ts +29 -0
- package/runtime/node_modules/@metro-labs/discord/src/format.ts +210 -0
- package/runtime/node_modules/@metro-labs/discord/src/group-actions.ts +50 -0
- package/runtime/node_modules/@metro-labs/discord/src/group.ts +115 -0
- package/runtime/node_modules/@metro-labs/discord/src/index.ts +127 -0
- package/runtime/node_modules/@metro-labs/discord/src/members.ts +215 -0
- package/runtime/node_modules/@metro-labs/discord/src/send-files.ts +43 -0
- package/runtime/node_modules/@metro-labs/discord/src/station.ts +19 -0
- package/runtime/node_modules/@metro-labs/discord/src/verify.ts +82 -0
- package/runtime/node_modules/@metro-labs/discord/src/voice-speak.ts +195 -0
- package/runtime/node_modules/@metro-labs/discord/src/voice-transcribe.ts +292 -0
- package/runtime/node_modules/@metro-labs/discord/src/voice.ts +197 -0
- package/runtime/node_modules/@metro-labs/discord/src/wire.ts +5 -0
- package/runtime/node_modules/@metro-labs/mcp/package.json +66 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/dedupe.ts +24 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/inbound.ts +367 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/media-note.ts +85 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/pending.ts +75 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/relay.ts +129 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/webhook-note.ts +51 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +311 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +335 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-error.ts +8 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +129 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-grant.ts +99 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-owner.ts +32 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +124 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +278 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +322 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +111 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair.ts +20 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/collection-api.ts +202 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +338 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-json.ts +31 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth-routes.ts +157 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +127 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +100 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-url.ts +57 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +333 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/cors.ts +29 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/crash-guard.ts +49 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/events.ts +117 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-auth.ts +164 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-oauth.ts +208 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +357 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +79 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/log.ts +42 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-client.ts +162 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-discovery.ts +120 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +65 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/pair-codes.ts +58 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +134 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/project-api.ts +214 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/protocol.ts +224 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +125 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-pair.ts +18 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-source.ts +105 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/secure-fs.ts +57 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +27 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +41 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +218 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor-io.ts +97 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor.ts +323 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-call.ts +22 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-error.ts +53 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +169 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +295 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-store.ts +181 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +91 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +215 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-map.ts +71 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-scope.ts +64 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/client.ts +37 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-collections.ts +218 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +147 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +329 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/ids.ts +19 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/key-map.ts +45 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +364 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +300 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +31 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +105 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +131 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/token-carry.ts +45 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +57 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/whatsapp-login.ts +18 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/accounts.ts +145 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/call-tools.ts +203 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/channel-owner.ts +29 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/ctx.ts +62 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/event-store.ts +91 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/frame-scope.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/group-tools.ts +129 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/index.ts +192 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/member-tools.ts +72 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/permission-relay.ts +66 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/raw-get-stream.ts +207 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +73 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-registry.ts +172 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-route.ts +43 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/session.ts +211 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/str.ts +5 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-dispatch.ts +212 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +397 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/upload-tool.ts +66 -0
- package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +240 -0
- package/runtime/node_modules/@metro-labs/mcp/src/server.ts +2 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +80 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-inline.ts +100 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-interactive.ts +161 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-resolve.ts +200 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-xmtp.ts +111 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +182 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attachments.ts +196 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +82 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/messaging-normalize.ts +88 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/registry.ts +35 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/station-runtime.ts +64 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/types.ts +107 -0
- package/runtime/node_modules/@metro-labs/telegram/package.json +25 -0
- package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +120 -0
- package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +295 -0
- package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +128 -0
- package/runtime/node_modules/@metro-labs/telegram/src/format.ts +172 -0
- package/runtime/node_modules/@metro-labs/telegram/src/index.ts +103 -0
- package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +166 -0
- package/runtime/node_modules/@metro-labs/telegram/src/members.ts +47 -0
- package/runtime/node_modules/@metro-labs/telegram/src/station.ts +17 -0
- package/runtime/node_modules/@metro-labs/telegram/src/types.ts +48 -0
- package/runtime/node_modules/@metro-labs/telegram/src/verify.ts +38 -0
- package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +3 -0
- package/runtime/node_modules/@metro-labs/telegram-user/package.json +28 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/accounts.ts +81 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/actions.ts +254 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/attachments.ts +87 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/client.ts +77 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/format.ts +141 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/group.ts +261 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/history.ts +52 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/inbound.ts +78 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/index.ts +46 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/login.ts +166 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/media-actions.ts +80 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/members.ts +75 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/normalize.ts +22 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/reactions.ts +167 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/station.ts +24 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/types.ts +7 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/wire.ts +1 -0
- package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
- package/runtime/node_modules/@metro-labs/webhook/src/station.ts +40 -0
- package/runtime/node_modules/@metro-labs/whatsapp/package.json +29 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +67 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +178 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +72 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +129 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +318 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +132 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +57 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +40 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +59 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/logger.ts +60 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +176 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +84 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +22 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/outbox.ts +38 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +141 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +17 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +98 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +6 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +5 -0
- package/runtime/node_modules/@metro-labs/xmtp/package.json +33 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +113 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +110 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +299 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +157 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +87 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +375 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +107 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +171 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +239 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +55 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +333 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +78 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +34 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +56 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +33 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +125 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +157 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +349 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +13 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +126 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +212 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +90 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +84 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +39 -0
- package/runtime/runtime.json +3 -0
- package/runtime/trains/.keep +0 -0
- package/LICENSE +0 -21
- package/README.md +0 -78
- package/dist/channels/discord.js +0 -148
- package/dist/channels/telegram.js +0 -137
- package/dist/lib/address.js +0 -21
- package/dist/lib/codex-rc.js +0 -274
- package/dist/lib/dotenv.js +0 -31
- package/dist/log.js +0 -12
- package/dist/paths.js +0 -45
- package/dist/tail.js +0 -161
- package/skills/metro/SKILL.md +0 -122
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { ConnectorVerifyError, parseConnectorUrl } from './connector-verify.js';
|
|
2
|
+
|
|
3
|
+
const DISCOVERY_TIMEOUT_MS = 10_000;
|
|
4
|
+
|
|
5
|
+
export interface OAuthServer {
|
|
6
|
+
issuer: string;
|
|
7
|
+
authorizationEndpoint: string;
|
|
8
|
+
tokenEndpoint: string;
|
|
9
|
+
registrationEndpoint: string | null;
|
|
10
|
+
supportsS256: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function refused(message: string): ConnectorVerifyError {
|
|
14
|
+
return new ConnectorVerifyError(message, 400);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
18
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const str = (value: unknown): string =>
|
|
22
|
+
typeof value === 'string' ? value : '';
|
|
23
|
+
|
|
24
|
+
async function getJson(url: URL): Promise<Record<string, unknown> | null> {
|
|
25
|
+
let res: Response;
|
|
26
|
+
try {
|
|
27
|
+
res = await fetch(url, {
|
|
28
|
+
redirect: 'manual',
|
|
29
|
+
signal: AbortSignal.timeout(DISCOVERY_TIMEOUT_MS),
|
|
30
|
+
headers: { accept: 'application/json' },
|
|
31
|
+
});
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
await res.body?.cancel().catch(() => undefined);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const body: unknown = await res.json().catch(() => null);
|
|
40
|
+
return isRecord(body) ? body : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function wellKnown(base: URL, suffix: string): URL {
|
|
44
|
+
const url = new URL(base.toString());
|
|
45
|
+
url.pathname = `/.well-known/${suffix}`;
|
|
46
|
+
url.search = '';
|
|
47
|
+
url.hash = '';
|
|
48
|
+
return url;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function resourceMetadataUrls(resource: URL): URL[] {
|
|
52
|
+
const path = resource.pathname.replace(/\/$/, '');
|
|
53
|
+
const urls = [wellKnown(resource, 'oauth-protected-resource')];
|
|
54
|
+
if (path !== '')
|
|
55
|
+
urls.unshift(
|
|
56
|
+
new URL(`/.well-known/oauth-protected-resource${path}`, resource.origin),
|
|
57
|
+
);
|
|
58
|
+
return urls;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function advertisesOAuth(resource: URL): Promise<boolean> {
|
|
62
|
+
for (const candidate of resourceMetadataUrls(resource)) {
|
|
63
|
+
const body = await getJson(candidate);
|
|
64
|
+
const servers: unknown = body?.authorization_servers;
|
|
65
|
+
const first: unknown = Array.isArray(servers) ? servers[0] : undefined;
|
|
66
|
+
if (typeof first === 'string' && first !== '') return true;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function authServerFor(resource: URL): Promise<URL> {
|
|
72
|
+
for (const candidate of resourceMetadataUrls(resource)) {
|
|
73
|
+
const body = await getJson(candidate);
|
|
74
|
+
const servers: unknown = body?.authorization_servers;
|
|
75
|
+
const first: unknown = Array.isArray(servers) ? servers[0] : undefined;
|
|
76
|
+
if (typeof first === 'string' && first !== '')
|
|
77
|
+
return parseConnectorUrl(first);
|
|
78
|
+
}
|
|
79
|
+
return parseConnectorUrl(resource.origin);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function toServer(body: Record<string, unknown>, issuer: URL): OAuthServer {
|
|
83
|
+
const authorizationEndpoint = str(body.authorization_endpoint);
|
|
84
|
+
const tokenEndpoint = str(body.token_endpoint);
|
|
85
|
+
if (authorizationEndpoint === '' || tokenEndpoint === '')
|
|
86
|
+
throw refused(
|
|
87
|
+
`${issuer.hostname} advertises OAuth but not where to sign in.`,
|
|
88
|
+
);
|
|
89
|
+
const methods = body.code_challenge_methods_supported;
|
|
90
|
+
const registration = str(body.registration_endpoint);
|
|
91
|
+
return {
|
|
92
|
+
issuer: str(body.issuer) === '' ? issuer.origin : str(body.issuer),
|
|
93
|
+
authorizationEndpoint,
|
|
94
|
+
tokenEndpoint,
|
|
95
|
+
registrationEndpoint: registration === '' ? null : registration,
|
|
96
|
+
supportsS256: Array.isArray(methods) ? methods.includes('S256') : false,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function authServerMetadataUrls(issuer: URL): URL[] {
|
|
101
|
+
const path = issuer.pathname.replace(/\/$/, '');
|
|
102
|
+
const suffixes = ['oauth-authorization-server', 'openid-configuration'];
|
|
103
|
+
const urls = suffixes.map((suffix) => wellKnown(issuer, suffix));
|
|
104
|
+
if (path === '') return urls;
|
|
105
|
+
const aware = suffixes.map(
|
|
106
|
+
(suffix) => new URL(`/.well-known/${suffix}${path}`, issuer.origin),
|
|
107
|
+
);
|
|
108
|
+
return [...aware, new URL(`${path}/.well-known/openid-configuration`, issuer.origin), ...urls];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function discoverOAuth(resource: URL): Promise<OAuthServer> {
|
|
112
|
+
const issuer = await authServerFor(resource);
|
|
113
|
+
for (const candidate of authServerMetadataUrls(issuer)) {
|
|
114
|
+
const body = await getJson(candidate);
|
|
115
|
+
if (body !== null) return toServer(body, issuer);
|
|
116
|
+
}
|
|
117
|
+
throw refused(
|
|
118
|
+
`${resource.hostname} needs authorization, but publishes no OAuth metadata Metro can use.`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import type { OAuthClient } from './oauth-client.js';
|
|
3
|
+
import type { OAuthServer } from './oauth-discovery.js';
|
|
4
|
+
|
|
5
|
+
const TTL_MS = 10 * 60_000;
|
|
6
|
+
const SWEEP_MS = 60_000;
|
|
7
|
+
const MAX_PENDING = 100;
|
|
8
|
+
|
|
9
|
+
export interface PendingAuth {
|
|
10
|
+
email: string;
|
|
11
|
+
name: string;
|
|
12
|
+
url: string;
|
|
13
|
+
resource: string;
|
|
14
|
+
returnTo: string;
|
|
15
|
+
verifier: string;
|
|
16
|
+
server: OAuthServer;
|
|
17
|
+
client: OAuthClient;
|
|
18
|
+
connectorId: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface Entry extends PendingAuth {
|
|
22
|
+
expiresAt: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const pending = new Map<string, Entry>();
|
|
26
|
+
let sweeper: ReturnType<typeof setInterval> | null = null;
|
|
27
|
+
|
|
28
|
+
function sweep(now = Date.now()): void {
|
|
29
|
+
for (const [state, entry] of pending)
|
|
30
|
+
if (entry.expiresAt <= now) pending.delete(state);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function ensureSweeper(): void {
|
|
34
|
+
if (sweeper !== null) return;
|
|
35
|
+
sweeper = setInterval(sweep, SWEEP_MS);
|
|
36
|
+
sweeper.unref?.();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function startPending(entry: PendingAuth, now = Date.now()): string {
|
|
40
|
+
sweep(now);
|
|
41
|
+
while (pending.size >= MAX_PENDING) {
|
|
42
|
+
const oldest = pending.keys().next();
|
|
43
|
+
if (oldest.done) break;
|
|
44
|
+
pending.delete(oldest.value);
|
|
45
|
+
}
|
|
46
|
+
const state = randomBytes(32).toString('base64url');
|
|
47
|
+
pending.set(state, { ...entry, expiresAt: now + TTL_MS });
|
|
48
|
+
ensureSweeper();
|
|
49
|
+
return state;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function takePending(
|
|
53
|
+
state: string,
|
|
54
|
+
now = Date.now(),
|
|
55
|
+
): PendingAuth | undefined {
|
|
56
|
+
const entry = pending.get(state);
|
|
57
|
+
if (entry === undefined) return undefined;
|
|
58
|
+
pending.delete(state);
|
|
59
|
+
return entry.expiresAt > now ? entry : undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function pendingCount(): number {
|
|
63
|
+
sweep();
|
|
64
|
+
return pending.size;
|
|
65
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
const TTL_MS = 10 * 60_000;
|
|
4
|
+
const SWEEP_MS = 60_000;
|
|
5
|
+
const MAX_CODES = 100;
|
|
6
|
+
|
|
7
|
+
export interface CodeStore<T> {
|
|
8
|
+
pattern: RegExp;
|
|
9
|
+
mint(entry: T, now?: number): { code: string; expiresAt: number };
|
|
10
|
+
take(code: string, now?: number): T | undefined;
|
|
11
|
+
count(): number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function makeCodeStore<T extends object>(prefix: string): CodeStore<T> {
|
|
15
|
+
const codes = new Map<string, T & { expiresAt: number }>();
|
|
16
|
+
let sweeper: ReturnType<typeof setInterval> | null = null;
|
|
17
|
+
|
|
18
|
+
const sweep = (now = Date.now()): void => {
|
|
19
|
+
for (const [code, entry] of codes)
|
|
20
|
+
if (entry.expiresAt <= now) codes.delete(code);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const ensureSweeper = (): void => {
|
|
24
|
+
if (sweeper !== null) return;
|
|
25
|
+
sweeper = setInterval(sweep, SWEEP_MS);
|
|
26
|
+
sweeper.unref?.();
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
pattern: new RegExp(`^${prefix}_[A-Za-z0-9_-]{16}$`),
|
|
31
|
+
mint(entry, now = Date.now()) {
|
|
32
|
+
sweep(now);
|
|
33
|
+
while (codes.size >= MAX_CODES) {
|
|
34
|
+
const oldest = codes.keys().next();
|
|
35
|
+
if (oldest.done) break;
|
|
36
|
+
codes.delete(oldest.value);
|
|
37
|
+
}
|
|
38
|
+
const code = `${prefix}_${randomBytes(12).toString('base64url')}`;
|
|
39
|
+
const expiresAt = now + TTL_MS;
|
|
40
|
+
codes.set(code, { ...entry, expiresAt });
|
|
41
|
+
ensureSweeper();
|
|
42
|
+
return { code, expiresAt };
|
|
43
|
+
},
|
|
44
|
+
take(code, now = Date.now()) {
|
|
45
|
+
const entry = codes.get(code);
|
|
46
|
+
if (entry === undefined) return undefined;
|
|
47
|
+
codes.delete(code);
|
|
48
|
+
if (entry.expiresAt <= now) return undefined;
|
|
49
|
+
const rest: Record<string, unknown> = { ...entry };
|
|
50
|
+
delete rest.expiresAt;
|
|
51
|
+
return rest as T;
|
|
52
|
+
},
|
|
53
|
+
count() {
|
|
54
|
+
sweep();
|
|
55
|
+
return codes.size;
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import {
|
|
3
|
+
closeSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
openSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
unlinkSync,
|
|
9
|
+
writeSync,
|
|
10
|
+
} from 'node:fs';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { log } from './log.js';
|
|
14
|
+
|
|
15
|
+
export const STATE_DIR =
|
|
16
|
+
process.env.METRO_STATE_DIR ?? join(homedir(), '.cache', 'metro');
|
|
17
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
18
|
+
|
|
19
|
+
export const CONFIG_DIR =
|
|
20
|
+
process.env.METRO_CONFIG_DIR ??
|
|
21
|
+
join(process.env.XDG_CONFIG_HOME ?? join(homedir(), '.config'), 'metro');
|
|
22
|
+
export const CONFIG_ENV_FILE = join(CONFIG_DIR, '.env');
|
|
23
|
+
|
|
24
|
+
export const TRAINS_ENV_FILE = join(homedir(), '.metro', '.env');
|
|
25
|
+
|
|
26
|
+
export const envSources = (): readonly string[] => [
|
|
27
|
+
join(process.cwd(), '.env'),
|
|
28
|
+
TRAINS_ENV_FILE,
|
|
29
|
+
CONFIG_ENV_FILE,
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const LINE_RE = /^\s*([A-Za-z_]\w*)\s*=\s*(.*?)\s*$/;
|
|
33
|
+
const QUOTED_RE = /^(['"])(.*)\1$/;
|
|
34
|
+
|
|
35
|
+
export function readDotenv(path: string): Record<string, string> {
|
|
36
|
+
if (!existsSync(path)) return {};
|
|
37
|
+
const out: Record<string, string> = {};
|
|
38
|
+
for (const line of readFileSync(path, 'utf8').split('\n')) {
|
|
39
|
+
const m = LINE_RE.exec(line);
|
|
40
|
+
if (m?.[1] !== undefined && m[2] !== undefined)
|
|
41
|
+
out[m[1]] = m[2].replace(QUOTED_RE, '$2');
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function loadMetroEnv(): void {
|
|
47
|
+
for (const path of envSources()) {
|
|
48
|
+
for (const [k, v] of Object.entries(readDotenv(path))) {
|
|
49
|
+
process.env[k] ??= v;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readLockPid(lockFile: string): number {
|
|
55
|
+
try {
|
|
56
|
+
return Number(readFileSync(lockFile, 'utf8').trim());
|
|
57
|
+
} catch {
|
|
58
|
+
return NaN;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function namesMetro(text: string): boolean {
|
|
63
|
+
return text.includes('server.ts') || text.includes('metro');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function holderIsMetro(pid: number): boolean {
|
|
67
|
+
try {
|
|
68
|
+
return namesMetro(readFileSync(`/proc/${pid}/cmdline`, 'utf8'));
|
|
69
|
+
} catch {
|
|
70
|
+
return holderCommand(pid);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function holderCommand(pid: number): boolean {
|
|
75
|
+
try {
|
|
76
|
+
return namesMetro(
|
|
77
|
+
execFileSync('ps', ['-o', 'command=', '-p', String(pid)], {
|
|
78
|
+
encoding: 'utf8',
|
|
79
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function lockHeldByLiveDaemon(pid: number): boolean {
|
|
88
|
+
if (!Number.isInteger(pid) || pid <= 0 || pid === process.pid) return false;
|
|
89
|
+
try {
|
|
90
|
+
process.kill(pid, 0);
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
return holderIsMetro(pid);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function writeOwnLock(lockFile: string): void {
|
|
98
|
+
const fd = openSync(lockFile, 'wx');
|
|
99
|
+
writeSync(fd, String(process.pid));
|
|
100
|
+
closeSync(fd);
|
|
101
|
+
process.on('exit', () => {
|
|
102
|
+
try {
|
|
103
|
+
if (readFileSync(lockFile, 'utf8').trim() === String(process.pid))
|
|
104
|
+
unlinkSync(lockFile);
|
|
105
|
+
} catch {
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function acquireLock(lockFile: string): void {
|
|
111
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
112
|
+
try {
|
|
113
|
+
writeOwnLock(lockFile);
|
|
114
|
+
return;
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if ((err as NodeJS.ErrnoException).code !== 'EEXIST') throw err;
|
|
117
|
+
const pid = readLockPid(lockFile);
|
|
118
|
+
if (lockHeldByLiveDaemon(pid)) {
|
|
119
|
+
log.info(
|
|
120
|
+
{ pid },
|
|
121
|
+
'a healthy `metro` daemon is already running; exiting (no second dispatcher)',
|
|
122
|
+
);
|
|
123
|
+
process.exit(0);
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
unlinkSync(lockFile);
|
|
127
|
+
} catch {
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
throw new Error(
|
|
132
|
+
`metro: could not acquire dispatcher lock (${lockFile}) after retries`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import { errMsg, log } from './log.js';
|
|
3
|
+
import {
|
|
4
|
+
apiFailure,
|
|
5
|
+
apiSession,
|
|
6
|
+
bodyField,
|
|
7
|
+
cors,
|
|
8
|
+
readJsonBody,
|
|
9
|
+
sendJson,
|
|
10
|
+
type ApiSession,
|
|
11
|
+
} from './api-http.js';
|
|
12
|
+
import { parseId } from '../db/ids.js';
|
|
13
|
+
import type { Member, Project } from '../db/projects.js';
|
|
14
|
+
|
|
15
|
+
const PREFIX = '/api/projects';
|
|
16
|
+
|
|
17
|
+
export interface ProjectApiDeps {
|
|
18
|
+
listProjects: (email: string) => Promise<Project[]>;
|
|
19
|
+
createProject: (email: string, name: unknown) => Promise<Project>;
|
|
20
|
+
renameProject: (
|
|
21
|
+
email: string,
|
|
22
|
+
id: string,
|
|
23
|
+
name: unknown,
|
|
24
|
+
) => Promise<Project>;
|
|
25
|
+
deleteProject: (
|
|
26
|
+
email: string,
|
|
27
|
+
id: string,
|
|
28
|
+
) => Promise<{ id: string; name: string }>;
|
|
29
|
+
listMembers: (email: string, id: string) => Promise<Member[]>;
|
|
30
|
+
addMember: (
|
|
31
|
+
email: string,
|
|
32
|
+
id: string,
|
|
33
|
+
invited: unknown,
|
|
34
|
+
role: unknown,
|
|
35
|
+
) => Promise<Member[]>;
|
|
36
|
+
setMemberRole: (
|
|
37
|
+
email: string,
|
|
38
|
+
id: string,
|
|
39
|
+
memberId: string,
|
|
40
|
+
role: unknown,
|
|
41
|
+
) => Promise<Member[]>;
|
|
42
|
+
removeMember: (
|
|
43
|
+
email: string,
|
|
44
|
+
id: string,
|
|
45
|
+
memberId: string,
|
|
46
|
+
) => Promise<Member[]>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type Routable =
|
|
50
|
+
| { kind: 'index' }
|
|
51
|
+
| { kind: 'project'; id: string }
|
|
52
|
+
| { kind: 'rename'; id: string }
|
|
53
|
+
| { kind: 'members'; id: string }
|
|
54
|
+
| { kind: 'member'; id: string; memberId: string };
|
|
55
|
+
|
|
56
|
+
type Target = Routable | { kind: 'unknown' } | null;
|
|
57
|
+
|
|
58
|
+
const ALLOWED: Record<Routable['kind'], string[]> = {
|
|
59
|
+
index: ['GET', 'POST'],
|
|
60
|
+
project: ['DELETE'],
|
|
61
|
+
rename: ['POST'],
|
|
62
|
+
members: ['GET', 'POST'],
|
|
63
|
+
member: ['POST', 'DELETE'],
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function memberTarget(id: string, rest: string[]): Target {
|
|
67
|
+
if (rest.length === 1) return { kind: 'members', id };
|
|
68
|
+
if (rest.length > 2) return { kind: 'unknown' };
|
|
69
|
+
const memberId = parseId(rest[1] ?? '');
|
|
70
|
+
return memberId === null
|
|
71
|
+
? { kind: 'unknown' }
|
|
72
|
+
: { kind: 'member', id, memberId };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function subTarget(id: string, rest: string[]): Target {
|
|
76
|
+
if (rest.length === 0) return { kind: 'project', id };
|
|
77
|
+
const head = rest[0];
|
|
78
|
+
if (head === 'members') return memberTarget(id, rest);
|
|
79
|
+
if (rest.length > 1) return { kind: 'unknown' };
|
|
80
|
+
if (head === 'rename') return { kind: 'rename', id };
|
|
81
|
+
return { kind: 'unknown' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function target(path: string): Target {
|
|
85
|
+
if (path === PREFIX || path === `${PREFIX}/`) return { kind: 'index' };
|
|
86
|
+
if (!path.startsWith(`${PREFIX}/`)) return null;
|
|
87
|
+
const segments = path.slice(PREFIX.length + 1).split('/').filter(Boolean);
|
|
88
|
+
const head = segments[0];
|
|
89
|
+
if (head === undefined) return { kind: 'index' };
|
|
90
|
+
const id = parseId(head);
|
|
91
|
+
return id === null ? { kind: 'unknown' } : subTarget(id, segments.slice(1));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function handleIndex(
|
|
95
|
+
req: IncomingMessage,
|
|
96
|
+
res: ServerResponse,
|
|
97
|
+
deps: ProjectApiDeps,
|
|
98
|
+
session: ApiSession,
|
|
99
|
+
): Promise<void> {
|
|
100
|
+
if (req.method === 'GET') {
|
|
101
|
+
sendJson(req, res, 200, { projects: await deps.listProjects(session.email) });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const made = await deps.createProject(
|
|
105
|
+
session.email,
|
|
106
|
+
bodyField(await readJsonBody(req), 'name'),
|
|
107
|
+
);
|
|
108
|
+
log.info({ id: made.id, name: made.name }, 'project-api: created project');
|
|
109
|
+
sendJson(req, res, 201, made);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function handleMembers(
|
|
113
|
+
req: IncomingMessage,
|
|
114
|
+
res: ServerResponse,
|
|
115
|
+
deps: ProjectApiDeps,
|
|
116
|
+
session: ApiSession,
|
|
117
|
+
id: string,
|
|
118
|
+
): Promise<void> {
|
|
119
|
+
if (req.method === 'GET') {
|
|
120
|
+
sendJson(req, res, 200, { members: await deps.listMembers(session.email, id) });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const body = await readJsonBody(req);
|
|
124
|
+
const members = await deps.addMember(
|
|
125
|
+
session.email,
|
|
126
|
+
id,
|
|
127
|
+
bodyField(body, 'email'),
|
|
128
|
+
bodyField(body, 'role'),
|
|
129
|
+
);
|
|
130
|
+
sendJson(req, res, 200, { members });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function handleMember(
|
|
134
|
+
req: IncomingMessage,
|
|
135
|
+
res: ServerResponse,
|
|
136
|
+
deps: ProjectApiDeps,
|
|
137
|
+
session: ApiSession,
|
|
138
|
+
tgt: { id: string; memberId: string },
|
|
139
|
+
): Promise<void> {
|
|
140
|
+
if (req.method === 'DELETE') {
|
|
141
|
+
const members = await deps.removeMember(session.email, tgt.id, tgt.memberId);
|
|
142
|
+
sendJson(req, res, 200, { members });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const members = await deps.setMemberRole(
|
|
146
|
+
session.email,
|
|
147
|
+
tgt.id,
|
|
148
|
+
tgt.memberId,
|
|
149
|
+
bodyField(await readJsonBody(req), 'role'),
|
|
150
|
+
);
|
|
151
|
+
sendJson(req, res, 200, { members });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function route(
|
|
155
|
+
req: IncomingMessage,
|
|
156
|
+
res: ServerResponse,
|
|
157
|
+
deps: ProjectApiDeps,
|
|
158
|
+
session: ApiSession,
|
|
159
|
+
tgt: Routable,
|
|
160
|
+
): Promise<void> {
|
|
161
|
+
try {
|
|
162
|
+
if (tgt.kind === 'index') await handleIndex(req, res, deps, session);
|
|
163
|
+
else if (tgt.kind === 'members')
|
|
164
|
+
await handleMembers(req, res, deps, session, tgt.id);
|
|
165
|
+
else if (tgt.kind === 'member')
|
|
166
|
+
await handleMember(req, res, deps, session, tgt);
|
|
167
|
+
else if (tgt.kind === 'rename')
|
|
168
|
+
sendJson(
|
|
169
|
+
req,
|
|
170
|
+
res,
|
|
171
|
+
200,
|
|
172
|
+
await deps.renameProject(
|
|
173
|
+
session.email,
|
|
174
|
+
tgt.id,
|
|
175
|
+
bodyField(await readJsonBody(req), 'name'),
|
|
176
|
+
),
|
|
177
|
+
);
|
|
178
|
+
else sendJson(req, res, 200, await deps.deleteProject(session.email, tgt.id));
|
|
179
|
+
} catch (err) {
|
|
180
|
+
apiFailure(req, res, err, 'project-api');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function handleProjectApiRequest(
|
|
185
|
+
req: IncomingMessage,
|
|
186
|
+
res: ServerResponse,
|
|
187
|
+
deps: ProjectApiDeps,
|
|
188
|
+
): boolean {
|
|
189
|
+
const tgt = target((req.url ?? '').split('?')[0] ?? '');
|
|
190
|
+
if (tgt === null) return false;
|
|
191
|
+
if (req.method === 'OPTIONS') {
|
|
192
|
+
res.writeHead(204, cors(req)).end();
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
if (tgt.kind === 'unknown') {
|
|
196
|
+
sendJson(req, res, 404, { error: 'no such project' });
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
if (!ALLOWED[tgt.kind].includes(req.method ?? '')) {
|
|
200
|
+
sendJson(req, res, 405, { error: 'method not allowed' });
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
const session = apiSession(req);
|
|
204
|
+
if (!session) {
|
|
205
|
+
sendJson(req, res, 401, { error: 'unauthorized' });
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
route(req, res, deps, session, tgt).catch((err: unknown) => {
|
|
209
|
+
log.warn({ err: errMsg(err) }, 'project-api: unhandled error');
|
|
210
|
+
if (!res.headersSent)
|
|
211
|
+
sendJson(req, res, 500, { error: 'project api failed' });
|
|
212
|
+
});
|
|
213
|
+
return true;
|
|
214
|
+
}
|