@amerged/ohmyhost-runtime 0.1.37 → 0.1.39

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.
Files changed (41) hide show
  1. package/README.md +1 -1
  2. package/dist/errors.d.ts +1 -1
  3. package/dist/errors.d.ts.map +1 -1
  4. package/dist/errors.js.map +1 -1
  5. package/dist/http.d.ts +8 -2
  6. package/dist/http.d.ts.map +1 -1
  7. package/dist/http.js +39 -11
  8. package/dist/http.js.map +1 -1
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/mail-attachment.d.ts +14 -0
  14. package/dist/mail-attachment.d.ts.map +1 -0
  15. package/dist/mail-attachment.js +186 -0
  16. package/dist/mail-attachment.js.map +1 -0
  17. package/dist/mail.d.ts +1 -0
  18. package/dist/mail.d.ts.map +1 -1
  19. package/dist/mail.js +11 -9
  20. package/dist/mail.js.map +1 -1
  21. package/dist/storage-batch-stream.d.ts.map +1 -1
  22. package/dist/storage-batch-stream.js +24 -27
  23. package/dist/storage-batch-stream.js.map +1 -1
  24. package/dist/storage.d.ts +2 -0
  25. package/dist/storage.d.ts.map +1 -1
  26. package/dist/storage.js +43 -51
  27. package/dist/storage.js.map +1 -1
  28. package/package.json +5 -1
  29. package/src/packages/customer-runtime/package.json +7 -2
  30. package/src/packages/customer-runtime/src/errors.ts +3 -0
  31. package/src/packages/customer-runtime/src/http.ts +63 -11
  32. package/src/packages/customer-runtime/src/index.ts +1 -1
  33. package/src/packages/customer-runtime/src/mail-attachment.ts +238 -0
  34. package/src/packages/customer-runtime/src/mail.ts +12 -6
  35. package/src/packages/customer-runtime/src/storage-batch-stream.ts +29 -25
  36. package/src/packages/customer-runtime/src/storage.ts +59 -48
  37. package/dist/mail-attachments.d.ts +0 -8
  38. package/dist/mail-attachments.d.ts.map +0 -1
  39. package/dist/mail-attachments.js +0 -32
  40. package/dist/mail-attachments.js.map +0 -1
  41. package/src/packages/customer-runtime/src/mail-attachments.ts +0 -53
@@ -4,9 +4,13 @@ const MAX_RESPONSE_BYTES = 32 * 1024;
4
4
  const DEFAULT_TIMEOUT_MILLISECONDS = 10_000;
5
5
 
6
6
  export interface GatewayConnection {
7
+ /** Injected gateway origin for server code; never expose the runtime key to a browser. */
7
8
  readonly endpoint: string;
9
+ /** Environment capability key from OHMYHOST_MAIL_KEY or OHMYHOST_STORAGE_KEY. */
8
10
  readonly key: string;
11
+ /** Use (request) => fetch(request); an explicit transport remains available for tests. */
9
12
  readonly fetch: (request: Request) => Promise<Response>;
13
+ /** Gateway header timeout: 100–30000 ms, default 10000 ms. */
10
14
  readonly timeoutMilliseconds?: number;
11
15
  }
12
16
 
@@ -45,7 +49,7 @@ export function validateGatewayConnection(value: GatewayConnection): ValidatedGa
45
49
  }
46
50
  if (typeof value.fetch !== "function") {
47
51
  configurationInvalid(
48
- "fetch is not a function; pass (request) => env.OHMYHOST_MAIL_GATEWAY.fetch(request) or env.OHMYHOST_STORAGE_GATEWAY.fetch(request)",
52
+ "fetch is not a function; pass (request) => fetch(request) in server code",
49
53
  );
50
54
  }
51
55
  const timeoutMilliseconds = value.timeoutMilliseconds ?? DEFAULT_TIMEOUT_MILLISECONDS;
@@ -112,19 +116,23 @@ export async function fetchWithTimeout(
112
116
  }
113
117
  }
114
118
 
