@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
@@ -7,6 +7,7 @@ import {
7
7
  postGatewayJson,
8
8
  requireConfiguredUlid,
9
9
  validateGatewayConnection,
10
+ throwGatewayRelayError,
10
11
  type GatewayConnection,
11
12
  } from "./http.js";
12
13
  import {
@@ -33,10 +34,12 @@ export interface StorageUploadInput {
33
34
  readonly bytes: Uint8Array;
34
35
  }
35
36
 
37
+ /** Server-only private files; use global fetch for gateway and signed-object transports. */
36
38
  export function createPrivateStorageClient(
37
39
  configuration: GatewayConnection & {
38
40
  readonly projectId: string;
39
41
  readonly environmentId: string;
42
+ /** Global fetch for short-lived signed object URLs; explicit transports remain test seams. */
40
43
  readonly capabilityFetch: (request: Request) => Promise<Response>;
41
44
  },
42
45
  ) {
@@ -79,18 +82,22 @@ export function createPrivateStorageClient(
79
82
  if (response.status !== 200) throwStorageError(response.status, response.body, environmentId);
80
83
  const record = response.body as Record<string, unknown>;
81
84
  if (record?.["state"] === "completed") {
82
- const completed = exactRecord(response.body, ["state", "transfer_id"]);
85
+ const completed = exactRecord(response.body, ["state", "transfer_id"], response.status);
83
86
  return Object.freeze({
84
87
  state: "completed" as const,
85
- transferId: requireUlid(completed["transfer_id"]),
88
+ transferId: requireUlid(completed["transfer_id"], response.status),
86
89
  });
87
90
  }
88
- const ready = exactRecord(response.body, ["state", "transfer_id", "capability"]);
89
- if (ready["state"] !== "ready") invalidResponse();
91
+ const ready = exactRecord(
92
+ response.body,
93
+ ["state", "transfer_id", "capability"],
94
+ response.status,
95
+ );
96
+ if (ready["state"] !== "ready") invalidResponse(response.status);
90
97
  return Object.freeze({
91
98
  state: "ready" as const,
92
- transferId: requireUlid(ready["transfer_id"]),
93
- capability: parseCapability(ready["capability"], "PUT", input.objectKey),
99
+ transferId: requireUlid(ready["transfer_id"], response.status),
100
+ capability: parseCapability(ready["capability"], "PUT", input.objectKey, response.status),
94
101
  });
95
102
  }
96
103
 
@@ -107,11 +114,12 @@ export function createPrivateStorageClient(
107
114
  if (response.status !== 200 && response.status !== 202) {
108
115
  throwStorageError(response.status, response.body, environmentId);
109
116
  }
110
- const record = exactRecord(response.body, ["state", "transfer_id"]);
111
- if (record["state"] !== "completed" && record["state"] !== "pending") invalidResponse();
117
+ const record = exactRecord(response.body, ["state", "transfer_id"], response.status);
118
+ if (record["state"] !== "completed" && record["state"] !== "pending")
119
+ invalidResponse(response.status);
112
120
  return Object.freeze({
113
121
  state: record["state"],
114
- transferId: requireUlid(record["transfer_id"]),
122
+ transferId: requireUlid(record["transfer_id"], response.status),
115
123
  });
116
124
  }
117
125
 
@@ -127,9 +135,9 @@ export function createPrivateStorageClient(
127
135
  8 * 1024,
128
136
  );
129
137
  if (response.status !== 200) throwStorageError(response.status, response.body, environmentId);
130
- const record = exactRecord(response.body, ["state", "capability"]);
131
- if (record["state"] !== "ready") invalidResponse();
132
- return parseCapability(record["capability"], "GET", objectKey);
138
+ const record = exactRecord(response.body, ["state", "capability"], response.status);
139
+ if (record["state"] !== "ready") invalidResponse(response.status);
140
+ return parseCapability(record["capability"], "GET", objectKey, response.status);
133
141
  }
134
142
 
135
143
  async function deleteObject(input: { idempotencyKey: string; objectKey: string }) {
@@ -142,11 +150,11 @@ export function createPrivateStorageClient(
142
150
  input.idempotencyKey,
143
151
  );
144
152
  if (response.status !== 200) throwStorageError(response.status, response.body, environmentId);
145
- const record = exactRecord(response.body, ["state", "deletion_id"]);
146
- if (record["state"] !== "completed") invalidResponse();
153
+ const record = exactRecord(response.body, ["state", "deletion_id"], response.status);
154
+ if (record["state"] !== "completed") invalidResponse(response.status);
147
155
  return Object.freeze({
148
156
  state: "completed" as const,
149
- deletionId: requireUlid(record["deletion_id"]),
157
+ deletionId: requireUlid(record["deletion_id"], response.status),
150
158
  });
151
159
  }
152
160
 
@@ -257,9 +265,9 @@ export function createPrivateStorageClient(
257
265
  controller.signal,
258
266
  );
259
267
  if (response.status !== 200) throwStorageError(response.status, value, environmentId);
260
- const record = exactRecord(value, ["state", "results"]);
268
+ const record = exactRecord(value, ["state", "results"], response.status);
261
269
  if (!Array.isArray(record["results"]) || record["results"].length !== objects.length)
262
- invalidResponse();
270
+ invalidResponse(response.status);
263
271
  const results = record["results"].map((value, index): StorageDeleteResult => {
264
272
  const candidate = value as Record<string, unknown>;
265
273
  const item = exactRecord(
@@ -269,22 +277,23 @@ export function createPrivateStorageClient(
269
277
  : candidate?.["state"] === "pending"
270
278
  ? ["object_key", "state", "retry_at"]
271
279
  : ["object_key", "state", "code", "retryable"],
280
+ response.status,
272
281
  );
273
282
  const requested = objects[index];
274
283
  if (requested === undefined || item["object_key"] !== requested.objectKey)
275
- invalidResponse();
284
+ invalidResponse(response.status);
276
285
  const objectKey = requested.objectKey;
277
286
  if (item["state"] === "completed")
278
287
  return Object.freeze({
279
288
  state: "completed",
280
289
  objectKey,
281
- deletionId: requireUlid(item["deletion_id"]),
290
+ deletionId: requireUlid(item["deletion_id"], response.status),
282
291
  });
283
292
  if (item["state"] === "pending") {
284
293
  return Object.freeze({
285
294
  state: "pending",
286
295
  objectKey,
287
- retryAt: requireRetryAt(item["retry_at"]),
296
+ retryAt: requireRetryAt(item["retry_at"], response.status),
288
297
  });
289
298
  }
290
299
  if (
@@ -293,7 +302,7 @@ export function createPrivateStorageClient(
293
302
  typeof item["retryable"] !== "boolean" ||
294
303
  (item["code"] === "object_not_found" && item["retryable"] !== false)
295
304
  )
296
- invalidResponse();
305
+ invalidResponse(response.status);
297
306
  return Object.freeze({
298
307
  state: "failed",
299
308
  objectKey,
@@ -305,10 +314,10 @@ export function createPrivateStorageClient(
305
314
  record["state"] !==
306
315
  (results.every((item) => item.state === "completed") ? "completed" : "partial")
307
316
  )
308
- invalidResponse();
317
+ invalidResponse(response.status);
309
318
  if (controller.signal.aborted || Date.now() >= deadline) {
310
319
  controller.abort();
311
- throw new CustomerRuntimeGatewayError("gateway_timeout", null, true);
320
+ throw new CustomerRuntimeGatewayError("gateway_timeout", response.status, true);
312
321
  }
313
322
  return Object.freeze(results);
314
323
  } finally {
@@ -341,16 +350,18 @@ function validateBatchKeys(value: readonly string[]): readonly string[] {
341
350
  return Object.freeze(Array.from(value));
342
351
  }
343
352
 
344
- function parseCapability(value: unknown, operation: "GET" | "PUT", objectKey: string) {
345
- const record = exactRecord(value, [
346
- "operation",
347
- "objectKey",
348
- "url",
349
- "expiresAt",
350
- "expectedContentLength",
351
- "requiredHeaders",
352
- ]);
353
- const headers = exactHeaders(record["requiredHeaders"]);
353
+ function parseCapability(
354
+ value: unknown,
355
+ operation: "GET" | "PUT",
356
+ objectKey: string,
357
+ status: number,
358
+ ) {
359
+ const record = exactRecord(
360
+ value,
361
+ ["operation", "objectKey", "url", "expiresAt", "expectedContentLength", "requiredHeaders"],
362
+ status,
363
+ );
364
+ const headers = exactHeaders(record["requiredHeaders"], status);
354
365
  if (
355
366
  record["operation"] !== operation ||
356
367
  record["objectKey"] !== objectKey ||
@@ -363,7 +374,7 @@ function parseCapability(value: unknown, operation: "GET" | "PUT", objectKey: st
363
374
  Number(record["expectedContentLength"]) < 0 ||
364
375
  Number(record["expectedContentLength"]) > 1_073_741_824))
365
376
  ) {
366
- invalidResponse();
377
+ invalidResponse(status);
367
378
  }
368
379
  return Object.freeze({
369
380
  operation,
@@ -376,8 +387,8 @@ function parseCapability(value: unknown, operation: "GET" | "PUT", objectKey: st
376
387
  });
377
388
  }
378
389
 
379
- function exactHeaders(value: unknown): Readonly<Record<string, string>> {
380
- if (typeof value !== "object" || value === null || Array.isArray(value)) invalidResponse();
390
+ function exactHeaders(value: unknown, status: number): Readonly<Record<string, string>> {
391
+ if (typeof value !== "object" || value === null || Array.isArray(value)) invalidResponse(status);
381
392
  const entries = Object.entries(value as Record<string, unknown>);
382
393
  if (
383
394
  entries.length > 16 ||
@@ -390,12 +401,13 @@ function exactHeaders(value: unknown): Readonly<Record<string, string>> {
390
401
  /[\r\n]/u.test(header),
391
402
  )
392
403
  ) {
393
- invalidResponse();
404
+ invalidResponse(status);
394
405
  }
395
406
  return Object.freeze(Object.fromEntries(entries) as Record<string, string>);
396
407
  }
397
408
 
398
409
  function throwStorageError(status: number, body: unknown, environmentId: string): never {
410
+ throwGatewayRelayError(status, body);
399
411
  const candidate = body as Record<string, unknown>;
400
412
  const record = exactRecord(
401
413
  body,
@@ -407,6 +419,7 @@ function throwStorageError(status: number, body: unknown, environmentId: string)
407
419
  : typeof body === "object" && body !== null && "transfer_id" in body
408
420
  ? ["code", "transfer_id"]
409
421
  : ["code"],
422
+ status,
410
423
  );
411
424
  const code = record["code"];
412
425
  if (status === 401 && code === "unauthorized") {
@@ -416,7 +429,7 @@ function throwStorageError(status: number, body: unknown, environmentId: string)
416
429
  throw new CustomerRuntimeGatewayError("storage_object_not_found", status, false);
417
430
  }
418
431
  if (status === 409 && code === "upload_expired") {
419
- requireUlid(record["transfer_id"]);
432
+ requireUlid(record["transfer_id"], status);
420
433
  throw new CustomerRuntimeGatewayError("storage_upload_expired", status, false);
421
434
  }
422
435
  if (status === 409 && code === "data_change_in_progress") {
@@ -432,11 +445,11 @@ function throwStorageError(status: number, body: unknown, environmentId: string)
432
445
  "storage_binding_data_changed",
433
446
  status,
434
447
  false,
435
- `environment ${environmentId} now uses a different data area; create a new deployment plan and redeploy that environment. Repeating this request will not update this deployment's binding`,
448
+ `the environment's data area changed while this request ran; read the current state for environment ${environmentId} and issue a new request`,
436
449
  );
437
450
  }
438
451
  if (status === 409 && code === "storage_delete_pending") {
439
- const retryAt = requireRetryAt(record["retry_at"]);
452
+ const retryAt = requireRetryAt(record["retry_at"], status);
440
453
  throw new CustomerRuntimeGatewayError(
441
454
  "storage_delete_pending",
442
455
  status,
@@ -450,20 +463,17 @@ function throwStorageError(status: number, body: unknown, environmentId: string)
450
463
  if (status === 413 && typeof code === "string") {
451
464
  throw new CustomerRuntimeGatewayError("storage_quota_exceeded", status, false);
452
465
  }
453
- if (status === 503 && code === "storage_gateway_unavailable") {
454
- throw new CustomerRuntimeGatewayError("gateway_unavailable", status, true);
455
- }
456
- return invalidResponse();
466
+ return invalidResponse(status);
457
467
  }
458
468
 
459
- function requireRetryAt(value: unknown): string {
469
+ function requireRetryAt(value: unknown, status: number): string {
460
470
  if (
461
471
  typeof value !== "string" ||
462
472
  !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(value) ||
463
473
  !Number.isFinite(Date.parse(value)) ||
464
474
  new Date(value).toISOString() !== value
465
475
  )
466
- invalidResponse();
476
+ invalidResponse(status);
467
477
  return value;
468
478
  }
469
479
 
@@ -507,8 +517,9 @@ function validSignedUrl(value: string): boolean {
507
517
  }
508
518
  }
509
519
 
510
- function requireUlid(value: unknown): string {
511
- if (typeof value !== "string" || !/^[0-9A-HJKMNP-TV-Z]{26}$/u.test(value)) invalidResponse();
520
+ function requireUlid(value: unknown, status: number | null = null): string {
521
+ if (typeof value !== "string" || !/^[0-9A-HJKMNP-TV-Z]{26}$/u.test(value))
522
+ invalidResponse(status);
512
523
  return value;
513
524
  }
514
525
 
@@ -1,8 +0,0 @@
1
- import { type GatewayConnection } from "./http.js";
2
- /** Stream owned attachments through the private mail binding using the environment's runtime key. */
3
- export declare function createMailAttachmentClient(configuration: GatewayConnection & {
4
- projectId: string;
5
- }): {
6
- download(downloadPath: string): Promise<Response>;
7
- };
8
- //# sourceMappingURL=mail-attachments.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"mail-attachments.d.ts","sourceRoot":"","sources":["../src/mail-attachments.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,WAAW,CAAC;AAEnB,qGAAqG;AACrG,wBAAgB,0BAA0B,CACxC,aAAa,EAAE,iBAAiB,GAAG;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE;2BAazB,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;EA8B1D"}
@@ -1,32 +0,0 @@
1
- import { validateGatewayConnection, fetchWithTimeout, requireConfiguredUlid, } from "./http.js";
2
- import { CustomerRuntimeGatewayError } from "./errors.js";
3
- /** Stream owned attachments through the private mail binding using the environment's runtime key. */
4
- export function createMailAttachmentClient(configuration) {
5
- const connection = validateGatewayConnection(configuration);
6
- const projectId = requireConfiguredUlid(configuration.projectId, "projectId", "OHMYHOST_PROJECT_ID");
7
- const pattern = new RegExp(`^/v1/projects/${projectId}/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");
8
- return {
9
- async download(downloadPath) {
10
- if (!pattern.test(downloadPath))
11
- throw new CustomerRuntimeGatewayError("gateway_request_invalid", null, false);
12
- const response = await fetchWithTimeout(connection.fetch, new Request(new URL(downloadPath, connection.endpoint), {
13
- redirect: "manual",
14
- headers: { authorization: `Bearer ${connection.key}` },
15
- }), connection.timeoutMilliseconds);
16
- if (response.status !== 200) {
17
- await response.body?.cancel();
18
- throw new CustomerRuntimeGatewayError(response.status === 410
19
- ? "mail_content_expired"
20
- : response.status === 413
21
- ? "mail_attachment_too_large"
22
- : response.status === 404
23
- ? "mail_message_not_found"
24
- : response.status === 401
25
- ? "unauthorized"
26
- : "gateway_unavailable", response.status, response.status >= 500);
27
- }
28
- return response;
29
- },
30
- };
31
- }
32
- //# sourceMappingURL=mail-attachments.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"mail-attachments.js","sourceRoot":"","sources":["../src/mail-attachments.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,yBAAyB,EACzB,gBAAgB,EAChB,qBAAqB,GAEtB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAC;AAC1D,qGAAqG;AACrG,MAAM,UAAU,0BAA0B,CACxC,aAAwD;IAExD,MAAM,UAAU,GAAG,yBAAyB,CAAC,aAAa,CAAC,CAAC;IAC5D,MAAM,SAAS,GAAG,qBAAqB,CACrC,aAAa,CAAC,SAAS,EACvB,WAAW,EACX,qBAAqB,CACtB,CAAC;IACF,MAAM,OAAO,GAAG,IAAI,MAAM,CACxB,iBAAiB,SAAS,uHAAuH,EACjJ,GAAG,CACJ,CAAC;IACF,OAAO;QACL,KAAK,CAAC,QAAQ,CAAC,YAAoB;YACjC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC;gBAC7B,MAAM,IAAI,2BAA2B,CAAC,yBAAyB,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;YAChF,MAAM,QAAQ,GAAG,MAAM,gBAAgB,CACrC,UAAU,CAAC,KAAK,EAChB,IAAI,OAAO,CAAC,IAAI,GAAG,CAAC,YAAY,EAAE,UAAU,CAAC,QAAQ,CAAC,EAAE;gBACtD,QAAQ,EAAE,QAAQ;gBAClB,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,UAAU,CAAC,GAAG,EAAE,EAAE;aACvD,CAAC,EACF,UAAU,CAAC,mBAAmB,CAC/B,CAAC;YACF,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;gBAC5B,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;gBAC9B,MAAM,IAAI,2BAA2B,CACnC,QAAQ,CAAC,MAAM,KAAK,GAAG;oBACrB,CAAC,CAAC,sBAAsB;oBACxB,CAAC,CAAC,QAAQ,CAAC,MAAM,KAAK,GAAG;wBACvB,CAAC,CAAC,2BAA2B;wBAC7B,CAAC,CAAC,QAAQ,CAAC,MAAM,KAAK,GAAG;4BACvB,CAAC,CAAC,wBAAwB;4BAC1B,CAAC,CAAC,QAAQ,CAAC,MAAM,KAAK,GAAG;gCACvB,CAAC,CAAC,cAAc;gCAChB,CAAC,CAAC,qBAAqB,EAC/B,QAAQ,CAAC,MAAM,EACf,QAAQ,CAAC,MAAM,IAAI,GAAG,CACvB,CAAC;YACJ,CAAC;YACD,OAAO,QAAQ,CAAC;QAClB,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -1,53 +0,0 @@
1
- import {
2
- validateGatewayConnection,
3
- fetchWithTimeout,
4
- requireConfiguredUlid,
5
- type GatewayConnection,
6
- } from "./http.js";
7
- import { CustomerRuntimeGatewayError } from "./errors.js";
8
- /** Stream owned attachments through the private mail binding using the environment's runtime key. */
9
- export function createMailAttachmentClient(
10
- configuration: GatewayConnection & { projectId: string },
11
- ) {
12
- const connection = validateGatewayConnection(configuration);
13
- const projectId = requireConfiguredUlid(
14
- configuration.projectId,
15
- "projectId",
16
- "OHMYHOST_PROJECT_ID",
17
- );
18
- const pattern = new RegExp(
19
- `^/v1/projects/${projectId}/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}$`,
20
- "u",
21
- );
22
- return {
23
- async download(downloadPath: string): Promise<Response> {
24
- if (!pattern.test(downloadPath))
25
- throw new CustomerRuntimeGatewayError("gateway_request_invalid", null, false);
26
- const response = await fetchWithTimeout(
27
- connection.fetch,
28
- new Request(new URL(downloadPath, connection.endpoint), {
29
- redirect: "manual",
30
- headers: { authorization: `Bearer ${connection.key}` },
31
- }),
32
- connection.timeoutMilliseconds,
33
- );
34
- if (response.status !== 200) {
35
- await response.body?.cancel();
36
- throw new CustomerRuntimeGatewayError(
37
- response.status === 410
38
- ? "mail_content_expired"
39
- : response.status === 413
40
- ? "mail_attachment_too_large"
41
- : response.status === 404
42
- ? "mail_message_not_found"
43
- : response.status === 401
44
- ? "unauthorized"
45
- : "gateway_unavailable",
46
- response.status,
47
- response.status >= 500,
48
- );
49
- }
50
- return response;
51
- },
52
- };
53
- }