graphile-presigned-url-plugin 1.15.1 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/index.d.ts +1 -0
- package/esm/index.js +1 -0
- package/esm/physical-bucket.js +10 -1
- package/esm/s3-failure.d.ts +30 -0
- package/esm/s3-failure.js +60 -0
- package/esm/s3-signer.js +15 -2
- package/index.d.ts +1 -0
- package/index.js +4 -1
- package/package.json +2 -2
- package/physical-bucket.js +10 -1
- package/s3-failure.d.ts +30 -0
- package/s3-failure.js +64 -0
- package/s3-signer.js +15 -2
package/esm/index.d.ts
CHANGED
|
@@ -38,6 +38,7 @@ export { mintPhysicalBucketName, provisionAndRecordPhysicalBucket, resolveS3, re
|
|
|
38
38
|
export { createPresignedUrlPlugin, PresignedUrlPlugin } from './plugin';
|
|
39
39
|
export { PresignedUrlPreset } from './preset';
|
|
40
40
|
export { type WithPgClient, withRequestPgClient } from './request-pg-client';
|
|
41
|
+
export { describeS3Failure, s3FailureError } from './s3-failure';
|
|
41
42
|
export { copyS3Object, deleteS3Object, generatePresignedGetUrl, generatePresignedPutUrl, headObject, readObjectPrefix } from './s3-signer';
|
|
42
43
|
export { clearBucketCache, clearStorageModuleCache, getBucketConfig, isS3BucketProvisioned, loadAllStorageModules, markS3BucketProvisioned, resolveStorageConfigFromCodec, resolveStorageModuleByFileId } from './storage-module-cache';
|
|
43
44
|
export type { BucketConfig, BucketNameResolver, EnsureBucketProvisioned, PresignedUrlPluginOptions, RequestUploadUrlInput, RequestUploadUrlPayload, S3Config, S3ConfigOrGetter, StorageModuleConfig, } from './types';
|
package/esm/index.js
CHANGED
|
@@ -36,5 +36,6 @@ export { mintPhysicalBucketName, provisionAndRecordPhysicalBucket, resolveS3, re
|
|
|
36
36
|
export { createPresignedUrlPlugin, PresignedUrlPlugin } from './plugin';
|
|
37
37
|
export { PresignedUrlPreset } from './preset';
|
|
38
38
|
export { withRequestPgClient } from './request-pg-client';
|
|
39
|
+
export { describeS3Failure, s3FailureError } from './s3-failure';
|
|
39
40
|
export { copyS3Object, deleteS3Object, generatePresignedGetUrl, generatePresignedPutUrl, headObject, readObjectPrefix } from './s3-signer';
|
|
40
41
|
export { clearBucketCache, clearStorageModuleCache, getBucketConfig, isS3BucketProvisioned, loadAllStorageModules, markS3BucketProvisioned, resolveStorageConfigFromCodec, resolveStorageModuleByFileId } from './storage-module-cache';
|
package/esm/physical-bucket.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { Logger } from '@pgpmjs/logger';
|
|
12
12
|
import { withRequestPgClient } from './request-pg-client';
|
|
13
|
+
import { s3FailureError } from './s3-failure';
|
|
13
14
|
import { isS3BucketProvisioned, markS3BucketProvisioned } from './storage-module-cache';
|
|
14
15
|
const log = new Logger('graphile-presigned-url:physical-bucket');
|
|
15
16
|
/**
|
|
@@ -91,7 +92,15 @@ export async function provisionAndRecordPhysicalBucket(options, withPgClient, st
|
|
|
91
92
|
const s3BucketName = mintPhysicalBucketName(options, databaseId, bucket.key);
|
|
92
93
|
if (options.ensureBucketProvisioned && !isS3BucketProvisioned(s3BucketName)) {
|
|
93
94
|
log.info(`Lazy-provisioning S3 bucket "${s3BucketName}" for database ${databaseId}`);
|
|
94
|
-
|
|
95
|
+
try {
|
|
96
|
+
await options.ensureBucketProvisioned(s3BucketName, bucket.type, databaseId, allowedOrigins);
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
// The first upload to a bucket is where an unreachable object store is
|
|
100
|
+
// discovered, and the transport's own message is routinely empty: name the
|
|
101
|
+
// endpoint it could not reach so the response says what is misconfigured.
|
|
102
|
+
throw s3FailureError('BUCKET_PROVISION_FAILED', { endpoint: resolveS3(options).endpoint, bucket: s3BucketName, databaseId }, err);
|
|
103
|
+
}
|
|
95
104
|
markS3BucketProvisioned(s3BucketName);
|
|
96
105
|
log.info(`Lazy-provisioned S3 bucket "${s3BucketName}" successfully`);
|
|
97
106
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning an S3 transport failure into a reason a caller can act on.
|
|
3
|
+
*
|
|
4
|
+
* The AWS SDK routinely fails with an **empty** `message`: an unreachable
|
|
5
|
+
* endpoint arrives as an `AggregateError` whose own message is `''` and whose
|
|
6
|
+
* per-address `errors` hold the `ECONNREFUSED`, and a hung socket arrives as a
|
|
7
|
+
* bare wrapper around its `cause`. Anything that reports `err.message` verbatim
|
|
8
|
+
* therefore hands the client a blank reason — which is how a server signing
|
|
9
|
+
* against the wrong endpoint (a missing `CDN_ENDPOINT`, so the library default
|
|
10
|
+
* `http://localhost:9000`, i.e. the pod's own loopback) presents as an upload
|
|
11
|
+
* that fails with nothing to diagnose.
|
|
12
|
+
*
|
|
13
|
+
* So a failure is described by walking to where the words actually are, and
|
|
14
|
+
* re-thrown naming the coordinates it was talking to, with the original kept as
|
|
15
|
+
* `cause` for the server log.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Describe a thrown S3 error in one line, including the nested errors an
|
|
19
|
+
* `AggregateError` (or a `cause` chain) hides its actual reason in.
|
|
20
|
+
*/
|
|
21
|
+
export declare function describeS3Failure(err: unknown, depth?: number): string;
|
|
22
|
+
/**
|
|
23
|
+
* Wrap a failed S3 call as an error whose message carries the operation, the
|
|
24
|
+
* coordinates it used, and the underlying reason — with the original as `cause`.
|
|
25
|
+
*
|
|
26
|
+
* `context` is rendered as `key=value` pairs in the order given; entries with no
|
|
27
|
+
* value are dropped, so a connection without an explicit endpoint (real AWS)
|
|
28
|
+
* does not print an empty one.
|
|
29
|
+
*/
|
|
30
|
+
export declare function s3FailureError(operation: string, context: Record<string, string | number | undefined | null>, err: unknown): Error;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning an S3 transport failure into a reason a caller can act on.
|
|
3
|
+
*
|
|
4
|
+
* The AWS SDK routinely fails with an **empty** `message`: an unreachable
|
|
5
|
+
* endpoint arrives as an `AggregateError` whose own message is `''` and whose
|
|
6
|
+
* per-address `errors` hold the `ECONNREFUSED`, and a hung socket arrives as a
|
|
7
|
+
* bare wrapper around its `cause`. Anything that reports `err.message` verbatim
|
|
8
|
+
* therefore hands the client a blank reason — which is how a server signing
|
|
9
|
+
* against the wrong endpoint (a missing `CDN_ENDPOINT`, so the library default
|
|
10
|
+
* `http://localhost:9000`, i.e. the pod's own loopback) presents as an upload
|
|
11
|
+
* that fails with nothing to diagnose.
|
|
12
|
+
*
|
|
13
|
+
* So a failure is described by walking to where the words actually are, and
|
|
14
|
+
* re-thrown naming the coordinates it was talking to, with the original kept as
|
|
15
|
+
* `cause` for the server log.
|
|
16
|
+
*/
|
|
17
|
+
/** How far to walk `errors` / `cause` before the chain stops being informative. */
|
|
18
|
+
const MAX_DEPTH = 4;
|
|
19
|
+
/**
|
|
20
|
+
* Describe a thrown S3 error in one line, including the nested errors an
|
|
21
|
+
* `AggregateError` (or a `cause` chain) hides its actual reason in.
|
|
22
|
+
*/
|
|
23
|
+
export function describeS3Failure(err, depth = 0) {
|
|
24
|
+
if (err === null || err === undefined)
|
|
25
|
+
return 'unknown error';
|
|
26
|
+
if (typeof err !== 'object')
|
|
27
|
+
return String(err);
|
|
28
|
+
const e = err;
|
|
29
|
+
const name = typeof e.name === 'string' && e.name !== 'Error' ? e.name : '';
|
|
30
|
+
const message = typeof e.message === 'string' ? e.message : '';
|
|
31
|
+
const head = message.length > 0 ? [name, message].filter(Boolean).join(': ') : name;
|
|
32
|
+
const status = e.$metadata?.httpStatusCode ? `HTTP ${e.$metadata.httpStatusCode}` : '';
|
|
33
|
+
let detail = '';
|
|
34
|
+
if (depth < MAX_DEPTH) {
|
|
35
|
+
const nested = Array.isArray(e.errors) ? e.errors : e.cause !== undefined ? [e.cause] : [];
|
|
36
|
+
const described = nested
|
|
37
|
+
.map((inner) => describeS3Failure(inner, depth + 1))
|
|
38
|
+
.filter((text) => text.length > 0 && text !== 'unknown error');
|
|
39
|
+
if (described.length > 0)
|
|
40
|
+
detail = `(${described.join('; ')})`;
|
|
41
|
+
}
|
|
42
|
+
const described = [head, status, detail].filter((part) => part.length > 0).join(' ');
|
|
43
|
+
return described.length > 0 ? described : 'unknown error';
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Wrap a failed S3 call as an error whose message carries the operation, the
|
|
47
|
+
* coordinates it used, and the underlying reason — with the original as `cause`.
|
|
48
|
+
*
|
|
49
|
+
* `context` is rendered as `key=value` pairs in the order given; entries with no
|
|
50
|
+
* value are dropped, so a connection without an explicit endpoint (real AWS)
|
|
51
|
+
* does not print an empty one.
|
|
52
|
+
*/
|
|
53
|
+
export function s3FailureError(operation, context, err) {
|
|
54
|
+
const coordinates = Object.entries(context)
|
|
55
|
+
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
|
56
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
57
|
+
.join(' ');
|
|
58
|
+
const prefix = coordinates.length > 0 ? `${operation}: ${coordinates}` : operation;
|
|
59
|
+
return new Error(`${prefix}: ${describeS3Failure(err)}`, { cause: err });
|
|
60
|
+
}
|
package/esm/s3-signer.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CopyObjectCommand, DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, PutObjectCommand, } from '@aws-sdk/client-s3';
|
|
2
2
|
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
3
3
|
import { Logger } from '@pgpmjs/logger';
|
|
4
|
+
import { s3FailureError } from './s3-failure';
|
|
4
5
|
const log = new Logger('graphile-presigned-url:s3');
|
|
5
6
|
/**
|
|
6
7
|
* Generate a presigned PUT URL for uploading a file to S3.
|
|
@@ -23,7 +24,13 @@ export async function generatePresignedPutUrl(s3Config, key, contentType, conten
|
|
|
23
24
|
ContentType: contentType,
|
|
24
25
|
ContentLength: contentLength,
|
|
25
26
|
});
|
|
26
|
-
|
|
27
|
+
let url;
|
|
28
|
+
try {
|
|
29
|
+
url = await getSignedUrl(s3Config.client, command, { expiresIn });
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
throw s3FailureError('PRESIGN_PUT_FAILED', { endpoint: s3Config.endpoint, bucket: s3Config.bucket, key, contentType }, err);
|
|
33
|
+
}
|
|
27
34
|
log.debug(`Generated presigned PUT URL for key=${key}, contentType=${contentType}, expires=${expiresIn}s`);
|
|
28
35
|
return url;
|
|
29
36
|
}
|
|
@@ -49,7 +56,13 @@ export async function generatePresignedGetUrl(s3Config, key, expiresIn = 3600, f
|
|
|
49
56
|
params.ResponseContentDisposition = `attachment; filename="${sanitized}"`;
|
|
50
57
|
}
|
|
51
58
|
const command = new GetObjectCommand(params);
|
|
52
|
-
|
|
59
|
+
let url;
|
|
60
|
+
try {
|
|
61
|
+
url = await getSignedUrl(s3Config.client, command, { expiresIn });
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
throw s3FailureError('PRESIGN_GET_FAILED', { endpoint: s3Config.endpoint, bucket: s3Config.bucket, key }, err);
|
|
65
|
+
}
|
|
53
66
|
log.debug(`Generated presigned GET URL for key=${key}, expires=${expiresIn}s`);
|
|
54
67
|
return url;
|
|
55
68
|
}
|
package/index.d.ts
CHANGED
|
@@ -38,6 +38,7 @@ export { mintPhysicalBucketName, provisionAndRecordPhysicalBucket, resolveS3, re
|
|
|
38
38
|
export { createPresignedUrlPlugin, PresignedUrlPlugin } from './plugin';
|
|
39
39
|
export { PresignedUrlPreset } from './preset';
|
|
40
40
|
export { type WithPgClient, withRequestPgClient } from './request-pg-client';
|
|
41
|
+
export { describeS3Failure, s3FailureError } from './s3-failure';
|
|
41
42
|
export { copyS3Object, deleteS3Object, generatePresignedGetUrl, generatePresignedPutUrl, headObject, readObjectPrefix } from './s3-signer';
|
|
42
43
|
export { clearBucketCache, clearStorageModuleCache, getBucketConfig, isS3BucketProvisioned, loadAllStorageModules, markS3BucketProvisioned, resolveStorageConfigFromCodec, resolveStorageModuleByFileId } from './storage-module-cache';
|
|
43
44
|
export type { BucketConfig, BucketNameResolver, EnsureBucketProvisioned, PresignedUrlPluginOptions, RequestUploadUrlInput, RequestUploadUrlPayload, S3Config, S3ConfigOrGetter, StorageModuleConfig, } from './types';
|
package/index.js
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* ```
|
|
29
29
|
*/
|
|
30
30
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
-
exports.resolveStorageModuleByFileId = exports.resolveStorageConfigFromCodec = exports.markS3BucketProvisioned = exports.loadAllStorageModules = exports.isS3BucketProvisioned = exports.getBucketConfig = exports.clearStorageModuleCache = exports.clearBucketCache = exports.readObjectPrefix = exports.headObject = exports.generatePresignedPutUrl = exports.generatePresignedGetUrl = exports.deleteS3Object = exports.copyS3Object = exports.withRequestPgClient = exports.PresignedUrlPreset = exports.PresignedUrlPlugin = exports.createPresignedUrlPlugin = exports.resolveS3ForDatabase = exports.resolveS3 = exports.provisionAndRecordPhysicalBucket = exports.mintPhysicalBucketName = exports.resolveManagedUploadTarget = exports.finalizeStagedUpload = exports.buildFileProjection = exports.assertUploadAllowedByBucket = exports.getFileRefFieldBinding = exports.FileRefFieldNotRegisteredError = exports.clearFileRefFieldCache = exports.createDownloadUrlPlugin = exports.resolveDefaultBucket = exports.validateCustomKey = exports.confirmUploadedBytes = exports.CONFIRM_PREFIX_BYTES = void 0;
|
|
31
|
+
exports.resolveStorageModuleByFileId = exports.resolveStorageConfigFromCodec = exports.markS3BucketProvisioned = exports.loadAllStorageModules = exports.isS3BucketProvisioned = exports.getBucketConfig = exports.clearStorageModuleCache = exports.clearBucketCache = exports.readObjectPrefix = exports.headObject = exports.generatePresignedPutUrl = exports.generatePresignedGetUrl = exports.deleteS3Object = exports.copyS3Object = exports.s3FailureError = exports.describeS3Failure = exports.withRequestPgClient = exports.PresignedUrlPreset = exports.PresignedUrlPlugin = exports.createPresignedUrlPlugin = exports.resolveS3ForDatabase = exports.resolveS3 = exports.provisionAndRecordPhysicalBucket = exports.mintPhysicalBucketName = exports.resolveManagedUploadTarget = exports.finalizeStagedUpload = exports.buildFileProjection = exports.assertUploadAllowedByBucket = exports.getFileRefFieldBinding = exports.FileRefFieldNotRegisteredError = exports.clearFileRefFieldCache = exports.createDownloadUrlPlugin = exports.resolveDefaultBucket = exports.validateCustomKey = exports.confirmUploadedBytes = exports.CONFIRM_PREFIX_BYTES = void 0;
|
|
32
32
|
var confirm_upload_1 = require("./confirm-upload");
|
|
33
33
|
Object.defineProperty(exports, "CONFIRM_PREFIX_BYTES", { enumerable: true, get: function () { return confirm_upload_1.CONFIRM_PREFIX_BYTES; } });
|
|
34
34
|
Object.defineProperty(exports, "confirmUploadedBytes", { enumerable: true, get: function () { return confirm_upload_1.confirmUploadedBytes; } });
|
|
@@ -59,6 +59,9 @@ var preset_1 = require("./preset");
|
|
|
59
59
|
Object.defineProperty(exports, "PresignedUrlPreset", { enumerable: true, get: function () { return preset_1.PresignedUrlPreset; } });
|
|
60
60
|
var request_pg_client_1 = require("./request-pg-client");
|
|
61
61
|
Object.defineProperty(exports, "withRequestPgClient", { enumerable: true, get: function () { return request_pg_client_1.withRequestPgClient; } });
|
|
62
|
+
var s3_failure_1 = require("./s3-failure");
|
|
63
|
+
Object.defineProperty(exports, "describeS3Failure", { enumerable: true, get: function () { return s3_failure_1.describeS3Failure; } });
|
|
64
|
+
Object.defineProperty(exports, "s3FailureError", { enumerable: true, get: function () { return s3_failure_1.s3FailureError; } });
|
|
62
65
|
var s3_signer_1 = require("./s3-signer");
|
|
63
66
|
Object.defineProperty(exports, "copyS3Object", { enumerable: true, get: function () { return s3_signer_1.copyS3Object; } });
|
|
64
67
|
Object.defineProperty(exports, "deleteS3Object", { enumerable: true, get: function () { return s3_signer_1.deleteS3Object; } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "graphile-presigned-url-plugin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"description": "Presigned URL upload plugin for PostGraphile v5 — requestUploadUrl mutation and downloadUrl computed field",
|
|
5
5
|
"author": "Constructive <developers@constructive.io>",
|
|
6
6
|
"homepage": "https://github.com/constructive-io/constructive",
|
|
@@ -62,5 +62,5 @@
|
|
|
62
62
|
"@types/node": "^22.19.11",
|
|
63
63
|
"makage": "^0.3.0"
|
|
64
64
|
},
|
|
65
|
-
"gitHead": "
|
|
65
|
+
"gitHead": "df6b6613c844b505813e5d02d3a4afd918b74eab"
|
|
66
66
|
}
|
package/physical-bucket.js
CHANGED
|
@@ -16,6 +16,7 @@ exports.resolveS3ForDatabase = resolveS3ForDatabase;
|
|
|
16
16
|
exports.provisionAndRecordPhysicalBucket = provisionAndRecordPhysicalBucket;
|
|
17
17
|
const logger_1 = require("@pgpmjs/logger");
|
|
18
18
|
const request_pg_client_1 = require("./request-pg-client");
|
|
19
|
+
const s3_failure_1 = require("./s3-failure");
|
|
19
20
|
const storage_module_cache_1 = require("./storage-module-cache");
|
|
20
21
|
const log = new logger_1.Logger('graphile-presigned-url:physical-bucket');
|
|
21
22
|
/**
|
|
@@ -97,7 +98,15 @@ async function provisionAndRecordPhysicalBucket(options, withPgClient, storageCo
|
|
|
97
98
|
const s3BucketName = mintPhysicalBucketName(options, databaseId, bucket.key);
|
|
98
99
|
if (options.ensureBucketProvisioned && !(0, storage_module_cache_1.isS3BucketProvisioned)(s3BucketName)) {
|
|
99
100
|
log.info(`Lazy-provisioning S3 bucket "${s3BucketName}" for database ${databaseId}`);
|
|
100
|
-
|
|
101
|
+
try {
|
|
102
|
+
await options.ensureBucketProvisioned(s3BucketName, bucket.type, databaseId, allowedOrigins);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
// The first upload to a bucket is where an unreachable object store is
|
|
106
|
+
// discovered, and the transport's own message is routinely empty: name the
|
|
107
|
+
// endpoint it could not reach so the response says what is misconfigured.
|
|
108
|
+
throw (0, s3_failure_1.s3FailureError)('BUCKET_PROVISION_FAILED', { endpoint: resolveS3(options).endpoint, bucket: s3BucketName, databaseId }, err);
|
|
109
|
+
}
|
|
101
110
|
(0, storage_module_cache_1.markS3BucketProvisioned)(s3BucketName);
|
|
102
111
|
log.info(`Lazy-provisioned S3 bucket "${s3BucketName}" successfully`);
|
|
103
112
|
}
|
package/s3-failure.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning an S3 transport failure into a reason a caller can act on.
|
|
3
|
+
*
|
|
4
|
+
* The AWS SDK routinely fails with an **empty** `message`: an unreachable
|
|
5
|
+
* endpoint arrives as an `AggregateError` whose own message is `''` and whose
|
|
6
|
+
* per-address `errors` hold the `ECONNREFUSED`, and a hung socket arrives as a
|
|
7
|
+
* bare wrapper around its `cause`. Anything that reports `err.message` verbatim
|
|
8
|
+
* therefore hands the client a blank reason — which is how a server signing
|
|
9
|
+
* against the wrong endpoint (a missing `CDN_ENDPOINT`, so the library default
|
|
10
|
+
* `http://localhost:9000`, i.e. the pod's own loopback) presents as an upload
|
|
11
|
+
* that fails with nothing to diagnose.
|
|
12
|
+
*
|
|
13
|
+
* So a failure is described by walking to where the words actually are, and
|
|
14
|
+
* re-thrown naming the coordinates it was talking to, with the original kept as
|
|
15
|
+
* `cause` for the server log.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Describe a thrown S3 error in one line, including the nested errors an
|
|
19
|
+
* `AggregateError` (or a `cause` chain) hides its actual reason in.
|
|
20
|
+
*/
|
|
21
|
+
export declare function describeS3Failure(err: unknown, depth?: number): string;
|
|
22
|
+
/**
|
|
23
|
+
* Wrap a failed S3 call as an error whose message carries the operation, the
|
|
24
|
+
* coordinates it used, and the underlying reason — with the original as `cause`.
|
|
25
|
+
*
|
|
26
|
+
* `context` is rendered as `key=value` pairs in the order given; entries with no
|
|
27
|
+
* value are dropped, so a connection without an explicit endpoint (real AWS)
|
|
28
|
+
* does not print an empty one.
|
|
29
|
+
*/
|
|
30
|
+
export declare function s3FailureError(operation: string, context: Record<string, string | number | undefined | null>, err: unknown): Error;
|
package/s3-failure.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Turning an S3 transport failure into a reason a caller can act on.
|
|
4
|
+
*
|
|
5
|
+
* The AWS SDK routinely fails with an **empty** `message`: an unreachable
|
|
6
|
+
* endpoint arrives as an `AggregateError` whose own message is `''` and whose
|
|
7
|
+
* per-address `errors` hold the `ECONNREFUSED`, and a hung socket arrives as a
|
|
8
|
+
* bare wrapper around its `cause`. Anything that reports `err.message` verbatim
|
|
9
|
+
* therefore hands the client a blank reason — which is how a server signing
|
|
10
|
+
* against the wrong endpoint (a missing `CDN_ENDPOINT`, so the library default
|
|
11
|
+
* `http://localhost:9000`, i.e. the pod's own loopback) presents as an upload
|
|
12
|
+
* that fails with nothing to diagnose.
|
|
13
|
+
*
|
|
14
|
+
* So a failure is described by walking to where the words actually are, and
|
|
15
|
+
* re-thrown naming the coordinates it was talking to, with the original kept as
|
|
16
|
+
* `cause` for the server log.
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.describeS3Failure = describeS3Failure;
|
|
20
|
+
exports.s3FailureError = s3FailureError;
|
|
21
|
+
/** How far to walk `errors` / `cause` before the chain stops being informative. */
|
|
22
|
+
const MAX_DEPTH = 4;
|
|
23
|
+
/**
|
|
24
|
+
* Describe a thrown S3 error in one line, including the nested errors an
|
|
25
|
+
* `AggregateError` (or a `cause` chain) hides its actual reason in.
|
|
26
|
+
*/
|
|
27
|
+
function describeS3Failure(err, depth = 0) {
|
|
28
|
+
if (err === null || err === undefined)
|
|
29
|
+
return 'unknown error';
|
|
30
|
+
if (typeof err !== 'object')
|
|
31
|
+
return String(err);
|
|
32
|
+
const e = err;
|
|
33
|
+
const name = typeof e.name === 'string' && e.name !== 'Error' ? e.name : '';
|
|
34
|
+
const message = typeof e.message === 'string' ? e.message : '';
|
|
35
|
+
const head = message.length > 0 ? [name, message].filter(Boolean).join(': ') : name;
|
|
36
|
+
const status = e.$metadata?.httpStatusCode ? `HTTP ${e.$metadata.httpStatusCode}` : '';
|
|
37
|
+
let detail = '';
|
|
38
|
+
if (depth < MAX_DEPTH) {
|
|
39
|
+
const nested = Array.isArray(e.errors) ? e.errors : e.cause !== undefined ? [e.cause] : [];
|
|
40
|
+
const described = nested
|
|
41
|
+
.map((inner) => describeS3Failure(inner, depth + 1))
|
|
42
|
+
.filter((text) => text.length > 0 && text !== 'unknown error');
|
|
43
|
+
if (described.length > 0)
|
|
44
|
+
detail = `(${described.join('; ')})`;
|
|
45
|
+
}
|
|
46
|
+
const described = [head, status, detail].filter((part) => part.length > 0).join(' ');
|
|
47
|
+
return described.length > 0 ? described : 'unknown error';
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Wrap a failed S3 call as an error whose message carries the operation, the
|
|
51
|
+
* coordinates it used, and the underlying reason — with the original as `cause`.
|
|
52
|
+
*
|
|
53
|
+
* `context` is rendered as `key=value` pairs in the order given; entries with no
|
|
54
|
+
* value are dropped, so a connection without an explicit endpoint (real AWS)
|
|
55
|
+
* does not print an empty one.
|
|
56
|
+
*/
|
|
57
|
+
function s3FailureError(operation, context, err) {
|
|
58
|
+
const coordinates = Object.entries(context)
|
|
59
|
+
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
|
60
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
61
|
+
.join(' ');
|
|
62
|
+
const prefix = coordinates.length > 0 ? `${operation}: ${coordinates}` : operation;
|
|
63
|
+
return new Error(`${prefix}: ${describeS3Failure(err)}`, { cause: err });
|
|
64
|
+
}
|
package/s3-signer.js
CHANGED
|
@@ -9,6 +9,7 @@ exports.headObject = headObject;
|
|
|
9
9
|
const client_s3_1 = require("@aws-sdk/client-s3");
|
|
10
10
|
const s3_request_presigner_1 = require("@aws-sdk/s3-request-presigner");
|
|
11
11
|
const logger_1 = require("@pgpmjs/logger");
|
|
12
|
+
const s3_failure_1 = require("./s3-failure");
|
|
12
13
|
const log = new logger_1.Logger('graphile-presigned-url:s3');
|
|
13
14
|
/**
|
|
14
15
|
* Generate a presigned PUT URL for uploading a file to S3.
|
|
@@ -31,7 +32,13 @@ async function generatePresignedPutUrl(s3Config, key, contentType, contentLength
|
|
|
31
32
|
ContentType: contentType,
|
|
32
33
|
ContentLength: contentLength,
|
|
33
34
|
});
|
|
34
|
-
|
|
35
|
+
let url;
|
|
36
|
+
try {
|
|
37
|
+
url = await (0, s3_request_presigner_1.getSignedUrl)(s3Config.client, command, { expiresIn });
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
throw (0, s3_failure_1.s3FailureError)('PRESIGN_PUT_FAILED', { endpoint: s3Config.endpoint, bucket: s3Config.bucket, key, contentType }, err);
|
|
41
|
+
}
|
|
35
42
|
log.debug(`Generated presigned PUT URL for key=${key}, contentType=${contentType}, expires=${expiresIn}s`);
|
|
36
43
|
return url;
|
|
37
44
|
}
|
|
@@ -57,7 +64,13 @@ async function generatePresignedGetUrl(s3Config, key, expiresIn = 3600, filename
|
|
|
57
64
|
params.ResponseContentDisposition = `attachment; filename="${sanitized}"`;
|
|
58
65
|
}
|
|
59
66
|
const command = new client_s3_1.GetObjectCommand(params);
|
|
60
|
-
|
|
67
|
+
let url;
|
|
68
|
+
try {
|
|
69
|
+
url = await (0, s3_request_presigner_1.getSignedUrl)(s3Config.client, command, { expiresIn });
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
throw (0, s3_failure_1.s3FailureError)('PRESIGN_GET_FAILED', { endpoint: s3Config.endpoint, bucket: s3Config.bucket, key }, err);
|
|
73
|
+
}
|
|
61
74
|
log.debug(`Generated presigned GET URL for key=${key}, expires=${expiresIn}s`);
|
|
62
75
|
return url;
|
|
63
76
|
}
|