@amerged/ohmyhost-runtime 0.1.10

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.
@@ -0,0 +1,57 @@
1
+ export type CustomerRuntimeGatewayErrorCode =
2
+ | "gateway_configuration_invalid"
3
+ | "gateway_request_invalid"
4
+ | "gateway_response_invalid"
5
+ | "gateway_timeout"
6
+ | "gateway_unavailable"
7
+ | "mail_credit_insufficient"
8
+ | "mail_rejected"
9
+ | "mail_reputation_suspended"
10
+ | "mail_send_conflict"
11
+ | "mail_send_limit_exceeded"
12
+ | "storage_conflict"
13
+ | "storage_object_not_found"
14
+ | "storage_quota_exceeded"
15
+ | "storage_upload_expired"
16
+ | "storage_upload_rejected"
17
+ | "unauthorized";
18
+
19
+ export type CustomerRuntimeErrorCode =
20
+ | CustomerRuntimeGatewayErrorCode
21
+ | "database_query_invalid"
22
+ | "database_response_invalid"
23
+ | "database_unavailable"
24
+ | "scheduled_context_invalid";
25
+
26
+ export class CustomerRuntimeError extends Error {
27
+ public constructor(
28
+ public readonly code: CustomerRuntimeErrorCode,
29
+ public readonly retryable: boolean,
30
+ ) {
31
+ super(code);
32
+ this.name = "CustomerRuntimeError";
33
+ }
34
+ }
35
+
36
+ /** A verified PostgreSQL statement failure, without SQL, row values or provider diagnostics. */
37
+ export class CustomerDatabaseError extends Error {
38
+ public readonly retryable: boolean;
39
+
40
+ public constructor(public readonly code: string) {
41
+ super("database_statement_failed");
42
+ if (!/^[0-9A-Z]{5}$/u.test(code)) throw new TypeError("Invalid SQLSTATE");
43
+ this.name = "CustomerDatabaseError";
44
+ this.retryable = code === "40001" || code === "40P01";
45
+ }
46
+ }
47
+
48
+ export class CustomerRuntimeGatewayError extends CustomerRuntimeError {
49
+ public constructor(
50
+ code: CustomerRuntimeGatewayErrorCode,
51
+ public readonly status: number | null,
52
+ retryable: boolean,
53
+ ) {
54
+ super(code, retryable);
55
+ this.name = "CustomerRuntimeGatewayError";
56
+ }
57
+ }
@@ -0,0 +1,135 @@
1
+ import { CustomerRuntimeGatewayError } from "./errors.js";
2
+
3
+ const MAX_RESPONSE_BYTES = 32 * 1024;
4
+ const DEFAULT_TIMEOUT_MILLISECONDS = 10_000;
5
+
6
+ export interface GatewayConnection {
7
+ readonly endpoint: string;
8
+ readonly key: string;
9
+ readonly fetch: (request: Request) => Promise<Response>;
10
+ readonly timeoutMilliseconds?: number;
11
+ }
12
+
13
+ export interface ValidatedGatewayConnection {
14
+ readonly endpoint: string;
15
+ readonly key: string;
16
+ readonly fetch: (request: Request) => Promise<Response>;
17
+ readonly timeoutMilliseconds: number;
18
+ }
19
+
20
+ export function validateGatewayConnection(value: GatewayConnection): ValidatedGatewayConnection {
21
+ assertServerRuntime();
22
+ let endpoint: URL;
23
+ try {
24
+ endpoint = new URL(value.endpoint);
25
+ } catch {
26
+ throw new CustomerRuntimeGatewayError("gateway_configuration_invalid", null, false);
27
+ }
28
+ const timeoutMilliseconds = value.timeoutMilliseconds ?? DEFAULT_TIMEOUT_MILLISECONDS;
29
+ if (
30
+ endpoint.protocol !== "https:" ||
31
+ endpoint.username !== "" ||
32
+ endpoint.password !== "" ||
33
+ endpoint.pathname !== "/" ||
34
+ endpoint.search !== "" ||
35
+ endpoint.hash !== "" ||
36
+ !/^[A-Za-z0-9_-]{43}$/u.test(value.key) ||
37
+ typeof value.fetch !== "function" ||
38
+ !Number.isSafeInteger(timeoutMilliseconds) ||
39
+ timeoutMilliseconds < 100 ||
40
+ timeoutMilliseconds > 30_000
41
+ ) {
42
+ throw new CustomerRuntimeGatewayError("gateway_configuration_invalid", null, false);
43
+ }
44
+ return Object.freeze({
45
+ endpoint: endpoint.href,
46
+ key: value.key,
47
+ fetch: value.fetch,
48
+ timeoutMilliseconds,
49
+ });
50
+ }
51
+
52
+ export async function postGatewayJson(
53
+ connection: ValidatedGatewayConnection,
54
+ path: string,
55
+ body: Readonly<Record<string, unknown>>,
56
+ maximumRequestBytes: number,
57
+ idempotencyKey?: string,
58
+ ): Promise<{ readonly status: number; readonly body: unknown }> {
59
+ const serializedBody = JSON.stringify(body);
60
+ if (new TextEncoder().encode(serializedBody).byteLength > maximumRequestBytes) {
61
+ throw new CustomerRuntimeGatewayError("gateway_request_invalid", null, false);
62
+ }
63
+ const response = await fetchWithTimeout(
64
+ connection.fetch,
65
+ new Request(new URL(path, connection.endpoint), {
66
+ method: "POST",
67
+ headers: {
68
+ authorization: `Bearer ${connection.key}`,
69
+ "content-type": "application/json",
70
+ ...(idempotencyKey === undefined ? {} : { "idempotency-key": idempotencyKey }),
71
+ },
72
+ body: serializedBody,
73
+ }),
74
+ connection.timeoutMilliseconds,
75
+ );
76
+ return Object.freeze({ status: response.status, body: await readBoundedJson(response) });
77
+ }
78
+
79
+ export async function fetchWithTimeout(
80
+ fetch: (request: Request) => Promise<Response>,
81
+ request: Request,
82
+ timeoutMilliseconds: number,
83
+ ): Promise<Response> {
84
+ const controller = new AbortController();
85
+ const timeout = setTimeout(() => controller.abort(), timeoutMilliseconds);
86
+ try {
87
+ return await fetch(new Request(request, { signal: controller.signal }));
88
+ } catch (error) {
89
+ if (error instanceof CustomerRuntimeGatewayError) throw error;
90
+ if (controller.signal.aborted) {
91
+ throw new CustomerRuntimeGatewayError("gateway_timeout", null, true);
92
+ }
93
+ throw new CustomerRuntimeGatewayError("gateway_unavailable", null, true);
94
+ } finally {
95
+ clearTimeout(timeout);
96
+ }
97
+ }
98
+
99
+ export function exactRecord(value: unknown, keys: readonly string[]): Record<string, unknown> {
100
+ if (typeof value !== "object" || value === null || Array.isArray(value)) invalidResponse();
101
+ const record = value as Record<string, unknown>;
102
+ const actual = Object.keys(record).sort();
103
+ const expected = [...keys].sort();
104
+ if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
105
+ invalidResponse();
106
+ }
107
+ return record;
108
+ }
109
+
110
+ export function invalidResponse(): never {
111
+ throw new CustomerRuntimeGatewayError("gateway_response_invalid", null, false);
112
+ }
113
+
114
+ function assertServerRuntime(): void {
115
+ if (typeof window !== "undefined") {
116
+ throw new CustomerRuntimeGatewayError("gateway_configuration_invalid", null, false);
117
+ }
118
+ }
119
+
120
+ async function readBoundedJson(response: Response): Promise<unknown> {
121
+ if (response.headers.get("content-type")?.split(";", 1)[0]?.trim() !== "application/json") {
122
+ invalidResponse();
123
+ }
124
+ const declared = response.headers.get("content-length");
125
+ if (declared !== null && (!/^[0-9]+$/u.test(declared) || Number(declared) > MAX_RESPONSE_BYTES)) {
126
+ invalidResponse();
127
+ }
128
+ const bytes = new Uint8Array(await response.arrayBuffer());
129
+ if (bytes.byteLength > MAX_RESPONSE_BYTES) invalidResponse();
130
+ try {
131
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
132
+ } catch {
133
+ return invalidResponse();
134
+ }
135
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./errors.js";
2
+ export * from "./database.js";
3
+ export * from "./mail.js";
4
+ export * from "./storage.js";
5
+ export type { GatewayConnection } from "./http.js";
@@ -0,0 +1,161 @@
1
+ import { CustomerRuntimeGatewayError } from "./errors.js";
2
+ import {
3
+ exactRecord,
4
+ invalidResponse,
5
+ postGatewayJson,
6
+ validateGatewayConnection,
7
+ type GatewayConnection,
8
+ } from "./http.js";
9
+
10
+ export interface TransactionalMailInput {
11
+ readonly idempotencyKey: string;
12
+ readonly to: string;
13
+ readonly subject: string;
14
+ readonly text: string;
15
+ readonly html: string;
16
+ }
17
+
18
+ export type TransactionalMailResult =
19
+ | {
20
+ readonly state: "accepted";
21
+ readonly requestId: string;
22
+ readonly messageId: string;
23
+ }
24
+ | {
25
+ readonly state: "uncertain";
26
+ readonly requestId: string;
27
+ };
28
+
29
+ export function createTransactionalMailClient(
30
+ configuration: GatewayConnection & { readonly projectId: string },
31
+ ) {
32
+ const connection = validateGatewayConnection(configuration);
33
+ const projectId = requireUlid(configuration.projectId);
34
+ return Object.freeze({
35
+ async send(input: TransactionalMailInput): Promise<TransactionalMailResult> {
36
+ validateMailInput(input);
37
+ const response = await postGatewayJson(
38
+ connection,
39
+ "/v1/send",
40
+ {
41
+ project_id: projectId,
42
+ to: input.to,
43
+ subject: input.subject,
44
+ text: input.text,
45
+ html: input.html,
46
+ },
47
+ 256 * 1024,
48
+ input.idempotencyKey,
49
+ );
50
+ if (response.status !== 202) throwMailError(response.status, response.body);
51
+ const raw = response.body as Record<string, unknown>;
52
+ const stateRecord = exactRecord(
53
+ response.body,
54
+ raw?.["state"] === "accepted"
55
+ ? ["state", "request_id", "message_id"]
56
+ : ["state", "request_id"],
57
+ );
58
+ const requestId = requireUlid(stateRecord["request_id"]);
59
+ if (stateRecord["state"] === "uncertain") {
60
+ return Object.freeze({ state: "uncertain" as const, requestId });
61
+ }
62
+ if (
63
+ stateRecord["state"] !== "accepted" ||
64
+ typeof stateRecord["message_id"] !== "string" ||
65
+ !/^[A-Za-z0-9._-]{1,256}$/u.test(stateRecord["message_id"])
66
+ ) {
67
+ invalidResponse();
68
+ }
69
+ return Object.freeze({
70
+ state: "accepted" as const,
71
+ requestId,
72
+ messageId: stateRecord["message_id"],
73
+ });
74
+ },
75
+ });
76
+ }
77
+
78
+ function validateMailInput(input: TransactionalMailInput): void {
79
+ if (
80
+ !validIdempotencyKey(input.idempotencyKey) ||
81
+ !validEmail(input.to) ||
82
+ !validContent(input.subject, 200, 998, false) ||
83
+ !validContent(input.text, 65_536, 65_536, true) ||
84
+ !validContent(input.html, 131_072, 131_072, true)
85
+ ) {
86
+ throw new CustomerRuntimeGatewayError("gateway_request_invalid", null, false);
87
+ }
88
+ }
89
+
90
+ function throwMailError(status: number, body: unknown): never {
91
+ const raw = body as Record<string, unknown>;
92
+ const record = exactRecord(
93
+ body,
94
+ raw?.["request_id"] === undefined ? ["code"] : ["code", "request_id"],
95
+ );
96
+ const code = record["code"];
97
+ if (record["request_id"] !== undefined) requireUlid(record["request_id"]);
98
+ if (status === 401 && code === "unauthorized") {
99
+ throw new CustomerRuntimeGatewayError("unauthorized", status, false);
100
+ }
101
+ if (status === 402 && code === "mail_send_credit_exhausted") {
102
+ throw new CustomerRuntimeGatewayError("mail_credit_insufficient", status, false);
103
+ }
104
+ if (status === 409 && code === "mail_send_conflict") {
105
+ throw new CustomerRuntimeGatewayError("mail_send_conflict", status, false);
106
+ }
107
+ if (status === 423 && code === "mail_reputation_suspended") {
108
+ throw new CustomerRuntimeGatewayError("mail_reputation_suspended", status, false);
109
+ }
110
+ if (status === 429 && code === "mail_send_daily_limit") {
111
+ throw new CustomerRuntimeGatewayError("mail_send_limit_exceeded", status, true);
112
+ }
113
+ if (status === 502 && code === "mail_rejected") {
114
+ throw new CustomerRuntimeGatewayError("mail_rejected", status, false);
115
+ }
116
+ if (status === 503 && typeof code === "string") {
117
+ throw new CustomerRuntimeGatewayError("gateway_unavailable", status, true);
118
+ }
119
+ return invalidResponse();
120
+ }
121
+
122
+ function requireUlid(value: unknown): string {
123
+ if (typeof value !== "string" || !/^[0-9A-HJKMNP-TV-Z]{26}$/u.test(value)) invalidResponse();
124
+ return value;
125
+ }
126
+
127
+ function validIdempotencyKey(value: string): boolean {
128
+ return /^[A-Za-z0-9._:-]{1,128}$/u.test(value);
129
+ }
130
+
131
+ function validEmail(value: string): boolean {
132
+ return (
133
+ value.length <= 254 &&
134
+ /^[a-z0-9](?:[a-z0-9._%+-]{0,62}[a-z0-9])?@(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/u.test(
135
+ value,
136
+ )
137
+ );
138
+ }
139
+
140
+ function validContent(
141
+ value: string,
142
+ maximumCharacters: number,
143
+ maximumBytes: number,
144
+ allowLineBreaks: boolean,
145
+ ): boolean {
146
+ const characters = [...value];
147
+ const invalidControl = characters.some((character) => {
148
+ const code = character.codePointAt(0);
149
+ return (
150
+ code === undefined ||
151
+ code === 127 ||
152
+ (code < 32 && (!allowLineBreaks || (code !== 9 && code !== 10 && code !== 13)))
153
+ );
154
+ });
155
+ return (
156
+ characters.length > 0 &&
157
+ characters.length <= maximumCharacters &&
158
+ new TextEncoder().encode(value).byteLength <= maximumBytes &&
159
+ !invalidControl
160
+ );
161
+ }
@@ -0,0 +1,294 @@
1
+ import { CustomerRuntimeGatewayError } from "./errors.js";
2
+ import {
3
+ exactRecord,
4
+ fetchWithTimeout,
5
+ invalidResponse,
6
+ postGatewayJson,
7
+ validateGatewayConnection,
8
+ type GatewayConnection,
9
+ } from "./http.js";
10
+
11
+ export interface StorageCapability {
12
+ readonly operation: "GET" | "PUT";
13
+ readonly objectKey: string;
14
+ readonly url: string;
15
+ readonly expiresAt: string;
16
+ readonly expectedContentLength: number | null;
17
+ readonly requiredHeaders: Readonly<Record<string, string>>;
18
+ }
19
+
20
+ export interface StorageUploadInput {
21
+ readonly idempotencyKey: string;
22
+ readonly objectKey: string;
23
+ readonly contentType: string;
24
+ readonly bytes: Uint8Array;
25
+ }
26
+
27
+ export function createPrivateStorageClient(
28
+ configuration: GatewayConnection & {
29
+ readonly projectId: string;
30
+ readonly environmentId: string;
31
+ readonly capabilityFetch: (request: Request) => Promise<Response>;
32
+ },
33
+ ) {
34
+ const connection = validateGatewayConnection(configuration);
35
+ if (typeof configuration.capabilityFetch !== "function") {
36
+ throw new CustomerRuntimeGatewayError("gateway_configuration_invalid", null, false);
37
+ }
38
+ const capabilityFetch = configuration.capabilityFetch;
39
+ const projectId = requireUlid(configuration.projectId);
40
+ const environmentId = requireUlid(configuration.environmentId);
41
+ const authenticatedBody = Object.freeze({ projectId, environmentId });
42
+
43
+ async function reserveUpload(
44
+ input: Omit<StorageUploadInput, "bytes"> & { contentLength: number },
45
+ ) {
46
+ validateStorageMutation(input.idempotencyKey, input.objectKey);
47
+ if (
48
+ !Number.isSafeInteger(input.contentLength) ||
49
+ input.contentLength < 0 ||
50
+ input.contentLength > 1_073_741_824 ||
51
+ !validContentType(input.contentType)
52
+ ) {
53
+ invalidRequest();
54
+ }
55
+ const response = await postGatewayJson(
56
+ connection,
57
+ "/v1/uploads",
58
+ { ...authenticatedBody, ...input },
59
+ 8 * 1024,
60
+ input.idempotencyKey,
61
+ );
62
+ if (response.status !== 200) throwStorageError(response.status, response.body);
63
+ const record = response.body as Record<string, unknown>;
64
+ if (record?.["state"] === "completed") {
65
+ const completed = exactRecord(response.body, ["state", "transfer_id"]);
66
+ return Object.freeze({
67
+ state: "completed" as const,
68
+ transferId: requireUlid(completed["transfer_id"]),
69
+ });
70
+ }
71
+ const ready = exactRecord(response.body, ["state", "transfer_id", "capability"]);
72
+ if (ready["state"] !== "ready") invalidResponse();
73
+ return Object.freeze({
74
+ state: "ready" as const,
75
+ transferId: requireUlid(ready["transfer_id"]),
76
+ capability: parseCapability(ready["capability"], "PUT", input.objectKey),
77
+ });
78
+ }
79
+
80
+ async function completeUpload(transferId: string) {
81
+ const response = await postGatewayJson(
82
+ connection,
83
+ "/v1/uploads/complete",
84
+ {
85
+ ...authenticatedBody,
86
+ transferId: requireUlid(transferId),
87
+ },
88
+ 8 * 1024,
89
+ );
90
+ if (response.status !== 200 && response.status !== 202) {
91
+ throwStorageError(response.status, response.body);
92
+ }
93
+ const record = exactRecord(response.body, ["state", "transfer_id"]);
94
+ if (record["state"] !== "completed" && record["state"] !== "pending") invalidResponse();
95
+ return Object.freeze({
96
+ state: record["state"],
97
+ transferId: requireUlid(record["transfer_id"]),
98
+ });
99
+ }
100
+
101
+ async function createSignedRead(objectKey: string) {
102
+ validateObjectKey(objectKey);
103
+ const response = await postGatewayJson(
104
+ connection,
105
+ "/v1/downloads",
106
+ {
107
+ ...authenticatedBody,
108
+ objectKey,
109
+ },
110
+ 8 * 1024,
111
+ );
112
+ if (response.status !== 200) throwStorageError(response.status, response.body);
113
+ const record = exactRecord(response.body, ["state", "capability"]);
114
+ if (record["state"] !== "ready") invalidResponse();
115
+ return parseCapability(record["capability"], "GET", objectKey);
116
+ }
117
+
118
+ async function deleteObject(input: { idempotencyKey: string; objectKey: string }) {
119
+ validateStorageMutation(input.idempotencyKey, input.objectKey);
120
+ const response = await postGatewayJson(
121
+ connection,
122
+ "/v1/deletions",
123
+ { ...authenticatedBody, objectKey: input.objectKey },
124
+ 8 * 1024,
125
+ input.idempotencyKey,
126
+ );
127
+ if (response.status !== 200) throwStorageError(response.status, response.body);
128
+ const record = exactRecord(response.body, ["state", "deletion_id"]);
129
+ if (record["state"] !== "completed") invalidResponse();
130
+ return Object.freeze({
131
+ state: "completed" as const,
132
+ deletionId: requireUlid(record["deletion_id"]),
133
+ });
134
+ }
135
+
136
+ async function upload(input: StorageUploadInput) {
137
+ const bytes = Uint8Array.from(input.bytes);
138
+ const reservation = await reserveUpload({
139
+ idempotencyKey: input.idempotencyKey,
140
+ objectKey: input.objectKey,
141
+ contentType: input.contentType,
142
+ contentLength: bytes.byteLength,
143
+ });
144
+ if (reservation.state === "completed") return reservation;
145
+ const uploaded = await fetchWithTimeout(
146
+ capabilityFetch,
147
+ new Request(reservation.capability.url, {
148
+ method: "PUT",
149
+ headers: reservation.capability.requiredHeaders,
150
+ body: bytes,
151
+ }),
152
+ connection.timeoutMilliseconds,
153
+ );
154
+ if (uploaded.status < 200 || uploaded.status >= 300) {
155
+ throw new CustomerRuntimeGatewayError("storage_upload_rejected", uploaded.status, false);
156
+ }
157
+ return completeUpload(reservation.transferId);
158
+ }
159
+
160
+ return Object.freeze({ reserveUpload, completeUpload, createSignedRead, deleteObject, upload });
161
+ }
162
+
163
+ function parseCapability(value: unknown, operation: "GET" | "PUT", objectKey: string) {
164
+ const record = exactRecord(value, [
165
+ "operation",
166
+ "objectKey",
167
+ "url",
168
+ "expiresAt",
169
+ "expectedContentLength",
170
+ "requiredHeaders",
171
+ ]);
172
+ const headers = exactHeaders(record["requiredHeaders"]);
173
+ if (
174
+ record["operation"] !== operation ||
175
+ record["objectKey"] !== objectKey ||
176
+ typeof record["url"] !== "string" ||
177
+ !validSignedUrl(record["url"]) ||
178
+ typeof record["expiresAt"] !== "string" ||
179
+ !Number.isFinite(Date.parse(record["expiresAt"])) ||
180
+ (record["expectedContentLength"] !== null &&
181
+ (!Number.isSafeInteger(record["expectedContentLength"]) ||
182
+ Number(record["expectedContentLength"]) < 0 ||
183
+ Number(record["expectedContentLength"]) > 1_073_741_824))
184
+ ) {
185
+ invalidResponse();
186
+ }
187
+ return Object.freeze({
188
+ operation,
189
+ objectKey,
190
+ url: record["url"],
191
+ expiresAt: record["expiresAt"],
192
+ expectedContentLength:
193
+ record["expectedContentLength"] === null ? null : Number(record["expectedContentLength"]),
194
+ requiredHeaders: headers,
195
+ });
196
+ }
197
+
198
+ function exactHeaders(value: unknown): Readonly<Record<string, string>> {
199
+ if (typeof value !== "object" || value === null || Array.isArray(value)) invalidResponse();
200
+ const entries = Object.entries(value as Record<string, unknown>);
201
+ if (
202
+ entries.length > 16 ||
203
+ entries.some(
204
+ ([name, header]) =>
205
+ !/^[a-z0-9-]{1,64}$/u.test(name) ||
206
+ /^(authorization|cookie|set-cookie)$/u.test(name) ||
207
+ typeof header !== "string" ||
208
+ header.length > 1_024 ||
209
+ /[\r\n]/u.test(header),
210
+ )
211
+ ) {
212
+ invalidResponse();
213
+ }
214
+ return Object.freeze(Object.fromEntries(entries) as Record<string, string>);
215
+ }
216
+
217
+ function throwStorageError(status: number, body: unknown): never {
218
+ const record = exactRecord(
219
+ body,
220
+ typeof body === "object" && body !== null && "transfer_id" in body
221
+ ? ["code", "transfer_id"]
222
+ : ["code"],
223
+ );
224
+ const code = record["code"];
225
+ if (status === 401 && code === "unauthorized") {
226
+ throw new CustomerRuntimeGatewayError("unauthorized", status, false);
227
+ }
228
+ if (status === 404 && code === "object_not_found") {
229
+ throw new CustomerRuntimeGatewayError("storage_object_not_found", status, false);
230
+ }
231
+ if (status === 409 && code === "upload_expired") {
232
+ requireUlid(record["transfer_id"]);
233
+ throw new CustomerRuntimeGatewayError("storage_upload_expired", status, false);
234
+ }
235
+ if (status === 409 && typeof code === "string") {
236
+ throw new CustomerRuntimeGatewayError("storage_conflict", status, false);
237
+ }
238
+ if (status === 413 && typeof code === "string") {
239
+ throw new CustomerRuntimeGatewayError("storage_quota_exceeded", status, false);
240
+ }
241
+ if (status === 503 && code === "storage_gateway_unavailable") {
242
+ throw new CustomerRuntimeGatewayError("gateway_unavailable", status, true);
243
+ }
244
+ return invalidResponse();
245
+ }
246
+
247
+ function validateStorageMutation(idempotencyKey: string, objectKey: string): void {
248
+ if (!/^[A-Za-z0-9._:-]{1,128}$/u.test(idempotencyKey)) invalidRequest();
249
+ validateObjectKey(objectKey);
250
+ }
251
+
252
+ function validateObjectKey(value: string): void {
253
+ const bytes = new TextEncoder().encode(value).byteLength;
254
+ if (
255
+ bytes < 1 ||
256
+ bytes > 512 ||
257
+ value.startsWith("/") ||
258
+ value.endsWith("/") ||
259
+ value.includes("//") ||
260
+ !value
261
+ .split("/")
262
+ .every((segment) => /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,126}[A-Za-z0-9])?$/u.test(segment))
263
+ ) {
264
+ invalidRequest();
265
+ }
266
+ }
267
+
268
+ function validContentType(value: string): boolean {
269
+ return /^[a-z0-9][a-z0-9!#$&^_.+-]{0,63}\/[a-z0-9][a-z0-9!#$&^_.+-]{0,63}$/u.test(value);
270
+ }
271
+
272
+ function validSignedUrl(value: string): boolean {
273
+ try {
274
+ const url = new URL(value);
275
+ return (
276
+ url.protocol === "https:" &&
277
+ url.username === "" &&
278
+ url.password === "" &&
279
+ url.hash === "" &&
280
+ value.length <= 8_192
281
+ );
282
+ } catch {
283
+ return false;
284
+ }
285
+ }
286
+
287
+ function requireUlid(value: unknown): string {
288
+ if (typeof value !== "string" || !/^[0-9A-HJKMNP-TV-Z]{26}$/u.test(value)) invalidResponse();
289
+ return value;
290
+ }
291
+
292
+ function invalidRequest(): never {
293
+ throw new CustomerRuntimeGatewayError("gateway_request_invalid", null, false);
294
+ }