@amerged/ohmyhost-runtime 0.1.38 → 0.1.40

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @amerged/ohmyhost-runtime
2
2
 
3
- ohmyho.st runtime client, version 0.1.38.
3
+ ohmyho.st runtime client, version 0.1.40.
4
4
 
5
5
  ```sh
6
6
  npm install @amerged/ohmyhost-runtime
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./errors.js";
2
2
  export * from "./database.js";
3
3
  export * from "./mail.js";
4
+ export * from "./mail-attachment.js";
4
5
  export * from "./storage.js";
5
6
  export type { GatewayConnection } from "./http.js";
6
7
  export * from "./mail-webhook.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,cAAc,CAAC;AAC7B,YAAY,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AACnD,cAAc,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,sBAAsB,CAAC;AACrC,cAAc,cAAc,CAAC;AAC7B,YAAY,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AACnD,cAAc,mBAAmB,CAAC"}
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./errors.js";
2
2
  export * from "./database.js";
3
3
  export * from "./mail.js";
4
+ export * from "./mail-attachment.js";
4
5
  export * from "./storage.js";
5
6
  export * from "./mail-webhook.js";
6
7
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,cAAc,CAAC;AAE7B,cAAc,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,sBAAsB,CAAC;AACrC,cAAc,cAAc,CAAC;AAE7B,cAAc,mBAAmB,CAAC"}
@@ -0,0 +1,14 @@
1
+ import { type GatewayConnection } from "./http.js";
2
+ export interface MailAttachmentDownloadOptions {
3
+ readonly signal?: AbortSignal;
4
+ }
5
+ /** Server-only receiving access using OHMYHOST_MAIL_KEY and global fetch. */
6
+ export declare function createMailAttachmentClient(configuration: GatewayConnection & {
7
+ readonly projectId: string;
8
+ /** Optional expected environment; otherwise use the canonical metadata download_path scope. */
9
+ readonly environmentId?: string;
10
+ }): Readonly<{
11
+ /** Consume to EOF for success, or cancel the stream when its bytes are no longer needed. */
12
+ download(downloadPath: string, options?: MailAttachmentDownloadOptions): Promise<ReadableStream<Uint8Array>>;
13
+ }>;
14
+ //# sourceMappingURL=mail-attachment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mail-attachment.d.ts","sourceRoot":"","sources":["../src/mail-attachment.ts"],"names":[],"mappings":"AACA,OAAO,EAOL,KAAK,iBAAiB,EACvB,MAAM,WAAW,CAAC;AAEnB,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B;AAED,6EAA6E;AAC7E,wBAAgB,0BAA0B,CACxC,aAAa,EAAE,iBAAiB,GAAG;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,+FAA+F;IAC/F,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;IAiBC,4FAA4F;2BAE5E,MAAM,YACX,6BAA6B,GACrC,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC;GAgEzC"}
@@ -0,0 +1,186 @@
1
+ import { CustomerRuntimeGatewayError } from "./errors.js";
2
+ import { exactRecord, fetchWithTimeout, invalidResponse, requireConfiguredUlid, throwGatewayRelayError, validateGatewayConnection, } from "./http.js";
3
+ /** Server-only receiving access using OHMYHOST_MAIL_KEY and global fetch. */
4
+ export function createMailAttachmentClient(configuration) {
5
+ const connection = validateGatewayConnection(configuration);
6
+ const projectId = requireConfiguredUlid(configuration.projectId, "projectId", "OHMYHOST_PROJECT_ID");
7
+ const environmentId = configuration.environmentId === undefined
8
+ ? undefined
9
+ : requireConfiguredUlid(configuration.environmentId, "environmentId", "OHMYHOST_ENVIRONMENT_ID");
10
+ return Object.freeze({
11
+ /** Consume to EOF for success, or cancel the stream when its bytes are no longer needed. */
12
+ async download(downloadPath, options = {}) {
13
+ const match = typeof downloadPath === "string"
14
+ ? /^\/v1\/projects\/([0-7][0-9A-HJKMNP-TV-Z]{25})\/environments\/([0-7][0-9A-HJKMNP-TV-Z]{25})\/mail\/messages\/([0-7][0-9A-HJKMNP-TV-Z]{25})\/attachments\/([A-Za-z0-9_-]{1,128})$/u.exec(downloadPath)
15
+ : null;
16
+ if (match === null ||
17
+ match[1] !== projectId ||
18
+ (environmentId !== undefined && match[2] !== environmentId)) {
19
+ throw new CustomerRuntimeGatewayError("gateway_request_invalid", null, false);
20
+ }
21
+ const body = JSON.stringify({
22
+ project_id: projectId,
23
+ environment_id: match[2],
24
+ message_id: match[3],
25
+ attachment_id: match[4],
26
+ });
27
+ if (new TextEncoder().encode(body).byteLength > 1024) {
28
+ throw new CustomerRuntimeGatewayError("gateway_request_invalid", null, false);
29
+ }
30
+ const cancellation = new AbortController();
31
+ const signal = options.signal === undefined
32
+ ? cancellation.signal
33
+ : AbortSignal.any([cancellation.signal, options.signal]);
34
+ if (signal.aborted)
35
+ throw new CustomerRuntimeGatewayError("gateway_timeout", null, true);
36
+ const response = await fetchWithTimeout(connection.fetch, new Request(new URL("/v1/attachments/download", connection.endpoint), {
37
+ method: "POST",
38
+ headers: {
39
+ authorization: `Bearer ${connection.key}`,
40
+ "content-type": "application/json",
41
+ },
42
+ body,
43
+ signal,
44
+ }), connection.timeoutMilliseconds);
45
+ if (response.status !== 200) {
46
+ try {
47
+ throwAttachmentError(response.status, await readAttachmentError(response, connection.timeoutMilliseconds));
48
+ }
49
+ finally {
50
+ cancellation.abort();
51
+ }
52
+ }
53
+ if (response.body === null ||
54
+ response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() !==
55
+ "application/octet-stream") {
56
+ cancellation.abort();
57
+ void response.body?.cancel().catch(ignoreCancellationError);
58
+ invalidResponse(response.status);
59
+ }
60
+ return attachmentStream(response.body, response.status, cancellation, signal);
61
+ },
62
+ });
63
+ }
64
+ /** A pull reads one source chunk; cancellation never drains or duplicates the attachment. */
65
+ function attachmentStream(body, status, cancellation, signal) {
66
+ const source = body.getReader();
67
+ let finished = false;
68
+ let abort;
69
+ function finish(cancel) {
70
+ if (finished)
71
+ return;
72
+ finished = true;
73
+ signal.removeEventListener("abort", abort);
74
+ if (cancel) {
75
+ cancellation.abort();
76
+ void source.cancel().catch(ignoreCancellationError);
77
+ }
78
+ source.releaseLock();
79
+ }
80
+ return new ReadableStream({
81
+ start(controller) {
82
+ abort = () => {
83
+ controller.error(new CustomerRuntimeGatewayError("gateway_timeout", status, true));
84
+ finish(true);
85
+ };
86
+ if (signal.aborted)
87
+ abort();
88
+ else
89
+ signal.addEventListener("abort", abort, { once: true });
90
+ },
91
+ async pull(controller) {
92
+ try {
93
+ const next = await source.read();
94
+ if (finished)
95
+ return;
96
+ if (next.done) {
97
+ finish(false);
98
+ controller.close();
99
+ }
100
+ else if (next.value instanceof Uint8Array) {
101
+ controller.enqueue(next.value);
102
+ }
103
+ else {
104
+ invalidResponse(status);
105
+ }
106
+ }
107
+ catch {
108
+ if (finished)
109
+ return;
110
+ finish(true);
111
+ controller.error(new CustomerRuntimeGatewayError("gateway_unavailable", status, true));
112
+ }
113
+ },
114
+ cancel() {
115
+ finish(true);
116
+ },
117
+ }, { highWaterMark: 0 });
118
+ }
119
+ /** Only small error records are buffered; binary successes never enter this parser. */
120
+ async function readAttachmentError(response, timeoutMilliseconds) {
121
+ const declared = response.headers.get("content-length");
122
+ if (response.headers.get("content-type")?.split(";", 1)[0]?.trim() !== "application/json" ||
123
+ response.body === null ||
124
+ (declared !== null && (!/^[0-9]+$/u.test(declared) || Number(declared) > 1024))) {
125
+ void response.body?.cancel().catch(ignoreCancellationError);
126
+ invalidResponse(response.status);
127
+ }
128
+ const source = response.body.getReader();
129
+ const bytes = new Uint8Array(1024);
130
+ let length = 0;
131
+ let finished = false;
132
+ let timer;
133
+ try {
134
+ const expired = new Promise((_resolve, reject) => {
135
+ timer = setTimeout(() => reject(new CustomerRuntimeGatewayError("gateway_timeout", response.status, true)), timeoutMilliseconds);
136
+ });
137
+ for (;;) {
138
+ const next = await Promise.race([source.read(), expired]);
139
+ if (next.done) {
140
+ finished = true;
141
+ break;
142
+ }
143
+ if (!(next.value instanceof Uint8Array) || length + next.value.byteLength > bytes.byteLength)
144
+ invalidResponse(response.status);
145
+ bytes.set(next.value, length);
146
+ length += next.value.byteLength;
147
+ }
148
+ try {
149
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, length)));
150
+ }
151
+ catch {
152
+ return invalidResponse(response.status);
153
+ }
154
+ }
155
+ catch (error) {
156
+ if (error instanceof CustomerRuntimeGatewayError)
157
+ throw error;
158
+ throw new CustomerRuntimeGatewayError("gateway_unavailable", response.status, true);
159
+ }
160
+ finally {
161
+ clearTimeout(timer);
162
+ if (!finished)
163
+ void source.cancel().catch(ignoreCancellationError);
164
+ source.releaseLock();
165
+ }
166
+ }
167
+ function ignoreCancellationError() {
168
+ // The original download failure remains the public error.
169
+ }
170
+ function throwAttachmentError(status, body) {
171
+ throwGatewayRelayError(status, body);
172
+ const record = exactRecord(body, ["code"], status);
173
+ const code = record["code"];
174
+ if (status === 400 && code === "invalid_request")
175
+ throw new CustomerRuntimeGatewayError("gateway_request_invalid", status, false);
176
+ if (status === 401 && code === "unauthorized")
177
+ throw new CustomerRuntimeGatewayError("unauthorized", status, false);
178
+ if (status === 404 && code === "resource_not_found")
179
+ throw new CustomerRuntimeGatewayError("mail_message_not_found", status, false);
180
+ if (status === 410 && code === "mail_content_expired")
181
+ throw new CustomerRuntimeGatewayError("mail_content_expired", status, false);
182
+ if (status === 413 && code === "mail_attachment_too_large")
183
+ throw new CustomerRuntimeGatewayError("mail_attachment_too_large", status, false);
184
+ return invalidResponse(status);
185
+ }
186
+ //# sourceMappingURL=mail-attachment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mail-attachment.js","sourceRoot":"","sources":["../src/mail-attachment.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amerged/ohmyhost-runtime",
3
- "version": "0.1.38",
3
+ "version": "0.1.40",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22"
@@ -22,6 +22,10 @@
22
22
  "types": "./dist/mail.d.ts",
23
23
  "import": "./dist/mail.js"
24
24
  },
25
+ "./mail-attachment": {
26
+ "types": "./dist/mail-attachment.d.ts",
27
+ "import": "./dist/mail-attachment.js"
28
+ },
25
29
  "./database": {
26
30
  "types": "./dist/database.d.ts",
27
31
  "import": "./dist/database.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ohmyhost/customer-runtime",
3
- "version": "0.1.38",
3
+ "version": "0.1.40",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -15,6 +15,11 @@
15
15
  "types": "./dist/mail.d.ts",
16
16
  "import": "./dist/mail.js"
17
17
  },
18
+ "./mail-attachment": {
19
+ "source": "./src/mail-attachment.ts",
20
+ "types": "./dist/mail-attachment.d.ts",
21
+ "import": "./dist/mail-attachment.js"
22
+ },
18
23
  "./database": {
19
24
  "source": "./src/database.ts",
20
25
  "types": "./dist/database.d.ts",
@@ -52,7 +57,7 @@
52
57
  },
53
58
  "devDependencies": {
54
59
  "kysely": "0.29.5",
55
- "better-auth": "1.7.1"
60
+ "better-auth": "1.7.7"
56
61
  },
57
62
  "license": "Apache-2.0"
58
63
  }
@@ -1,6 +1,7 @@
1
1
  export * from "./errors.js";
2
2
  export * from "./database.js";
3
3
  export * from "./mail.js";
4
+ export * from "./mail-attachment.js";
4
5
  export * from "./storage.js";
5
6
  export type { GatewayConnection } from "./http.js";
6
7
  export * from "./mail-webhook.js";
@@ -0,0 +1,238 @@
1
+ import { CustomerRuntimeGatewayError } from "./errors.js";
2
+ import {
3
+ exactRecord,
4
+ fetchWithTimeout,
5
+ invalidResponse,
6
+ requireConfiguredUlid,
7
+ throwGatewayRelayError,
8
+ validateGatewayConnection,
9
+ type GatewayConnection,
10
+ } from "./http.js";
11
+
12
+ export interface MailAttachmentDownloadOptions {
13
+ readonly signal?: AbortSignal;
14
+ }
15
+
16
+ /** Server-only receiving access using OHMYHOST_MAIL_KEY and global fetch. */
17
+ export function createMailAttachmentClient(
18
+ configuration: GatewayConnection & {
19
+ readonly projectId: string;
20
+ /** Optional expected environment; otherwise use the canonical metadata download_path scope. */
21
+ readonly environmentId?: string;
22
+ },
23
+ ) {
24
+ const connection = validateGatewayConnection(configuration);
25
+ const projectId = requireConfiguredUlid(
26
+ configuration.projectId,
27
+ "projectId",
28
+ "OHMYHOST_PROJECT_ID",
29
+ );
30
+ const environmentId =
31
+ configuration.environmentId === undefined
32
+ ? undefined
33
+ : requireConfiguredUlid(
34
+ configuration.environmentId,
35
+ "environmentId",
36
+ "OHMYHOST_ENVIRONMENT_ID",
37
+ );
38
+ return Object.freeze({
39
+ /** Consume to EOF for success, or cancel the stream when its bytes are no longer needed. */
40
+ async download(
41
+ downloadPath: string,
42
+ options: MailAttachmentDownloadOptions = {},
43
+ ): Promise<ReadableStream<Uint8Array>> {
44
+ const match =
45
+ typeof downloadPath === "string"
46
+ ? /^\/v1\/projects\/([0-7][0-9A-HJKMNP-TV-Z]{25})\/environments\/([0-7][0-9A-HJKMNP-TV-Z]{25})\/mail\/messages\/([0-7][0-9A-HJKMNP-TV-Z]{25})\/attachments\/([A-Za-z0-9_-]{1,128})$/u.exec(
47
+ downloadPath,
48
+ )
49
+ : null;
50
+ if (
51
+ match === null ||
52
+ match[1] !== projectId ||
53
+ (environmentId !== undefined && match[2] !== environmentId)
54
+ ) {
55
+ throw new CustomerRuntimeGatewayError("gateway_request_invalid", null, false);
56
+ }
57
+ const body = JSON.stringify({
58
+ project_id: projectId,
59
+ environment_id: match[2],
60
+ message_id: match[3],
61
+ attachment_id: match[4],
62
+ });
63
+ if (new TextEncoder().encode(body).byteLength > 1024) {
64
+ throw new CustomerRuntimeGatewayError("gateway_request_invalid", null, false);
65
+ }
66
+ const cancellation = new AbortController();
67
+ const signal =
68
+ options.signal === undefined
69
+ ? cancellation.signal
70
+ : AbortSignal.any([cancellation.signal, options.signal]);
71
+ if (signal.aborted) throw new CustomerRuntimeGatewayError("gateway_timeout", null, true);
72
+ const response = await fetchWithTimeout(
73
+ connection.fetch,
74
+ new Request(new URL("/v1/attachments/download", connection.endpoint), {
75
+ method: "POST",
76
+ headers: {
77
+ authorization: `Bearer ${connection.key}`,
78
+ "content-type": "application/json",
79
+ },
80
+ body,
81
+ signal,
82
+ }),
83
+ connection.timeoutMilliseconds,
84
+ );
85
+ if (response.status !== 200) {
86
+ try {
87
+ throwAttachmentError(
88
+ response.status,
89
+ await readAttachmentError(response, connection.timeoutMilliseconds),
90
+ );
91
+ } finally {
92
+ cancellation.abort();
93
+ }
94
+ }
95
+ if (
96
+ response.body === null ||
97
+ response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() !==
98
+ "application/octet-stream"
99
+ ) {
100
+ cancellation.abort();
101
+ void response.body?.cancel().catch(ignoreCancellationError);
102
+ invalidResponse(response.status);
103
+ }
104
+ return attachmentStream(response.body, response.status, cancellation, signal);
105
+ },
106
+ });
107
+ }
108
+
109
+ /** A pull reads one source chunk; cancellation never drains or duplicates the attachment. */
110
+ function attachmentStream(
111
+ body: ReadableStream<Uint8Array>,
112
+ status: number,
113
+ cancellation: AbortController,
114
+ signal: AbortSignal,
115
+ ): ReadableStream<Uint8Array> {
116
+ const source = body.getReader();
117
+ let finished = false;
118
+ let abort: () => void;
119
+ function finish(cancel: boolean) {
120
+ if (finished) return;
121
+ finished = true;
122
+ signal.removeEventListener("abort", abort);
123
+ if (cancel) {
124
+ cancellation.abort();
125
+ void source.cancel().catch(ignoreCancellationError);
126
+ }
127
+ source.releaseLock();
128
+ }
129
+ return new ReadableStream<Uint8Array>(
130
+ {
131
+ start(controller) {
132
+ abort = () => {
133
+ controller.error(new CustomerRuntimeGatewayError("gateway_timeout", status, true));
134
+ finish(true);
135
+ };
136
+ if (signal.aborted) abort();
137
+ else signal.addEventListener("abort", abort, { once: true });
138
+ },
139
+ async pull(controller) {
140
+ try {
141
+ const next = await source.read();
142
+ if (finished) return;
143
+ if (next.done) {
144
+ finish(false);
145
+ controller.close();
146
+ } else if (next.value instanceof Uint8Array) {
147
+ controller.enqueue(next.value);
148
+ } else {
149
+ invalidResponse(status);
150
+ }
151
+ } catch {
152
+ if (finished) return;
153
+ finish(true);
154
+ controller.error(new CustomerRuntimeGatewayError("gateway_unavailable", status, true));
155
+ }
156
+ },
157
+ cancel() {
158
+ finish(true);
159
+ },
160
+ },
161
+ { highWaterMark: 0 },
162
+ );
163
+ }
164
+
165
+ /** Only small error records are buffered; binary successes never enter this parser. */
166
+ async function readAttachmentError(
167
+ response: Response,
168
+ timeoutMilliseconds: number,
169
+ ): Promise<unknown> {
170
+ const declared = response.headers.get("content-length");
171
+ if (
172
+ response.headers.get("content-type")?.split(";", 1)[0]?.trim() !== "application/json" ||
173
+ response.body === null ||
174
+ (declared !== null && (!/^[0-9]+$/u.test(declared) || Number(declared) > 1024))
175
+ ) {
176
+ void response.body?.cancel().catch(ignoreCancellationError);
177
+ invalidResponse(response.status);
178
+ }
179
+ const source = response.body.getReader();
180
+ const bytes = new Uint8Array(1024);
181
+ let length = 0;
182
+ let finished = false;
183
+ let timer: ReturnType<typeof setTimeout> | undefined;
184
+ try {
185
+ const expired = new Promise<never>((_resolve, reject) => {
186
+ timer = setTimeout(
187
+ () => reject(new CustomerRuntimeGatewayError("gateway_timeout", response.status, true)),
188
+ timeoutMilliseconds,
189
+ );
190
+ });
191
+ for (;;) {
192
+ const next = await Promise.race([source.read(), expired]);
193
+ if (next.done) {
194
+ finished = true;
195
+ break;
196
+ }
197
+ if (!(next.value instanceof Uint8Array) || length + next.value.byteLength > bytes.byteLength)
198
+ invalidResponse(response.status);
199
+ bytes.set(next.value, length);
200
+ length += next.value.byteLength;
201
+ }
202
+ try {
203
+ return JSON.parse(
204
+ new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, length)),
205
+ ) as unknown;
206
+ } catch {
207
+ return invalidResponse(response.status);
208
+ }
209
+ } catch (error) {
210
+ if (error instanceof CustomerRuntimeGatewayError) throw error;
211
+ throw new CustomerRuntimeGatewayError("gateway_unavailable", response.status, true);
212
+ } finally {
213
+ clearTimeout(timer);
214
+ if (!finished) void source.cancel().catch(ignoreCancellationError);
215
+ source.releaseLock();
216
+ }
217
+ }
218
+
219
+ function ignoreCancellationError(): void {
220
+ // The original download failure remains the public error.
221
+ }
222
+
223
+ function throwAttachmentError(status: number, body: unknown): never {
224
+ throwGatewayRelayError(status, body);
225
+ const record = exactRecord(body, ["code"], status);
226
+ const code = record["code"];
227
+ if (status === 400 && code === "invalid_request")
228
+ throw new CustomerRuntimeGatewayError("gateway_request_invalid", status, false);
229
+ if (status === 401 && code === "unauthorized")
230
+ throw new CustomerRuntimeGatewayError("unauthorized", status, false);
231
+ if (status === 404 && code === "resource_not_found")
232
+ throw new CustomerRuntimeGatewayError("mail_message_not_found", status, false);
233
+ if (status === 410 && code === "mail_content_expired")
234
+ throw new CustomerRuntimeGatewayError("mail_content_expired", status, false);
235
+ if (status === 413 && code === "mail_attachment_too_large")
236
+ throw new CustomerRuntimeGatewayError("mail_attachment_too_large", status, false);
237
+ return invalidResponse(status);
238
+ }