@lanes-sh/link 0.1.0
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/LICENSE +202 -0
- package/README.md +144 -0
- package/bin/lanes +42 -0
- package/instructions/agents/lanes-link-scout.md +73 -0
- package/instructions/skills/lanes-link/SKILL.md +187 -0
- package/package.json +95 -0
- package/src/audit/chain.ts +278 -0
- package/src/audit/conformance.ts +374 -0
- package/src/audit/fanout.ts +97 -0
- package/src/audit/index.ts +218 -0
- package/src/audit/stdout.ts +60 -0
- package/src/auth/index.ts +220 -0
- package/src/auth/oauth/metadata.ts +75 -0
- package/src/auth/oauth/server.ts +385 -0
- package/src/auth/oauth/store.ts +215 -0
- package/src/auth/oidc.ts +206 -0
- package/src/auth/remote.ts +72 -0
- package/src/cli/argv.ts +94 -0
- package/src/cli/callback-page.ts +256 -0
- package/src/cli/commands/connect/accounts.ts +94 -0
- package/src/cli/commands/connect/authorise.ts +298 -0
- package/src/cli/commands/connect/client.ts +284 -0
- package/src/cli/commands/connect/index.ts +398 -0
- package/src/cli/commands/connect/outcome.ts +119 -0
- package/src/cli/commands/connect/requirements.ts +103 -0
- package/src/cli/commands/connect/scopes-gate.ts +146 -0
- package/src/cli/commands/connect/settle.ts +136 -0
- package/src/cli/commands/connect/setup.ts +276 -0
- package/src/cli/commands/mcp/assets.ts +189 -0
- package/src/cli/commands/mcp/harnesses.ts +143 -0
- package/src/cli/commands/mcp/list.ts +70 -0
- package/src/cli/commands/mcp/register.ts +199 -0
- package/src/cli/commands/mcp/stdio.ts +57 -0
- package/src/cli/commands/mcp.ts +22 -0
- package/src/cli/commands/operate/attach.ts +121 -0
- package/src/cli/commands/operate/audit.ts +119 -0
- package/src/cli/commands/operate/inspect.ts +355 -0
- package/src/cli/commands/operate/outputs.ts +206 -0
- package/src/cli/commands/operate/policy.ts +80 -0
- package/src/cli/commands/operate/serve.ts +55 -0
- package/src/cli/commands/operate/status.ts +133 -0
- package/src/cli/commands/operate/token.ts +70 -0
- package/src/cli/commands/operate.ts +27 -0
- package/src/cli/commands/owner/memory.ts +110 -0
- package/src/cli/commands/owner/shared.ts +125 -0
- package/src/cli/commands/owner/skills.ts +92 -0
- package/src/cli/commands/owner/vault.ts +140 -0
- package/src/cli/commands/owner.ts +46 -0
- package/src/cli/commands/profile/removal.ts +278 -0
- package/src/cli/commands/profile/remove.ts +315 -0
- package/src/cli/commands/profile.ts +156 -0
- package/src/cli/commands/secrets.ts +176 -0
- package/src/cli/commands/setup.ts +150 -0
- package/src/cli/commands/target.ts +310 -0
- package/src/cli/config-edit.ts +397 -0
- package/src/cli/endpoint-url.ts +77 -0
- package/src/cli/identity.ts +109 -0
- package/src/cli/lanes.ts +78 -0
- package/src/cli/main.ts +333 -0
- package/src/cli/oauth-error.ts +13 -0
- package/src/cli/oauth-exchange.ts +146 -0
- package/src/cli/oauth.ts +354 -0
- package/src/cli/output.ts +184 -0
- package/src/cli/prompt.ts +180 -0
- package/src/cli/publish.ts +185 -0
- package/src/cli/runtime/discovery.ts +123 -0
- package/src/cli/runtime/open.ts +342 -0
- package/src/cli/runtime/registry.ts +185 -0
- package/src/cli/runtime/select.ts +124 -0
- package/src/cli/runtime.ts +34 -0
- package/src/cli/scopes.ts +63 -0
- package/src/cli/usage.ts +111 -0
- package/src/cli/version.ts +25 -0
- package/src/connectivity/auth/README.md +36 -0
- package/src/connectivity/auth/api-key/index.ts +43 -0
- package/src/connectivity/auth/authorize.ts +49 -0
- package/src/connectivity/auth/basic/index.ts +68 -0
- package/src/connectivity/auth/bearer/index.ts +13 -0
- package/src/connectivity/auth/credential.ts +19 -0
- package/src/connectivity/auth/header/index.ts +18 -0
- package/src/connectivity/auth/index.ts +35 -0
- package/src/connectivity/auth/none/index.ts +12 -0
- package/src/connectivity/auth/oauth-authcode/broker.ts +261 -0
- package/src/connectivity/auth/oauth-authcode/index.ts +64 -0
- package/src/connectivity/auth/oauth-authcode/provider.ts +279 -0
- package/src/connectivity/auth/oauth-authcode/refresh.ts +118 -0
- package/src/connectivity/auth/resolve.ts +61 -0
- package/src/connectivity/auth/strategy/index.ts +14 -0
- package/src/connectivity/capability.ts +164 -0
- package/src/connectivity/connector.ts +175 -0
- package/src/connectivity/context.ts +77 -0
- package/src/connectivity/index.ts +94 -0
- package/src/connectivity/mail/attachments.ts +368 -0
- package/src/connectivity/mail/compose.ts +73 -0
- package/src/connectivity/mail/index.ts +40 -0
- package/src/connectivity/mail/message.ts +82 -0
- package/src/connectivity/mail/nodemailer.d.ts +83 -0
- package/src/connectivity/mail/staging.ts +143 -0
- package/src/connectivity/mail/url.ts +301 -0
- package/src/connectivity/manifest/auth.ts +135 -0
- package/src/connectivity/manifest/bundles.ts +23 -0
- package/src/connectivity/manifest/connector.ts +168 -0
- package/src/connectivity/manifest/credential-ref.ts +73 -0
- package/src/connectivity/manifest/identity.ts +43 -0
- package/src/connectivity/manifest/index.ts +46 -0
- package/src/connectivity/manifest/primitives.ts +20 -0
- package/src/connectivity/manifest/provider.ts +208 -0
- package/src/connectivity/manifest/requirements.ts +146 -0
- package/src/connectivity/manifest/setup.ts +55 -0
- package/src/connectivity/provider.ts +163 -0
- package/src/connectivity/transports/README.md +33 -0
- package/src/connectivity/transports/composite/index.ts +68 -0
- package/src/connectivity/transports/dav/calendar.ts +217 -0
- package/src/connectivity/transports/dav/capabilities.ts +151 -0
- package/src/connectivity/transports/dav/client.ts +226 -0
- package/src/connectivity/transports/dav/contacts.ts +75 -0
- package/src/connectivity/transports/dav/ical.ts +412 -0
- package/src/connectivity/transports/dav/index.ts +143 -0
- package/src/connectivity/transports/dav/operations.ts +43 -0
- package/src/connectivity/transports/dav/request.ts +161 -0
- package/src/connectivity/transports/dav/xml.ts +123 -0
- package/src/connectivity/transports/factory.ts +181 -0
- package/src/connectivity/transports/fs/capabilities.ts +88 -0
- package/src/connectivity/transports/fs/commands.ts +258 -0
- package/src/connectivity/transports/fs/index.ts +121 -0
- package/src/connectivity/transports/fs/operations.ts +45 -0
- package/src/connectivity/transports/fs/paths.ts +120 -0
- package/src/connectivity/transports/fs/result.ts +12 -0
- package/src/connectivity/transports/http/index.ts +255 -0
- package/src/connectivity/transports/imap/attachment.ts +166 -0
- package/src/connectivity/transports/imap/capabilities.ts +158 -0
- package/src/connectivity/transports/imap/client.ts +398 -0
- package/src/connectivity/transports/imap/commands.ts +385 -0
- package/src/connectivity/transports/imap/index.ts +152 -0
- package/src/connectivity/transports/imap/operations.ts +64 -0
- package/src/connectivity/transports/imap/parse.ts +130 -0
- package/src/connectivity/transports/imap/parser.ts +272 -0
- package/src/connectivity/transports/imap/result.ts +15 -0
- package/src/connectivity/transports/imap/send.ts +92 -0
- package/src/connectivity/transports/imap/socket.ts +111 -0
- package/src/connectivity/transports/imap/utf7.ts +136 -0
- package/src/connectivity/transports/index.ts +20 -0
- package/src/connectivity/transports/local/index.ts +173 -0
- package/src/connectivity/transports/mcp/index.ts +215 -0
- package/src/deployments/README.md +63 -0
- package/src/deployments/adapters/audit-blob.ts +203 -0
- package/src/deployments/adapters/filesystem.ts +184 -0
- package/src/deployments/adapters/gcp-secret-manager.ts +492 -0
- package/src/deployments/adapters/gcs.ts +191 -0
- package/src/deployments/adapters/otlp.ts +128 -0
- package/src/deployments/adapters/s3.ts +195 -0
- package/src/deployments/azure/README.md +21 -0
- package/src/deployments/bootstrap.ts +177 -0
- package/src/deployments/deploy.ts +290 -0
- package/src/deployments/driver.ts +157 -0
- package/src/deployments/drivers.ts +35 -0
- package/src/deployments/gcp/Dockerfile +70 -0
- package/src/deployments/gcp/cloudbuild.yaml +31 -0
- package/src/deployments/gcp/driver.ts +175 -0
- package/src/deployments/gcp/gcloud.ts +178 -0
- package/src/deployments/gcp/provision.ts +290 -0
- package/src/deployments/gcp/survey.ts +319 -0
- package/src/deployments/local/README.md +12 -0
- package/src/deployments/prepare.ts +257 -0
- package/src/deployments/steps.ts +137 -0
- package/src/deployments/target.ts +295 -0
- package/src/deployments/upload.ts +207 -0
- package/src/dispatch/context.ts +195 -0
- package/src/dispatch/dispatch.ts +350 -0
- package/src/dispatch/index.ts +32 -0
- package/src/dispatch/staging.ts +102 -0
- package/src/policy/index.ts +179 -0
- package/src/policy/limits.ts +77 -0
- package/src/profile/authorization.ts +81 -0
- package/src/profile/files.ts +71 -0
- package/src/profile/index.ts +76 -0
- package/src/profile/layout.ts +123 -0
- package/src/profile/load.ts +199 -0
- package/src/profile/primitives.ts +45 -0
- package/src/profile/schema.ts +347 -0
- package/src/profile/secret-detection.ts +162 -0
- package/src/profile/targets.ts +152 -0
- package/src/profile/workspace.ts +262 -0
- package/src/providers/custom/index.ts +21 -0
- package/src/providers/custom/load.ts +115 -0
- package/src/providers/custom/template.ts +156 -0
- package/src/providers/example/provider.ts +207 -0
- package/src/providers/google/calendar/index.ts +66 -0
- package/src/providers/google/calendar/redact.ts +40 -0
- package/src/providers/google/contacts/index.ts +50 -0
- package/src/providers/google/contacts/redact.ts +21 -0
- package/src/providers/google/docs/index.ts +45 -0
- package/src/providers/google/drive/hints.ts +28 -0
- package/src/providers/google/drive/index.ts +34 -0
- package/src/providers/google/drive/redact.ts +39 -0
- package/src/providers/google/drive-mcp/index.ts +21 -0
- package/src/providers/google/gmail/api.ts +42 -0
- package/src/providers/google/gmail/attachment.ts +142 -0
- package/src/providers/google/gmail/hints.ts +55 -0
- package/src/providers/google/gmail/index.ts +112 -0
- package/src/providers/google/gmail/redact.ts +56 -0
- package/src/providers/google/gmail/send.ts +365 -0
- package/src/providers/google/gmail-mcp/index.ts +35 -0
- package/src/providers/google/index.ts +10 -0
- package/src/providers/google/shared/oauth.ts +122 -0
- package/src/providers/google/shared/scopes.ts +99 -0
- package/src/providers/google/shared/setup.ts +80 -0
- package/src/providers/google/sheets/hints.ts +45 -0
- package/src/providers/google/sheets/index.ts +70 -0
- package/src/providers/google/sheets/redact.ts +45 -0
- package/src/providers/google/specs/calendar.v3.json +1829 -0
- package/src/providers/google/specs/docs.v1.json +381 -0
- package/src/providers/google/specs/drive.v3.json +2208 -0
- package/src/providers/google/specs/gmail.v1.json +2578 -0
- package/src/providers/google/specs/people.v1.json +506 -0
- package/src/providers/google/specs/sheets.v4.json +1269 -0
- package/src/providers/google/specs/tasks.v1.json +840 -0
- package/src/providers/google/specs/vendor.ts +661 -0
- package/src/providers/google/tasks/index.ts +53 -0
- package/src/providers/google/tasks/redact.ts +34 -0
- package/src/providers/harness.ts +95 -0
- package/src/providers/icloud/calendar/index.ts +27 -0
- package/src/providers/icloud/contacts/index.ts +17 -0
- package/src/providers/icloud/drive/index.ts +47 -0
- package/src/providers/icloud/index.ts +8 -0
- package/src/providers/icloud/mail/index.ts +37 -0
- package/src/providers/icloud/shared/setup.ts +66 -0
- package/src/providers/index.ts +93 -0
- package/src/providers/linear/index.ts +11 -0
- package/src/providers/linear/scopes.ts +7 -0
- package/src/providers/memory/provider.ts +429 -0
- package/src/providers/notion/index.ts +19 -0
- package/src/providers/owner.ts +49 -0
- package/src/providers/scopes.ts +26 -0
- package/src/providers/setup/plan.ts +141 -0
- package/src/providers/setup/provider.ts +323 -0
- package/src/providers/shared/frontmatter.ts +119 -0
- package/src/providers/skills/provider.ts +283 -0
- package/src/providers/skills/store.ts +252 -0
- package/src/providers/vault/provider.ts +194 -0
- package/src/registry/index.ts +36 -0
- package/src/registry/policy-bridge.ts +32 -0
- package/src/registry/reconcile.ts +313 -0
- package/src/registry/registry.ts +240 -0
- package/src/secrets/document.ts +293 -0
- package/src/secrets/index.ts +154 -0
- package/src/secrets/system.ts +173 -0
- package/src/secrets/vault.ts +336 -0
- package/src/server/attachments.ts +197 -0
- package/src/server/container.ts +96 -0
- package/src/server/edge.ts +53 -0
- package/src/server/endpoint.ts +352 -0
- package/src/server/generations.ts +362 -0
- package/src/server/harness.ts +400 -0
- package/src/server/index.ts +331 -0
- package/src/server/logging.ts +41 -0
- package/src/server/mcp/build.ts +68 -0
- package/src/server/mcp/icon.ts +145 -0
- package/src/server/mcp/index.ts +32 -0
- package/src/server/mcp/instructions.ts +245 -0
- package/src/server/mcp/naming.ts +39 -0
- package/src/server/mcp/prompts.ts +78 -0
- package/src/server/mcp/resources.ts +106 -0
- package/src/server/mcp/routing.ts +117 -0
- package/src/server/mcp/schema.ts +78 -0
- package/src/server/mcp/tools.ts +186 -0
- package/src/server/mcp/visibility.ts +132 -0
- package/src/server/oauth.ts +222 -0
- package/src/server/rebinding.ts +53 -0
- package/src/server/stdio.ts +192 -0
- package/src/stores/blobs/conformance.ts +223 -0
- package/src/stores/blobs/index.ts +125 -0
- package/src/stores/blobs/testing.ts +49 -0
- package/src/stores/state/index.ts +247 -0
- package/src/stores/state/keys.ts +68 -0
- package/src/stores/state/testing.ts +41 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connectivity — what a provider declares, and the code behind each
|
|
3
|
+
* declaration.
|
|
4
|
+
*
|
|
5
|
+
* Two axes, deliberately independent, one folder per option on each:
|
|
6
|
+
*
|
|
7
|
+
* transports/ how we reach a service (the manifest's `connector.kind`)
|
|
8
|
+
* auth/ how we prove who we are (the manifest's `auth.kind`)
|
|
9
|
+
*
|
|
10
|
+
* A provider picks one from each column and adds nothing else. That is why
|
|
11
|
+
* iCloud can speak IMAP with an app password while Gmail speaks HTTP with
|
|
12
|
+
* OAuth, and why neither costs the other any code.
|
|
13
|
+
*
|
|
14
|
+
* This barrel is also the **provider authoring surface**: a provider must be
|
|
15
|
+
* writable without reading the rest of the codebase. If you need something not
|
|
16
|
+
* exported here, that is a bug in this file rather than a reason to reach into
|
|
17
|
+
* another component. See `docs/detailed/creating-a-provider.md`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export type { ScopedStore, Logger, ConnectionInfo, ProviderContext } from './context.ts';
|
|
21
|
+
|
|
22
|
+
export type {
|
|
23
|
+
Capability,
|
|
24
|
+
CapabilityBase,
|
|
25
|
+
CapabilityResult,
|
|
26
|
+
ToolCapability,
|
|
27
|
+
ToolResult,
|
|
28
|
+
ResourceCapability,
|
|
29
|
+
ResourceContents,
|
|
30
|
+
ResourceResult,
|
|
31
|
+
ResourceListResult,
|
|
32
|
+
PromptCapability,
|
|
33
|
+
PromptMessage,
|
|
34
|
+
PromptResult,
|
|
35
|
+
} from './capability.ts';
|
|
36
|
+
export {
|
|
37
|
+
isTool,
|
|
38
|
+
isResource,
|
|
39
|
+
isPrompt,
|
|
40
|
+
isToolResult,
|
|
41
|
+
isResourceResult,
|
|
42
|
+
isResourceListResult,
|
|
43
|
+
isPromptResult,
|
|
44
|
+
} from './capability.ts';
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Local providers — our own code, for `example` and the M3 owner layer.
|
|
48
|
+
* Everything else is a manifest.
|
|
49
|
+
*/
|
|
50
|
+
export type { AuthRequirement, ProviderDefinition } from './provider.ts';
|
|
51
|
+
export { defineLocalProvider, defineProviderWithCapabilities } from './provider.ts';
|
|
52
|
+
|
|
53
|
+
export type {
|
|
54
|
+
ProviderManifest,
|
|
55
|
+
ConnectorConfig,
|
|
56
|
+
AuthBroker,
|
|
57
|
+
AuthConfig,
|
|
58
|
+
SetupDeclaration,
|
|
59
|
+
SetupPrompt,
|
|
60
|
+
IdentityDeclaration,
|
|
61
|
+
ScopeBundle,
|
|
62
|
+
SetupRequirement,
|
|
63
|
+
SetupNeeds,
|
|
64
|
+
} from './manifest/index.ts';
|
|
65
|
+
export {
|
|
66
|
+
defineProvider,
|
|
67
|
+
providerManifestSchema,
|
|
68
|
+
setupSchema,
|
|
69
|
+
bundleSchema,
|
|
70
|
+
credentialRefForConnection,
|
|
71
|
+
rotatableCredentialRefs,
|
|
72
|
+
setupRequirements,
|
|
73
|
+
UNNAMED_ID,
|
|
74
|
+
RESERVED_PROVIDER_IDS,
|
|
75
|
+
READ_BUNDLE,
|
|
76
|
+
WRITE_BUNDLE,
|
|
77
|
+
} from './manifest/index.ts';
|
|
78
|
+
|
|
79
|
+
export type {
|
|
80
|
+
AnyConnector,
|
|
81
|
+
Connector,
|
|
82
|
+
ConnectorContext,
|
|
83
|
+
ConnectorKind,
|
|
84
|
+
DiscoveryContext,
|
|
85
|
+
DiscoveredCapability,
|
|
86
|
+
AuthStrategy,
|
|
87
|
+
AuthStrategyContext,
|
|
88
|
+
} from './connector.ts';
|
|
89
|
+
|
|
90
|
+
// Re-exported so a provider author imports from one place.
|
|
91
|
+
export type { RedactionRule } from '#audit';
|
|
92
|
+
export { keepKeys, redaction, redactAllValues } from '#audit';
|
|
93
|
+
export type { SecretRef, ScopedSecrets } from '#secrets';
|
|
94
|
+
export type { BlobStore, BlobKey, BlobMetadata } from '#stores/blobs';
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import type { BlobStore } from '#stores/blobs';
|
|
5
|
+
import type { ResolvedAttachment } from './message.ts';
|
|
6
|
+
import { getStaged } from './staging.ts';
|
|
7
|
+
import { fetchFromUrl, type AddressLookup } from './url.ts';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Turning a named attachment into bytes.
|
|
11
|
+
*
|
|
12
|
+
* The whole point of this file is that **the bytes never pass through the model**.
|
|
13
|
+
* Before it existed, the only way to attach anything was for the caller to emit
|
|
14
|
+
* base64 in the tool call, which puts a 239 KB PDF at roughly 320,000 characters
|
|
15
|
+
* — past what a model can write in one message, and absurd even when it fits.
|
|
16
|
+
*
|
|
17
|
+
* So a caller names a file and the endpoint fetches it. Each reference carries
|
|
18
|
+
* exactly one source key:
|
|
19
|
+
*
|
|
20
|
+
* path a file on the machine running this endpoint
|
|
21
|
+
* url fetched over HTTPS, with the checks in `url.ts`
|
|
22
|
+
* handle bytes staged earlier through the upload route
|
|
23
|
+
* message_id an attachment already sitting in the mailbox being used
|
|
24
|
+
* data base64 inline — the escape hatch, not the path
|
|
25
|
+
*
|
|
26
|
+
* `path` is deliberately unrestricted: no allowlist, no confinement to a root.
|
|
27
|
+
* The endpoint already holds its owner's credentials, so the filesystem is
|
|
28
|
+
* treated the same way, and `docs/detailed/creating-a-provider.md`'s note that provider
|
|
29
|
+
* code is trusted code applies here too. What makes that defensible is the audit
|
|
30
|
+
* trail rather than a sandbox — every resolved attachment carries its origin and
|
|
31
|
+
* a SHA-256, and the manifest's `redact` block keeps both, so "was this file ever
|
|
32
|
+
* mailed out" stays an answerable question. Nothing here ever returns bytes to
|
|
33
|
+
* the caller; see `receiptFor`.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** The source keys, in the order they are reported when a caller supplies two. */
|
|
37
|
+
const SOURCE_KEYS = ['path', 'url', 'handle', 'message_id', 'data'] as const;
|
|
38
|
+
|
|
39
|
+
export const attachmentRefSchema = z
|
|
40
|
+
.strictObject({
|
|
41
|
+
path: z
|
|
42
|
+
.string()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe('Path to a file on the machine running this endpoint. Read as-is.'),
|
|
45
|
+
url: z.string().optional().describe('HTTPS URL. The endpoint fetches it; you do not.'),
|
|
46
|
+
handle: z.string().optional().describe('Handle returned by a staged upload.'),
|
|
47
|
+
message_id: z
|
|
48
|
+
.string()
|
|
49
|
+
.optional()
|
|
50
|
+
.describe('Re-attach an attachment already on a message in this mailbox.'),
|
|
51
|
+
attachment_id: z
|
|
52
|
+
.string()
|
|
53
|
+
.optional()
|
|
54
|
+
.describe('Which attachment on that message. Required with message_id where the provider ids them.'),
|
|
55
|
+
data: z
|
|
56
|
+
.string()
|
|
57
|
+
.optional()
|
|
58
|
+
.describe(
|
|
59
|
+
'Base64 file content, for small files only. Prefer path, url, handle, or message_id — those keep the bytes out of the conversation entirely.',
|
|
60
|
+
),
|
|
61
|
+
filename: z.string().optional().describe('Overrides the name derived from the source.'),
|
|
62
|
+
content_type: z.string().optional().describe('Overrides the type derived from the source.'),
|
|
63
|
+
})
|
|
64
|
+
.describe('One attachment, named by exactly one of path, url, handle, message_id, or data.');
|
|
65
|
+
|
|
66
|
+
export type AttachmentRef = z.infer<typeof attachmentRefSchema>;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The same shape as JSON Schema, for connectors whose capabilities are
|
|
70
|
+
* discovered rather than authored.
|
|
71
|
+
*
|
|
72
|
+
* Generated from the Zod schema rather than written twice: `imap` declares raw
|
|
73
|
+
* JSON Schema and a hand-written provider declares Zod, and two hand-maintained
|
|
74
|
+
* copies of one shape drift the moment a key is added to one of them.
|
|
75
|
+
*/
|
|
76
|
+
export const attachmentsJsonSchema = ((): Record<string, unknown> => {
|
|
77
|
+
// `$schema` is meaningful at the root of a document and noise on a property,
|
|
78
|
+
// which is the only place this is ever embedded.
|
|
79
|
+
const { $schema: _root, ...schema } = z.toJSONSchema(z.array(attachmentRefSchema), {
|
|
80
|
+
io: 'input',
|
|
81
|
+
}) as Record<string, unknown>;
|
|
82
|
+
return schema;
|
|
83
|
+
})();
|
|
84
|
+
|
|
85
|
+
/** Pulls bytes for `message_id` out of whatever mailbox the caller is already in. */
|
|
86
|
+
export type MailboxAttachmentSource = (reference: {
|
|
87
|
+
readonly messageId: string;
|
|
88
|
+
readonly attachmentId: string | undefined;
|
|
89
|
+
}) => Promise<{
|
|
90
|
+
readonly bytes: Uint8Array;
|
|
91
|
+
readonly filename: string | null;
|
|
92
|
+
readonly contentType: string | null;
|
|
93
|
+
}>;
|
|
94
|
+
|
|
95
|
+
export interface ResolveOptions {
|
|
96
|
+
/**
|
|
97
|
+
* Total raw bytes allowed across every attachment.
|
|
98
|
+
*
|
|
99
|
+
* Raw rather than encoded, and the caller derives it from whatever its vendor
|
|
100
|
+
* accepts: base64 inflates by 4/3, so a 20 MB message ceiling is about 14.5 MB
|
|
101
|
+
* of files. The composed message is checked exactly afterwards — this is the
|
|
102
|
+
* early, cheap refusal that avoids reading 40 MB to then reject it.
|
|
103
|
+
*/
|
|
104
|
+
readonly maxTotalBytes: number;
|
|
105
|
+
readonly storage?: BlobStore | undefined;
|
|
106
|
+
readonly mailbox?: MailboxAttachmentSource | undefined;
|
|
107
|
+
readonly fetch?: typeof globalThis.fetch | undefined;
|
|
108
|
+
readonly addresses?: AddressLookup | undefined;
|
|
109
|
+
readonly signal?: AbortSignal | undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Resolve every reference, or throw explaining which one failed and why.
|
|
114
|
+
*
|
|
115
|
+
* Takes `unknown` on purpose. One caller has already validated against this
|
|
116
|
+
* schema and the other has not — `imap` reads its arguments as raw casts, so
|
|
117
|
+
* this is the only gate on that path and has to behave like one.
|
|
118
|
+
*/
|
|
119
|
+
export async function resolveAttachments(
|
|
120
|
+
input: unknown,
|
|
121
|
+
options: ResolveOptions,
|
|
122
|
+
): Promise<ResolvedAttachment[]> {
|
|
123
|
+
if (input === undefined || input === null) return [];
|
|
124
|
+
|
|
125
|
+
const parsed = z.array(attachmentRefSchema).safeParse(input);
|
|
126
|
+
if (!parsed.success) {
|
|
127
|
+
throw new Error(`attachments is not shaped right: ${parsed.error.issues[0]?.message}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const resolved: ResolvedAttachment[] = [];
|
|
131
|
+
let total = 0;
|
|
132
|
+
|
|
133
|
+
for (const [index, ref] of parsed.data.entries()) {
|
|
134
|
+
const where = `attachments[${index}]`;
|
|
135
|
+
const attachment = await resolveOne(ref, where, options);
|
|
136
|
+
|
|
137
|
+
total += attachment.bytes.byteLength;
|
|
138
|
+
if (total > options.maxTotalBytes) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Attachments total more than ${options.maxTotalBytes} bytes, which is over what this account accepts for one message.`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
resolved.push(attachment);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return resolved;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function resolveOne(
|
|
151
|
+
ref: AttachmentRef,
|
|
152
|
+
where: string,
|
|
153
|
+
options: ResolveOptions,
|
|
154
|
+
): Promise<ResolvedAttachment> {
|
|
155
|
+
const present = SOURCE_KEYS.filter((key) => ref[key] !== undefined && ref[key] !== '');
|
|
156
|
+
|
|
157
|
+
if (present.length === 0) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`${where} names no file. Give exactly one of ${SOURCE_KEYS.join(', ')}, e.g. { "path": "/Users/you/invoice.pdf" }.`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
if (present.length > 1) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`${where} names ${present.length} sources (${present.join(', ')}). Give exactly one — they are alternatives, not layers.`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const found = await bytesFor(present[0]!, ref, where, options);
|
|
169
|
+
|
|
170
|
+
const filename = ref.filename ?? found.filename ?? 'attachment';
|
|
171
|
+
return {
|
|
172
|
+
filename,
|
|
173
|
+
contentType: ref.content_type ?? found.contentType ?? guessContentType(filename),
|
|
174
|
+
bytes: found.bytes,
|
|
175
|
+
sha256: createHash('sha256').update(found.bytes).digest('hex'),
|
|
176
|
+
origin: found.origin,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
interface FoundBytes {
|
|
181
|
+
readonly bytes: Uint8Array;
|
|
182
|
+
readonly filename: string | null;
|
|
183
|
+
readonly contentType: string | null;
|
|
184
|
+
readonly origin: string;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function bytesFor(
|
|
188
|
+
source: (typeof SOURCE_KEYS)[number],
|
|
189
|
+
ref: AttachmentRef,
|
|
190
|
+
where: string,
|
|
191
|
+
options: ResolveOptions,
|
|
192
|
+
): Promise<FoundBytes> {
|
|
193
|
+
switch (source) {
|
|
194
|
+
case 'path':
|
|
195
|
+
return await fromPath(ref.path!, where, options.maxTotalBytes);
|
|
196
|
+
|
|
197
|
+
case 'url': {
|
|
198
|
+
const fetched = await fetchFromUrl({
|
|
199
|
+
url: ref.url!,
|
|
200
|
+
maxBytes: options.maxTotalBytes,
|
|
201
|
+
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
202
|
+
...(options.addresses ? { addresses: options.addresses } : {}),
|
|
203
|
+
signal: options.signal,
|
|
204
|
+
});
|
|
205
|
+
return {
|
|
206
|
+
bytes: fetched.bytes,
|
|
207
|
+
filename: fetched.filename ?? basename(new URL(ref.url!).pathname),
|
|
208
|
+
contentType: fetched.contentType,
|
|
209
|
+
origin: `url:${ref.url!}`,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
case 'handle':
|
|
214
|
+
return await fromHandle(ref.handle!, where, options.storage);
|
|
215
|
+
|
|
216
|
+
case 'message_id': {
|
|
217
|
+
if (!options.mailbox) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
`${where} uses message_id, which this provider cannot resolve. Fetch the attachment and stage it, or use path or url.`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
const found = await options.mailbox({
|
|
223
|
+
messageId: ref.message_id!,
|
|
224
|
+
attachmentId: ref.attachment_id,
|
|
225
|
+
});
|
|
226
|
+
return {
|
|
227
|
+
bytes: found.bytes,
|
|
228
|
+
filename: found.filename,
|
|
229
|
+
contentType: found.contentType,
|
|
230
|
+
origin: `mailbox:${ref.message_id!}`,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
case 'data': {
|
|
235
|
+
const bytes = decodeBase64(ref.data!, where);
|
|
236
|
+
return { bytes, filename: null, contentType: null, origin: 'inline' };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function fromPath(path: string, where: string, maxBytes: number): Promise<FoundBytes> {
|
|
242
|
+
let bytes: Uint8Array;
|
|
243
|
+
try {
|
|
244
|
+
bytes = new Uint8Array(await readFile(path));
|
|
245
|
+
} catch (failure) {
|
|
246
|
+
const code = (failure as { code?: string }).code;
|
|
247
|
+
if (code === 'ENOENT') throw new Error(`${where}: no file at ${path}.`);
|
|
248
|
+
if (code === 'EACCES') throw new Error(`${where}: ${path} is not readable by this endpoint.`);
|
|
249
|
+
if (code === 'EISDIR') throw new Error(`${where}: ${path} is a directory, not a file.`);
|
|
250
|
+
throw new Error(`${where}: could not read ${path} — ${(failure as Error).message}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (bytes.byteLength > maxBytes) {
|
|
254
|
+
throw new Error(
|
|
255
|
+
`${where}: ${path} is ${bytes.byteLength} bytes, over the ${maxBytes} byte limit for one message.`,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
bytes,
|
|
261
|
+
filename: basename(path),
|
|
262
|
+
contentType: null,
|
|
263
|
+
origin: `path:${path}`,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function fromHandle(
|
|
268
|
+
handle: string,
|
|
269
|
+
where: string,
|
|
270
|
+
storage: BlobStore | undefined,
|
|
271
|
+
): Promise<FoundBytes> {
|
|
272
|
+
if (!storage) {
|
|
273
|
+
throw new Error(`${where} uses a handle, but this provider has no staging store.`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const stored = await getStaged(storage, handle);
|
|
277
|
+
if (!stored) {
|
|
278
|
+
throw new Error(
|
|
279
|
+
`${where}: no staged attachment "${handle}". Handles expire, so stage the file again.`,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
bytes: stored.bytes,
|
|
285
|
+
filename: stored.filename,
|
|
286
|
+
contentType: stored.contentType,
|
|
287
|
+
origin: `handle:${handle}`,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function decodeBase64(value: string, where: string): Uint8Array {
|
|
292
|
+
// Both alphabets, because the two obvious places a caller gets base64 from
|
|
293
|
+
// disagree: a mail API hands back base64url (RFC 4648 §5) while every
|
|
294
|
+
// general-purpose encoder emits the standard one. Rejecting the former would
|
|
295
|
+
// be a correct-looking failure with a corrupt-file outcome.
|
|
296
|
+
const normalized = value.replaceAll('-', '+').replaceAll('_', '/').replaceAll(/\s+/g, '');
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
const buffer = Buffer.from(normalized, 'base64');
|
|
300
|
+
// Buffer.from is lenient and silently drops invalid characters, so a typo
|
|
301
|
+
// becomes a shorter file rather than an error. Re-encoding and comparing
|
|
302
|
+
// lengths catches that.
|
|
303
|
+
const expected = Math.floor((normalized.replace(/=+$/, '').length * 3) / 4);
|
|
304
|
+
if (buffer.byteLength !== expected) throw new Error('not base64');
|
|
305
|
+
return new Uint8Array(buffer);
|
|
306
|
+
} catch {
|
|
307
|
+
throw new Error(`${where}: data is not valid base64.`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function basename(path: string): string | null {
|
|
312
|
+
const name = path.split(/[/\\]/).pop();
|
|
313
|
+
return name ? decodeURIComponent(name) : null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Type from the filename, since three of the five sources do not report one.
|
|
318
|
+
*
|
|
319
|
+
* A table rather than `Bun.file().type`, which would resolve this in one line:
|
|
320
|
+
* Bun-specific APIs are confined to two named files, and a mail composer is not
|
|
321
|
+
* one of them. Deliberately short — what an attachment actually is, not a mime
|
|
322
|
+
* database. Anything unlisted becomes `application/octet-stream`, which every
|
|
323
|
+
* mail client handles as "a file", so the failure mode is a generic icon rather
|
|
324
|
+
* than a broken attachment.
|
|
325
|
+
*/
|
|
326
|
+
const CONTENT_TYPES: Readonly<Record<string, string>> = {
|
|
327
|
+
pdf: 'application/pdf',
|
|
328
|
+
txt: 'text/plain',
|
|
329
|
+
md: 'text/markdown',
|
|
330
|
+
csv: 'text/csv',
|
|
331
|
+
html: 'text/html',
|
|
332
|
+
json: 'application/json',
|
|
333
|
+
xml: 'application/xml',
|
|
334
|
+
ics: 'text/calendar',
|
|
335
|
+
vcf: 'text/vcard',
|
|
336
|
+
png: 'image/png',
|
|
337
|
+
jpg: 'image/jpeg',
|
|
338
|
+
jpeg: 'image/jpeg',
|
|
339
|
+
gif: 'image/gif',
|
|
340
|
+
webp: 'image/webp',
|
|
341
|
+
svg: 'image/svg+xml',
|
|
342
|
+
heic: 'image/heic',
|
|
343
|
+
tiff: 'image/tiff',
|
|
344
|
+
zip: 'application/zip',
|
|
345
|
+
gz: 'application/gzip',
|
|
346
|
+
mp3: 'audio/mpeg',
|
|
347
|
+
m4a: 'audio/mp4',
|
|
348
|
+
wav: 'audio/wav',
|
|
349
|
+
mp4: 'video/mp4',
|
|
350
|
+
mov: 'video/quicktime',
|
|
351
|
+
doc: 'application/msword',
|
|
352
|
+
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
353
|
+
xls: 'application/vnd.ms-excel',
|
|
354
|
+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
355
|
+
ppt: 'application/vnd.ms-powerpoint',
|
|
356
|
+
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
357
|
+
// `.pages`, `.numbers` and `.key` are absent on purpose. Their registered
|
|
358
|
+
// media types carry a vendor name, and `architecture.test.ts` refuses one
|
|
359
|
+
// anywhere a request passes through — correctly, even though an IANA type is
|
|
360
|
+
// not the vendor *knowledge* that rule is aimed at. They fall through to
|
|
361
|
+
// octet-stream, which mail clients show as a file and the recipient's system
|
|
362
|
+
// opens by extension anyway. Adding them back will fail the suite.
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
function guessContentType(filename: string): string {
|
|
366
|
+
const extension = filename.split('.').pop()?.toLowerCase();
|
|
367
|
+
return (extension ? CONTENT_TYPES[extension] : undefined) ?? 'application/octet-stream';
|
|
368
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import nodemailer from 'nodemailer';
|
|
2
|
+
import type { ComposedMessage, OutgoingMessage } from './message.ts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Turn a message into the bytes that will go out.
|
|
6
|
+
*
|
|
7
|
+
* Nodemailer rather than a hand-rolled builder, and the reasoning is the same one
|
|
8
|
+
* that admits `postal-mime` on the reading side: the protocol talk is short and
|
|
9
|
+
* stable, but *composition* is not. Quoted-printable, the 998-octet line limit,
|
|
10
|
+
* RFC 2047 encoded-words in a subject, `multipart/mixed` boundaries that must not
|
|
11
|
+
* occur in any part body (RFC 2046 §5.1.1), base64 wrapped at 76 columns (RFC
|
|
12
|
+
* 2045 §6.8), `Content-Disposition` filenames, `Message-ID` and `Date` — each
|
|
13
|
+
* fails quietly and partially, which is the worst way for mail to break. It is
|
|
14
|
+
* zero-dependency and MIT-0, so accepting it costs nothing but the name.
|
|
15
|
+
*
|
|
16
|
+
* `streamTransport` with `buffer` composes without sending. That was originally
|
|
17
|
+
* so the copy filed in Sent is byte-identical to what the recipient got rather
|
|
18
|
+
* than a re-render of the same intent; it is now also how an HTTP mail API gets
|
|
19
|
+
* a message to base64url, since those APIs take assembled MIME and will not
|
|
20
|
+
* assemble it for you.
|
|
21
|
+
*/
|
|
22
|
+
export async function composeMime(input: {
|
|
23
|
+
/**
|
|
24
|
+
* Omit to leave the `From` header off entirely.
|
|
25
|
+
*
|
|
26
|
+
* SMTP always has one — it is the authenticated account, and the envelope needs
|
|
27
|
+
* it. An HTTP mail API does not: it fills the header from the credential, and
|
|
28
|
+
* guessing a value to hand it risks writing an address the account is not
|
|
29
|
+
* allowed to send as.
|
|
30
|
+
*/
|
|
31
|
+
readonly from?: string | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* The human name to show beside the address — "Ada Lovelace <a@b>".
|
|
34
|
+
*
|
|
35
|
+
* Its absence is why a sent message can look machine-generated: a bare address
|
|
36
|
+
* is what a recipient sees when nothing supplies one. It is not a spam signal
|
|
37
|
+
* on its own (alignment and DKIM decide that), but it is the difference between
|
|
38
|
+
* mail from a person and mail from a process.
|
|
39
|
+
*
|
|
40
|
+
* Passed as a structured pair rather than a pre-joined string so nodemailer
|
|
41
|
+
* does the RFC 2047 encoding — a name with an accent or a comma in it needs it,
|
|
42
|
+
* and hand-joining is where that breaks.
|
|
43
|
+
*/
|
|
44
|
+
readonly fromName?: string | undefined;
|
|
45
|
+
readonly message: OutgoingMessage;
|
|
46
|
+
}): Promise<ComposedMessage> {
|
|
47
|
+
const { from, fromName, message } = input;
|
|
48
|
+
|
|
49
|
+
const composer = nodemailer.createTransport({ streamTransport: true, buffer: true });
|
|
50
|
+
const built = await composer.sendMail({
|
|
51
|
+
...(from ? { from: fromName ? { name: fromName, address: from } : from } : {}),
|
|
52
|
+
to: [...message.to],
|
|
53
|
+
...(message.cc?.length ? { cc: [...message.cc] } : {}),
|
|
54
|
+
...(message.bcc?.length ? { bcc: [...message.bcc] } : {}),
|
|
55
|
+
subject: message.subject,
|
|
56
|
+
...(message.text ? { text: message.text } : {}),
|
|
57
|
+
...(message.html ? { html: message.html } : {}),
|
|
58
|
+
// Threading is two headers, and clients that only read one of them are
|
|
59
|
+
// common enough that setting just `In-Reply-To` orphans the reply.
|
|
60
|
+
...(message.inReplyTo ? { inReplyTo: message.inReplyTo, references: [message.inReplyTo] } : {}),
|
|
61
|
+
...(message.attachments?.length
|
|
62
|
+
? {
|
|
63
|
+
attachments: message.attachments.map((attachment) => ({
|
|
64
|
+
filename: attachment.filename,
|
|
65
|
+
content: attachment.bytes,
|
|
66
|
+
contentType: attachment.contentType,
|
|
67
|
+
})),
|
|
68
|
+
}
|
|
69
|
+
: {}),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return { messageId: built.messageId, raw: new Uint8Array(built.message) };
|
|
73
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mail composition, shared by every transport that sends it.
|
|
3
|
+
*
|
|
4
|
+
* A third folder beside `transports/` and `auth/`, and it earns the place by
|
|
5
|
+
* being needed from both sides of that split: SMTP composes and submits in one
|
|
6
|
+
* step, while an HTTP mail API wants assembled MIME handed to it as a field. The
|
|
7
|
+
* alternative was a second MIME builder, which is a second set of boundary and
|
|
8
|
+
* transfer-encoding bugs.
|
|
9
|
+
*
|
|
10
|
+
* Nothing here names a vendor.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type {
|
|
14
|
+
AttachmentReceipt,
|
|
15
|
+
ComposedMessage,
|
|
16
|
+
OutgoingMessage,
|
|
17
|
+
ResolvedAttachment,
|
|
18
|
+
} from './message.ts';
|
|
19
|
+
export { receiptFor } from './message.ts';
|
|
20
|
+
|
|
21
|
+
export { composeMime } from './compose.ts';
|
|
22
|
+
|
|
23
|
+
export type { AttachmentRef, MailboxAttachmentSource, ResolveOptions } from './attachments.ts';
|
|
24
|
+
export { attachmentRefSchema, attachmentsJsonSchema, resolveAttachments } from './attachments.ts';
|
|
25
|
+
|
|
26
|
+
export type { StagedFile, StagedMetadata } from './staging.ts';
|
|
27
|
+
export {
|
|
28
|
+
getStaged,
|
|
29
|
+
isHandle,
|
|
30
|
+
newHandle,
|
|
31
|
+
putStaged,
|
|
32
|
+
stagedBytesKey,
|
|
33
|
+
stagedMetaKey,
|
|
34
|
+
sweepStaged,
|
|
35
|
+
STAGED_PREFIX,
|
|
36
|
+
STAGED_TTL_MS,
|
|
37
|
+
} from './staging.ts';
|
|
38
|
+
|
|
39
|
+
export type { AddressLookup, FetchedFile } from './url.ts';
|
|
40
|
+
export { fetchFromUrl, isBlocked } from './url.ts';
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an outgoing message is, independent of how it gets sent.
|
|
3
|
+
*
|
|
4
|
+
* Its own folder because two transports now compose one: SMTP submits the bytes
|
|
5
|
+
* itself, and an HTTP mail API wants the same bytes base64url'd into a JSON
|
|
6
|
+
* field. Both need identical MIME, and a second composer would be a second set
|
|
7
|
+
* of quoted-printable and boundary bugs.
|
|
8
|
+
*
|
|
9
|
+
* No vendor name here, and none in the files beside it — this is protocol, and
|
|
10
|
+
* `architecture.test.ts` checks that claim.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* An attachment with its bytes in hand.
|
|
15
|
+
*
|
|
16
|
+
* The resolver produces these; a composer consumes them. By the time one exists
|
|
17
|
+
* the reference has already been read, fetched, or pulled from a mailbox, so
|
|
18
|
+
* nothing downstream needs to know which it was.
|
|
19
|
+
*/
|
|
20
|
+
export interface ResolvedAttachment {
|
|
21
|
+
readonly filename: string;
|
|
22
|
+
readonly contentType: string;
|
|
23
|
+
readonly bytes: Uint8Array;
|
|
24
|
+
/**
|
|
25
|
+
* Hex SHA-256 of `bytes`.
|
|
26
|
+
*
|
|
27
|
+
* Carried on the resolved attachment rather than recomputed by each caller
|
|
28
|
+
* because both consumers want it: the audit log records it, and the tool
|
|
29
|
+
* result hands it back as a receipt. Computing it twice from the same buffer
|
|
30
|
+
* would be waste, and computing it in only one place is how the two disagree.
|
|
31
|
+
*/
|
|
32
|
+
readonly sha256: string;
|
|
33
|
+
/**
|
|
34
|
+
* Where the bytes came from, for the audit log — `path:/Users/…`,
|
|
35
|
+
* `url:https://…`, `mailbox:18f…`, `handle:att_…`, or `inline`.
|
|
36
|
+
*
|
|
37
|
+
* A description, never something to parse. It exists because with unrestricted
|
|
38
|
+
* paths this string is the only record of which file left the machine.
|
|
39
|
+
*/
|
|
40
|
+
readonly origin: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface OutgoingMessage {
|
|
44
|
+
readonly to: readonly string[];
|
|
45
|
+
readonly cc?: readonly string[] | undefined;
|
|
46
|
+
readonly bcc?: readonly string[] | undefined;
|
|
47
|
+
readonly subject: string;
|
|
48
|
+
readonly text?: string | undefined;
|
|
49
|
+
readonly html?: string | undefined;
|
|
50
|
+
readonly inReplyTo?: string | undefined;
|
|
51
|
+
readonly attachments?: readonly ResolvedAttachment[] | undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The exact bytes of a composed message, before anything transmits them. */
|
|
55
|
+
export interface ComposedMessage {
|
|
56
|
+
readonly messageId: string;
|
|
57
|
+
readonly raw: Uint8Array;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* What a send reports about one attachment.
|
|
62
|
+
*
|
|
63
|
+
* Deliberately not the bytes. Echoing content back would rebuild the
|
|
64
|
+
* context-window problem this whole path exists to avoid, and — because a
|
|
65
|
+
* `path` attachment may name any readable file — it would turn a send tool into
|
|
66
|
+
* a general file-read tool by way of the result.
|
|
67
|
+
*/
|
|
68
|
+
export interface AttachmentReceipt {
|
|
69
|
+
readonly filename: string;
|
|
70
|
+
readonly bytes: number;
|
|
71
|
+
readonly content_type: string;
|
|
72
|
+
readonly sha256: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function receiptFor(attachment: ResolvedAttachment): AttachmentReceipt {
|
|
76
|
+
return {
|
|
77
|
+
filename: attachment.filename,
|
|
78
|
+
bytes: attachment.bytes.byteLength,
|
|
79
|
+
content_type: attachment.contentType,
|
|
80
|
+
sha256: attachment.sha256,
|
|
81
|
+
};
|
|
82
|
+
}
|