runline 0.37.0 → 0.38.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/dist/credentials/http.d.ts +17 -3
- package/dist/credentials/http.js +31 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/plugins/_shared/attachments.js +97 -0
- package/dist/plugins/_shared/microsoftAuth.js +33 -4
- package/dist/plugins/_shared/schemas.js +8 -0
- package/dist/plugins/agentMail/src/drafts.js +2 -1
- package/dist/plugins/agentMail/src/inboxes.js +2 -1
- package/dist/plugins/agentMail/src/messages.js +2 -1
- package/dist/plugins/agentMail/src/schemas.js +4 -41
- package/dist/plugins/agentMail/src/shared.js +28 -50
- package/dist/plugins/agentMail/src/threads.js +2 -1
- package/dist/plugins/gmail/src/index.js +10 -22
- package/dist/plugins/microsoftCalendar/src/index.js +513 -17
- package/dist/plugins/microsoftFiles/src/index.js +3 -3
- package/dist/plugins/microsoftMail/src/index.js +305 -54
- package/dist/plugins/todoist/src/credentials.js +4 -11
- package/dist/plugins/todoist/src/index.js +390 -281
- package/dist/plugins/zohoMail/src/credentials.js +92 -0
- package/dist/plugins/zohoMail/src/index.js +241 -0
- package/package.json +1 -1
|
@@ -7,11 +7,25 @@ export interface ResourceOptions {
|
|
|
7
7
|
}
|
|
8
8
|
/** Internal send primitive. Callers validate destination policy before reaching it. */
|
|
9
9
|
export declare function sendResource(url: string, method: HttpMethod, headers: Headers, body: Buffer | undefined, options: ResourceOptions): Promise<Response>;
|
|
10
|
+
export interface PresignedRequestOptions {
|
|
11
|
+
/** Exact origins the URL may be on; anything else is refused. */
|
|
12
|
+
allowedOrigins: string[];
|
|
13
|
+
fetch: typeof globalThis.fetch;
|
|
14
|
+
method?: "GET" | "PUT" | "DELETE";
|
|
15
|
+
/** Request headers; credentials and cookies are never among them. */
|
|
16
|
+
headers?: Record<string, string>;
|
|
17
|
+
body?: Uint8Array;
|
|
18
|
+
maxResponseBytes?: number;
|
|
19
|
+
}
|
|
10
20
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
21
|
+
* One request to a provider-issued preauthenticated URL — a signed
|
|
22
|
+
* download, an upload session — without bearer tokens or cookies.
|
|
23
|
+
* Hosts approve exact origins and enforce DNS/IP egress through their
|
|
24
|
+
* fetch hook. The signed URL is itself secret: errors never include it.
|
|
25
|
+
* No redirects or retries.
|
|
14
26
|
*/
|
|
27
|
+
export declare function presignedRequest(value: string, options: PresignedRequestOptions): Promise<Response>;
|
|
28
|
+
/** A GET of a provider-issued preauthenticated URL; see presignedRequest. */
|
|
15
29
|
export declare function downloadResource(value: string, options: {
|
|
16
30
|
allowedOrigins: string[];
|
|
17
31
|
fetch: typeof globalThis.fetch;
|
package/dist/credentials/http.js
CHANGED
|
@@ -78,11 +78,19 @@ export async function sendResource(url, method, headers, body, options) {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
81
|
+
* Headers a presigned request never sends: the URL is its only authority,
|
|
82
|
+
* so no credential or cookie rides along, and Destination belongs to the
|
|
83
|
+
* credential transport's COPY and MOVE.
|
|
84
84
|
*/
|
|
85
|
-
|
|
85
|
+
const PRESIGNED_REFUSED_HEADERS = ["authorization", "cookie", "destination"];
|
|
86
|
+
/**
|
|
87
|
+
* One request to a provider-issued preauthenticated URL — a signed
|
|
88
|
+
* download, an upload session — without bearer tokens or cookies.
|
|
89
|
+
* Hosts approve exact origins and enforce DNS/IP egress through their
|
|
90
|
+
* fetch hook. The signed URL is itself secret: errors never include it.
|
|
91
|
+
* No redirects or retries.
|
|
92
|
+
*/
|
|
93
|
+
export async function presignedRequest(value, options) {
|
|
86
94
|
let url;
|
|
87
95
|
try {
|
|
88
96
|
url = new URL(value);
|
|
@@ -101,9 +109,23 @@ export async function downloadResource(value, options) {
|
|
|
101
109
|
maxResponseBytes <= 0 ||
|
|
102
110
|
maxResponseBytes > 64 * 1024 * 1024)
|
|
103
111
|
throw new AuthError("invalid_definition");
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
112
|
+
const method = options.method ?? "GET";
|
|
113
|
+
const headers = new Headers();
|
|
114
|
+
try {
|
|
115
|
+
for (const [name, value] of Object.entries(options.headers ?? {})) {
|
|
116
|
+
if (PRESIGNED_REFUSED_HEADERS.includes(name.toLowerCase()))
|
|
117
|
+
throw new Error();
|
|
118
|
+
headers.set(name, value);
|
|
119
|
+
}
|
|
120
|
+
if (options.body !== undefined && method !== "PUT")
|
|
121
|
+
throw new Error();
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
throw new AuthError("request_not_allowed");
|
|
125
|
+
}
|
|
126
|
+
return sendResource(url.toString(), method, headers, options.body === undefined ? undefined : Buffer.from(options.body), { fetch: options.fetch, timeoutMs: 20_000, maxResponseBytes });
|
|
127
|
+
}
|
|
128
|
+
/** A GET of a provider-issued preauthenticated URL; see presignedRequest. */
|
|
129
|
+
export function downloadResource(value, options) {
|
|
130
|
+
return presignedRequest(value, options);
|
|
109
131
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export { awsEndpoint, awsRegion, parseAwsXml } from "./credentials/aws.js";
|
|
|
16
16
|
export type { AwsCredentialSpec } from "./credentials/aws-plugin.js";
|
|
17
17
|
export { awsCredential, awsFailed, awsJson, awsList, awsQuery, awsTarget, } from "./credentials/aws-plugin.js";
|
|
18
18
|
export { brokerFor } from "./credentials/broker.js";
|
|
19
|
-
export { downloadResource } from "./credentials/http.js";
|
|
19
|
+
export { downloadResource, type PresignedRequestOptions, presignedRequest, } from "./credentials/http.js";
|
|
20
20
|
export type { CredentialCall, EmptyAnswer, ErrorEnvelope, StaticCredentialSpec, } from "./credentials/plugin.js";
|
|
21
21
|
export { answerFailed, configChoice, configFlag, credentialJson, credentialOk, credentialRequest, credentialSocketUrl, errorIdentifier, failureMessage, grantSchema, graphqlFailed, hostLabel, httpsBase, jsonAnswer, jsonOrAcknowledged, multipartBody, pathSegment, pathSegments, pathWithin, refuseUnderHost, requestFailed, responseFailed, slashEncodedSegment, staticCredential, } from "./credentials/plugin.js";
|
|
22
22
|
export { AwsAccessKeySchema, AwsSessionSchema, CredentialRegistry, OAuthGrantSchema, OAuthTokensSchema, staticSecretSchema, validateCredential, } from "./credentials/registry.js";
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ export { buildAuthUrl, exchangeAuthCode, generatePKCE, OAUTH_CALLBACK_PORT, OAUT
|
|
|
10
10
|
export { awsEndpoint, awsRegion, parseAwsXml } from "./credentials/aws.js";
|
|
11
11
|
export { awsCredential, awsFailed, awsJson, awsList, awsQuery, awsTarget, } from "./credentials/aws-plugin.js";
|
|
12
12
|
export { brokerFor } from "./credentials/broker.js";
|
|
13
|
-
export { downloadResource } from "./credentials/http.js";
|
|
13
|
+
export { downloadResource, presignedRequest, } from "./credentials/http.js";
|
|
14
14
|
export { answerFailed, configChoice, configFlag, credentialJson, credentialOk, credentialRequest, credentialSocketUrl, errorIdentifier, failureMessage, grantSchema, graphqlFailed, hostLabel, httpsBase, jsonAnswer, jsonOrAcknowledged, multipartBody, pathSegment, pathSegments, pathWithin, refuseUnderHost, requestFailed, responseFailed, slashEncodedSegment, staticCredential, } from "./credentials/plugin.js";
|
|
15
15
|
export { AwsAccessKeySchema, AwsSessionSchema, CredentialRegistry, OAuthGrantSchema, OAuthTokensSchema, staticSecretSchema, validateCredential, } from "./credentials/registry.js";
|
|
16
16
|
export { CredentialTransport } from "./credentials/transport.js";
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import * as t from "typebox";
|
|
3
|
+
import { DownloadedFile, standardBase64 } from "./base64.js";
|
|
4
|
+
import { readLocalFile } from "./localFile.js";
|
|
5
|
+
const STRICT = { additionalProperties: false };
|
|
6
|
+
const id = t.String({ minLength: 1, pattern: "\\S" });
|
|
7
|
+
/** What any attachment may say about itself, whatever its bytes are. */
|
|
8
|
+
export const attachmentMetadata = {
|
|
9
|
+
name: t.Optional(t.String({ ...id, description: "File name; defaults to the path's name." })),
|
|
10
|
+
mimeType: t.Optional(t.String({ ...id, description: "MIME type, e.g. application/pdf." })),
|
|
11
|
+
inline: t.Optional(t.Boolean({
|
|
12
|
+
description: "Embed in the HTML body, referenced as cid:<contentId>, instead of attaching.",
|
|
13
|
+
})),
|
|
14
|
+
contentId: t.Optional(t.String({ ...id, description: "Content-ID for inline images." })),
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* An attachment's bytes, given one way each and named as gmail and
|
|
18
|
+
* googleDrive name them: `contentBase64` (base64, or a
|
|
19
|
+
* googleDrive.file.download result passed straight through),
|
|
20
|
+
* `contentPath` (a file on the runline host) or `content` (UTF-8 text).
|
|
21
|
+
* `sizeNote` states the plugin's own limit.
|
|
22
|
+
*/
|
|
23
|
+
export function attachmentSources(sizeNote) {
|
|
24
|
+
return [
|
|
25
|
+
t.Object({
|
|
26
|
+
...attachmentMetadata,
|
|
27
|
+
contentBase64: t.Union([t.String(), DownloadedFile], {
|
|
28
|
+
description: `Base64 bytes (standard or base64url; padding and whitespace normalized), or a googleDrive.file.download result, whose name and mimeType are used when not given here. ${sizeNote}`,
|
|
29
|
+
}),
|
|
30
|
+
}, STRICT),
|
|
31
|
+
t.Object({
|
|
32
|
+
...attachmentMetadata,
|
|
33
|
+
contentPath: t.String({
|
|
34
|
+
...id,
|
|
35
|
+
description: `A regular file on the runline host, read at call time. ${sizeNote}`,
|
|
36
|
+
}),
|
|
37
|
+
}, STRICT),
|
|
38
|
+
t.Object({
|
|
39
|
+
...attachmentMetadata,
|
|
40
|
+
content: t.String({
|
|
41
|
+
description: `UTF-8 text content, e.g. a generated CSV. ${sizeNote}`,
|
|
42
|
+
}),
|
|
43
|
+
}, STRICT),
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Bytes given as base64, or as a googleDrive.file.download result, which
|
|
48
|
+
* also supplies a name and type. `subject` names the input in errors
|
|
49
|
+
* (`gmail: attachment 0`).
|
|
50
|
+
*/
|
|
51
|
+
export function base64File(value, subject) {
|
|
52
|
+
const result = typeof value === "string" ? undefined : value;
|
|
53
|
+
const content = standardBase64(typeof value === "string" ? value : value.contentBase64);
|
|
54
|
+
if (content === undefined)
|
|
55
|
+
throw new Error(`${subject} contentBase64 is not valid base64`);
|
|
56
|
+
return {
|
|
57
|
+
bytes: Buffer.from(content, "base64"),
|
|
58
|
+
name: result?.name || undefined,
|
|
59
|
+
mimeType: result?.mimeType || undefined,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* An attachment input's bytes. A download result passed as contentBase64
|
|
64
|
+
* supplies the name and type the input leaves out; a local file is named
|
|
65
|
+
* for its path; text is text/plain. Oversized bytes, invalid base64 and
|
|
66
|
+
* unreadable files are refused here, before any request.
|
|
67
|
+
*/
|
|
68
|
+
export async function attachmentFile(input, index, limits) {
|
|
69
|
+
const subject = `${limits.plugin}: attachment ${index}`;
|
|
70
|
+
let file;
|
|
71
|
+
if ("contentBase64" in input) {
|
|
72
|
+
file = base64File(input.contentBase64, subject);
|
|
73
|
+
}
|
|
74
|
+
else if ("contentPath" in input) {
|
|
75
|
+
file = {
|
|
76
|
+
bytes: await readLocalFile(input.contentPath, {
|
|
77
|
+
subject: `${subject} contentPath`,
|
|
78
|
+
maxBytes: limits.maxBytes,
|
|
79
|
+
oversize: limits.oversize,
|
|
80
|
+
}),
|
|
81
|
+
name: basename(input.contentPath),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
file = {
|
|
86
|
+
bytes: Buffer.from(input.content, "utf8"),
|
|
87
|
+
mimeType: "text/plain",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (file.bytes.length > limits.maxBytes)
|
|
91
|
+
throw new Error(limits.oversize);
|
|
92
|
+
return {
|
|
93
|
+
bytes: file.bytes,
|
|
94
|
+
name: input.name ?? file.name,
|
|
95
|
+
mimeType: input.mimeType ?? file.mimeType,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AuthError, brokerFor, downloadResource, jsonAnswer,
|
|
1
|
+
import { AuthError, brokerFor, downloadResource, jsonAnswer, presignedRequest, responseFailed, } from "runline";
|
|
2
2
|
import { MICROSOFT_GRANT, microsoftCredential, microsoftEndpoint, microsoftUserBase, } from "./microsoftCredentials.js";
|
|
3
3
|
export { microsoftCredential, microsoftDriveBase, } from "./microsoftCredentials.js";
|
|
4
4
|
export function userBase(ctx) {
|
|
@@ -7,7 +7,11 @@ export function userBase(ctx) {
|
|
|
7
7
|
export async function microsoftProbe(ctx, plugin, scopes) {
|
|
8
8
|
return brokerFor(ctx, microsoftCredential(plugin, scopes)).probe();
|
|
9
9
|
}
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* One Graph request under the plugin's credential. `accept` is JSON
|
|
12
|
+
* unless the caller reads raw bytes, such as an attachment's `$value`.
|
|
13
|
+
*/
|
|
14
|
+
export async function graphResponse(ctx, plugin, scopes, method, path, body, contentType, accept = "application/json") {
|
|
11
15
|
if (!path.startsWith("/") || path.startsWith("//"))
|
|
12
16
|
throw new AuthError("request_not_allowed");
|
|
13
17
|
return brokerFor(ctx, microsoftCredential(plugin, scopes)).request({
|
|
@@ -15,12 +19,21 @@ export async function graphResponse(ctx, plugin, scopes, method, path, body, con
|
|
|
15
19
|
path: path.slice(1),
|
|
16
20
|
method: method,
|
|
17
21
|
headers: {
|
|
18
|
-
Accept:
|
|
22
|
+
Accept: accept,
|
|
19
23
|
...(contentType ? { "Content-Type": contentType } : {}),
|
|
20
24
|
},
|
|
21
25
|
body,
|
|
22
26
|
});
|
|
23
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* A failed Graph response, by its status and Graph's `error.code`
|
|
30
|
+
* (`ErrorItemNotFound`, `ErrorAccessDenied`); Graph's message is never read.
|
|
31
|
+
*/
|
|
32
|
+
export function graphFailed(plugin, response) {
|
|
33
|
+
return responseFailed(plugin, response, (body) => ({
|
|
34
|
+
code: body?.error?.code,
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
24
37
|
/**
|
|
25
38
|
* A Graph JSON request that must succeed; its answer is read by
|
|
26
39
|
* `jsonAnswer`. JSON and binary requests share the same destination,
|
|
@@ -29,7 +42,7 @@ export async function graphResponse(ctx, plugin, scopes, method, path, body, con
|
|
|
29
42
|
export async function graphRequest(ctx, plugin, scopes, method, path, body) {
|
|
30
43
|
const res = await graphResponse(ctx, plugin, scopes, method, path, body === undefined ? undefined : JSON.stringify(body), body === undefined ? undefined : "application/json");
|
|
31
44
|
if (!res.ok)
|
|
32
|
-
throw
|
|
45
|
+
throw await graphFailed(plugin, res);
|
|
33
46
|
return jsonAnswer(res);
|
|
34
47
|
}
|
|
35
48
|
/** Graph-issued signed download URLs carry their own authority, never a bearer header. */
|
|
@@ -57,6 +70,22 @@ export async function microsoftDownload(value) {
|
|
|
57
70
|
fetch: globalThis.fetch,
|
|
58
71
|
});
|
|
59
72
|
}
|
|
73
|
+
/** Where Graph's Outlook attachment upload sessions live, as Microsoft documents them. */
|
|
74
|
+
const OUTLOOK_UPLOAD_ORIGIN = "https://outlook.office.com";
|
|
75
|
+
/**
|
|
76
|
+
* One request to an Outlook attachment upload session's `uploadUrl`. The
|
|
77
|
+
* URL carries its own authority, so the request has no bearer token; a
|
|
78
|
+
* URL on any other origin is refused.
|
|
79
|
+
*/
|
|
80
|
+
export function outlookUpload(uploadUrl, init) {
|
|
81
|
+
if (typeof uploadUrl !== "string")
|
|
82
|
+
throw new AuthError("invalid_response");
|
|
83
|
+
return presignedRequest(uploadUrl, {
|
|
84
|
+
allowedOrigins: [OUTLOOK_UPLOAD_ORIGIN],
|
|
85
|
+
fetch: globalThis.fetch,
|
|
86
|
+
...init,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
60
89
|
/**
|
|
61
90
|
* A Microsoft Graph plugin's OAuth declaration: the multi-tenant endpoints,
|
|
62
91
|
* `offline_access` for a refresh token, the grant every Graph plugin
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* At least one of `fields` must be given: "provide a change", "name a
|
|
3
|
+
* recipient". Keyed off the field record itself, so the rule cannot drift
|
|
4
|
+
* from the fields it governs.
|
|
5
|
+
*/
|
|
6
|
+
export function atLeastOne(fields) {
|
|
7
|
+
return { anyOf: Object.keys(fields).map((key) => ({ required: [key] })) };
|
|
8
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as t from "typebox";
|
|
2
|
-
import { atLeastOne
|
|
2
|
+
import { atLeastOne } from "../../_shared/schemas.js";
|
|
3
|
+
import { attachments, clientId, contentFields, draftFields, id, idList, inboxFields, labelFields, listFields, STRICT, sendFields, strings, timestamp, } from "./schemas.js";
|
|
3
4
|
import { attachmentPath, draftPath, inboxPath, request, sendMessage, } from "./shared.js";
|
|
4
5
|
const list = t.Object({ ...inboxFields, ...listFields }, STRICT);
|
|
5
6
|
const draft = t.Object(draftFields, STRICT);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as t from "typebox";
|
|
2
|
-
import { atLeastOne
|
|
2
|
+
import { atLeastOne } from "../../_shared/schemas.js";
|
|
3
|
+
import { clientId, id, inboxFields, pageFields, STRICT } from "./schemas.js";
|
|
3
4
|
import { inboxPath, request } from "./shared.js";
|
|
4
5
|
const metadataValue = t.Union([
|
|
5
6
|
t.String({ maxLength: 256 }),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as t from "typebox";
|
|
2
|
-
import { atLeastOne
|
|
2
|
+
import { atLeastOne } from "../../_shared/schemas.js";
|
|
3
|
+
import { id, inboxFields, labelFields, mailListFields, messageContentFields, messageFields, recipientFields, replyContentFields, STRICT, searchFields, sendFields, strings, } from "./schemas.js";
|
|
3
4
|
import { attachmentPath, inboxPath, messagePath, request, sendMessage, } from "./shared.js";
|
|
4
5
|
const list = t.Object({
|
|
5
6
|
...inboxFields,
|
|
@@ -1,16 +1,8 @@
|
|
|
1
1
|
import * as t from "typebox";
|
|
2
|
-
import {
|
|
2
|
+
import { attachmentMetadata, attachmentSources, } from "../../_shared/attachments.js";
|
|
3
3
|
export const STRICT = { additionalProperties: false };
|
|
4
4
|
export const MAX_REQUEST_BYTES = 6_000_000;
|
|
5
5
|
export const id = t.String({ minLength: 1, pattern: "\\S" });
|
|
6
|
-
/**
|
|
7
|
-
* At least one of `fields` must be given: "provide a change", "name a
|
|
8
|
-
* recipient". Keyed off the field record itself, so the rule cannot drift
|
|
9
|
-
* from the fields it governs.
|
|
10
|
-
*/
|
|
11
|
-
export function atLeastOne(fields) {
|
|
12
|
-
return { anyOf: Object.keys(fields).map((key) => ({ required: [key] })) };
|
|
13
|
-
}
|
|
14
6
|
export const inboxFields = {
|
|
15
7
|
inboxId: t.String({
|
|
16
8
|
...id,
|
|
@@ -82,40 +74,11 @@ export const sendFields = {
|
|
|
82
74
|
})),
|
|
83
75
|
};
|
|
84
76
|
/**
|
|
85
|
-
* Attachment bytes come one way each,
|
|
86
|
-
*
|
|
87
|
-
* AgentMail's own `url`. A googleDrive download result passes straight
|
|
88
|
-
* through as `contentBase64`.
|
|
77
|
+
* Attachment bytes come one way each, as every mail plugin takes them,
|
|
78
|
+
* plus AgentMail's own `url`, which AgentMail fetches itself.
|
|
89
79
|
*/
|
|
90
|
-
const attachmentMetadata = {
|
|
91
|
-
name: t.Optional(t.String({ ...id, description: "File name; defaults to the path's name." })),
|
|
92
|
-
mimeType: t.Optional(t.String({ ...id, description: "MIME type, e.g. application/pdf." })),
|
|
93
|
-
inline: t.Optional(t.Boolean({
|
|
94
|
-
description: "Embed in the HTML body, referenced as cid:<contentId>, instead of attaching.",
|
|
95
|
-
})),
|
|
96
|
-
contentId: t.Optional(t.String({ ...id, description: "Content-ID for inline images." })),
|
|
97
|
-
};
|
|
98
|
-
const SIZE_NOTE = "The entire serialized request is limited to 6 MB; use url for larger files.";
|
|
99
80
|
export const attachment = t.Union([
|
|
100
|
-
|
|
101
|
-
...attachmentMetadata,
|
|
102
|
-
contentBase64: t.Union([t.String(), DownloadedFile], {
|
|
103
|
-
description: `Base64 bytes (standard or base64url; padding and whitespace normalized), or a googleDrive.file.download result, whose name and mimeType are used when not given here. ${SIZE_NOTE}`,
|
|
104
|
-
}),
|
|
105
|
-
}, STRICT),
|
|
106
|
-
t.Object({
|
|
107
|
-
...attachmentMetadata,
|
|
108
|
-
contentPath: t.String({
|
|
109
|
-
...id,
|
|
110
|
-
description: `A regular file on the runline host, read at call time. ${SIZE_NOTE}`,
|
|
111
|
-
}),
|
|
112
|
-
}, STRICT),
|
|
113
|
-
t.Object({
|
|
114
|
-
...attachmentMetadata,
|
|
115
|
-
content: t.String({
|
|
116
|
-
description: `UTF-8 text content, e.g. a generated CSV. ${SIZE_NOTE}`,
|
|
117
|
-
}),
|
|
118
|
-
}, STRICT),
|
|
81
|
+
...attachmentSources("The entire serialized request is limited to 6 MB; use url for larger files."),
|
|
119
82
|
t.Object({
|
|
120
83
|
...attachmentMetadata,
|
|
121
84
|
url: t.String({
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { basename } from "node:path";
|
|
2
1
|
import { AuthError, credentialRequest, jsonAnswer, pathSegment, responseFailed, } from "runline";
|
|
3
2
|
import * as t from "typebox";
|
|
4
3
|
import { Check } from "typebox/value";
|
|
5
|
-
import {
|
|
6
|
-
import { readLocalFile } from "../../_shared/localFile.js";
|
|
4
|
+
import { attachmentFile } from "../../_shared/attachments.js";
|
|
7
5
|
import { obj } from "../../_shared/provider.js";
|
|
8
6
|
import { agentMailCredential } from "./credentials.js";
|
|
9
7
|
import { id, MAX_REQUEST_BYTES } from "./schemas.js";
|
|
@@ -32,59 +30,39 @@ function wireFields(fields) {
|
|
|
32
30
|
}
|
|
33
31
|
/** The bytes a request body can still hold once base64 inflates them by 4/3. */
|
|
34
32
|
const MAX_FILE_BYTES = Math.floor((MAX_REQUEST_BYTES * 3) / 4);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
33
|
+
/**
|
|
34
|
+
* One attachment as AgentMail's SendAttachment: its bytes as base64, or
|
|
35
|
+
* its URL for AgentMail to fetch.
|
|
36
|
+
*/
|
|
37
|
+
async function wireAttachment(input, index) {
|
|
38
|
+
let source;
|
|
39
|
+
let file;
|
|
40
|
+
if ("url" in input) {
|
|
41
|
+
try {
|
|
42
|
+
const url = new URL(input.url);
|
|
43
|
+
if (url.protocol !== "https:" || url.username || url.password)
|
|
44
|
+
throw new Error();
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
throw new Error(`agentMail: attachment ${index} url requires HTTPS without embedded credentials`);
|
|
48
|
+
}
|
|
49
|
+
source = { url: input.url };
|
|
50
|
+
file = { name: input.name, mimeType: input.mimeType };
|
|
48
51
|
}
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
+
else {
|
|
53
|
+
const resolved = await attachmentFile(input, index, {
|
|
54
|
+
plugin: "agentMail",
|
|
52
55
|
maxBytes: MAX_FILE_BYTES,
|
|
53
56
|
oversize: `agentMail: attachment ${index} exceeds the 6 MB request limit once encoded; use url`,
|
|
54
57
|
});
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
try {
|
|
60
|
-
const url = new URL(input.url);
|
|
61
|
-
if (url.protocol !== "https:" || url.username || url.password)
|
|
62
|
-
throw new Error();
|
|
58
|
+
if (!resolved.bytes.length)
|
|
59
|
+
throw new Error(`agentMail: attachment ${index} is empty`);
|
|
60
|
+
source = { content: resolved.bytes.toString("base64") };
|
|
61
|
+
file = resolved;
|
|
63
62
|
}
|
|
64
|
-
catch {
|
|
65
|
-
throw new Error(`agentMail: attachment ${index} url requires HTTPS without embedded credentials`);
|
|
66
|
-
}
|
|
67
|
-
return { url: input.url };
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* One attachment as AgentMail's SendAttachment. A download result passed
|
|
71
|
-
* as contentBase64 supplies the name and type the input leaves out; a
|
|
72
|
-
* local file is named for its path; text defaults to text/plain.
|
|
73
|
-
*/
|
|
74
|
-
async function wireAttachment(input, index) {
|
|
75
|
-
const source = await attachmentSource(input, index);
|
|
76
|
-
if ("content" in source && !source.content)
|
|
77
|
-
throw new Error(`agentMail: attachment ${index} is empty`);
|
|
78
|
-
const result = "contentBase64" in input && typeof input.contentBase64 !== "string"
|
|
79
|
-
? input.contentBase64
|
|
80
|
-
: undefined;
|
|
81
63
|
return Object.fromEntries(Object.entries({
|
|
82
|
-
filename:
|
|
83
|
-
|
|
84
|
-
("contentPath" in input ? basename(input.contentPath) : undefined),
|
|
85
|
-
content_type: input.mimeType ??
|
|
86
|
-
(result?.mimeType || undefined) ??
|
|
87
|
-
("content" in input ? "text/plain" : undefined),
|
|
64
|
+
filename: file.name,
|
|
65
|
+
content_type: file.mimeType,
|
|
88
66
|
content_disposition: input.inline === undefined
|
|
89
67
|
? undefined
|
|
90
68
|
: input.inline
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as t from "typebox";
|
|
2
|
-
import { atLeastOne
|
|
2
|
+
import { atLeastOne } from "../../_shared/schemas.js";
|
|
3
|
+
import { id, inboxFields, labelFields, mailListFields, pageFields, STRICT, searchFields, strings, threadFields, } from "./schemas.js";
|
|
3
4
|
import { attachmentPath, inboxPath, request, threadPath } from "./shared.js";
|
|
4
5
|
const list = t.Object({
|
|
5
6
|
...inboxFields,
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { pathSegment, } from "runline";
|
|
21
21
|
import * as t from "typebox";
|
|
22
|
-
import {
|
|
22
|
+
import { base64File } from "../../_shared/attachments.js";
|
|
23
|
+
import { DownloadedFile } from "../../_shared/base64.js";
|
|
23
24
|
import { renderEmailJsx } from "../../_shared/emailJsx.js";
|
|
24
25
|
import { googleCredential, googleJsonRequest, googleOAuth, } from "../../_shared/googleAuth.js";
|
|
25
26
|
import { GoogleTimestamp, Id, NonEmptyString, stringEnum, } from "../../_shared/googleSchemas.js";
|
|
@@ -111,30 +112,17 @@ function normalizeAttachment(input, index) {
|
|
|
111
112
|
throw new Error(`gmail: attachment ${index} must be an object`);
|
|
112
113
|
}
|
|
113
114
|
const content = input.contentBase64;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
typeof content
|
|
118
|
-
typeof content.contentBase64 === "string"
|
|
119
|
-
? content.contentBase64
|
|
120
|
-
: undefined;
|
|
121
|
-
if (typeof contentBase64 !== "string") {
|
|
115
|
+
if (typeof content !== "string" &&
|
|
116
|
+
(typeof content !== "object" ||
|
|
117
|
+
!content ||
|
|
118
|
+
typeof content.contentBase64 !== "string")) {
|
|
122
119
|
throw new Error(`gmail: attachment ${index} contentBase64 must be a base64 string`);
|
|
123
120
|
}
|
|
124
|
-
const
|
|
125
|
-
if (normalized === undefined) {
|
|
126
|
-
throw new Error(`gmail: attachment ${index} contentBase64 is not valid base64`);
|
|
127
|
-
}
|
|
128
|
-
// A download result passed as contentBase64 names its own file and type.
|
|
129
|
-
const result = typeof content === "object" && content ? content : {};
|
|
130
|
-
const named = (value) => typeof value === "string" && value ? value : undefined;
|
|
121
|
+
const file = base64File(content, `gmail: attachment ${index}`);
|
|
131
122
|
return {
|
|
132
|
-
name: input.name ??
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
`attachment-${index + 1}`,
|
|
136
|
-
mimeType: input.mimeType ?? named(result.mimeType) ?? "application/octet-stream",
|
|
137
|
-
contentBase64: normalized,
|
|
123
|
+
name: input.name ?? input.filename ?? file.name ?? `attachment-${index + 1}`,
|
|
124
|
+
mimeType: input.mimeType ?? file.mimeType ?? "application/octet-stream",
|
|
125
|
+
contentBase64: file.bytes.toString("base64"),
|
|
138
126
|
};
|
|
139
127
|
}
|
|
140
128
|
function attachmentPart(att) {
|