@stage-labs/metro 0.1.0-beta.2 → 0.1.0-beta.200
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 +11 -0
- package/dist/claude-settings.js +40 -0
- package/dist/claude.js +163 -0
- package/dist/cli.js +77 -567
- package/dist/control.js +74 -0
- package/dist/hold.js +211 -0
- package/dist/local.js +43 -0
- package/dist/mcp-config.js +25 -0
- package/dist/node-name.js +71 -0
- package/dist/onboarding.js +44 -0
- package/dist/provider-flags.js +9 -0
- package/dist/route.js +58 -0
- package/dist/runtime-install.js +97 -0
- package/dist/runtime.js +74 -0
- package/dist/serve.js +158 -0
- package/dist/service.js +243 -0
- package/dist/tail.js +102 -144
- package/dist/update.js +45 -0
- package/dist/version.js +97 -0
- package/package.json +14 -23
- package/runtime/marketplace/.claude-plugin/marketplace.json +12 -0
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +17 -0
- package/runtime/marketplace/plugin/.mcp.json +1 -0
- package/runtime/marketplace/plugin/bin/guard.mjs +71 -0
- package/runtime/marketplace/plugin/bin/metro-plugin.mjs +30 -0
- package/runtime/marketplace/plugin/bin/policy.mjs +97 -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 +24 -0
- package/runtime/node_modules/@metro-labs/core/package.json +47 -0
- package/runtime/node_modules/@metro-labs/core/src/endpoints.ts +7 -0
- package/runtime/node_modules/@metro-labs/core/src/events.ts +119 -0
- package/runtime/node_modules/@metro-labs/core/src/identity.ts +7 -0
- package/runtime/node_modules/@metro-labs/core/src/ids.ts +24 -0
- package/runtime/node_modules/@metro-labs/core/src/is-record.ts +2 -0
- package/runtime/node_modules/@metro-labs/core/src/lines.ts +91 -0
- package/runtime/node_modules/@metro-labs/core/src/log.ts +42 -0
- package/runtime/node_modules/@metro-labs/core/src/protocol.ts +227 -0
- package/runtime/node_modules/@metro-labs/core/src/secure-fs.ts +74 -0
- package/runtime/node_modules/@metro-labs/core/src/station-names.ts +13 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/account-files.ts +41 -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/profile.ts +58 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/sender-profile.ts +64 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/station-runtime.ts +61 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/train-boot.ts +42 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/train-events.ts +89 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/types.ts +123 -0
- package/runtime/node_modules/@metro-labs/core/src/str.ts +5 -0
- package/runtime/node_modules/@metro-labs/core/src/tickets.ts +37 -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/agent-user/agent-fs.ts +134 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/files-api.ts +12 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/files.ts +119 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/home-fs.ts +113 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/job-env.ts +51 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/provision.ts +91 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedule-detail.ts +100 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules-api.ts +27 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules.ts +252 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/unit-files.ts +61 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/user.ts +91 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/view.ts +87 -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-lookups.ts +55 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/account-routes.ts +76 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/accounts-api.ts +357 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/admin.ts +30 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/allowlist.ts +59 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/api.ts +131 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +132 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +305 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +168 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/keys.ts +35 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/map.ts +128 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/policy-route.ts +27 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/scope.ts +64 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/sender-cards.ts +86 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/senders.ts +38 -0
- package/runtime/node_modules/@metro-labs/daemon/src/approvals/api.ts +55 -0
- package/runtime/node_modules/@metro-labs/daemon/src/approvals/needs.ts +2 -0
- package/runtime/node_modules/@metro-labs/daemon/src/approvals/pending.ts +149 -0
- package/runtime/node_modules/@metro-labs/daemon/src/approvals/preview.ts +66 -0
- package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +221 -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 +21 -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 +26 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/dedupe.ts +12 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/inbound.ts +370 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/media-note.ts +90 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/pending.ts +78 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/react-note.ts +13 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/relay.ts +129 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/reply-id.ts +1 -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 +355 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +359 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/login.ts +208 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/memory.ts +82 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/onboarding.ts +60 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/plugin-install.ts +149 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session-files.ts +75 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +283 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/settings.ts +109 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +271 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/skills.ts +120 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/version.ts +113 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/api.ts +306 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/config.ts +176 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/gates.ts +72 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/health.ts +18 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-client.ts +159 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-discovery.ts +135 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-routes.ts +158 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth.ts +166 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/plugin-sync.ts +131 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/reach.ts +27 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-policy.ts +90 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-target.ts +29 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay.ts +348 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/store.ts +358 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/tools.ts +81 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/url.ts +37 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/verify.ts +106 -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-reaper.ts +174 -0
- package/runtime/node_modules/@metro-labs/daemon/src/files/attach-serve.ts +123 -0
- package/runtime/node_modules/@metro-labs/daemon/src/files/upload-api.ts +278 -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 +233 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-auth.ts +208 -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 +173 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-translate.ts +194 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +137 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/effort.ts +77 -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 +237 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/frames.ts +118 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +274 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-auth.ts +160 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-client.ts +46 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-setup.ts +150 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-stream.ts +139 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-translate.ts +223 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini.ts +208 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +199 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-config.ts +299 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-signin.ts +193 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-store.ts +70 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter.ts +90 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/pkce.ts +36 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/provider-models.ts +66 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/served.ts +18 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/subscription.ts +120 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/text.ts +15 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage.ts +244 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/accounts.ts +156 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/attachment-schema.ts +91 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/call-tools.ts +226 -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 +90 -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 +137 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/instructions.ts +19 -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-prompt.ts +87 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-relay.ts +79 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-gate.ts +83 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-snapshot.ts +79 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/profile-lookup.ts +60 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/profile-tool.ts +96 -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/read-tool.ts +87 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/request-identity.ts +46 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-slot.ts +48 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session.ts +202 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-catalog.ts +88 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-def.ts +9 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-dispatch.ts +225 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-schemas.ts +333 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/upload-tool.ts +61 -0
- package/runtime/node_modules/@metro-labs/daemon/src/metro-user/api.ts +43 -0
- package/runtime/node_modules/@metro-labs/daemon/src/metro-user/helper-script.ts +126 -0
- package/runtime/node_modules/@metro-labs/daemon/src/metro-user/move.ts +165 -0
- package/runtime/node_modules/@metro-labs/daemon/src/metro-user/privilege.ts +48 -0
- package/runtime/node_modules/@metro-labs/daemon/src/monitor/api.ts +132 -0
- package/runtime/node_modules/@metro-labs/daemon/src/net/tunnel.ts +316 -0
- package/runtime/node_modules/@metro-labs/daemon/src/policy/policy.ts +118 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/bearer.ts +35 -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 +365 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +208 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/session-apis.ts +71 -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 +39 -0
- package/runtime/node_modules/@metro-labs/daemon/src/server/machine.ts +58 -0
- package/runtime/node_modules/@metro-labs/daemon/src/server/owner.ts +26 -0
- package/runtime/node_modules/@metro-labs/daemon/src/server/update.ts +90 -0
- package/runtime/node_modules/@metro-labs/daemon/src/server.ts +2 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-driver.ts +42 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-inline.ts +128 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-interactive.ts +134 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-outlook.ts +63 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-resolve.ts +225 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-session.ts +288 -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 +265 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/registry.ts +51 -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/stations/webhook-endpoints.ts +51 -0
- package/runtime/node_modules/@metro-labs/daemon/src/terminal/api.ts +84 -0
- package/runtime/node_modules/@metro-labs/daemon/src/terminal/socket.ts +124 -0
- package/runtime/node_modules/@metro-labs/daemon/src/terminal/tickets.ts +15 -0
- package/runtime/node_modules/@metro-labs/daemon/src/vault/api.ts +45 -0
- package/runtime/node_modules/@metro-labs/daemon/src/vault/config.ts +51 -0
- package/runtime/node_modules/@metro-labs/daemon/src/vault/firewall.ts +48 -0
- package/runtime/node_modules/@metro-labs/daemon/src/vault/index.ts +96 -0
- package/runtime/node_modules/@metro-labs/daemon/src/vault/install.ts +56 -0
- package/runtime/node_modules/@metro-labs/daemon/src/vault/paths.ts +26 -0
- package/runtime/node_modules/@metro-labs/daemon/src/vault/proxy.ts +148 -0
- package/runtime/node_modules/@metro-labs/daemon/src/vault/store.ts +130 -0
- package/runtime/node_modules/@metro-labs/daemon/src/vault/trust.ts +61 -0
- package/runtime/node_modules/@metro-labs/discord-bot/package.json +26 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +117 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +240 -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 +174 -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 +122 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +215 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/profile.ts +68 -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 +22 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +82 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +1 -0
- package/runtime/node_modules/@metro-labs/http/package.json +29 -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 +138 -0
- package/runtime/node_modules/@metro-labs/http/src/cors.ts +29 -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/workos-token.ts +149 -0
- package/runtime/node_modules/@metro-labs/outlook/package.json +25 -0
- package/runtime/node_modules/@metro-labs/outlook/src/accounts.ts +107 -0
- package/runtime/node_modules/@metro-labs/outlook/src/actions.ts +20 -0
- package/runtime/node_modules/@metro-labs/outlook/src/attachments.ts +97 -0
- package/runtime/node_modules/@metro-labs/outlook/src/auth.ts +150 -0
- package/runtime/node_modules/@metro-labs/outlook/src/browser.ts +53 -0
- package/runtime/node_modules/@metro-labs/outlook/src/config.ts +36 -0
- package/runtime/node_modules/@metro-labs/outlook/src/format.ts +141 -0
- package/runtime/node_modules/@metro-labs/outlook/src/graph.ts +60 -0
- package/runtime/node_modules/@metro-labs/outlook/src/html.ts +72 -0
- package/runtime/node_modules/@metro-labs/outlook/src/inbound.ts +104 -0
- package/runtime/node_modules/@metro-labs/outlook/src/index.ts +12 -0
- package/runtime/node_modules/@metro-labs/outlook/src/login.ts +150 -0
- package/runtime/node_modules/@metro-labs/outlook/src/me.ts +41 -0
- package/runtime/node_modules/@metro-labs/outlook/src/outbound.ts +116 -0
- package/runtime/node_modules/@metro-labs/outlook/src/query.ts +119 -0
- package/runtime/node_modules/@metro-labs/outlook/src/state.ts +43 -0
- package/runtime/node_modules/@metro-labs/outlook/src/station.ts +15 -0
- package/runtime/node_modules/@metro-labs/outlook/src/trust.ts +70 -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 +256 -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 +128 -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 +57 -0
- package/runtime/node_modules/@metro-labs/telegram/src/index.ts +7 -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/profile.ts +70 -0
- package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +167 -0
- package/runtime/node_modules/@metro-labs/telegram/src/station.ts +27 -0
- package/runtime/node_modules/@metro-labs/telegram/src/types.ts +6 -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 +119 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +246 -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 +173 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +103 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +95 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/profile.ts +65 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +20 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +56 -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 +1 -0
- package/runtime/node_modules/@metro-labs/threema/package.json +26 -0
- package/runtime/node_modules/@metro-labs/threema/src/accounts.ts +106 -0
- package/runtime/node_modules/@metro-labs/threema/src/actions.ts +134 -0
- package/runtime/node_modules/@metro-labs/threema/src/api.ts +152 -0
- package/runtime/node_modules/@metro-labs/threema/src/crypto.ts +100 -0
- package/runtime/node_modules/@metro-labs/threema/src/files.ts +58 -0
- package/runtime/node_modules/@metro-labs/threema/src/format.ts +169 -0
- package/runtime/node_modules/@metro-labs/threema/src/groups.ts +96 -0
- package/runtime/node_modules/@metro-labs/threema/src/ids.ts +21 -0
- package/runtime/node_modules/@metro-labs/threema/src/inbound.ts +89 -0
- package/runtime/node_modules/@metro-labs/threema/src/index.ts +24 -0
- package/runtime/node_modules/@metro-labs/threema/src/messages.ts +264 -0
- package/runtime/node_modules/@metro-labs/threema/src/outbound.ts +160 -0
- package/runtime/node_modules/@metro-labs/threema/src/station.ts +13 -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 +213 -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 +388 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +83 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +40 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +7 -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/names.ts +28 -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 +24 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +94 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +5 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +1 -0
- package/runtime/node_modules/@metro-labs/xmtp/package.json +34 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +114 -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 +101 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +118 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +244 -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-payloads.ts +316 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +69 -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 +47 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +122 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +139 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/names.ts +61 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/profile.ts +153 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/sender.ts +112 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/smart.ts +72 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +17 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +79 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +198 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +88 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +98 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +51 -0
- package/runtime/runtime.json +3 -0
- package/runtime/server.ts +1 -0
- package/runtime/stations.json +37 -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/skills/metro/SKILL.md +0 -122
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export const MAX_TEXT_CHARS = 20_000;
|
|
2
|
+
|
|
3
|
+
const ENTITIES: Record<string, string> = {
|
|
4
|
+
amp: '&',
|
|
5
|
+
lt: '<',
|
|
6
|
+
gt: '>',
|
|
7
|
+
quot: '"',
|
|
8
|
+
apos: "'",
|
|
9
|
+
nbsp: ' ',
|
|
10
|
+
ndash: '-',
|
|
11
|
+
mdash: '-',
|
|
12
|
+
hellip: '...',
|
|
13
|
+
rsquo: "'",
|
|
14
|
+
lsquo: "'",
|
|
15
|
+
rdquo: '"',
|
|
16
|
+
ldquo: '"',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function codePoint(n: number): string {
|
|
20
|
+
return Number.isInteger(n) && n > 0 && n <= 0x10ffff ? String.fromCodePoint(n) : '';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function decodeEntities(text: string): string {
|
|
24
|
+
return text.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (whole, name: string) => {
|
|
25
|
+
if (name.startsWith('#x') || name.startsWith('#X')) return codePoint(parseInt(name.slice(2), 16));
|
|
26
|
+
if (name.startsWith('#')) return codePoint(Number(name.slice(1)));
|
|
27
|
+
return ENTITIES[name.toLowerCase()] ?? whole;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function htmlToText(html: string): string {
|
|
32
|
+
const text = html
|
|
33
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
34
|
+
.replace(/<(head|style|script|title)\b[\s\S]*?<\/\1\s*>/gi, '')
|
|
35
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
36
|
+
.replace(/<li\b[^>]*>/gi, '\n- ')
|
|
37
|
+
.replace(/<\/(p|div|tr|h[1-6]|blockquote|table|ul|ol)\s*>/gi, '\n')
|
|
38
|
+
.replace(/<(p|div|tr|h[1-6]|blockquote|table)\b[^>]*>/gi, '\n')
|
|
39
|
+
.replace(/<\/t[dh]\s*>/gi, ' ')
|
|
40
|
+
.replace(/<[^>]*>/g, '');
|
|
41
|
+
return tidy(decodeEntities(text));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function tidy(text: string): string {
|
|
45
|
+
return text
|
|
46
|
+
.replace(/\r\n?/g, '\n')
|
|
47
|
+
.replace(/[ \t ]+/g, ' ')
|
|
48
|
+
.split('\n')
|
|
49
|
+
.map((l) => l.trim())
|
|
50
|
+
.join('\n')
|
|
51
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
52
|
+
.trim();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function capText(text: string, max = MAX_TEXT_CHARS): string {
|
|
56
|
+
if (text.length <= max) return text;
|
|
57
|
+
return `${text.slice(0, max)}\n[... ${String(text.length - max)} more characters]`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function bodyText(body: { contentType?: unknown; content?: unknown } | undefined): string {
|
|
61
|
+
const content = typeof body?.content === 'string' ? body.content : '';
|
|
62
|
+
return String(body?.contentType).toLowerCase() === 'html' ? htmlToText(content) : tidy(content);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function escapeHtml(text: string): string {
|
|
66
|
+
return text
|
|
67
|
+
.replace(/&/g, '&')
|
|
68
|
+
.replace(/</g, '<')
|
|
69
|
+
.replace(/>/g, '>')
|
|
70
|
+
.replace(/"/g, '"')
|
|
71
|
+
.replace(/\r?\n/g, '<br>');
|
|
72
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { log } from '@metro-labs/core/log';
|
|
2
|
+
import { emitInbound, reportAttachment } from '@metro-labs/core/stations/train-events';
|
|
3
|
+
import type { Account } from './accounts.js';
|
|
4
|
+
import { listFiles, metaOf, saveFile, type MailFile } from './attachments.js';
|
|
5
|
+
import { addressOf, inboundEnvelope, MESSAGE_FIELDS, type GraphMessage } from './format.js';
|
|
6
|
+
import { graphJson, GraphGone } from './graph.js';
|
|
7
|
+
import { noteSeen } from './state.js';
|
|
8
|
+
import { screen, type Screened } from './trust.js';
|
|
9
|
+
|
|
10
|
+
export const POLL_MS = Number(process.env.METRO_OUTLOOK_POLL_MS) || 30_000;
|
|
11
|
+
|
|
12
|
+
const FIRST_PAGE = `/me/mailFolders/inbox/messages/delta?$select=${MESSAGE_FIELDS}`;
|
|
13
|
+
const PAGE_SIZE = { prefer: 'odata.maxpagesize=50' };
|
|
14
|
+
|
|
15
|
+
interface DeltaPage {
|
|
16
|
+
value?: GraphMessage[];
|
|
17
|
+
'@odata.nextLink'?: string;
|
|
18
|
+
'@odata.deltaLink'?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function fresh(acct: Account, m: GraphMessage, since: string): boolean {
|
|
22
|
+
if (m['@removed'] !== undefined) return false;
|
|
23
|
+
if (acct.state.seen.includes(m.id)) return false;
|
|
24
|
+
if (addressOf(m.from) === acct.email) return false;
|
|
25
|
+
return Date.parse(m.receivedDateTime ?? '') >= Date.parse(since);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const reasonOf = (err: unknown): string => (err instanceof Error ? err.message : String(err));
|
|
29
|
+
|
|
30
|
+
async function screened(acct: Account, m: GraphMessage): Promise<Screened> {
|
|
31
|
+
return screen(acct, m).catch((err: unknown) => {
|
|
32
|
+
process.stderr.write(`outlook[${acct.id}] could not read the headers of a message, so it counts as unverified: ${reasonOf(err)}\n`);
|
|
33
|
+
return { verified: false };
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function deliver(acct: Account, m: GraphMessage): Promise<boolean> {
|
|
38
|
+
const verdict = await screened(acct, m);
|
|
39
|
+
if ('skip' in verdict) {
|
|
40
|
+
log.debug({ account: acct.id, reason: verdict.skip }, 'outlook: skipped an automated mail');
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const files: MailFile[] =
|
|
44
|
+
m.hasAttachments === true
|
|
45
|
+
? await listFiles(acct, m.id).catch((err: unknown) => {
|
|
46
|
+
process.stderr.write(`outlook[${acct.id}] could not list the files of a message: ${reasonOf(err)}\n`);
|
|
47
|
+
return [];
|
|
48
|
+
})
|
|
49
|
+
: [];
|
|
50
|
+
const env = inboundEnvelope(acct.id, acct.email, m, files.map(metaOf), verdict.verified);
|
|
51
|
+
emitInbound(acct.id, env);
|
|
52
|
+
const at = { station: 'outlook', account: acct.id, line: String(env.line), forId: String(env.id) };
|
|
53
|
+
for (const [index, file] of files.entries())
|
|
54
|
+
reportAttachment(saveFile(acct, m.id, file, index), { ...at, index }, { saved: { kind: file.kind, size: file.size }, failed: { kind: file.kind } });
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function walk(acct: Account, start: string, onMessage: (m: GraphMessage) => Promise<void>): Promise<string | null> {
|
|
59
|
+
let next: string | undefined = start;
|
|
60
|
+
while (next !== undefined) {
|
|
61
|
+
const page: DeltaPage = await graphJson<DeltaPage>(acct, next, { headers: PAGE_SIZE });
|
|
62
|
+
for (const m of page.value ?? []) await onMessage(m);
|
|
63
|
+
if (page['@odata.deltaLink'] !== undefined) return page['@odata.deltaLink'];
|
|
64
|
+
next = page['@odata.nextLink'];
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function syncOnce(acct: Account): Promise<number> {
|
|
70
|
+
const state = acct.state;
|
|
71
|
+
const since = state.syncedAt;
|
|
72
|
+
if (state.deltaLink === null || since === null) {
|
|
73
|
+
state.syncedAt = since ?? new Date().toISOString();
|
|
74
|
+
state.deltaLink = await walk(acct, FIRST_PAGE, () => Promise.resolve());
|
|
75
|
+
acct.save();
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
let count = 0;
|
|
79
|
+
try {
|
|
80
|
+
state.deltaLink = await walk(acct, state.deltaLink, async (m) => {
|
|
81
|
+
if (!fresh(acct, m, since)) return;
|
|
82
|
+
if (await deliver(acct, m)) count += 1;
|
|
83
|
+
noteSeen(state, m.id);
|
|
84
|
+
});
|
|
85
|
+
} catch (err) {
|
|
86
|
+
if (!(err instanceof GraphGone)) throw err;
|
|
87
|
+
state.deltaLink = null;
|
|
88
|
+
}
|
|
89
|
+
acct.save();
|
|
90
|
+
return count;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function startPolling(acct: Account, intervalMs = POLL_MS): void {
|
|
94
|
+
const tick = (): void => {
|
|
95
|
+
syncOnce(acct)
|
|
96
|
+
.catch((err: unknown) => {
|
|
97
|
+
process.stderr.write(`outlook[${acct.id}] mail check failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
98
|
+
})
|
|
99
|
+
.finally(() => {
|
|
100
|
+
setTimeout(tick, intervalMs);
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
tick();
|
|
104
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { readCalls } from '@metro-labs/core/trains/protocol';
|
|
2
|
+
import { announceAccounts } from '@metro-labs/core/stations/train-boot';
|
|
3
|
+
import { Account, accounts, loadAccounts } from './accounts.js';
|
|
4
|
+
import { handleCall } from './actions.js';
|
|
5
|
+
import { startPolling } from './inbound.js';
|
|
6
|
+
|
|
7
|
+
readCalls('outlook', handleCall);
|
|
8
|
+
|
|
9
|
+
for (const cfg of loadAccounts()) accounts.set(cfg.id, new Account(cfg));
|
|
10
|
+
announceAccounts('outlook', accounts.keys());
|
|
11
|
+
|
|
12
|
+
for (const acct of accounts.values()) startPolling(acct);
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import {
|
|
2
|
+
OutlookAuthError,
|
|
3
|
+
pollDeviceCode,
|
|
4
|
+
requestDeviceCode,
|
|
5
|
+
type DeviceCode,
|
|
6
|
+
type FetchLike,
|
|
7
|
+
type Poll,
|
|
8
|
+
type Tokens,
|
|
9
|
+
} from './auth.js';
|
|
10
|
+
import { authorizeUrl, newState, pkcePair, redeemCode, type Pkce } from './browser.js';
|
|
11
|
+
import { verifyMailbox, type Mailbox } from './me.js';
|
|
12
|
+
|
|
13
|
+
export { parseMailbox } from './me.js';
|
|
14
|
+
|
|
15
|
+
export { OutlookAuthError as OutlookLoginError } from './auth.js';
|
|
16
|
+
export { NOT_SET_UP } from './config.js';
|
|
17
|
+
export { failureOf } from './auth.js';
|
|
18
|
+
|
|
19
|
+
export interface OutlookLoginResult {
|
|
20
|
+
config: Record<string, unknown>;
|
|
21
|
+
identity: Record<string, string>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface OutlookLoginEvents {
|
|
25
|
+
onDone: (result: OutlookLoginResult) => void;
|
|
26
|
+
onFailed: (message: string) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface OutlookLoginDeps {
|
|
30
|
+
fetch?: FetchLike;
|
|
31
|
+
now?: () => number;
|
|
32
|
+
mailbox?: string | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const SLOW_DOWN_MS = 5_000;
|
|
36
|
+
|
|
37
|
+
function resultOf(tokens: Tokens, tenantId: string | null, mailbox: Mailbox, now: number): OutlookLoginResult {
|
|
38
|
+
return {
|
|
39
|
+
config: {
|
|
40
|
+
accountEmail: mailbox.email,
|
|
41
|
+
refreshToken: tokens.refreshToken,
|
|
42
|
+
accessToken: tokens.accessToken,
|
|
43
|
+
expiresAt: tokens.expiresAt,
|
|
44
|
+
tenantId,
|
|
45
|
+
createdAt: new Date(now).toISOString(),
|
|
46
|
+
},
|
|
47
|
+
identity: { email: mailbox.email, ...(mailbox.name === '' ? {} : { name: mailbox.name }) },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class OutlookBrowserLogin {
|
|
52
|
+
readonly state = newState();
|
|
53
|
+
readonly authorizeUrl: string;
|
|
54
|
+
private readonly pkce: Pkce = pkcePair();
|
|
55
|
+
private used = false;
|
|
56
|
+
private readonly fetchImpl: FetchLike;
|
|
57
|
+
private readonly now: () => number;
|
|
58
|
+
private readonly mailbox: string | null;
|
|
59
|
+
|
|
60
|
+
constructor(deps: OutlookLoginDeps = {}) {
|
|
61
|
+
this.fetchImpl = deps.fetch ?? ((input, init) => fetch(input, init));
|
|
62
|
+
this.now = deps.now ?? Date.now;
|
|
63
|
+
this.mailbox = deps.mailbox ?? null;
|
|
64
|
+
this.authorizeUrl = authorizeUrl(this.pkce.challenge, this.state, this.mailbox);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async finish(code: string, state: string): Promise<OutlookLoginResult> {
|
|
68
|
+
if (state !== this.state)
|
|
69
|
+
throw new OutlookAuthError('This sign-in link belongs to another attempt. Start again from the Channels page.');
|
|
70
|
+
if (this.used) throw new OutlookAuthError('This sign-in was already used. Start again from the Channels page.');
|
|
71
|
+
this.used = true;
|
|
72
|
+
const { tokens, tenantId } = await redeemCode(code, this.pkce.verifier, this.fetchImpl, this.now());
|
|
73
|
+
return resultOf(tokens, tenantId, await verifyMailbox(tokens.accessToken, this.fetchImpl, this.mailbox), this.now());
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class OutlookLogin {
|
|
78
|
+
private code: DeviceCode | null = null;
|
|
79
|
+
private timer: ReturnType<typeof setTimeout> | null = null;
|
|
80
|
+
private stopped = false;
|
|
81
|
+
private readonly fetchImpl: FetchLike;
|
|
82
|
+
private readonly now: () => number;
|
|
83
|
+
private readonly mailbox: string | null;
|
|
84
|
+
|
|
85
|
+
constructor(private readonly events: OutlookLoginEvents, deps: OutlookLoginDeps = {}) {
|
|
86
|
+
this.fetchImpl = deps.fetch ?? ((input, init) => fetch(input, init));
|
|
87
|
+
this.now = deps.now ?? Date.now;
|
|
88
|
+
this.mailbox = deps.mailbox ?? null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async start(): Promise<DeviceCode> {
|
|
92
|
+
const code = await requestDeviceCode(this.fetchImpl, this.now());
|
|
93
|
+
this.code = code;
|
|
94
|
+
this.schedule(code.intervalMs);
|
|
95
|
+
return code;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
cancel(): Promise<void> {
|
|
99
|
+
this.stopped = true;
|
|
100
|
+
if (this.timer !== null) clearTimeout(this.timer);
|
|
101
|
+
this.timer = null;
|
|
102
|
+
return Promise.resolve();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private schedule(delayMs: number): void {
|
|
106
|
+
if (this.stopped) return;
|
|
107
|
+
this.timer = setTimeout(() => {
|
|
108
|
+
this.tick().catch((err: unknown) => {
|
|
109
|
+
this.fail(err instanceof OutlookAuthError ? err.message : 'Metro could not finish the Microsoft sign-in.');
|
|
110
|
+
});
|
|
111
|
+
}, delayMs);
|
|
112
|
+
this.timer.unref();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private fail(message: string): void {
|
|
116
|
+
if (this.stopped) return;
|
|
117
|
+
this.stopped = true;
|
|
118
|
+
this.events.onFailed(message);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private async tick(): Promise<void> {
|
|
122
|
+
const code = this.code;
|
|
123
|
+
if (code === null || this.stopped) return;
|
|
124
|
+
if (this.now() >= code.expiresAt) {
|
|
125
|
+
this.fail('The sign-in code expired before it was used. Start again.');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
await this.settle(code, await pollDeviceCode(code, this.fetchImpl, this.now()));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private async settle(code: DeviceCode, poll: Poll): Promise<void> {
|
|
132
|
+
if (poll.kind === 'pending') {
|
|
133
|
+
this.schedule(code.intervalMs);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (poll.kind === 'slow_down') {
|
|
137
|
+
code.intervalMs += SLOW_DOWN_MS;
|
|
138
|
+
this.schedule(code.intervalMs);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (poll.kind === 'failed') {
|
|
142
|
+
this.fail(poll.message);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const mailbox = await verifyMailbox(poll.tokens.accessToken, this.fetchImpl, this.mailbox);
|
|
146
|
+
if (this.stopped) return;
|
|
147
|
+
this.stopped = true;
|
|
148
|
+
this.events.onDone(resultOf(poll.tokens, poll.tenantId, mailbox, this.now()));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { OutlookAuthError, type FetchLike } from './auth.js';
|
|
2
|
+
import { graphBase } from './config.js';
|
|
3
|
+
|
|
4
|
+
export interface Mailbox {
|
|
5
|
+
email: string;
|
|
6
|
+
name: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const text = (v: unknown): string => (typeof v === 'string' ? v.trim() : '');
|
|
10
|
+
|
|
11
|
+
const MAILBOX_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
12
|
+
|
|
13
|
+
export function parseMailbox(value: unknown): string | null {
|
|
14
|
+
const mailbox = text(value).toLowerCase();
|
|
15
|
+
if (mailbox === '') return null;
|
|
16
|
+
if (!MAILBOX_RE.test(mailbox))
|
|
17
|
+
throw new OutlookAuthError('Type the whole address of the mailbox, like andy@company.com, or leave it blank.');
|
|
18
|
+
return mailbox;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const wrongMailbox = (actual: string, wanted: string): string =>
|
|
22
|
+
`You signed in as ${actual}, not ${wanted}. Nothing was connected. Start again and pick ${wanted} on Microsoft's page, or use "Use another account".`;
|
|
23
|
+
|
|
24
|
+
export async function verifyMailbox(accessToken: string, fetchImpl: FetchLike, wanted: string | null = null): Promise<Mailbox> {
|
|
25
|
+
let res: Response;
|
|
26
|
+
try {
|
|
27
|
+
res = await fetchImpl(`${graphBase()}/me?$select=mail,userPrincipalName,displayName`, {
|
|
28
|
+
headers: { authorization: `Bearer ${accessToken}`, accept: 'application/json' },
|
|
29
|
+
});
|
|
30
|
+
} catch (err) {
|
|
31
|
+
throw new OutlookAuthError(`Metro could not reach Microsoft Graph: ${err instanceof Error ? err.message : String(err)}`);
|
|
32
|
+
}
|
|
33
|
+
if (!res.ok) throw new OutlookAuthError(`Microsoft Graph refused the new sign-in (${String(res.status)}), so nothing was connected.`);
|
|
34
|
+
const body = (await res.json().catch(() => ({}))) as Record<string, unknown>;
|
|
35
|
+
const mail = text(body.mail).toLowerCase();
|
|
36
|
+
const principal = text(body.userPrincipalName).toLowerCase();
|
|
37
|
+
const email = mail || principal;
|
|
38
|
+
if (email === '') throw new OutlookAuthError('Microsoft Graph did not say which mailbox this is, so nothing was connected.');
|
|
39
|
+
if (wanted !== null && wanted !== mail && wanted !== principal) throw new OutlookAuthError(wrongMailbox(email, wanted));
|
|
40
|
+
return { email, name: text(body.displayName) };
|
|
41
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { respond } from '@metro-labs/core/stations/station-runtime';
|
|
2
|
+
import { TrainError } from '@metro-labs/core/train-error';
|
|
3
|
+
import { accountOf, type Account } from './accounts.js';
|
|
4
|
+
import { assertSendable, attachFile, filesOf, type OutgoingFile } from './attachments.js';
|
|
5
|
+
import { conversationOfLine, lineOf } from './format.js';
|
|
6
|
+
import { graph, graphJson, jsonInit, messagePath, odataQuote, queryOf } from './graph.js';
|
|
7
|
+
import { escapeHtml } from './html.js';
|
|
8
|
+
|
|
9
|
+
type Args = Record<string, unknown>;
|
|
10
|
+
|
|
11
|
+
export const EPOCH = '1900-01-01T00:00:00Z';
|
|
12
|
+
|
|
13
|
+
const str = (v: unknown): string => (typeof v === 'string' ? v.trim() : '');
|
|
14
|
+
|
|
15
|
+
export async function latestInConversation(acct: Account, conversationId: string): Promise<string> {
|
|
16
|
+
const filter = `receivedDateTime ge ${EPOCH} and conversationId eq ${odataQuote(conversationId)}`;
|
|
17
|
+
const params = queryOf({ $filter: filter, $orderby: 'receivedDateTime desc', $top: '1', $select: 'id' });
|
|
18
|
+
const body = await graphJson<{ value?: { id: string }[] }>(acct, `/me/messages?${params}`);
|
|
19
|
+
const id = body.value?.[0]?.id;
|
|
20
|
+
if (id === undefined)
|
|
21
|
+
throw new TrainError('outlook_empty_conversation', 'Outlook has no message in this conversation to answer', { retryable: false });
|
|
22
|
+
return id;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function discard(acct: Account, draftId: string): Promise<void> {
|
|
26
|
+
await graph(acct, messagePath(draftId), { method: 'DELETE' }).catch((err: unknown) => {
|
|
27
|
+
process.stderr.write(`outlook[${acct.id}] could not remove an unsent draft: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function withFiles(acct: Account, target: string, verb: 'reply' | 'replyAll', comment: string, files: OutgoingFile[]): Promise<string[]> {
|
|
32
|
+
const draft = await graphJson<{ id?: string }>(acct, `${messagePath(target)}/${verb === 'reply' ? 'createReply' : 'createReplyAll'}`, jsonInit('POST', { comment }));
|
|
33
|
+
const draftId = draft.id;
|
|
34
|
+
if (draftId === undefined) throw new TrainError('outlook_graph_error', 'Outlook did not open a reply draft', { retryable: true });
|
|
35
|
+
const labels: string[] = [];
|
|
36
|
+
try {
|
|
37
|
+
for (const file of files) labels.push(await attachFile(acct, draftId, file));
|
|
38
|
+
await graph(acct, `${messagePath(draftId)}/send`, { method: 'POST' });
|
|
39
|
+
} catch (err) {
|
|
40
|
+
await discard(acct, draftId);
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
return labels;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const ADDRESS_RE = /^[^@\s/]+@[^@\s/]+\.[^@\s/]+$/;
|
|
47
|
+
const SUBJECT_MAX = 255;
|
|
48
|
+
|
|
49
|
+
export const isAddress = (target: string): boolean => target.includes('@');
|
|
50
|
+
|
|
51
|
+
export function subjectOf(subject: string, text: string): string {
|
|
52
|
+
const chosen = subject !== '' ? subject : (text.split('\n').find((l) => l.trim() !== '') ?? '').trim();
|
|
53
|
+
if (chosen === '') throw new TrainError('outlook_subject_required', 'a new email needs a subject or some text', { retryable: false });
|
|
54
|
+
return chosen.length > SUBJECT_MAX ? `${chosen.slice(0, SUBJECT_MAX - 3)}...` : chosen;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function compose(id: string, acct: Account, to: string, args: Args, text: string, files: OutgoingFile[]): Promise<void> {
|
|
58
|
+
if (!ADDRESS_RE.test(to)) throw new TrainError('outlook_bad_address', `not an email address: ${to}`, { retryable: false });
|
|
59
|
+
const draft = await graphJson<{ id?: string; conversationId?: string }>(acct, '/me/messages', jsonInit('POST', {
|
|
60
|
+
subject: subjectOf(str(args.subject), text),
|
|
61
|
+
body: { contentType: 'Text', content: text },
|
|
62
|
+
toRecipients: [{ emailAddress: { address: to } }],
|
|
63
|
+
}));
|
|
64
|
+
if (draft.id === undefined) throw new TrainError('outlook_graph_error', 'Outlook did not open a draft', { retryable: true });
|
|
65
|
+
const labels: string[] = [];
|
|
66
|
+
try {
|
|
67
|
+
for (const file of files) labels.push(await attachFile(acct, draft.id, file));
|
|
68
|
+
await graph(acct, `${messagePath(draft.id)}/send`, { method: 'POST' });
|
|
69
|
+
} catch (err) {
|
|
70
|
+
await discard(acct, draft.id);
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
const line = draft.conversationId === undefined ? undefined : lineOf(acct.id, draft.conversationId);
|
|
74
|
+
respond(id, { result: { account: acct.id, messageId: draft.id, ...(line === undefined ? {} : { line }), ...(labels.length > 0 ? { attachments: labels } : {}) } });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface Outgoing {
|
|
78
|
+
target: string;
|
|
79
|
+
acct: Account;
|
|
80
|
+
files: OutgoingFile[];
|
|
81
|
+
text: string;
|
|
82
|
+
replyTo: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function outgoing(args: Args): Promise<Outgoing> {
|
|
86
|
+
const line = str(args.line);
|
|
87
|
+
const parsed = conversationOfLine(line);
|
|
88
|
+
if (parsed === null) throw new TrainError('outlook_bad_line', `not an outlook line: ${line}`, { retryable: false });
|
|
89
|
+
const acct = accountOf(args);
|
|
90
|
+
const files = filesOf(args.attachments);
|
|
91
|
+
const text = typeof args.text === 'string' ? args.text : '';
|
|
92
|
+
if (text.trim() === '' && files.length === 0)
|
|
93
|
+
throw new TrainError('outlook_text_required', 'give some text or a file to send', { retryable: false });
|
|
94
|
+
await assertSendable(files);
|
|
95
|
+
return { target: parsed.conversationId, acct, files, text, replyTo: str(args.replyTo) };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function answer(id: string, args: Args, verb: 'reply' | 'replyAll'): Promise<void> {
|
|
99
|
+
const { target: conversation, acct, files, text, replyTo } = await outgoing(args);
|
|
100
|
+
if (isAddress(conversation)) {
|
|
101
|
+
if (verb === 'reply' || replyTo !== '')
|
|
102
|
+
throw new TrainError('outlook_reply_needs_thread', 'reply on the thread\'s own line; an address line starts a new email', { retryable: false });
|
|
103
|
+
await compose(id, acct, conversation.toLowerCase(), args, text, files);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const target = replyTo === '' ? await latestInConversation(acct, conversation) : replyTo;
|
|
107
|
+
const comment = escapeHtml(text);
|
|
108
|
+
let labels: string[] = [];
|
|
109
|
+
if (files.length === 0) await graph(acct, `${messagePath(target)}/${verb}`, jsonInit('POST', { comment }));
|
|
110
|
+
else labels = await withFiles(acct, target, verb, comment, files);
|
|
111
|
+
respond(id, { result: { account: acct.id, repliedTo: target, ...(labels.length > 0 ? { attachments: labels } : {}) } });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const reply = (id: string, args: Args): Promise<void> => answer(id, args, 'reply');
|
|
115
|
+
|
|
116
|
+
export const send = (id: string, args: Args): Promise<void> => answer(id, args, str(args.replyTo) === '' ? 'replyAll' : 'reply');
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { respond } from '@metro-labs/core/stations/station-runtime';
|
|
2
|
+
import { TrainError } from '@metro-labs/core/train-error';
|
|
3
|
+
import { accountOf, type Account } from './accounts.js';
|
|
4
|
+
import { listFiles, saveFile } from './attachments.js';
|
|
5
|
+
import { conversationOfLine, fullOf, LIST_FIELDS, MESSAGE_FIELDS, summaryOf, type GraphMessage } from './format.js';
|
|
6
|
+
import { graph, graphJson, jsonInit, messagePath, odataQuote, queryOf } from './graph.js';
|
|
7
|
+
import { EPOCH } from './outbound.js';
|
|
8
|
+
|
|
9
|
+
type Args = Record<string, unknown>;
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_LIMIT = 20;
|
|
12
|
+
export const MAX_LIMIT = 50;
|
|
13
|
+
|
|
14
|
+
const str = (v: unknown): string => (typeof v === 'string' ? v.trim() : '');
|
|
15
|
+
|
|
16
|
+
export interface ReadQuery {
|
|
17
|
+
conversationId: string | null;
|
|
18
|
+
query: string;
|
|
19
|
+
from: string;
|
|
20
|
+
since: string;
|
|
21
|
+
until: string;
|
|
22
|
+
before: string;
|
|
23
|
+
unreadOnly: boolean;
|
|
24
|
+
limit: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isoOf(raw: string, field: string): string {
|
|
28
|
+
if (raw === '') return '';
|
|
29
|
+
const at = Date.parse(raw);
|
|
30
|
+
if (Number.isNaN(at)) throw new TrainError('outlook_bad_date', `${field} must be a date, for example 2026-09-24 or 2026-09-24T10:00:00Z`, { retryable: false });
|
|
31
|
+
return new Date(at).toISOString();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function readQueryOf(args: Args): ReadQuery {
|
|
35
|
+
const line = str(args.line);
|
|
36
|
+
const conversation = line === '' ? null : conversationOfLine(line);
|
|
37
|
+
if (line !== '' && conversation === null) throw new TrainError('outlook_bad_line', `not an outlook line: ${line}`, { retryable: false });
|
|
38
|
+
const limit = typeof args.limit === 'number' && args.limit > 0 ? Math.min(Math.floor(args.limit), MAX_LIMIT) : DEFAULT_LIMIT;
|
|
39
|
+
return {
|
|
40
|
+
conversationId: conversation?.conversationId ?? null,
|
|
41
|
+
query: str(args.query).replace(/"/g, ''),
|
|
42
|
+
from: str(args.from).toLowerCase(),
|
|
43
|
+
since: isoOf(str(args.since), 'since'),
|
|
44
|
+
until: isoOf(str(args.until), 'until'),
|
|
45
|
+
before: str(args.before),
|
|
46
|
+
unreadOnly: args.unreadOnly === true,
|
|
47
|
+
limit,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function filterOf(q: ReadQuery, beforeAt: string): string {
|
|
52
|
+
const parts = [`receivedDateTime ge ${q.since === '' ? EPOCH : q.since}`];
|
|
53
|
+
const until = [q.until, beforeAt].filter((s) => s !== '').sort()[0];
|
|
54
|
+
if (until !== undefined) parts.push(`receivedDateTime lt ${until}`);
|
|
55
|
+
if (q.conversationId !== null) parts.push(`conversationId eq ${odataQuote(q.conversationId)}`);
|
|
56
|
+
if (q.from !== '') parts.push(`from/emailAddress/address eq ${odataQuote(q.from)}`);
|
|
57
|
+
if (q.unreadOnly) parts.push('isRead eq false');
|
|
58
|
+
return parts.join(' and ');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function inWindow(at: number, q: ReadQuery, beforeAt: string): boolean {
|
|
62
|
+
if (q.since !== '' && at < Date.parse(q.since)) return false;
|
|
63
|
+
if (q.until !== '' && at >= Date.parse(q.until)) return false;
|
|
64
|
+
return beforeAt === '' || at < Date.parse(beforeAt);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function matches(q: ReadQuery, m: GraphMessage, beforeAt: string): boolean {
|
|
68
|
+
if (q.conversationId !== null && m.conversationId !== q.conversationId) return false;
|
|
69
|
+
if (q.unreadOnly && m.isRead === true) return false;
|
|
70
|
+
return inWindow(Date.parse(m.receivedDateTime ?? ''), q, beforeAt);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function receivedAt(acct: Account, messageId: string): Promise<string> {
|
|
74
|
+
if (messageId === '') return '';
|
|
75
|
+
const m = await graphJson<GraphMessage>(acct, `${messagePath(messageId)}?$select=receivedDateTime`);
|
|
76
|
+
return m.receivedDateTime ?? '';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function searchParams(q: ReadQuery, beforeAt: string): string {
|
|
80
|
+
if (q.query === '')
|
|
81
|
+
return queryOf({ $filter: filterOf(q, beforeAt), $orderby: 'receivedDateTime desc', $top: String(q.limit), $select: LIST_FIELDS });
|
|
82
|
+
const terms = q.from === '' ? q.query : `from:${q.from} ${q.query}`;
|
|
83
|
+
return queryOf({ $search: `"${terms}"`, $top: String(MAX_LIMIT), $select: LIST_FIELDS });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function list(id: string, acct: Account, q: ReadQuery): Promise<void> {
|
|
87
|
+
const beforeAt = await receivedAt(acct, q.before);
|
|
88
|
+
const body = await graphJson<{ value?: GraphMessage[] }>(acct, `/me/messages?${searchParams(q, beforeAt)}`);
|
|
89
|
+
const found = (body.value ?? []).filter((m) => q.query === '' || matches(q, m, beforeAt)).slice(0, q.limit);
|
|
90
|
+
respond(id, {
|
|
91
|
+
result: {
|
|
92
|
+
account: acct.id,
|
|
93
|
+
mode: q.query === '' ? 'filter' : 'search',
|
|
94
|
+
messages: found.map((m) => summaryOf(acct.id, m)),
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function one(id: string, acct: Account, messageId: string): Promise<void> {
|
|
100
|
+
const m = await graphJson<GraphMessage>(acct, `${messagePath(messageId)}?$select=${MESSAGE_FIELDS}`);
|
|
101
|
+
const files = m.hasAttachments === true ? await listFiles(acct, m.id) : [];
|
|
102
|
+
const attachments = [];
|
|
103
|
+
for (const [index, file] of files.entries()) {
|
|
104
|
+
const saved = await saveFile(acct, m.id, file, index);
|
|
105
|
+
attachments.push({ name: file.name, mime: file.mime, kind: file.kind, size: saved.bytes, local_path: saved.path });
|
|
106
|
+
}
|
|
107
|
+
if (m.isRead !== true) await graph(acct, messagePath(m.id), jsonInit('PATCH', { isRead: true }));
|
|
108
|
+
respond(id, { result: { account: acct.id, message: { ...fullOf(acct.id, m), is_read: true, attachments } } });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function read(id: string, args: Args): Promise<void> {
|
|
112
|
+
const acct = accountOf(args);
|
|
113
|
+
const messageId = str(args.messageId);
|
|
114
|
+
if (messageId !== '') {
|
|
115
|
+
await one(id, acct, messageId);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
await list(id, acct, readQueryOf(args));
|
|
119
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { readJson, writeSecure } from '@metro-labs/core/secure-fs';
|
|
2
|
+
import { accountFiles } from '@metro-labs/core/stations/account-files';
|
|
3
|
+
|
|
4
|
+
export interface AccountState {
|
|
5
|
+
accessToken: string;
|
|
6
|
+
refreshToken: string;
|
|
7
|
+
expiresAt: number;
|
|
8
|
+
deltaLink: string | null;
|
|
9
|
+
syncedAt: string | null;
|
|
10
|
+
seen: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const SEEN_MAX = 500;
|
|
14
|
+
|
|
15
|
+
export const stateFiles = accountFiles('OUTLOOK_STATE_DIR', 'outlook-state-');
|
|
16
|
+
|
|
17
|
+
const statePath =(accountId: string): string => stateFiles.path(accountId);
|
|
18
|
+
|
|
19
|
+
const text = (v: unknown): string => (typeof v === 'string' ? v : '');
|
|
20
|
+
const nullable = (v: unknown): string | null => (typeof v === 'string' && v !== '' ? v : null);
|
|
21
|
+
|
|
22
|
+
export function loadState(accountId: string, seed: Partial<AccountState>): AccountState {
|
|
23
|
+
const saved = readJson<Record<string, unknown>>(statePath(accountId), {});
|
|
24
|
+
const refreshToken = text(saved.refreshToken) || text(seed.refreshToken);
|
|
25
|
+
const fromFile = text(saved.refreshToken) !== '';
|
|
26
|
+
return {
|
|
27
|
+
refreshToken,
|
|
28
|
+
accessToken: fromFile ? text(saved.accessToken) : text(seed.accessToken),
|
|
29
|
+
expiresAt: fromFile ? Number(saved.expiresAt) || 0 : Number(seed.expiresAt) || 0,
|
|
30
|
+
deltaLink: nullable(saved.deltaLink) ?? seed.deltaLink ?? null,
|
|
31
|
+
syncedAt: nullable(saved.syncedAt),
|
|
32
|
+
seen: Array.isArray(saved.seen) ? saved.seen.filter((s): s is string => typeof s === 'string') : [],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function saveState(accountId: string, state: AccountState): void {
|
|
37
|
+
writeSecure(statePath(accountId), JSON.stringify(state));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function noteSeen(state: AccountState, messageId: string): void {
|
|
41
|
+
state.seen.push(messageId);
|
|
42
|
+
if (state.seen.length > SEEN_MAX) state.seen.splice(0, state.seen.length - SEEN_MAX);
|
|
43
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ReadFilter, Station, Verb } from '@metro-labs/core/stations/types';
|
|
2
|
+
import { stateFiles } from './state.js';
|
|
3
|
+
|
|
4
|
+
export const outlookStation: Station = {
|
|
5
|
+
name: 'outlook',
|
|
6
|
+
hasAccounts: true,
|
|
7
|
+
hasTrain: true,
|
|
8
|
+
messageVerbs: new Set<Verb>(['send', 'reply', 'read']),
|
|
9
|
+
readFilters: new Set<ReadFilter>(['account', 'query', 'from', 'until', 'unread_only', 'message_id']),
|
|
10
|
+
approvals: false,
|
|
11
|
+
attachmentMode: 'canonical',
|
|
12
|
+
forget: stateFiles.forget,
|
|
13
|
+
forgetExcept: stateFiles.forgetExcept,
|
|
14
|
+
tools: [],
|
|
15
|
+
};
|