@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.
Files changed (225) hide show
  1. package/dist/api.js +103 -0
  2. package/dist/cli.js +134 -565
  3. package/dist/control.js +102 -0
  4. package/dist/prompt.js +10 -0
  5. package/dist/runtime.js +94 -0
  6. package/dist/store.js +70 -0
  7. package/dist/update.js +33 -0
  8. package/dist/version.js +97 -0
  9. package/package.json +34 -22
  10. package/runtime/node_modules/@metro-labs/discord/package.json +28 -0
  11. package/runtime/node_modules/@metro-labs/discord/src/accounts.ts +119 -0
  12. package/runtime/node_modules/@metro-labs/discord/src/actions.ts +388 -0
  13. package/runtime/node_modules/@metro-labs/discord/src/api-base.ts +1 -0
  14. package/runtime/node_modules/@metro-labs/discord/src/attachments.ts +29 -0
  15. package/runtime/node_modules/@metro-labs/discord/src/format.ts +210 -0
  16. package/runtime/node_modules/@metro-labs/discord/src/group-actions.ts +50 -0
  17. package/runtime/node_modules/@metro-labs/discord/src/group.ts +115 -0
  18. package/runtime/node_modules/@metro-labs/discord/src/index.ts +127 -0
  19. package/runtime/node_modules/@metro-labs/discord/src/members.ts +215 -0
  20. package/runtime/node_modules/@metro-labs/discord/src/send-files.ts +43 -0
  21. package/runtime/node_modules/@metro-labs/discord/src/station.ts +19 -0
  22. package/runtime/node_modules/@metro-labs/discord/src/verify.ts +82 -0
  23. package/runtime/node_modules/@metro-labs/discord/src/voice-speak.ts +195 -0
  24. package/runtime/node_modules/@metro-labs/discord/src/voice-transcribe.ts +292 -0
  25. package/runtime/node_modules/@metro-labs/discord/src/voice.ts +197 -0
  26. package/runtime/node_modules/@metro-labs/discord/src/wire.ts +5 -0
  27. package/runtime/node_modules/@metro-labs/mcp/package.json +66 -0
  28. package/runtime/node_modules/@metro-labs/mcp/src/channels/dedupe.ts +24 -0
  29. package/runtime/node_modules/@metro-labs/mcp/src/channels/inbound.ts +367 -0
  30. package/runtime/node_modules/@metro-labs/mcp/src/channels/media-note.ts +85 -0
  31. package/runtime/node_modules/@metro-labs/mcp/src/channels/pending.ts +75 -0
  32. package/runtime/node_modules/@metro-labs/mcp/src/channels/relay.ts +129 -0
  33. package/runtime/node_modules/@metro-labs/mcp/src/channels/webhook-note.ts +51 -0
  34. package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +311 -0
  35. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +335 -0
  36. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-error.ts +8 -0
  37. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +129 -0
  38. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-grant.ts +99 -0
  39. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-owner.ts +32 -0
  40. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +124 -0
  41. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +278 -0
  42. package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +322 -0
  43. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +111 -0
  44. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair.ts +20 -0
  45. package/runtime/node_modules/@metro-labs/mcp/src/daemon/collection-api.ts +202 -0
  46. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +338 -0
  47. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-json.ts +31 -0
  48. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth-routes.ts +157 -0
  49. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +127 -0
  50. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +100 -0
  51. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-url.ts +57 -0
  52. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +333 -0
  53. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cors.ts +29 -0
  54. package/runtime/node_modules/@metro-labs/mcp/src/daemon/crash-guard.ts +49 -0
  55. package/runtime/node_modules/@metro-labs/mcp/src/daemon/events.ts +117 -0
  56. package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-auth.ts +164 -0
  57. package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-oauth.ts +208 -0
  58. package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +357 -0
  59. package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +79 -0
  60. package/runtime/node_modules/@metro-labs/mcp/src/daemon/log.ts +42 -0
  61. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-client.ts +162 -0
  62. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-discovery.ts +120 -0
  63. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +65 -0
  64. package/runtime/node_modules/@metro-labs/mcp/src/daemon/pair-codes.ts +58 -0
  65. package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +134 -0
  66. package/runtime/node_modules/@metro-labs/mcp/src/daemon/project-api.ts +214 -0
  67. package/runtime/node_modules/@metro-labs/mcp/src/daemon/protocol.ts +224 -0
  68. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +125 -0
  69. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-pair.ts +18 -0
  70. package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-source.ts +105 -0
  71. package/runtime/node_modules/@metro-labs/mcp/src/daemon/secure-fs.ts +57 -0
  72. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +27 -0
  73. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +41 -0
  74. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +218 -0
  75. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor-io.ts +97 -0
  76. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor.ts +323 -0
  77. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-call.ts +22 -0
  78. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-error.ts +53 -0
  79. package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +169 -0
  80. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +295 -0
  81. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-store.ts +181 -0
  82. package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +91 -0
  83. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +215 -0
  84. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-map.ts +71 -0
  85. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-scope.ts +64 -0
  86. package/runtime/node_modules/@metro-labs/mcp/src/db/client.ts +37 -0
  87. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-collections.ts +218 -0
  88. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +147 -0
  89. package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +329 -0
  90. package/runtime/node_modules/@metro-labs/mcp/src/db/ids.ts +19 -0
  91. package/runtime/node_modules/@metro-labs/mcp/src/db/key-map.ts +45 -0
  92. package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +364 -0
  93. package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +300 -0
  94. package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +31 -0
  95. package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +105 -0
  96. package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +131 -0
  97. package/runtime/node_modules/@metro-labs/mcp/src/db/token-carry.ts +45 -0
  98. package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +57 -0
  99. package/runtime/node_modules/@metro-labs/mcp/src/db/whatsapp-login.ts +18 -0
  100. package/runtime/node_modules/@metro-labs/mcp/src/mcp/accounts.ts +145 -0
  101. package/runtime/node_modules/@metro-labs/mcp/src/mcp/call-tools.ts +203 -0
  102. package/runtime/node_modules/@metro-labs/mcp/src/mcp/channel-owner.ts +29 -0
  103. package/runtime/node_modules/@metro-labs/mcp/src/mcp/ctx.ts +62 -0
  104. package/runtime/node_modules/@metro-labs/mcp/src/mcp/event-store.ts +91 -0
  105. package/runtime/node_modules/@metro-labs/mcp/src/mcp/frame-scope.ts +21 -0
  106. package/runtime/node_modules/@metro-labs/mcp/src/mcp/group-tools.ts +129 -0
  107. package/runtime/node_modules/@metro-labs/mcp/src/mcp/index.ts +192 -0
  108. package/runtime/node_modules/@metro-labs/mcp/src/mcp/member-tools.ts +72 -0
  109. package/runtime/node_modules/@metro-labs/mcp/src/mcp/permission-relay.ts +66 -0
  110. package/runtime/node_modules/@metro-labs/mcp/src/mcp/raw-get-stream.ts +207 -0
  111. package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +73 -0
  112. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-registry.ts +172 -0
  113. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-route.ts +43 -0
  114. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session.ts +211 -0
  115. package/runtime/node_modules/@metro-labs/mcp/src/mcp/str.ts +5 -0
  116. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-dispatch.ts +212 -0
  117. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +397 -0
  118. package/runtime/node_modules/@metro-labs/mcp/src/mcp/upload-tool.ts +66 -0
  119. package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +240 -0
  120. package/runtime/node_modules/@metro-labs/mcp/src/server.ts +2 -0
  121. package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +80 -0
  122. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-inline.ts +100 -0
  123. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-interactive.ts +161 -0
  124. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-resolve.ts +200 -0
  125. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-xmtp.ts +111 -0
  126. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +182 -0
  127. package/runtime/node_modules/@metro-labs/mcp/src/stations/attachments.ts +196 -0
  128. package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +82 -0
  129. package/runtime/node_modules/@metro-labs/mcp/src/stations/messaging-normalize.ts +88 -0
  130. package/runtime/node_modules/@metro-labs/mcp/src/stations/registry.ts +35 -0
  131. package/runtime/node_modules/@metro-labs/mcp/src/stations/station-runtime.ts +64 -0
  132. package/runtime/node_modules/@metro-labs/mcp/src/stations/types.ts +107 -0
  133. package/runtime/node_modules/@metro-labs/telegram/package.json +25 -0
  134. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +120 -0
  135. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +295 -0
  136. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +128 -0
  137. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +172 -0
  138. package/runtime/node_modules/@metro-labs/telegram/src/index.ts +103 -0
  139. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +166 -0
  140. package/runtime/node_modules/@metro-labs/telegram/src/members.ts +47 -0
  141. package/runtime/node_modules/@metro-labs/telegram/src/station.ts +17 -0
  142. package/runtime/node_modules/@metro-labs/telegram/src/types.ts +48 -0
  143. package/runtime/node_modules/@metro-labs/telegram/src/verify.ts +38 -0
  144. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +3 -0
  145. package/runtime/node_modules/@metro-labs/telegram-user/package.json +28 -0
  146. package/runtime/node_modules/@metro-labs/telegram-user/src/accounts.ts +81 -0
  147. package/runtime/node_modules/@metro-labs/telegram-user/src/actions.ts +254 -0
  148. package/runtime/node_modules/@metro-labs/telegram-user/src/attachments.ts +87 -0
  149. package/runtime/node_modules/@metro-labs/telegram-user/src/client.ts +77 -0
  150. package/runtime/node_modules/@metro-labs/telegram-user/src/format.ts +141 -0
  151. package/runtime/node_modules/@metro-labs/telegram-user/src/group.ts +261 -0
  152. package/runtime/node_modules/@metro-labs/telegram-user/src/history.ts +52 -0
  153. package/runtime/node_modules/@metro-labs/telegram-user/src/inbound.ts +78 -0
  154. package/runtime/node_modules/@metro-labs/telegram-user/src/index.ts +46 -0
  155. package/runtime/node_modules/@metro-labs/telegram-user/src/login.ts +166 -0
  156. package/runtime/node_modules/@metro-labs/telegram-user/src/media-actions.ts +80 -0
  157. package/runtime/node_modules/@metro-labs/telegram-user/src/members.ts +75 -0
  158. package/runtime/node_modules/@metro-labs/telegram-user/src/normalize.ts +22 -0
  159. package/runtime/node_modules/@metro-labs/telegram-user/src/reactions.ts +167 -0
  160. package/runtime/node_modules/@metro-labs/telegram-user/src/station.ts +24 -0
  161. package/runtime/node_modules/@metro-labs/telegram-user/src/types.ts +7 -0
  162. package/runtime/node_modules/@metro-labs/telegram-user/src/wire.ts +1 -0
  163. package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
  164. package/runtime/node_modules/@metro-labs/webhook/src/station.ts +40 -0
  165. package/runtime/node_modules/@metro-labs/whatsapp/package.json +29 -0
  166. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +67 -0
  167. package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
  168. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +178 -0
  169. package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +72 -0
  170. package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +129 -0
  171. package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +318 -0
  172. package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
  173. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +132 -0
  174. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +57 -0
  175. package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +40 -0
  176. package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +59 -0
  177. package/runtime/node_modules/@metro-labs/whatsapp/src/logger.ts +60 -0
  178. package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +176 -0
  179. package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +84 -0
  180. package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +22 -0
  181. package/runtime/node_modules/@metro-labs/whatsapp/src/outbox.ts +38 -0
  182. package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +141 -0
  183. package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +17 -0
  184. package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +98 -0
  185. package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +6 -0
  186. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +5 -0
  187. package/runtime/node_modules/@metro-labs/xmtp/package.json +33 -0
  188. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +113 -0
  189. package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +110 -0
  190. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +299 -0
  191. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +157 -0
  192. package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +87 -0
  193. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +375 -0
  194. package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +107 -0
  195. package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +171 -0
  196. package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +239 -0
  197. package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +55 -0
  198. package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +333 -0
  199. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +78 -0
  200. package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +34 -0
  201. package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +56 -0
  202. package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +33 -0
  203. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +125 -0
  204. package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +157 -0
  205. package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
  206. package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +349 -0
  207. package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +13 -0
  208. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +126 -0
  209. package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +212 -0
  210. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +90 -0
  211. package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +84 -0
  212. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +39 -0
  213. package/runtime/runtime.json +3 -0
  214. package/runtime/trains/.keep +0 -0
  215. package/LICENSE +0 -21
  216. package/README.md +0 -78
  217. package/dist/channels/discord.js +0 -148
  218. package/dist/channels/telegram.js +0 -137
  219. package/dist/lib/address.js +0 -21
  220. package/dist/lib/codex-rc.js +0 -274
  221. package/dist/lib/dotenv.js +0 -31
  222. package/dist/log.js +0 -12
  223. package/dist/paths.js +0 -45
  224. package/dist/tail.js +0 -161
  225. package/skills/metro/SKILL.md +0 -122