115
- export function exactRecord(value: unknown, keys: readonly string[]): Record<string, unknown> {
116
- if (typeof value !== "object" || value === null || Array.isArray(value)) invalidResponse();
119
+ export function exactRecord(
120
+ value: unknown,
121
+ keys: readonly string[],
122
+ status: number | null = null,
123
+ ): Record<string, unknown> {
124
+ if (typeof value !== "object" || value === null || Array.isArray(value)) invalidResponse(status);
117
125
  const record = value as Record<string, unknown>;
118
126
  const actual = Object.keys(record).sort();
119
127
  const expected = [...keys].sort();
120
128
  if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
121
- invalidResponse();
129
+ invalidResponse(status);
122
130
  }
123
131
  return record;
124
132
  }
125
133
 
126
- export function invalidResponse(): never {
127
- throw new CustomerRuntimeGatewayError("gateway_response_invalid", null, false);
134
+ export function invalidResponse(status: number | null = null): never {
135
+ throw new CustomerRuntimeGatewayError("gateway_response_invalid", status, false);
128
136
  }
129
137
 
130
138
  /** A malformed platform ID is a configuration mistake, not a gateway answer. */
@@ -152,17 +160,61 @@ function assertServerRuntime(): void {
152
160
 
153
161
  async function readBoundedJson(response: Response): Promise<unknown> {
154
162
  if (response.headers.get("content-type")?.split(";", 1)[0]?.trim() !== "application/json") {
155
- invalidResponse();
163
+ invalidResponse(response.status);
156
164
  }
157
165
  const declared = response.headers.get("content-length");
158
166
  if (declared !== null && (!/^[0-9]+$/u.test(declared) || Number(declared) > MAX_RESPONSE_BYTES)) {
159
- invalidResponse();
167
+ invalidResponse(response.status);
168
+ }
169
+ let bytes: Uint8Array;
170
+ try {
171
+ bytes = new Uint8Array(await response.arrayBuffer());
172
+ } catch {
173
+ throw new CustomerRuntimeGatewayError("gateway_unavailable", response.status, true);
160
174
  }
161
- const bytes = new Uint8Array(await response.arrayBuffer());
162
- if (bytes.byteLength > MAX_RESPONSE_BYTES) invalidResponse();
175
+ if (bytes.byteLength > MAX_RESPONSE_BYTES) invalidResponse(response.status);
163
176
  try {
164
177
  return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
165
178
  } catch {
166
- return invalidResponse();
179
+ return invalidResponse(response.status);
180
+ }
181
+ }
182
+
183
+ /** Relay failures share one vocabulary across mail, storage and batch control responses. */
184
+ export function throwGatewayRelayError(status: number, body: unknown): void {
185
+ if (typeof body !== "object" || body === null || Array.isArray(body)) return;
186
+ const code = (body as Record<string, unknown>)["code"];
187
+ if (
188
+ (status === 403 && (code === "gateway_not_enabled" || code === "gateway_route_denied")) ||
189
+ (status === 429 && code === "gateway_rate_limited") ||
190
+ (status === 503 &&
191
+ (code === "mail_gateway_unavailable" || code === "storage_gateway_unavailable"))
192
+ ) {
193
+ exactRecord(body, ["code"], status);
194
+ if (code === "gateway_not_enabled") {
195
+ throw new CustomerRuntimeGatewayError(
196
+ code,
197
+ status,
198
+ false,
199
+ "enable the required mail or storage capability in ohmyhost.yaml and redeploy",
200
+ );
201
+ }
202
+ if (code === "gateway_route_denied") {
203
+ throw new CustomerRuntimeGatewayError(
204
+ code,
205
+ status,
206
+ false,
207
+ "use the current runtime client's supported gateway routes through (request) => fetch(request)",
208
+ );
209
+ }
210
+ if (code === "gateway_rate_limited") {
211
+ throw new CustomerRuntimeGatewayError(
212
+ code,
213
+ status,
214
+ true,
215
+ "wait before repeating this request with the same idempotency keys",
216
+ );
217
+ }
218
+ throw new CustomerRuntimeGatewayError("gateway_unavailable", status, true);
167
219
  }
168
220
  }
@@ -1,7 +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";
7
- export * from "./mail-attachments.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
+ }
@@ -5,6 +5,7 @@ import {
5
5
  postGatewayJson,
6
6
  requireConfiguredUlid,
7
7
  validateGatewayConnection,
8
+ throwGatewayRelayError,
8
9
  type GatewayConnection,
9
10
  } from "./http.js";
10
11
 
@@ -38,6 +39,7 @@ export type TransactionalMailResult =
38
39
  readonly requestId: string;
39
40
  };
