@stage-labs/metro 0.1.0-beta.13 → 0.1.0-beta.131
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 +14 -0
- package/dist/claude-settings.js +40 -0
- package/dist/claude.js +176 -0
- package/dist/cli.js +101 -0
- package/dist/control.js +74 -0
- package/dist/hold.js +211 -0
- package/dist/local.js +93 -0
- package/dist/mcp-config.js +25 -0
- package/dist/node-name.js +71 -0
- package/dist/onboarding.js +43 -0
- package/dist/plugin.js +120 -0
- package/dist/provider-flags.js +9 -0
- package/dist/route.js +38 -0
- package/dist/runtime-install.js +97 -0
- package/dist/runtime.js +74 -0
- package/dist/serve.js +150 -0
- package/dist/service.js +242 -0
- package/dist/tail.js +119 -0
- package/dist/update.js +45 -0
- package/dist/version.js +97 -0
- package/package.json +14 -27
- package/runtime/marketplace/.claude-plugin/marketplace.json +12 -0
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +18 -0
- package/runtime/marketplace/plugin/.mcp.json +1 -0
- package/runtime/marketplace/plugin/bin/guard.mjs +55 -0
- package/runtime/marketplace/plugin/bin/metro-plugin.mjs +122 -0
- package/runtime/marketplace/plugin/bin/session-start.mjs +31 -0
- package/runtime/marketplace/plugin/hooks/hooks.json +25 -0
- package/runtime/marketplace/plugin/orchestrator.md +16 -0
- package/runtime/marketplace/plugin/skills/refresh/SKILL.md +15 -0
- package/runtime/node_modules/@metro-labs/core/package.json +43 -0
- package/runtime/node_modules/@metro-labs/core/src/address.ts +6 -0
- package/runtime/node_modules/@metro-labs/core/src/endpoints.ts +7 -0
- package/runtime/node_modules/@metro-labs/core/src/events.ts +120 -0
- package/runtime/node_modules/@metro-labs/core/src/identity.ts +7 -0
- package/runtime/node_modules/@metro-labs/core/src/ids.ts +21 -0
- package/runtime/node_modules/@metro-labs/core/src/is-record.ts +2 -0
- package/runtime/node_modules/@metro-labs/core/src/lines.ts +87 -0
- package/runtime/node_modules/@metro-labs/core/src/log.ts +42 -0
- package/runtime/node_modules/@metro-labs/core/src/protocol.ts +226 -0
- package/runtime/node_modules/@metro-labs/core/src/secure-fs.ts +57 -0
- package/runtime/node_modules/@metro-labs/core/src/station-names.ts +13 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/account-store.ts +60 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/attachments.ts +196 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +88 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/station-runtime.ts +64 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/types.ts +108 -0
- package/runtime/node_modules/@metro-labs/core/src/str.ts +5 -0
- package/runtime/node_modules/@metro-labs/core/src/train-error.ts +53 -0
- package/runtime/node_modules/@metro-labs/core/src/version.ts +2 -0
- package/runtime/node_modules/@metro-labs/daemon/package.json +48 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/account-attach.ts +19 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/account-routes.ts +70 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/accounts-api.ts +365 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/admin.ts +43 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/allowlist.ts +37 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/api.ts +300 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +122 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +371 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +155 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/keys.ts +45 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/map.ts +95 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/scope.ts +64 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/seed.ts +33 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/senders.ts +38 -0
- package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +209 -0
- package/runtime/node_modules/@metro-labs/daemon/src/boot/connect-hint.ts +39 -0
- package/runtime/node_modules/@metro-labs/daemon/src/boot/crash-guard.ts +49 -0
- package/runtime/node_modules/@metro-labs/daemon/src/boot/local-owner.ts +14 -0
- package/runtime/node_modules/@metro-labs/daemon/src/boot/paths.ts +106 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/addressed.ts +25 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/dedupe.ts +24 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/inbound.ts +377 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/media-note.ts +85 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/pending.ts +75 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/relay.ts +129 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/webhook-note.ts +51 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/api.ts +309 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +358 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/login.ts +169 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/onboarding.ts +56 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/plugin-install.ts +140 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session-files.ts +90 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +253 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/settings.ts +117 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +239 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/skills.ts +154 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/version.ts +109 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/api.ts +310 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/cli-api.ts +63 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/config.ts +175 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/json.ts +21 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/model.ts +77 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-client.ts +166 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-discovery.ts +139 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-pending.ts +65 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-routes.ts +171 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth.ts +155 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/plugin-sync.ts +122 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/reach.ts +27 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-target.ts +39 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay.ts +296 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/store.ts +286 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/tools.ts +98 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/url.ts +70 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/verify.ts +332 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/watch.ts +54 -0
- package/runtime/node_modules/@metro-labs/daemon/src/files/attach-grant.ts +99 -0
- package/runtime/node_modules/@metro-labs/daemon/src/files/attach-owner.ts +32 -0
- package/runtime/node_modules/@metro-labs/daemon/src/files/attach-serve.ts +121 -0
- package/runtime/node_modules/@metro-labs/daemon/src/files/upload-api.ts +294 -0
- package/runtime/node_modules/@metro-labs/daemon/src/files/upload-store.ts +181 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +259 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-auth.ts +231 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-device.ts +91 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-stream.ts +288 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-translate.ts +203 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +231 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/eventstream.ts +123 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +187 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +229 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +269 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-config.ts +207 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter.ts +90 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/provider-models.ts +65 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/served.ts +17 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage.ts +229 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/accounts.ts +151 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/call-tools.ts +216 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/channel-owner.ts +29 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/ctx.ts +62 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/event-store.ts +91 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/frame-scope.ts +21 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/group-tools.ts +129 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/index.ts +191 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/member-tools.ts +72 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-relay.ts +66 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/raw-get-stream.ts +207 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/request-identity.ts +47 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-registry.ts +172 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-route.ts +43 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session.ts +215 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-dispatch.ts +215 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-schemas.ts +400 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/upload-tool.ts +66 -0
- package/runtime/node_modules/@metro-labs/daemon/src/monitor/api.ts +237 -0
- package/runtime/node_modules/@metro-labs/daemon/src/net/tunnel.ts +389 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/body.ts +24 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/http.ts +372 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/identity.ts +54 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +223 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/session-apis.ts +60 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/session.ts +29 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/threema-callback.ts +103 -0
- package/runtime/node_modules/@metro-labs/daemon/src/server/control.ts +64 -0
- package/runtime/node_modules/@metro-labs/daemon/src/server/machine.ts +61 -0
- package/runtime/node_modules/@metro-labs/daemon/src/server/update.ts +116 -0
- package/runtime/node_modules/@metro-labs/daemon/src/server.ts +2 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-inline.ts +100 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-interactive.ts +161 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-resolve.ts +202 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-session.ts +280 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-xmtp.ts +111 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach.ts +248 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/materialize.ts +261 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/registry.ts +37 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/runtime-deps.ts +82 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor-io.ts +97 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor.ts +320 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/threema-callbacks.ts +32 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/train-call.ts +22 -0
- package/runtime/node_modules/@metro-labs/daemon/src/terminal/api.ts +96 -0
- package/runtime/node_modules/@metro-labs/daemon/src/terminal/socket.ts +123 -0
- package/runtime/node_modules/@metro-labs/daemon/src/terminal/tickets.ts +40 -0
- package/runtime/node_modules/@metro-labs/discord-bot/package.json +28 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +118 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +388 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/api-base.ts +1 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +29 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +220 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +50 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/group.ts +115 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +127 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +215 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +43 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/station.ts +19 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +82 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/voice-speak.ts +195 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/voice-transcribe.ts +292 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/voice.ts +197 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +5 -0
- package/runtime/node_modules/@metro-labs/http/package.json +33 -0
- package/runtime/node_modules/@metro-labs/http/src/api-error.ts +8 -0
- package/runtime/node_modules/@metro-labs/http/src/api-http.ts +95 -0
- package/runtime/node_modules/@metro-labs/http/src/cors.ts +29 -0
- package/runtime/node_modules/@metro-labs/http/src/identity-key.ts +33 -0
- package/runtime/node_modules/@metro-labs/http/src/identity-registry.ts +13 -0
- package/runtime/node_modules/@metro-labs/http/src/mode-api.ts +32 -0
- package/runtime/node_modules/@metro-labs/http/src/return-to.ts +21 -0
- package/runtime/node_modules/@metro-labs/http/src/signed-identity.ts +36 -0
- package/runtime/node_modules/@metro-labs/telegram/package.json +26 -0
- package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +80 -0
- package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +254 -0
- package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +87 -0
- package/runtime/node_modules/@metro-labs/telegram/src/client.ts +77 -0
- package/runtime/node_modules/@metro-labs/telegram/src/format.ts +156 -0
- package/runtime/node_modules/@metro-labs/telegram/src/group.ts +261 -0
- package/runtime/node_modules/@metro-labs/telegram/src/history.ts +52 -0
- package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +78 -0
- package/runtime/node_modules/@metro-labs/telegram/src/index.ts +46 -0
- package/runtime/node_modules/@metro-labs/telegram/src/login.ts +166 -0
- package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +80 -0
- package/runtime/node_modules/@metro-labs/telegram/src/members.ts +75 -0
- package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +22 -0
- package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +167 -0
- package/runtime/node_modules/@metro-labs/telegram/src/station.ts +24 -0
- package/runtime/node_modules/@metro-labs/telegram/src/types.ts +7 -0
- package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +1 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/package.json +25 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +120 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +295 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +201 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +109 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +166 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +17 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +58 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/verify.ts +38 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +3 -0
- package/runtime/node_modules/@metro-labs/threema/package.json +26 -0
- package/runtime/node_modules/@metro-labs/threema/src/accounts.ts +100 -0
- package/runtime/node_modules/@metro-labs/threema/src/actions.ts +219 -0
- package/runtime/node_modules/@metro-labs/threema/src/api.ts +130 -0
- package/runtime/node_modules/@metro-labs/threema/src/crypto.ts +132 -0
- package/runtime/node_modules/@metro-labs/threema/src/format.ts +123 -0
- package/runtime/node_modules/@metro-labs/threema/src/ids.ts +21 -0
- package/runtime/node_modules/@metro-labs/threema/src/index.ts +29 -0
- package/runtime/node_modules/@metro-labs/threema/src/station.ts +10 -0
- package/runtime/node_modules/@metro-labs/threema/src/verify.ts +117 -0
- package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
- package/runtime/node_modules/@metro-labs/webhook/src/station.ts +37 -0
- package/runtime/node_modules/@metro-labs/whatsapp/package.json +27 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +66 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +190 -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 +347 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +138 -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 +185 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/resolve.ts +52 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +18 -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 +112 -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 +335 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +80 -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 +53 -0
- package/runtime/runtime.json +3 -0
- package/runtime/server.ts +1 -0
- package/runtime/stations.json +39 -0
- package/runtime/trains/.keep +0 -0
- package/LICENSE +0 -21
- package/README.md +0 -252
- package/dist/broker.js +0 -202
- package/dist/cache.js +0 -69
- package/dist/cli/actions.js +0 -206
- package/dist/cli/config.js +0 -185
- package/dist/cli/index.js +0 -188
- package/dist/cli/skill.js +0 -62
- package/dist/cli/tail.js +0 -164
- package/dist/cli/util.js +0 -72
- package/dist/cli/webhook.js +0 -81
- package/dist/codex-rc.js +0 -245
- package/dist/dispatcher.js +0 -128
- package/dist/history.js +0 -121
- package/dist/ipc.js +0 -72
- package/dist/log.js +0 -15
- package/dist/monitor.js +0 -194
- package/dist/paths.js +0 -68
- package/dist/registry.js +0 -48
- package/dist/stations/claude.js +0 -45
- package/dist/stations/codex.js +0 -68
- package/dist/stations/discord.js +0 -216
- package/dist/stations/index.js +0 -129
- package/dist/stations/telegram-md.js +0 -34
- package/dist/stations/telegram-upload.js +0 -113
- package/dist/stations/telegram.js +0 -234
- package/dist/stations/webhook.js +0 -103
- package/dist/tunnel.js +0 -64
- package/dist/webhooks.js +0 -41
- package/docs/broker.md +0 -214
- package/docs/monitor.md +0 -163
- package/docs/uri-scheme.md +0 -124
- package/docs/users.md +0 -226
- package/skills/metro/SKILL.md +0 -256
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { whyUnreachable } from './reach.js';
|
|
3
|
+
import {
|
|
4
|
+
ConnectorVerifyError,
|
|
5
|
+
type OAuthTokens,
|
|
6
|
+
} from './verify.js';
|
|
7
|
+
import type { OAuthServer } from './oauth-discovery.js';
|
|
8
|
+
import { isRecord } from '@metro-labs/core/is-record';
|
|
9
|
+
|
|
10
|
+
const TOKEN_TIMEOUT_MS = 15_000;
|
|
11
|
+
const CLIENT_NAME = 'Metro';
|
|
12
|
+
|
|
13
|
+
export interface OAuthClient {
|
|
14
|
+
clientId: string;
|
|
15
|
+
clientSecret?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function refused(message: string): ConnectorVerifyError {
|
|
19
|
+
return new ConnectorVerifyError(message, 400);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function newVerifier(): string {
|
|
23
|
+
return randomBytes(32).toString('base64url');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function challengeOf(verifier: string): string {
|
|
27
|
+
return createHash('sha256').update(verifier).digest('base64url');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function registerClient(
|
|
31
|
+
server: OAuthServer,
|
|
32
|
+
redirectUri: string,
|
|
33
|
+
): Promise<OAuthClient> {
|
|
34
|
+
if (server.registrationEndpoint === null)
|
|
35
|
+
throw refused(
|
|
36
|
+
'That server signs in with OAuth but registers no clients on its own. ' +
|
|
37
|
+
`Register an app with it yourself, with ${redirectUri} as its redirect URL, ` +
|
|
38
|
+
"and add the connector again with that app's client ID.",
|
|
39
|
+
);
|
|
40
|
+
let res: Response;
|
|
41
|
+
try {
|
|
42
|
+
res = await fetch(server.registrationEndpoint, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
redirect: 'manual',
|
|
45
|
+
signal: AbortSignal.timeout(TOKEN_TIMEOUT_MS),
|
|
46
|
+
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
47
|
+
body: JSON.stringify({
|
|
48
|
+
client_name: CLIENT_NAME,
|
|
49
|
+
redirect_uris: [redirectUri],
|
|
50
|
+
grant_types: ['authorization_code', 'refresh_token'],
|
|
51
|
+
response_types: ['code'],
|
|
52
|
+
token_endpoint_auth_method: 'none',
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
} catch (err) {
|
|
56
|
+
throw refused(`Metro could not reach that server to register: ${whyUnreachable(err)}`);
|
|
57
|
+
}
|
|
58
|
+
const body: unknown = await res.json().catch(() => null);
|
|
59
|
+
if (!res.ok || !isRecord(body) || typeof body.client_id !== 'string')
|
|
60
|
+
throw refused('That server refused to register Metro as a client.');
|
|
61
|
+
const secret = body.client_secret;
|
|
62
|
+
return {
|
|
63
|
+
clientId: body.client_id,
|
|
64
|
+
...(typeof secret === 'string' ? { clientSecret: secret } : {}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface AuthorizeInput {
|
|
69
|
+
server: OAuthServer;
|
|
70
|
+
client: OAuthClient;
|
|
71
|
+
redirectUri: string;
|
|
72
|
+
state: string;
|
|
73
|
+
verifier: string;
|
|
74
|
+
resource: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function authorizeUrl(input: AuthorizeInput): string {
|
|
78
|
+
const url = new URL(input.server.authorizationEndpoint);
|
|
79
|
+
url.searchParams.set('response_type', 'code');
|
|
80
|
+
url.searchParams.set('client_id', input.client.clientId);
|
|
81
|
+
url.searchParams.set('redirect_uri', input.redirectUri);
|
|
82
|
+
url.searchParams.set('state', input.state);
|
|
83
|
+
url.searchParams.set('code_challenge', challengeOf(input.verifier));
|
|
84
|
+
url.searchParams.set('code_challenge_method', 'S256');
|
|
85
|
+
url.searchParams.set('resource', input.resource);
|
|
86
|
+
if (input.server.scopes.length > 0)
|
|
87
|
+
url.searchParams.set('scope', input.server.scopes.join(' '));
|
|
88
|
+
return url.toString();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function tokensFrom(body: Record<string, unknown>): OAuthTokens {
|
|
92
|
+
const access = body.access_token;
|
|
93
|
+
if (typeof access !== 'string' || access === '')
|
|
94
|
+
throw refused('That server did not return an access token.');
|
|
95
|
+
const refresh = body.refresh_token;
|
|
96
|
+
const expires = body.expires_in;
|
|
97
|
+
return {
|
|
98
|
+
accessToken: access,
|
|
99
|
+
...(typeof refresh === 'string' ? { refreshToken: refresh } : {}),
|
|
100
|
+
...(typeof expires === 'number'
|
|
101
|
+
? { expiresAt: Date.now() + expires * 1000 }
|
|
102
|
+
: {}),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function postToken(
|
|
107
|
+
server: OAuthServer,
|
|
108
|
+
client: OAuthClient,
|
|
109
|
+
form: Record<string, string>,
|
|
110
|
+
): Promise<OAuthTokens> {
|
|
111
|
+
const params = new URLSearchParams({
|
|
112
|
+
...form,
|
|
113
|
+
client_id: client.clientId,
|
|
114
|
+
...(client.clientSecret === undefined
|
|
115
|
+
? {}
|
|
116
|
+
: { client_secret: client.clientSecret }),
|
|
117
|
+
});
|
|
118
|
+
let res: Response;
|
|
119
|
+
try {
|
|
120
|
+
res = await fetch(server.tokenEndpoint, {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
redirect: 'manual',
|
|
123
|
+
signal: AbortSignal.timeout(TOKEN_TIMEOUT_MS),
|
|
124
|
+
headers: {
|
|
125
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
126
|
+
accept: 'application/json',
|
|
127
|
+
},
|
|
128
|
+
body: params.toString(),
|
|
129
|
+
});
|
|
130
|
+
} catch (err) {
|
|
131
|
+
throw refused(`Metro could not reach that server to finish signing in: ${whyUnreachable(err)}`);
|
|
132
|
+
}
|
|
133
|
+
const body: unknown = await res.json().catch(() => null);
|
|
134
|
+
if (!res.ok || !isRecord(body))
|
|
135
|
+
throw refused('That server refused to complete the sign-in.');
|
|
136
|
+
return tokensFrom(body);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function exchangeCode(
|
|
140
|
+
server: OAuthServer,
|
|
141
|
+
client: OAuthClient,
|
|
142
|
+
input: { code: string; redirectUri: string; verifier: string; resource: string },
|
|
143
|
+
): Promise<OAuthTokens> {
|
|
144
|
+
return postToken(server, client, {
|
|
145
|
+
grant_type: 'authorization_code',
|
|
146
|
+
code: input.code,
|
|
147
|
+
redirect_uri: input.redirectUri,
|
|
148
|
+
code_verifier: input.verifier,
|
|
149
|
+
resource: input.resource,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function refreshTokens(
|
|
154
|
+
server: OAuthServer,
|
|
155
|
+
client: OAuthClient,
|
|
156
|
+
refreshToken: string,
|
|
157
|
+
resource: string,
|
|
158
|
+
scope?: string,
|
|
159
|
+
): Promise<OAuthTokens> {
|
|
160
|
+
return postToken(server, client, {
|
|
161
|
+
grant_type: 'refresh_token',
|
|
162
|
+
refresh_token: refreshToken,
|
|
163
|
+
resource,
|
|
164
|
+
...(scope === undefined ? {} : { scope }),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { ConnectorVerifyError, parseConnectorUrl } from './verify.js';
|
|
2
|
+
import { isRecord } from '@metro-labs/core/is-record';
|
|
3
|
+
|
|
4
|
+
const DISCOVERY_TIMEOUT_MS = 10_000;
|
|
5
|
+
|
|
6
|
+
export interface OAuthServer {
|
|
7
|
+
issuer: string;
|
|
8
|
+
authorizationEndpoint: string;
|
|
9
|
+
tokenEndpoint: string;
|
|
10
|
+
registrationEndpoint: string | null;
|
|
11
|
+
supportsS256: boolean;
|
|
12
|
+
scopes: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ResourceMetadata {
|
|
16
|
+
issuer: URL;
|
|
17
|
+
scopes: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function refused(message: string): ConnectorVerifyError {
|
|
21
|
+
return new ConnectorVerifyError(message, 400);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const str = (value: unknown): string =>
|
|
25
|
+
typeof value === 'string' ? value : '';
|
|
26
|
+
|
|
27
|
+
async function getJson(url: URL): Promise<Record<string, unknown> | null> {
|
|
28
|
+
let res: Response;
|
|
29
|
+
try {
|
|
30
|
+
res = await fetch(url, {
|
|
31
|
+
redirect: 'manual',
|
|
32
|
+
signal: AbortSignal.timeout(DISCOVERY_TIMEOUT_MS),
|
|
33
|
+
headers: { accept: 'application/json' },
|
|
34
|
+
});
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
if (!res.ok) {
|
|
39
|
+
await res.body?.cancel().catch(() => undefined);
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const body: unknown = await res.json().catch(() => null);
|
|
43
|
+
return isRecord(body) ? body : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function wellKnown(base: URL, suffix: string): URL {
|
|
47
|
+
const url = new URL(base.toString());
|
|
48
|
+
url.pathname = `/.well-known/${suffix}`;
|
|
49
|
+
url.search = '';
|
|
50
|
+
url.hash = '';
|
|
51
|
+
return url;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function resourceMetadataUrls(resource: URL): URL[] {
|
|
55
|
+
const path = resource.pathname.replace(/\/$/, '');
|
|
56
|
+
const urls = [wellKnown(resource, 'oauth-protected-resource')];
|
|
57
|
+
if (path !== '')
|
|
58
|
+
urls.unshift(
|
|
59
|
+
new URL(`/.well-known/oauth-protected-resource${path}`, resource.origin),
|
|
60
|
+
);
|
|
61
|
+
return urls;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function advertisesOAuth(resource: URL): Promise<boolean> {
|
|
65
|
+
for (const candidate of resourceMetadataUrls(resource)) {
|
|
66
|
+
const body = await getJson(candidate);
|
|
67
|
+
const servers: unknown = body?.authorization_servers;
|
|
68
|
+
const first: unknown = Array.isArray(servers) ? servers[0] : undefined;
|
|
69
|
+
if (typeof first === 'string' && first !== '') return true;
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function scopesOf(body: Record<string, unknown> | null): string[] {
|
|
75
|
+
const raw: unknown = body?.scopes_supported;
|
|
76
|
+
if (!Array.isArray(raw)) return [];
|
|
77
|
+
const out: string[] = [];
|
|
78
|
+
for (const entry of raw) {
|
|
79
|
+
const scope = str(entry).trim();
|
|
80
|
+
if (scope !== '' && !/\s/.test(scope) && !out.includes(scope)) out.push(scope);
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function resourceMetadata(resource: URL): Promise<ResourceMetadata> {
|
|
86
|
+
for (const candidate of resourceMetadataUrls(resource)) {
|
|
87
|
+
const body = await getJson(candidate);
|
|
88
|
+
const servers: unknown = body?.authorization_servers;
|
|
89
|
+
const first: unknown = Array.isArray(servers) ? servers[0] : undefined;
|
|
90
|
+
if (typeof first === 'string' && first !== '')
|
|
91
|
+
return { issuer: parseConnectorUrl(first), scopes: scopesOf(body) };
|
|
92
|
+
}
|
|
93
|
+
return { issuer: parseConnectorUrl(resource.origin), scopes: [] };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function toServer(
|
|
97
|
+
body: Record<string, unknown>,
|
|
98
|
+
issuer: URL,
|
|
99
|
+
scopes: string[],
|
|
100
|
+
): OAuthServer {
|
|
101
|
+
const authorizationEndpoint = str(body.authorization_endpoint);
|
|
102
|
+
const tokenEndpoint = str(body.token_endpoint);
|
|
103
|
+
if (authorizationEndpoint === '' || tokenEndpoint === '')
|
|
104
|
+
throw refused(
|
|
105
|
+
`${issuer.hostname} advertises OAuth but not where to sign in.`,
|
|
106
|
+
);
|
|
107
|
+
const methods = body.code_challenge_methods_supported;
|
|
108
|
+
const registration = str(body.registration_endpoint);
|
|
109
|
+
return {
|
|
110
|
+
issuer: str(body.issuer) === '' ? issuer.origin : str(body.issuer),
|
|
111
|
+
authorizationEndpoint,
|
|
112
|
+
tokenEndpoint,
|
|
113
|
+
registrationEndpoint: registration === '' ? null : registration,
|
|
114
|
+
supportsS256: Array.isArray(methods) ? methods.includes('S256') : false,
|
|
115
|
+
scopes,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function authServerMetadataUrls(issuer: URL): URL[] {
|
|
120
|
+
const path = issuer.pathname.replace(/\/$/, '');
|
|
121
|
+
const suffixes = ['oauth-authorization-server', 'openid-configuration'];
|
|
122
|
+
const urls = suffixes.map((suffix) => wellKnown(issuer, suffix));
|
|
123
|
+
if (path === '') return urls;
|
|
124
|
+
const aware = suffixes.map(
|
|
125
|
+
(suffix) => new URL(`/.well-known/${suffix}${path}`, issuer.origin),
|
|
126
|
+
);
|
|
127
|
+
return [...aware, new URL(`${path}/.well-known/openid-configuration`, issuer.origin), ...urls];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function discoverOAuth(resource: URL): Promise<OAuthServer> {
|
|
131
|
+
const { issuer, scopes } = await resourceMetadata(resource);
|
|
132
|
+
for (const candidate of authServerMetadataUrls(issuer)) {
|
|
133
|
+
const body = await getJson(candidate);
|
|
134
|
+
if (body !== null) return toServer(body, issuer, scopes);
|
|
135
|
+
}
|
|
136
|
+
throw refused(
|
|
137
|
+
`${resource.hostname} needs authorization, but publishes no OAuth metadata Metro can use.`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
@@ -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
|
+
subject: 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,171 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import { errMsg, log } from '@metro-labs/core/log';
|
|
3
|
+
import { bodyField, readJsonBody, sendJson, type ApiSession } from '@metro-labs/http/api-http';
|
|
4
|
+
import {
|
|
5
|
+
beginOAuth,
|
|
6
|
+
completeOAuth,
|
|
7
|
+
prepareOAuth,
|
|
8
|
+
takePending,
|
|
9
|
+
type PendingAuth,
|
|
10
|
+
} from './oauth.js';
|
|
11
|
+
import { connectorClient } from './config.js';
|
|
12
|
+
import { parseConnectorUrl } from './verify.js';
|
|
13
|
+
import type { OAuthAuth } from './verify.js';
|
|
14
|
+
import type { Connector, PendingConnectorInput } from './model.js';
|
|
15
|
+
|
|
16
|
+
const asText = (value: unknown): string =>
|
|
17
|
+
typeof value === 'string' ? value : '';
|
|
18
|
+
|
|
19
|
+
export function hostOf(url: string): string {
|
|
20
|
+
try {
|
|
21
|
+
return new URL(url).hostname;
|
|
22
|
+
} catch {
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface OAuthRouteDeps {
|
|
28
|
+
createPendingConnector: (
|
|
29
|
+
subject: string,
|
|
30
|
+
project: string,
|
|
31
|
+
input: PendingConnectorInput,
|
|
32
|
+
) => Promise<Connector>;
|
|
33
|
+
reconnectConnector: (
|
|
34
|
+
subject: string,
|
|
35
|
+
id: string,
|
|
36
|
+
auth: OAuthAuth,
|
|
37
|
+
) => Promise<Connector>;
|
|
38
|
+
getConnector: (subject: string, id: string) => Promise<Connector>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function startOAuth(
|
|
42
|
+
req: IncomingMessage,
|
|
43
|
+
res: ServerResponse,
|
|
44
|
+
deps: OAuthRouteDeps,
|
|
45
|
+
session: ApiSession,
|
|
46
|
+
project: string,
|
|
47
|
+
body: unknown,
|
|
48
|
+
payload: (row: Connector) => Record<string, unknown>,
|
|
49
|
+
): Promise<void> {
|
|
50
|
+
const url = parseConnectorUrl(bodyField(body, 'url'));
|
|
51
|
+
const returnTo = asText(bodyField(body, 'returnTo'));
|
|
52
|
+
const clientId = bodyField(body, 'clientId');
|
|
53
|
+
const clientSecret = bodyField(body, 'clientSecret');
|
|
54
|
+
const prepared = await prepareOAuth({
|
|
55
|
+
url,
|
|
56
|
+
client: connectorClient(clientId, clientSecret),
|
|
57
|
+
returnTo,
|
|
58
|
+
});
|
|
59
|
+
const row = await deps.createPendingConnector(session.subject, project, {
|
|
60
|
+
name: bodyField(body, 'name'),
|
|
61
|
+
url: bodyField(body, 'url'),
|
|
62
|
+
clientId,
|
|
63
|
+
clientSecret,
|
|
64
|
+
});
|
|
65
|
+
const authorize = beginOAuth(prepared, {
|
|
66
|
+
subject: session.subject,
|
|
67
|
+
name: row.name,
|
|
68
|
+
url,
|
|
69
|
+
returnTo,
|
|
70
|
+
connectorId: row.id,
|
|
71
|
+
});
|
|
72
|
+
log.info(
|
|
73
|
+
{ id: row.id, host: url.hostname },
|
|
74
|
+
'connector-api: stored unconnected, sending the user to sign in',
|
|
75
|
+
);
|
|
76
|
+
sendJson(req, res, 201, { ...payload(row), authorizeUrl: authorize });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function handleConnect(
|
|
80
|
+
req: IncomingMessage,
|
|
81
|
+
res: ServerResponse,
|
|
82
|
+
deps: OAuthRouteDeps,
|
|
83
|
+
session: ApiSession,
|
|
84
|
+
id: string,
|
|
85
|
+
): Promise<void> {
|
|
86
|
+
const row = await deps.getConnector(session.subject, id);
|
|
87
|
+
const url = parseConnectorUrl(row.url);
|
|
88
|
+
const body = await readJsonBody(req);
|
|
89
|
+
const returnTo = asText(bodyField(body, 'returnTo'));
|
|
90
|
+
const prepared = await prepareOAuth({ url, client: row.client, returnTo });
|
|
91
|
+
const authorize = beginOAuth(prepared, {
|
|
92
|
+
subject: session.subject,
|
|
93
|
+
name: row.name,
|
|
94
|
+
url,
|
|
95
|
+
returnTo,
|
|
96
|
+
connectorId: row.id,
|
|
97
|
+
});
|
|
98
|
+
log.info(
|
|
99
|
+
{ id: row.id, host: url.hostname },
|
|
100
|
+
'connector-api: sending the user back to sign in',
|
|
101
|
+
);
|
|
102
|
+
sendJson(req, res, 202, { status: 'oauth', authorizeUrl: authorize });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function landing(entry: PendingAuth, id?: string): string {
|
|
106
|
+
const target = id ?? entry.connectorId;
|
|
107
|
+
return target === undefined ? '#/connectors' : `#/connector/${target}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function backTo(entry: PendingAuth, error?: string, id?: string): string {
|
|
111
|
+
const suffix =
|
|
112
|
+
error === undefined
|
|
113
|
+
? ''
|
|
114
|
+
: `?connector_error=${encodeURIComponent(error)}`;
|
|
115
|
+
return `${entry.returnTo}${suffix}${landing(entry, id)}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function redirect(res: ServerResponse, location: string): void {
|
|
119
|
+
res.writeHead(302, { location, 'cache-control': 'no-store' }).end();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function saveOAuth(
|
|
123
|
+
deps: OAuthRouteDeps,
|
|
124
|
+
entry: PendingAuth,
|
|
125
|
+
auth: OAuthAuth,
|
|
126
|
+
): Promise<Connector> {
|
|
127
|
+
return deps.reconnectConnector(entry.subject, entry.connectorId, auth);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function settleCallback(
|
|
131
|
+
res: ServerResponse,
|
|
132
|
+
deps: OAuthRouteDeps,
|
|
133
|
+
entry: PendingAuth,
|
|
134
|
+
code: string,
|
|
135
|
+
): Promise<void> {
|
|
136
|
+
try {
|
|
137
|
+
const auth = await completeOAuth(entry, code);
|
|
138
|
+
const saved = await saveOAuth(deps, entry, auth);
|
|
139
|
+
log.info(
|
|
140
|
+
{ id: saved.id, name: saved.name, host: hostOf(saved.url) },
|
|
141
|
+
'connector-api: oauth sign-in completed',
|
|
142
|
+
);
|
|
143
|
+
redirect(res, backTo(entry, undefined, saved.id));
|
|
144
|
+
} catch (err) {
|
|
145
|
+
log.warn({ err: errMsg(err) }, 'connector-api: oauth sign-in failed');
|
|
146
|
+
redirect(res, backTo(entry, errMsg(err)));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function handleCallback(
|
|
151
|
+
req: IncomingMessage,
|
|
152
|
+
res: ServerResponse,
|
|
153
|
+
deps: OAuthRouteDeps,
|
|
154
|
+
): void {
|
|
155
|
+
const query = new URLSearchParams((req.url ?? '').split('?')[1] ?? '');
|
|
156
|
+
const entry = takePending(query.get('state') ?? '');
|
|
157
|
+
if (entry === undefined) {
|
|
158
|
+
sendJson(req, res, 400, { error: 'that sign-in has expired — start it again' });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const denied = query.get('error');
|
|
162
|
+
const code = query.get('code') ?? '';
|
|
163
|
+
if (denied !== null || code === '') {
|
|
164
|
+
redirect(res, backTo(entry, denied ?? 'no authorization code came back'));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
settleCallback(res, deps, entry, code).catch((err: unknown) => {
|
|
168
|
+
log.warn({ err: errMsg(err) }, 'connector-api: oauth callback failed');
|
|
169
|
+
if (!res.headersSent) redirect(res, backTo(entry, 'sign-in failed'));
|
|
170
|
+
});
|
|
171
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { publicBaseOrDefault } from '../files/attach-serve.js';
|
|
2
|
+
import {
|
|
3
|
+
ConnectorVerifyError,
|
|
4
|
+
type OAuthAuth,
|
|
5
|
+
type OAuthTokens,
|
|
6
|
+
} from './verify.js';
|
|
7
|
+
import { validateReturnTo } from '@metro-labs/http/return-to';
|
|
8
|
+
import {
|
|
9
|
+
authorizeUrl,
|
|
10
|
+
exchangeCode,
|
|
11
|
+
newVerifier,
|
|
12
|
+
refreshTokens,
|
|
13
|
+
registerClient,
|
|
14
|
+
type OAuthClient,
|
|
15
|
+
} from './oauth-client.js';
|
|
16
|
+
import { discoverOAuth, type OAuthServer } from './oauth-discovery.js';
|
|
17
|
+
import { startPending, takePending, type PendingAuth } from './oauth-pending.js';
|
|
18
|
+
|
|
19
|
+
export { takePending, type PendingAuth };
|
|
20
|
+
|
|
21
|
+
const REFRESH_SKEW_MS = 300_000;
|
|
22
|
+
|
|
23
|
+
const CALLBACK_PATH = '/api/connectors/callback';
|
|
24
|
+
|
|
25
|
+
const callbackUri = (): string =>
|
|
26
|
+
`${publicBaseOrDefault()}${CALLBACK_PATH}`;
|
|
27
|
+
|
|
28
|
+
function refused(message: string): ConnectorVerifyError {
|
|
29
|
+
return new ConnectorVerifyError(message, 400);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const resourceOf = (url: URL): string => url.toString();
|
|
33
|
+
|
|
34
|
+
export interface PrepareInput {
|
|
35
|
+
url: URL;
|
|
36
|
+
client: OAuthClient | null;
|
|
37
|
+
returnTo: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface PreparedOAuth {
|
|
41
|
+
server: OAuthServer;
|
|
42
|
+
client: OAuthClient;
|
|
43
|
+
redirectUri: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function prepareOAuth(input: PrepareInput): Promise<PreparedOAuth> {
|
|
47
|
+
if (!validateReturnTo(input.returnTo))
|
|
48
|
+
throw refused('that return address is not one Metro will send you back to');
|
|
49
|
+
const server = await discoverOAuth(input.url);
|
|
50
|
+
const redirectUri = callbackUri();
|
|
51
|
+
const client = input.client ?? (await registerClient(server, redirectUri));
|
|
52
|
+
return { server, client, redirectUri };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface BeginInput {
|
|
56
|
+
connectorId: string;
|
|
57
|
+
subject: string;
|
|
58
|
+
name: string;
|
|
59
|
+
url: URL;
|
|
60
|
+
returnTo: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function beginOAuth(prepared: PreparedOAuth, input: BeginInput): string {
|
|
64
|
+
const { server, client, redirectUri } = prepared;
|
|
65
|
+
const verifier = newVerifier();
|
|
66
|
+
const resource = resourceOf(input.url);
|
|
67
|
+
const state = startPending({
|
|
68
|
+
subject: input.subject,
|
|
69
|
+
name: input.name,
|
|
70
|
+
url: input.url.toString(),
|
|
71
|
+
resource,
|
|
72
|
+
returnTo: input.returnTo,
|
|
73
|
+
verifier,
|
|
74
|
+
server,
|
|
75
|
+
client,
|
|
76
|
+
connectorId: input.connectorId,
|
|
77
|
+
});
|
|
78
|
+
return authorizeUrl({
|
|
79
|
+
server,
|
|
80
|
+
client,
|
|
81
|
+
redirectUri,
|
|
82
|
+
state,
|
|
83
|
+
verifier,
|
|
84
|
+
resource,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function authOf(pendingAuth: PendingAuth, tokens: OAuthTokens): OAuthAuth {
|
|
89
|
+
return {
|
|
90
|
+
kind: 'oauth',
|
|
91
|
+
issuer: pendingAuth.server.issuer,
|
|
92
|
+
tokenEndpoint: pendingAuth.server.tokenEndpoint,
|
|
93
|
+
clientId: pendingAuth.client.clientId,
|
|
94
|
+
...(pendingAuth.client.clientSecret === undefined
|
|
95
|
+
? {}
|
|
96
|
+
: { clientSecret: pendingAuth.client.clientSecret }),
|
|
97
|
+
...(pendingAuth.server.scopes.length === 0
|
|
98
|
+
? {}
|
|
99
|
+
: { scope: pendingAuth.server.scopes.join(' ') }),
|
|
100
|
+
...tokens,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function completeOAuth(
|
|
105
|
+
entry: PendingAuth,
|
|
106
|
+
code: string,
|
|
107
|
+
): Promise<OAuthAuth> {
|
|
108
|
+
const tokens = await exchangeCode(entry.server, entry.client, {
|
|
109
|
+
code,
|
|
110
|
+
redirectUri: callbackUri(),
|
|
111
|
+
verifier: entry.verifier,
|
|
112
|
+
resource: entry.resource,
|
|
113
|
+
});
|
|
114
|
+
return authOf(entry, tokens);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function oauthExpired(auth: OAuthAuth, now = Date.now()): boolean {
|
|
118
|
+
return auth.expiresAt !== undefined && auth.expiresAt - REFRESH_SKEW_MS <= now;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function refreshOAuth(
|
|
122
|
+
auth: OAuthAuth,
|
|
123
|
+
resource: string,
|
|
124
|
+
): Promise<OAuthAuth> {
|
|
125
|
+
if (auth.refreshToken === undefined)
|
|
126
|
+
throw refused('that connector needs signing in again');
|
|
127
|
+
const server = {
|
|
128
|
+
issuer: auth.issuer,
|
|
129
|
+
authorizationEndpoint: '',
|
|
130
|
+
tokenEndpoint: auth.tokenEndpoint,
|
|
131
|
+
registrationEndpoint: null,
|
|
132
|
+
supportsS256: true,
|
|
133
|
+
scopes: [],
|
|
134
|
+
};
|
|
135
|
+
const client = {
|
|
136
|
+
clientId: auth.clientId,
|
|
137
|
+
...(auth.clientSecret === undefined
|
|
138
|
+
? {}
|
|
139
|
+
: { clientSecret: auth.clientSecret }),
|
|
140
|
+
};
|
|
141
|
+
const tokens = await refreshTokens(
|
|
142
|
+
server,
|
|
143
|
+
client,
|
|
144
|
+
auth.refreshToken,
|
|
145
|
+
resource,
|
|
146
|
+
auth.scope,
|
|
147
|
+
);
|
|
148
|
+
return {
|
|
149
|
+
...auth,
|
|
150
|
+
...tokens,
|
|
151
|
+
...(tokens.refreshToken === undefined
|
|
152
|
+
? { refreshToken: auth.refreshToken }
|
|
153
|
+
: {}),
|
|
154
|
+
};
|
|
155
|
+
}
|