@@ -0,0 +1,164 @@
1
+ import {
2
+ createPublicKey,
3
+ verify as cryptoVerify,
4
+ type JsonWebKey as CryptoJsonWebKey,
5
+ } from 'node:crypto';
6
+
7
+ export class GoogleAuthError extends Error {}
8
+
9
+ interface GoogleClaims {
10
+ email: string;
11
+ emailVerified: boolean;
12
+ aud: string;
13
+ iss: string;
14
+ exp: number;
15
+ }
16
+
17
+ interface Jwk {
18
+ kid: string;
19
+ kty: string;
20
+ alg?: string;
21
+ n: string;
22
+ e: string;
23
+ }
24
+
25
+ interface VerifyOptions {
26
+ clientId: string;
27
+ now?: number;
28
+ fetchCerts?: () => Promise<Jwk[]>;
29
+ clockToleranceSec?: number;
30
+ expectedNonce?: string;
31
+ }
32
+
33
+ const DEFAULT_CERTS_URL = 'https://www.googleapis.com/oauth2/v3/certs';
34
+ const certsUrl = (): string => {
35
+ const v = process.env.GOOGLE_OAUTH_JWKS_URL?.trim();
36
+ return v !== undefined && v !== '' ? v : DEFAULT_CERTS_URL;
37
+ };
38
+ const GOOGLE_ISSUERS = new Set([
39
+ 'https://accounts.google.com',
40
+ 'accounts.google.com',
41
+ ]);
42
+
43
+ const decodeSegment = (seg: string): Buffer =>
44
+ Buffer.from(seg.replace(/-/g, '+').replace(/_/g, '/'), 'base64');
45
+
46
+ const decodeJson = (seg: string): unknown => {
47
+ try {
48
+ return JSON.parse(decodeSegment(seg).toString('utf8'));
49
+ } catch {
50
+ throw new GoogleAuthError('malformed token segment');
51
+ }
52
+ };
53
+
54
+ let certCache: { keys: Jwk[]; expiresAt: number } | null = null;
55
+
56
+ async function fetchGoogleCerts(): Promise<Jwk[]> {
57
+ const res = await fetch(certsUrl());
58
+ if (!res.ok)
59
+ throw new GoogleAuthError(`google certs fetch failed: ${res.status}`);
60
+ const body = (await res.json()) as { keys?: Jwk[] };
61
+ const maxAge = /max-age=(\d+)/.exec(res.headers.get('cache-control') ?? '');
62
+ const ttlMs = maxAge ? Number(maxAge[1]) * 1000 : 3600_000;
63
+ const keys = body.keys ?? [];
64
+ certCache = { keys, expiresAt: Date.now() + ttlMs };
65
+ return keys;
66
+ }
67
+
68
+ async function loadCerts(
69
+ fetcher: () => Promise<Jwk[]>,
70
+ ): Promise<Jwk[]> {
71
+ if (certCache && certCache.expiresAt > Date.now()) return certCache.keys;
72
+ return fetcher();
73
+ }
74
+
75
+ function jwkPublicKey(jwk: Jwk): ReturnType<typeof createPublicKey> {
76
+ return createPublicKey({
77
+ key: jwk as unknown as CryptoJsonWebKey,
78
+ format: 'jwk',
79
+ });
80
+ }
81
+
82
+ function verifySignature(token: string, jwk: Jwk): boolean {
83
+ const parts = token.split('.');
84
+ const signingInput = `${parts[0]}.${parts[1]}`;
85
+ const signature = decodeSegment(parts[2] ?? '');
86
+ return cryptoVerify(
87
+ 'RSA-SHA256',
88
+ Buffer.from(signingInput),
89
+ jwkPublicKey(jwk),
90
+ signature,
91
+ );
92
+ }
93
+
94
+ interface Header {
95
+ alg?: string;
96
+ kid?: string;
97
+ }
98
+
99
+ interface Payload {
100
+ iss?: string;
101
+ aud?: string;
102
+ exp?: number;
103
+ email?: string;
104
+ email_verified?: boolean | string;
105
+ nonce?: string;
106
+ }
107
+
108
+ function assertHeader(header: Header): asserts header is Header & { kid: string } {
109
+ if (header.alg !== 'RS256')
110
+ throw new GoogleAuthError(`unsupported alg ${String(header.alg)}`);
111
+ if (!header.kid) throw new GoogleAuthError('missing kid');
112
+ }
113
+
114
+ function assertNotExpired(payload: Payload, opts: VerifyOptions): number {
115
+ const now = Math.floor((opts.now ?? Date.now()) / 1000);
116
+ const skew = opts.clockToleranceSec ?? 60;
117
+ if (typeof payload.exp !== 'number' || payload.exp + skew < now)
118
+ throw new GoogleAuthError('token expired');
119
+ return payload.exp;
120
+ }
121
+
122
+ function verifiedEmail(payload: Payload): string {
123
+ const verified =
124
+ payload.email_verified === true || payload.email_verified === 'true';
125
+ if (!payload.email || !verified)
126
+ throw new GoogleAuthError('email not verified');
127
+ return payload.email.toLowerCase();
128
+ }
129
+
130
+ function validateClaims(payload: Payload, opts: VerifyOptions): GoogleClaims {
131
+ if (!payload.iss || !GOOGLE_ISSUERS.has(payload.iss))
132
+ throw new GoogleAuthError(`unexpected issuer ${String(payload.iss)}`);
133
+ if (payload.aud !== opts.clientId)
134
+ throw new GoogleAuthError('audience mismatch');
135
+ if (opts.expectedNonce !== undefined && payload.nonce !== opts.expectedNonce)
136
+ throw new GoogleAuthError('nonce mismatch');
137
+ const exp = assertNotExpired(payload, opts);
138
+ return {
139
+ email: verifiedEmail(payload),
140
+ emailVerified: true,
141
+ aud: payload.aud,
142
+ iss: payload.iss,
143
+ exp,
144
+ };
145
+ }
146
+
147
+ export async function verifyGoogleIdToken(
148
+ token: string,
149
+ opts: VerifyOptions,
150
+ ): Promise<GoogleClaims> {
151
+ const parts = token.split('.');
152
+ if (parts.length !== 3) throw new GoogleAuthError('not a JWT');
153
+ const header = decodeJson(parts[0] ?? '') as Header;
154
+ const payload = decodeJson(parts[1] ?? '') as Payload;
155
+ assertHeader(header);
156
+
157
+ const keys = await loadCerts(opts.fetchCerts ?? fetchGoogleCerts);
158
+ const jwk = keys.find((k) => k.kid === header.kid);
159
+ if (!jwk) throw new GoogleAuthError('signing key not found');
160
+ if (!verifySignature(token, jwk))
161
+ throw new GoogleAuthError('invalid signature');
162
+
163
+ return validateClaims(payload, opts);
164
+ }
@@ -0,0 +1,208 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { errMsg, log } from './log.js';
3
+ import { verifyGoogleIdToken } from './google-auth.js';
4
+ import { newNonce, signSession, signState, verifyState } from './session.js';
5
+ import { ensureUserWithProject } from '../db/projects.js';
6
+
7
+ export interface OAuthConfig {
8
+ clientId: string;
9
+ clientSecret: string;
10
+ redirectUri: string;
11
+ authUrl: string;
12
+ tokenUrl: string;
13
+ sessionSecret: string;
14
+ sessionTtlSec: number;
15
+ }
16
+
17
+ const DEFAULT_REDIRECT_URI = 'https://mcp.metro.box/auth/google/callback';
18
+ const DEFAULT_AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
19
+ const DEFAULT_TOKEN_URL = 'https://oauth2.googleapis.com/token';
20
+ const PREVIEW = /^([a-z0-9-]+--)?metro-ui\.netlify\.app$/;
21
+
22
+ const DEFAULT_SESSION_TTL_SEC = 30 * 24 * 3600;
23
+
24
+ const envOr = (name: string, def: string): string => {
25
+ const v = process.env[name]?.trim();
26
+ return v !== undefined && v !== '' ? v : def;
27
+ };
28
+
29
+ export function sessionTtlFromEnv(): number {
30
+ const ttl = Number(process.env.METRO_SESSION_TTL_SEC);
31
+ return Number.isFinite(ttl) && ttl > 0 ? ttl : DEFAULT_SESSION_TTL_SEC;
32
+ }
33
+
34
+ function oauthConfigFromEnv(): OAuthConfig | null {
35
+ const clientId = process.env.GOOGLE_OAUTH_CLIENT_ID?.trim() ?? '';
36
+ const clientSecret = process.env.GOOGLE_OAUTH_CLIENT_SECRET?.trim() ?? '';
37
+ const sessionSecret = process.env.METRO_SESSION_SECRET?.trim() ?? '';
38
+ if (clientId === '' || clientSecret === '' || sessionSecret === '') return null;
39
+ return {
40
+ clientId,
41
+ clientSecret,
42
+ sessionSecret,
43
+ redirectUri: envOr('GOOGLE_OAUTH_REDIRECT_URI', DEFAULT_REDIRECT_URI),
44
+ authUrl: envOr('GOOGLE_OAUTH_AUTH_URL', DEFAULT_AUTH_URL),
45
+ tokenUrl: envOr('GOOGLE_OAUTH_TOKEN_URL', DEFAULT_TOKEN_URL),
46
+ sessionTtlSec: sessionTtlFromEnv(),
47
+ };
48
+ }
49
+
50
+ export function validateReturnTo(returnTo: string): boolean {
51
+ let url: URL;
52
+ try {
53
+ url = new URL(returnTo);
54
+ } catch {
55
+ return false;
56
+ }
57
+ const host = url.hostname;
58
+ if (url.protocol === 'http:' && (host === 'localhost' || host === '127.0.0.1'))
59
+ return true;
60
+ if (url.protocol !== 'https:') return false;
61
+ if (host === 'metro.box' || host === 'localhost') return true;
62
+ return PREVIEW.test(host);
63
+ }
64
+
65
+ export function buildStartRedirect(
66
+ cfg: OAuthConfig,
67
+ returnTo: string,
68
+ now?: number,
69
+ ): string {
70
+ if (!validateReturnTo(returnTo)) throw new Error('invalid return_to');
71
+ const nonce = newNonce();
72
+ const state = signState(
73
+ { return_to: returnTo, nonce },
74
+ cfg.sessionSecret,
75
+ { now },
76
+ );
77
+ const u = new URL(cfg.authUrl);
78
+ u.searchParams.set('client_id', cfg.clientId);
79
+ u.searchParams.set('redirect_uri', cfg.redirectUri);
80
+ u.searchParams.set('response_type', 'code');
81
+ u.searchParams.set('scope', 'openid email');
82
+ u.searchParams.set('state', state);
83
+ u.searchParams.set('nonce', nonce);
84
+ u.searchParams.set('prompt', 'select_account');
85
+ return u.toString();
86
+ }
87
+
88
+ function withFragment(returnTo: string, frag: string): string {
89
+ return `${returnTo.split('#')[0]}#${frag}`;
90
+ }
91
+
92
+ export interface CallbackDeps {
93
+ exchangeCode: (code: string) => Promise<{ id_token?: string }>;
94
+ verifyIdToken: (idToken: string, nonce: string) => Promise<{ email: string }>;
95
+ ensureUser: (email: string) => Promise<string>;
96
+ }
97
+
98
+ export async function completeCallback(
99
+ cfg: OAuthConfig,
100
+ params: { code?: string; state?: string },
101
+ deps: CallbackDeps,
102
+ now?: number,
103
+ ): Promise<string> {
104
+ if (!params.state) throw new Error('missing state');
105
+ const { return_to, nonce } = verifyState(params.state, cfg.sessionSecret, now);
106
+ if (!validateReturnTo(return_to)) throw new Error('invalid return_to');
107
+ if (!params.code) return withFragment(return_to, 'error=exchange');
108
+
109
+ const tokens = await deps.exchangeCode(params.code);
110
+ if (!tokens.id_token) return withFragment(return_to, 'error=exchange');
111
+
112
+ let email: string;
113
+ try {
114
+ ({ email } = await deps.verifyIdToken(tokens.id_token, nonce));
115
+ } catch {
116
+ return withFragment(return_to, 'error=verify');
117
+ }
118
+
119
+ const userId = await deps.ensureUser(email);
120
+ const agentIds: string[] = [];
121
+ log.info({ userId }, 'google auth: session issued');
122
+
123
+ const session = signSession({ email, agentIds }, cfg.sessionSecret, {
124
+ ttlSec: cfg.sessionTtlSec,
125
+ now,
126
+ });
127
+ return withFragment(return_to, `session=${encodeURIComponent(session)}`);
128
+ }
129
+
130
+ async function exchangeCode(
131
+ cfg: OAuthConfig,
132
+ code: string,
133
+ ): Promise<{ id_token?: string }> {
134
+ const res = await fetch(cfg.tokenUrl, {
135
+ method: 'POST',
136
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
137
+ body: new URLSearchParams({
138
+ code,
139
+ client_id: cfg.clientId,
140
+ client_secret: cfg.clientSecret,
141
+ redirect_uri: cfg.redirectUri,
142
+ grant_type: 'authorization_code',
143
+ }),
144
+ });
145
+ if (!res.ok) throw new Error(`token exchange failed: ${res.status}`);
146
+ return (await res.json()) as { id_token?: string };
147
+ }
148
+
149
+ function defaultDeps(cfg: OAuthConfig): CallbackDeps {
150
+ return {
151
+ exchangeCode: (code) => exchangeCode(cfg, code),
152
+ verifyIdToken: (idToken, nonce) =>
153
+ verifyGoogleIdToken(idToken, {
154
+ clientId: cfg.clientId,
155
+ expectedNonce: nonce,
156
+ }).then((c) => ({ email: c.email })),
157
+ ensureUser: ensureUserWithProject,
158
+
159
+ };
160
+ }
161
+
162
+ function serveStart(cfg: OAuthConfig, url: URL, res: ServerResponse): void {
163
+ const redirect = buildStartRedirect(cfg, url.searchParams.get('return_to') ?? '');
164
+ res.writeHead(302, { location: redirect }).end();
165
+ }
166
+
167
+ async function serveCallback(
168
+ cfg: OAuthConfig,
169
+ url: URL,
170
+ res: ServerResponse,
171
+ ): Promise<void> {
172
+ const redirect = await completeCallback(
173
+ cfg,
174
+ {
175
+ code: url.searchParams.get('code') ?? undefined,
176
+ state: url.searchParams.get('state') ?? undefined,
177
+ },
178
+ defaultDeps(cfg),
179
+ );
180
+ res.writeHead(302, { location: redirect }).end();
181
+ }
182
+
183
+ export async function handleGoogleAuthRequest(
184
+ req: IncomingMessage,
185
+ res: ServerResponse,
186
+ ): Promise<boolean> {
187
+ const url = new URL(req.url ?? '/', 'http://localhost');
188
+ const path = url.pathname;
189
+ if (path !== '/auth/google/start' && path !== '/auth/google/callback')
190
+ return false;
191
+ if (req.method !== 'GET') {
192
+ res.writeHead(405).end();
193
+ return true;
194
+ }
195
+ const cfg = oauthConfigFromEnv();
196
+ if (!cfg) {
197
+ res.writeHead(503).end('google login not configured');
198
+ return true;
199
+ }
200
+ try {
201
+ if (path === '/auth/google/start') serveStart(cfg, url, res);
202
+ else await serveCallback(cfg, url, res);
203
+ } catch (e) {
204
+ log.warn({ err: errMsg(e) }, 'google auth request failed');
205
+ res.writeHead(400).end('bad request');
206
+ }
207
+ return true;
208
+ }