40
41
 
42
+ /** Server-only transactional mail over the injected gateway URL and global fetch. */
41
43
  export function createTransactionalMailClient(
42
44
  configuration: GatewayConnection & { readonly projectId: string },
43
45
  ) {
@@ -72,8 +74,9 @@ export function createTransactionalMailClient(
72
74
  raw?.["state"] === "accepted"
73
75
  ? ["state", "request_id", "message_id"]
74
76
  : ["state", "request_id"],
77
+ response.status,
75
78
  );
76
- const requestId = requireUlid(stateRecord["request_id"]);
79
+ const requestId = requireUlid(stateRecord["request_id"], response.status);
77
80
  if (stateRecord["state"] === "uncertain") {
78
81
  return Object.freeze({ state: "uncertain" as const, requestId });
79
82
  }
@@ -82,7 +85,7 @@ export function createTransactionalMailClient(
82
85
  typeof stateRecord["message_id"] !== "string" ||
83
86
  !/^[A-Za-z0-9._-]{1,256}$/u.test(stateRecord["message_id"])
84
87
  ) {
85
- invalidResponse();
88
+ invalidResponse(response.status);
86
89
  }
87
90
  return Object.freeze({
88
91
  state: "accepted" as const,
@@ -108,13 +111,15 @@ function validateMailInput(input: TransactionalMailInput): void {
108
111
  }
109
112
 
110
113
  function throwMailError(status: number, body: unknown): never {
114
+ throwGatewayRelayError(status, body);
111
115
  const raw = body as Record<string, unknown>;
112
116
  const record = exactRecord(
113
117
  body,
114
118
  raw?.["request_id"] === undefined ? ["code"] : ["code", "request_id"],
119
+ status,
115
120
  );
116
121
  const code = record["code"];
117
- if (record["request_id"] !== undefined) requireUlid(record["request_id"]);
122
+ if (record["request_id"] !== undefined) requireUlid(record["request_id"], status);
118
123
  if (status === 400 && code === "invalid_request") {
119
124
  throw new CustomerRuntimeGatewayError("gateway_request_invalid", status, false);
120
125
  }
@@ -145,11 +150,12 @@ function throwMailError(status: number, body: unknown): never {
145
150
  if (status === 503 && typeof code === "string") {
146
151
  throw new CustomerRuntimeGatewayError("gateway_unavailable", status, true);
147
152
  }
148
- return invalidResponse();
153
+ return invalidResponse(status);
149
154
  }
150
155
 
151
- function requireUlid(value: unknown): string {
152
- if (typeof value !== "string" || !/^[0-9A-HJKMNP-TV-Z]{26}$/u.test(value)) invalidResponse();
156
+ function requireUlid(value: unknown, status: number | null = null): string {
157
+ if (typeof value !== "string" || !/^[0-9A-HJKMNP-TV-Z]{26}$/u.test(value))
158
+ invalidResponse(status);
153
159
  return value;
154
160
  }
155
161
 
@@ -28,6 +28,7 @@ const MAX_HEADER_BYTES = 2048;
28
28
  async function timedRead(
29
29
  reader: ReadableStreamDefaultReader<Uint8Array>,
30
30
  timeoutMilliseconds: number,
31
+ status: number,
31
32
  signal?: AbortSignal,
32
33
  ): Promise<ReadableStreamReadResult<Uint8Array>> {
33
34
  let timer: ReturnType<typeof setTimeout> | undefined;
@@ -35,13 +36,13 @@ async function timedRead(
35
36
  try {
36
37
  if (signal?.aborted) {
37
38
  void reader.cancel().catch(() => undefined);
38
- throw new CustomerRuntimeGatewayError("gateway_timeout", null, true);
39
+ throw new CustomerRuntimeGatewayError("gateway_timeout", status, true);
39
40
  }
40
41
  return await Promise.race([
41
42
  reader.read(),
42
43
  new Promise<never>((_resolve, reject) => {
43
44
  const abort = () => {
44
- reject(new CustomerRuntimeGatewayError("gateway_timeout", null, true));
45
+ reject(new CustomerRuntimeGatewayError("gateway_timeout", status, true));
45
46
  void reader.cancel().catch(() => undefined);
46
47
  };
47
48
  timer = setTimeout(abort, timeoutMilliseconds);
@@ -53,8 +54,8 @@ async function timedRead(
53
54
  ]);
54
55
  } catch (error) {
55
56
  if (error instanceof CustomerRuntimeGatewayError) throw error;
56
- if (signal?.aborted) throw new CustomerRuntimeGatewayError("gateway_timeout", null, true);
57
- throw new CustomerRuntimeGatewayError("gateway_unavailable", null, true);
57
+ if (signal?.aborted) throw new CustomerRuntimeGatewayError("gateway_timeout", status, true);
58
+ throw new CustomerRuntimeGatewayError("gateway_unavailable", status, true);
58
59
  } finally {
59
60
  clearTimeout(timer);
60
61
  if (abortListener !== undefined) signal?.removeEventListener("abort", abortListener);
@@ -72,7 +73,7 @@ export function decodeStorageBatch(
72
73
  response.body === null
73
74
  ) {
74
75
  void response.body?.cancel().catch(() => undefined);
75
- invalidResponse();
76
+ invalidResponse(response.status);
76
77
  }
77
78
  const source = response.body.getReader();
78
79
  const decoder = new TextDecoder("utf-8", { fatal: true });
@@ -101,9 +102,9 @@ export function decodeStorageBatch(
101
102
  async function chunk(maximum: number): Promise<Uint8Array> {
102
103
  if (cancelled) throw cancellationError();
103
104
  while (offset === buffer.byteLength) {
104
- const next = await timedRead(source, timeoutMilliseconds);
105
+ const next = await timedRead(source, timeoutMilliseconds, response.status);
105
106
  if (cancelled) throw cancellationError();
106
- if (next.done || !(next.value instanceof Uint8Array)) invalidResponse();
107
+ if (next.done || !(next.value instanceof Uint8Array)) invalidResponse(response.status);
107
108
  buffer = next.value;
108
109
  offset = 0;
109
110
  }
@@ -126,7 +127,7 @@ export function decodeStorageBatch(
126
127
  const length = new DataView((await exactBytes(4)).buffer).getUint32(0);
127
128
  controlBytes += 4 + length;
128
129
  if (length < 1 || length > MAX_HEADER_BYTES || controlBytes > MAX_CONTROL_BYTES)
129
- invalidResponse();
130
+ invalidResponse(response.status);
130
131
  const bytes = await exactBytes(length);
131
132
  return parseHeader(bytes);
132
133
  }
@@ -135,7 +136,7 @@ export function decodeStorageBatch(
135
136
  if (buffer.byteLength - offset < 4) return undefined;
136
137
  const length = new DataView(buffer.buffer, buffer.byteOffset + offset, 4).getUint32(0);
137
138
  if (length < 1 || length > MAX_HEADER_BYTES || controlBytes + 4 + length > MAX_CONTROL_BYTES)
138
- invalidResponse();
139
+ invalidResponse(response.status);
139
140
  if (buffer.byteLength - offset < 4 + length) return undefined;
140
141
  controlBytes += 4 + length;
141
142
  const bytes = buffer.subarray(offset + 4, offset + 4 + length);
@@ -146,7 +147,7 @@ export function decodeStorageBatch(
146
147
  try {
147
148
  return JSON.parse(decoder.decode(bytes)) as unknown;
148
149
  } catch {
149
- return invalidResponse();
150
+ return invalidResponse(response.status);
150
151
  }
151
152
  }
152
153
  const iterator = iterate();
@@ -173,7 +174,7 @@ export function decodeStorageBatch(
173
174
  try {
174
175
  for (let index = 0; index < keys.length; index++) {
175
176
  const objectKey = keys[index];
176
- if (objectKey === undefined) invalidResponse();
177
+ if (objectKey === undefined) invalidResponse(response.status);
177
178
  if (!currentConsumed)
178
179
  throw new CustomerRuntimeGatewayError(
179
180
  "gateway_request_invalid",
@@ -190,8 +191,10 @@ export function decodeStorageBatch(
190
191
  candidate?.["state"] === "found"
191
192
  ? ["index", "objectKey", "state", "contentType", "contentLength", "etag"]
192
193
  : ["index", "objectKey", "state"],
194
+ response.status,
193
195
  );
194
- if (record["index"] !== index || record["objectKey"] !== keys[index]) invalidResponse();
196
+ if (record["index"] !== index || record["objectKey"] !== keys[index])
197
+ invalidResponse(response.status);
195
198
  if (record["state"] === "not_found") {
196
199
  yield Object.freeze({ state: "not_found", objectKey });
197
200
  continue;
@@ -208,7 +211,7 @@ export function decodeStorageBatch(
208
211
  typeof record["etag"] !== "string" ||
209
212
  !/^[0-9a-f]{32}(?:-[1-9][0-9]{0,4})?$/u.test(record["etag"])
210
213
  )
211
- invalidResponse();
214
+ invalidResponse(response.status);
212
215
  let remaining = Number(record["contentLength"]);
213
216
  currentConsumed = remaining === 0;
214
217
  const body = new ReadableStream<Uint8Array>(
@@ -268,18 +271,19 @@ export function decodeStorageBatch(
268
271
  );
269
272
  if (bodyFailure !== undefined) throw bodyFailure;
270
273
  const buffered = bufferedHeader();
271
- const terminal = exactRecord(buffered === undefined ? await header() : buffered, [
272
- "state",
273
- "count",
274
- ]);
274
+ const terminal = exactRecord(
275
+ buffered === undefined ? await header() : buffered,
276
+ ["state", "count"],
277
+ response.status,
278
+ );
275
279
  if (
276
280
  terminal["state"] !== "completed" ||
277
281
  terminal["count"] !== keys.length ||
278
282
  offset !== buffer.byteLength
279
283
  )
280
- invalidResponse();
281
- const end = await timedRead(source, timeoutMilliseconds);
282
- if (!end.done) invalidResponse();
284
+ invalidResponse(response.status);
285
+ const end = await timedRead(source, timeoutMilliseconds, response.status);
286
+ if (!end.done) invalidResponse(response.status);
283
287
  if (cancelled) throw cancellationError();
284
288
  finished = true;
285
289
  } finally {
@@ -303,7 +307,7 @@ export async function readStorageBatchJson(
303
307
  response.body === null
304
308
  ) {
305
309
  void response.body?.cancel().catch(() => undefined);
306
- invalidResponse();
310
+ invalidResponse(response.status);
307
311
  }
308
312
  const reader = response.body.getReader();
309
313
  const chunks: Uint8Array[] = [];
@@ -311,14 +315,14 @@ export async function readStorageBatchJson(
311
315
  let finished = false;
312
316
  try {
313
317
  for (;;) {
314
- const next = await timedRead(reader, timeoutMilliseconds, signal);
318
+ const next = await timedRead(reader, timeoutMilliseconds, response.status, signal);
315
319
  if (next.done) {
316
320
  finished = true;
317
321
  break;
318
322
  }
319
- if (!(next.value instanceof Uint8Array)) invalidResponse();
323
+ if (!(next.value instanceof Uint8Array)) invalidResponse(response.status);
320
324
  length += next.value.byteLength;
321
- if (length > MAX_CONTROL_BYTES) invalidResponse();
325
+ if (length > MAX_CONTROL_BYTES) invalidResponse(response.status);
322
326
  chunks.push(next.value);
323
327
  }
324
328
  } finally {
@@ -334,6 +338,6 @@ export async function readStorageBatchJson(
334
338
  try {
335
339
  return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)) as unknown;
336
340
  } catch {
337
- return invalidResponse();
341
+ return invalidResponse(response.status);
338
342
  }
339
343
  }