@dimah-s3/server 0.8.6 → 1.0.1

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 (72) hide show
  1. package/dist/adapters/express.js +13 -1
  2. package/dist/adapters/express.js.map +1 -1
  3. package/dist/adapters/fastify.js +13 -1
  4. package/dist/adapters/fastify.js.map +1 -1
  5. package/dist/adapters/node.js +13 -1
  6. package/dist/adapters/node.js.map +1 -1
  7. package/dist/adapters/svelte-kit.d.ts +2 -0
  8. package/dist/adapters/svelte-kit.d.ts.map +1 -1
  9. package/dist/adapters/svelte-kit.js.map +1 -1
  10. package/dist/adapters/types.d.ts.map +1 -1
  11. package/dist/api/assert-feature-enabled.d.ts.map +1 -1
  12. package/dist/api/bind-endpoints.d.ts.map +1 -1
  13. package/dist/api/context.d.ts.map +1 -1
  14. package/dist/api/index.d.ts +2 -2
  15. package/dist/api/index.d.ts.map +1 -1
  16. package/dist/api/index.js +218 -147
  17. package/dist/api/index.js.map +1 -1
  18. package/dist/{router.d.ts → api/router.d.ts} +3 -2
  19. package/dist/api/router.d.ts.map +1 -0
  20. package/dist/api/routes/confirm.d.ts.map +1 -1
  21. package/dist/api/routes/delete.d.ts.map +1 -1
  22. package/dist/api/routes/download.d.ts.map +1 -1
  23. package/dist/api/routes/index.d.ts +1 -7
  24. package/dist/api/routes/index.d.ts.map +1 -1
  25. package/dist/api/routes/multipart/abort.d.ts +1 -5
  26. package/dist/api/routes/multipart/abort.d.ts.map +1 -1
  27. package/dist/api/routes/multipart/complete.d.ts.map +1 -1
  28. package/dist/api/routes/multipart/init.d.ts +0 -2
  29. package/dist/api/routes/multipart/init.d.ts.map +1 -1
  30. package/dist/api/routes/multipart/list-parts.d.ts.map +1 -1
  31. package/dist/api/routes/multipart/part.d.ts.map +1 -1
  32. package/dist/api/routes/upload.d.ts.map +1 -1
  33. package/dist/errors.d.ts +4 -2
  34. package/dist/errors.d.ts.map +1 -1
  35. package/dist/helpers/expires.d.ts +6 -0
  36. package/dist/helpers/expires.d.ts.map +1 -0
  37. package/dist/helpers/features.d.ts +34 -0
  38. package/dist/helpers/features.d.ts.map +1 -0
  39. package/dist/helpers/head-object.d.ts +11 -0
  40. package/dist/helpers/head-object.d.ts.map +1 -0
  41. package/dist/{internal-helpers.d.ts → helpers/hooks.d.ts} +1 -4
  42. package/dist/helpers/hooks.d.ts.map +1 -0
  43. package/dist/helpers/index.d.ts +12 -1
  44. package/dist/helpers/index.d.ts.map +1 -1
  45. package/dist/helpers/is-aws-not-found.d.ts +3 -1
  46. package/dist/helpers/is-aws-not-found.d.ts.map +1 -1
  47. package/dist/helpers/list-parts.d.ts +8 -0
  48. package/dist/helpers/list-parts.d.ts.map +1 -0
  49. package/dist/helpers/request.d.ts +3 -0
  50. package/dist/helpers/request.d.ts.map +1 -0
  51. package/dist/helpers/resolve-request-acl.d.ts +10 -0
  52. package/dist/helpers/resolve-request-acl.d.ts.map +1 -0
  53. package/dist/helpers/resolve-target.d.ts +2 -33
  54. package/dist/helpers/resolve-target.d.ts.map +1 -1
  55. package/dist/index.d.ts +1 -2
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +188 -111
  58. package/dist/index.js.map +1 -1
  59. package/dist/plugin/apply-plugins.d.ts.map +1 -1
  60. package/dist/plugin/types.d.ts +5 -5
  61. package/dist/plugin/types.d.ts.map +1 -1
  62. package/dist/plugins.js +184 -104
  63. package/dist/plugins.js.map +1 -1
  64. package/dist/types/config.d.ts +27 -9
  65. package/dist/types/config.d.ts.map +1 -1
  66. package/dist/types/hook-contexts.d.ts +8 -8
  67. package/dist/types/hook-contexts.d.ts.map +1 -1
  68. package/dist/types/index.d.ts +1 -1
  69. package/dist/types/index.d.ts.map +1 -1
  70. package/package.json +8 -8
  71. package/dist/internal-helpers.d.ts.map +0 -1
  72. package/dist/router.d.ts.map +0 -1
@@ -0,0 +1,8 @@
1
+ import { type Part, type S3Client } from "@aws-sdk/client-s3";
2
+ /** Page through ListParts until S3 reports no more parts. */
3
+ export declare function listAllParts(client: S3Client, input: {
4
+ bucket: string;
5
+ key: string;
6
+ uploadId: string;
7
+ }): Promise<Part[]>;
8
+ //# sourceMappingURL=list-parts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-parts.d.ts","sourceRoot":"","sources":["../../src/helpers/list-parts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,KAAK,IAAI,EAAE,KAAK,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAGhF,6DAA6D;AAC7D,wBAAsB,YAAY,CAChC,MAAM,EAAE,QAAQ,EAChB,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACvD,OAAO,CAAC,IAAI,EAAE,CAAC,CAsBjB"}
@@ -0,0 +1,3 @@
1
+ /** Build a synthetic Request for server-side `api` calls. */
2
+ export declare function requestFromHeaders(headers?: HeadersInit): Request;
3
+ //# sourceMappingURL=request.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../../src/helpers/request.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,wBAAgB,kBAAkB,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAEjE"}
@@ -0,0 +1,10 @@
1
+ import type { S3ObjectAcl } from "@dimah-s3/core";
2
+ import type { AclPolicy } from "../types/config";
3
+ export type { AclPolicy };
4
+ /**
5
+ * Resolve object ACL. Default is `private`. A server `acl` is forced.
6
+ * Client `acl` is used only when `allowClientAcl` is set and no server ACL
7
+ * is configured.
8
+ */
9
+ export declare function resolveRequestAcl(policy: AclPolicy | undefined, requested?: S3ObjectAcl): S3ObjectAcl;
10
+ //# sourceMappingURL=resolve-request-acl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-request-acl.d.ts","sourceRoot":"","sources":["../../src/helpers/resolve-request-acl.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,YAAY,EAAE,SAAS,EAAE,CAAC;AAE1B;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,SAAS,GAAG,SAAS,EAC7B,SAAS,CAAC,EAAE,WAAW,GACtB,WAAW,CAIb"}
@@ -1,8 +1,5 @@
1
- import type { DownloadConfig, DeleteConfig, FeatureToggle, KeyPrefix, MultipartConfig, ResolvedDimahS3Config, ResolveKeyContext, UploadConfig } from "../types";
2
- type KeyPolicy = {
3
- prefix?: KeyPrefix;
4
- resolveKey?: (context: ResolveKeyContext) => string | Promise<string>;
5
- };
1
+ import type { KeyPolicy, ResolvedDimahS3Config, ResolveKeyContext } from "../types";
2
+ export type { KeyPolicy };
6
3
  /**
7
4
  * Normalize a client-proposed object key: strip leading slashes, reject
8
5
  * `.` / `..` segments, NUL, and backslashes.
@@ -20,32 +17,4 @@ export declare function resolveRequestTarget(config: ResolvedDimahS3Config, poli
20
17
  key: string;
21
18
  bucket: string;
22
19
  }>;
23
- export declare function normalizeFeature<T extends {
24
- enabled?: boolean;
25
- }>(value: FeatureToggle<T> | undefined): (T & {
26
- enabled: boolean;
27
- }) | undefined;
28
- export declare function isFeatureEnabled(config: ResolvedDimahS3Config, feature: "upload" | "download" | "delete" | "multipart"): boolean;
29
- export declare function applyMultipartDefault(config: ResolvedDimahS3Config, multipartInput: FeatureToggle<MultipartConfig> | undefined): ResolvedDimahS3Config;
30
- export type NormalizedFeatures = {
31
- upload?: UploadConfig & {
32
- enabled: boolean;
33
- };
34
- download?: DownloadConfig & {
35
- enabled: boolean;
36
- };
37
- delete?: DeleteConfig & {
38
- enabled: boolean;
39
- };
40
- multipart?: MultipartConfig & {
41
- enabled: boolean;
42
- };
43
- };
44
- export declare function normalizeFeatures(config: {
45
- upload?: FeatureToggle<UploadConfig>;
46
- download?: FeatureToggle<DownloadConfig>;
47
- delete?: FeatureToggle<DeleteConfig>;
48
- multipart?: FeatureToggle<MultipartConfig>;
49
- }): NormalizedFeatures;
50
- export {};
51
20
  //# sourceMappingURL=resolve-target.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"resolve-target.d.ts","sourceRoot":"","sources":["../../src/helpers/resolve-target.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,cAAc,EACd,YAAY,EACZ,aAAa,EACb,SAAS,EACT,eAAe,EACf,qBAAqB,EACrB,iBAAiB,EACjB,YAAY,EACb,MAAM,UAAU,CAAC;AAElB,KAAK,SAAS,GAAG;IACf,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACvE,CAAC;AAEF;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAavD;AASD,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,SAAS,GAAG,SAAS,EAC7B,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,MAAM,CAAC,CAYjB;AAED,wBAAgB,aAAa,CAC3B,MAAM,EAAE,qBAAqB,EAC7B,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,CAQR;AAED,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,qBAAqB,EAC7B,MAAM,EAAE,SAAS,GAAG,SAAS,EAC7B,KAAK,EAAE;IACL,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GACA,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAU1C;AAED,wBAAgB,gBAAgB,CAAC,CAAC,SAAS;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,EAC9D,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,SAAS,GAClC,CAAC,CAAC,GAAG;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,GAAG,SAAS,CAKxC;AAED,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,qBAAqB,EAC7B,OAAO,EAAE,QAAQ,GAAG,UAAU,GAAG,QAAQ,GAAG,WAAW,GACtD,OAAO,CAET;AAED,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,qBAAqB,EAC7B,cAAc,EAAE,aAAa,CAAC,eAAe,CAAC,GAAG,SAAS,GACzD,qBAAqB,CAmBvB;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,CAAC,EAAE,YAAY,GAAG;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IAC7C,QAAQ,CAAC,EAAE,cAAc,GAAG;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IACjD,MAAM,CAAC,EAAE,YAAY,GAAG;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IAC7C,SAAS,CAAC,EAAE,eAAe,GAAG;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;CACpD,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IACxC,MAAM,CAAC,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;IACrC,QAAQ,CAAC,EAAE,aAAa,CAAC,cAAc,CAAC,CAAC;IACzC,MAAM,CAAC,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;IACrC,SAAS,CAAC,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;CAC5C,GAAG,kBAAkB,CAOrB"}
1
+ {"version":3,"file":"resolve-target.d.ts","sourceRoot":"","sources":["../../src/helpers/resolve-target.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,SAAS,EACT,qBAAqB,EACrB,iBAAiB,EAClB,MAAM,SAAS,CAAC;AAEjB,YAAY,EAAE,SAAS,EAAE,CAAC;AAE1B;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAavD;AASD,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,SAAS,GAAG,SAAS,EAC7B,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,MAAM,CAAC,CAYjB;AAED,wBAAgB,aAAa,CAC3B,MAAM,EAAE,qBAAqB,EAC7B,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,CAQR;AAED,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,qBAAqB,EAC7B,MAAM,EAAE,SAAS,GAAG,SAAS,EAC7B,KAAK,EAAE;IACL,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GACA,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAU1C"}
package/dist/index.d.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  export { dimahS3 } from "./dimah-s3";
2
2
  export type { DimahS3, DimahS3Api, MultipartAliasApi } from "./dimah-s3";
3
- export { DimahS3Error, errors, isAPIError, isDimahS3Error, isS3ErrorCode, } from "./errors";
3
+ export { DimahS3Error, errors, isAPIError, isDimahS3Error, isS3ErrorCode, S3_ERROR_CODES, } from "./errors";
4
4
  export type { ServerErrors } from "./errors";
5
- export { resolveObjectAcl } from "./helpers/resolve-object-acl";
6
5
  export { createS3Endpoint, createS3Middleware } from "./api";
7
6
  export type { S3EndpointContext } from "./api";
8
7
  export { definePlugin } from "./plugin";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACzE,OAAO,EACL,YAAY,EACZ,MAAM,EACN,UAAU,EACV,cAAc,EACd,aAAa,GACd,MAAM,UAAU,CAAC;AAClB,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAEhE,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,OAAO,CAAC;AAC7D,YAAY,EAAE,iBAAiB,EAAE,MAAM,OAAO,CAAC;AAE/C,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,YAAY,EACV,aAAa,EACb,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,GACd,MAAM,UAAU,CAAC;AAElB,YAAY,EACV,aAAa,EACb,cAAc,EACd,YAAY,EACZ,aAAa,EACb,SAAS,EACT,eAAe,EACf,qBAAqB,EACrB,YAAY,GACb,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACzE,OAAO,EACL,YAAY,EACZ,MAAM,EACN,UAAU,EACV,cAAc,EACd,aAAa,EACb,cAAc,GACf,MAAM,UAAU,CAAC;AAClB,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAE7C,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,OAAO,CAAC;AAC7D,YAAY,EAAE,iBAAiB,EAAE,MAAM,OAAO,CAAC;AAE/C,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,YAAY,EACV,aAAa,EACb,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,GACd,MAAM,UAAU,CAAC;AAElB,YAAY,EACV,aAAa,EACb,cAAc,EACd,YAAY,EACZ,aAAa,EACb,SAAS,EACT,eAAe,EACf,qBAAqB,EACrB,YAAY,GACb,MAAM,SAAS,CAAC"}
package/dist/index.js CHANGED
@@ -1,36 +1,11 @@
1
- import { S3_API_ROUTES, isAPIError, DimahS3Error, S3_ERROR_CODES, confirmBodySchema, deleteQuerySchema, downloadQuerySchema, multipartAbortBodySchema, multipartCompleteBodySchema, multipartInitBodySchema, multipartListPartsQuerySchema, multipartSignPartBodySchema, uploadBodySchema, parseFileName, buildContentDisposition, normalizeS3ApiBasePath, S3_API_BASE_PATH } from '@dimah-s3/core';
2
- export { DimahS3Error, isAPIError, isDimahS3Error, isS3ErrorCode } from '@dimah-s3/core';
3
- import { GetObjectAclCommand, HeadObjectCommand, DeleteObjectCommand, GetObjectCommand, AbortMultipartUploadCommand, ListPartsCommand, CompleteMultipartUploadCommand, CreateMultipartUploadCommand, UploadPartCommand, PutObjectCommand } from '@aws-sdk/client-s3';
1
+ import { S3_API_ROUTES, isAPIError, DimahS3Error, S3_ERROR_CODES, confirmBodySchema, deleteQuerySchema, downloadQuerySchema, multipartAbortBodySchema, multipartCompleteBodySchema, multipartInitBodySchema, multipartListPartsQuerySchema, multipartSignPartBodySchema, uploadBodySchema, parseFileName, S3_MAX_EXPIRES_IN, buildContentDisposition, S3_MAX_POST_OBJECT_BYTES, normalizeS3ApiBasePath, S3_API_BASE_PATH, S3_DEFAULT_EXPIRES_IN } from '@dimah-s3/core';
2
+ export { DimahS3Error, S3_ERROR_CODES, isAPIError, isDimahS3Error, isS3ErrorCode } from '@dimah-s3/core';
3
+ import { DeleteObjectCommand, GetObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, PutObjectCommand, GetObjectAclCommand, ListPartsCommand, CompleteMultipartUploadCommand, HeadObjectCommand, AbortMultipartUploadCommand } from '@aws-sdk/client-s3';
4
4
  import { createMiddleware, createEndpoint, createRouter, toResponse } from 'better-call';
5
5
  import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
6
6
  import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
7
7
 
8
8
  // src/dimah-s3.ts
9
-
10
- // src/helpers/is-aws-not-found.ts
11
- function isAwsNotFound(err) {
12
- const e = err;
13
- return e?.name === "NoSuchKey" || e?.name === "NotFound" || e?.Code === "NoSuchKey" || e?.Code === "NotFound" || e?.$metadata?.httpStatusCode === 404;
14
- }
15
- var DEFAULT_ACL_LOOKUP_TIMEOUT_MS = 1500;
16
- async function resolveObjectAcl(s3, bucket, key, timeoutMs = DEFAULT_ACL_LOOKUP_TIMEOUT_MS) {
17
- const controller = new AbortController();
18
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
19
- try {
20
- const result = await s3.send(
21
- new GetObjectAclCommand({ Bucket: bucket, Key: key }),
22
- { abortSignal: controller.signal }
23
- );
24
- const isPublic = result.Grants?.some(
25
- (g) => g.Grantee?.URI === "http://acs.amazonaws.com/groups/global/AllUsers" && (g.Permission === "READ" || g.Permission === "FULL_CONTROL")
26
- );
27
- return isPublic ? "public-read" : "private";
28
- } catch {
29
- return void 0;
30
- } finally {
31
- clearTimeout(timeout);
32
- }
33
- }
34
9
  var errors = {
35
10
  notFound: () => DimahS3Error.from("NOT_FOUND", S3_ERROR_CODES.NOT_FOUND),
36
11
  unauthorized: () => DimahS3Error.from("UNAUTHORIZED", S3_ERROR_CODES.UNAUTHORIZED),
@@ -65,13 +40,69 @@ var errors = {
65
40
  "BAD_REQUEST",
66
41
  S3_ERROR_CODES.FILE_SIZE_REQUIRED_MULTIPART
67
42
  ),
43
+ multipartPartMissing: (partNumber) => DimahS3Error.from("BAD_REQUEST", {
44
+ code: S3_ERROR_CODES.MULTIPART_PART_MISSING.code,
45
+ message: S3_ERROR_CODES.MULTIPART_PART_MISSING.message.replaceAll(
46
+ "{partNumber}",
47
+ String(partNumber)
48
+ ),
49
+ params: { partNumber }
50
+ }),
51
+ payloadTooLarge: (message = S3_ERROR_CODES.PAYLOAD_TOO_LARGE.message) => DimahS3Error.from("PAYLOAD_TOO_LARGE", {
52
+ ...S3_ERROR_CODES.PAYLOAD_TOO_LARGE,
53
+ message
54
+ }),
68
55
  validationError: (message) => DimahS3Error.from("BAD_REQUEST", {
69
56
  code: S3_ERROR_CODES.VALIDATION_ERROR.code,
70
57
  message
71
58
  })
72
59
  };
73
60
 
74
- // src/internal-helpers.ts
61
+ // src/helpers/is-aws-not-found.ts
62
+ function isAwsNotFound(err) {
63
+ const e = err;
64
+ return e?.name === "NoSuchKey" || e?.name === "NotFound" || e?.name === "NoSuchUpload" || e?.Code === "NoSuchKey" || e?.Code === "NotFound" || e?.Code === "NoSuchUpload" || e?.$metadata?.httpStatusCode === 404;
65
+ }
66
+ async function sendOrObjectNotFound(send) {
67
+ try {
68
+ return await send();
69
+ } catch (err) {
70
+ if (isAwsNotFound(err)) throw errors.objectNotFound();
71
+ throw err;
72
+ }
73
+ }
74
+ var DEFAULT_ACL_LOOKUP_TIMEOUT_MS = 1500;
75
+ async function resolveObjectAcl(s3, bucket, key, timeoutMs = DEFAULT_ACL_LOOKUP_TIMEOUT_MS) {
76
+ const controller = new AbortController();
77
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
78
+ try {
79
+ const result = await s3.send(
80
+ new GetObjectAclCommand({ Bucket: bucket, Key: key }),
81
+ { abortSignal: controller.signal }
82
+ );
83
+ const isPublic = result.Grants?.some(
84
+ (g) => g.Grantee?.URI === "http://acs.amazonaws.com/groups/global/AllUsers" && (g.Permission === "READ" || g.Permission === "FULL_CONTROL")
85
+ );
86
+ return isPublic ? "public-read" : "private";
87
+ } catch {
88
+ return void 0;
89
+ } finally {
90
+ clearTimeout(timeout);
91
+ }
92
+ }
93
+
94
+ // src/helpers/resolve-request-acl.ts
95
+ function resolveRequestAcl(policy, requested) {
96
+ if (policy?.acl) return policy.acl;
97
+ if (policy?.allowClientAcl && requested) return requested;
98
+ return "private";
99
+ }
100
+ function normalizeExpiresIn(value, maxExpiresIn = S3_MAX_EXPIRES_IN) {
101
+ const n = Number(value);
102
+ const expires = Number.isFinite(n) && n > 0 ? Math.floor(n) : S3_DEFAULT_EXPIRES_IN;
103
+ const max = Number.isFinite(maxExpiresIn) && maxExpiresIn > 0 ? Math.floor(maxExpiresIn) : S3_MAX_EXPIRES_IN;
104
+ return Math.min(expires, max);
105
+ }
75
106
  async function runHook(hook, context) {
76
107
  if (!hook) return;
77
108
  try {
@@ -97,10 +128,8 @@ async function runLifecycleHook(hook, context) {
97
128
  throw errors.internalError();
98
129
  }
99
130
  }
100
- function normalizeExpiresIn(value) {
101
- const n = Number(value);
102
- return Number.isFinite(n) && n > 0 ? Math.floor(n) : 600;
103
- }
131
+
132
+ // src/helpers/request.ts
104
133
  function requestFromHeaders(headers) {
105
134
  return new Request("http://dimah-s3.local", { headers });
106
135
  }
@@ -156,11 +185,56 @@ async function resolveRequestTarget(config, policy, input) {
156
185
  });
157
186
  return { key, bucket };
158
187
  }
188
+ async function headObjectOrNotFound(client, bucket, key) {
189
+ return sendOrObjectNotFound(
190
+ () => client.send(new HeadObjectCommand({ Bucket: bucket, Key: key }))
191
+ );
192
+ }
193
+ async function headObjectAfterMultipartComplete(client, bucket, key) {
194
+ const send = () => sendOrObjectNotFound(
195
+ () => client.send(new HeadObjectCommand({ Bucket: bucket, Key: key }))
196
+ );
197
+ let head = await send();
198
+ for (let attempt = 0; attempt < 4 && !head.ContentLength; attempt++) {
199
+ await new Promise((r) => setTimeout(r, 250 * 2 ** attempt));
200
+ head = await send();
201
+ }
202
+ return head;
203
+ }
204
+ function requireContentLength(head) {
205
+ if (typeof head.ContentLength !== "number") {
206
+ throw errors.internalError();
207
+ }
208
+ return head.ContentLength;
209
+ }
210
+ async function listAllParts(client, input) {
211
+ const parts = [];
212
+ let partNumberMarker;
213
+ let isTruncated = true;
214
+ while (isTruncated) {
215
+ const response = await sendOrObjectNotFound(
216
+ () => client.send(
217
+ new ListPartsCommand({
218
+ Bucket: input.bucket,
219
+ Key: input.key,
220
+ UploadId: input.uploadId,
221
+ ...partNumberMarker ? { PartNumberMarker: partNumberMarker } : {}
222
+ })
223
+ )
224
+ );
225
+ parts.push(...response.Parts ?? []);
226
+ isTruncated = response.IsTruncated === true;
227
+ partNumberMarker = isTruncated ? response.NextPartNumberMarker : void 0;
228
+ }
229
+ return parts;
230
+ }
231
+
232
+ // src/helpers/features.ts
159
233
  function normalizeFeature(value) {
160
234
  if (value === void 0) return void 0;
161
235
  if (value === false) return { enabled: false };
162
236
  if (value === true) return { enabled: true };
163
- return { ...value, enabled: value.enabled ?? true };
237
+ return { ...value, enabled: true };
164
238
  }
165
239
  function isFeatureEnabled(config, feature) {
166
240
  return config[feature]?.enabled === true;
@@ -179,7 +253,10 @@ function applyMultipartDefault(config, multipartInput) {
179
253
  multipart: {
180
254
  ...next.multipart,
181
255
  prefix: next.multipart.prefix ?? next.upload.prefix,
182
- resolveKey: next.multipart.resolveKey ?? next.upload.resolveKey
256
+ resolveKey: next.multipart.resolveKey ?? next.upload.resolveKey,
257
+ acl: next.multipart.acl ?? next.upload.acl,
258
+ allowClientAcl: next.multipart.allowClientAcl ?? next.upload.allowClientAcl,
259
+ requireFileSize: next.multipart.requireFileSize ?? next.upload.requireFileSize
183
260
  }
184
261
  };
185
262
  }
@@ -193,6 +270,13 @@ function normalizeFeatures(config) {
193
270
  multipart: normalizeFeature(config.multipart)
194
271
  };
195
272
  }
273
+ function assertExclusiveBucketFlags(config) {
274
+ if (config.allowClientBucket && config.buckets?.length) {
275
+ throw new Error(
276
+ "dimahS3: set either allowClientBucket or buckets, not both. allowClientBucket honors any client-sent bucket; buckets is an allowlist."
277
+ );
278
+ }
279
+ }
196
280
 
197
281
  // src/api/assert-feature-enabled.ts
198
282
  function assertFeatureEnabled(config, feature) {
@@ -256,9 +340,7 @@ async function handleConfirm(config, input, request) {
256
340
  key,
257
341
  bucket
258
342
  });
259
- const head = await config.client.send(
260
- new HeadObjectCommand({ Bucket: bucket, Key: key })
261
- );
343
+ const head = await headObjectOrNotFound(config.client, bucket, key);
262
344
  const acl = config.resolveObjectAcl ? await resolveObjectAcl(config.client, bucket, key) : void 0;
263
345
  const fileName = parseFileName(head.ContentDisposition);
264
346
  const context = {
@@ -266,7 +348,7 @@ async function handleConfirm(config, input, request) {
266
348
  key,
267
349
  bucket,
268
350
  contentType: head.ContentType,
269
- contentLength: head.ContentLength ?? 0,
351
+ contentLength: requireContentLength(head),
270
352
  eTag: head.ETag?.replace(/"/g, ""),
271
353
  metadata: head.Metadata,
272
354
  acl,
@@ -307,16 +389,7 @@ async function handleDelete(config, input, request) {
307
389
  key,
308
390
  bucket
309
391
  });
310
- try {
311
- await config.client.send(
312
- new HeadObjectCommand({ Bucket: bucket, Key: key })
313
- );
314
- } catch (err) {
315
- if (isAwsNotFound(err)) {
316
- throw errors.objectNotFound();
317
- }
318
- throw err;
319
- }
392
+ await headObjectOrNotFound(config.client, bucket, key);
320
393
  await config.client.send(
321
394
  new DeleteObjectCommand({ Bucket: bucket, Key: key })
322
395
  );
@@ -338,7 +411,7 @@ async function handleDownload(config, input, request) {
338
411
  bucket: input.bucket,
339
412
  fileName: input.fileName
340
413
  });
341
- const expiresIn = normalizeExpiresIn(input.expiresIn);
414
+ const expiresIn = normalizeExpiresIn(input.expiresIn, config.maxExpiresIn);
342
415
  const fileName = input.fileName;
343
416
  await runHook(config.download?.guard, {
344
417
  request,
@@ -346,16 +419,7 @@ async function handleDownload(config, input, request) {
346
419
  bucket,
347
420
  fileName
348
421
  });
349
- try {
350
- await config.client.send(
351
- new HeadObjectCommand({ Bucket: bucket, Key: key })
352
- );
353
- } catch (err) {
354
- if (isAwsNotFound(err)) {
355
- throw errors.objectNotFound();
356
- }
357
- throw err;
358
- }
422
+ await headObjectOrNotFound(config.client, bucket, key);
359
423
  const url = await getSignedUrl(
360
424
  config.client,
361
425
  new GetObjectCommand({
@@ -396,12 +460,14 @@ async function handleAbort(config, input, request) {
396
460
  bucket,
397
461
  uploadId
398
462
  });
399
- await config.client.send(
400
- new AbortMultipartUploadCommand({
401
- Bucket: bucket,
402
- Key: key,
403
- UploadId: uploadId
404
- })
463
+ await sendOrObjectNotFound(
464
+ () => config.client.send(
465
+ new AbortMultipartUploadCommand({
466
+ Bucket: bucket,
467
+ Key: key,
468
+ UploadId: uploadId
469
+ })
470
+ )
405
471
  );
406
472
  await runLifecycleHook(config.multipart?.onAbort, {
407
473
  request,
@@ -435,32 +501,34 @@ async function handleComplete(config, input, request) {
435
501
  uploadId,
436
502
  parts: partRefs
437
503
  });
438
- const listed = await config.client.send(
439
- new ListPartsCommand({ Bucket: bucket, Key: key, UploadId: uploadId })
440
- );
441
- const listedParts = listed.Parts ?? [];
504
+ const listedParts = await listAllParts(config.client, {
505
+ bucket,
506
+ key,
507
+ uploadId
508
+ });
442
509
  const completeParts = parts.map((partNumber) => {
443
510
  const found = listedParts.find((p) => p.PartNumber === partNumber);
444
- return { PartNumber: partNumber, ETag: found?.ETag ?? "" };
511
+ if (!found?.ETag) {
512
+ throw errors.multipartPartMissing(partNumber);
513
+ }
514
+ return { PartNumber: partNumber, ETag: found.ETag };
445
515
  });
446
- const completeResult = await config.client.send(
447
- new CompleteMultipartUploadCommand({
448
- Bucket: bucket,
449
- Key: key,
450
- UploadId: uploadId,
451
- MultipartUpload: { Parts: completeParts }
452
- })
516
+ const completeResult = await sendOrObjectNotFound(
517
+ () => config.client.send(
518
+ new CompleteMultipartUploadCommand({
519
+ Bucket: bucket,
520
+ Key: key,
521
+ UploadId: uploadId,
522
+ MultipartUpload: { Parts: completeParts }
523
+ })
524
+ )
453
525
  );
454
- let head = await config.client.send(
455
- new HeadObjectCommand({ Bucket: bucket, Key: key })
526
+ const head = await headObjectAfterMultipartComplete(
527
+ config.client,
528
+ bucket,
529
+ key
456
530
  );
457
- for (let attempt = 0; attempt < 4 && !head.ContentLength; attempt++) {
458
- await new Promise((r) => setTimeout(r, 250 * 2 ** attempt));
459
- head = await config.client.send(
460
- new HeadObjectCommand({ Bucket: bucket, Key: key })
461
- );
462
- }
463
- const contentLength = head.ContentLength ?? 0;
531
+ const contentLength = requireContentLength(head);
464
532
  const contentType = head.ContentType;
465
533
  const eTag = (head.ETag ?? completeResult.ETag ?? "").replace(/"/g, "");
466
534
  const metadata = head.Metadata ?? {};
@@ -512,7 +580,7 @@ async function handleMultipartInit(config, input, request) {
512
580
  fileName: input.fileName,
513
581
  contentType: input.contentType
514
582
  });
515
- const acl = input.acl === "public-read" ? "public-read" : "private";
583
+ const acl = resolveRequestAcl(config.multipart, input.acl);
516
584
  const fileSize = typeof input.fileSize === "number" && input.fileSize > 0 ? Math.floor(input.fileSize) : void 0;
517
585
  if (config.multipart?.requireFileSize && fileSize === void 0) {
518
586
  throw errors.fileSizeRequiredMultipart();
@@ -521,7 +589,11 @@ async function handleMultipartInit(config, input, request) {
521
589
  request,
522
590
  key,
523
591
  bucket,
524
- fileSize
592
+ fileSize,
593
+ contentType: input.contentType,
594
+ metadata: input.metadata,
595
+ acl,
596
+ fileName: input.fileName
525
597
  });
526
598
  const { UploadId } = await config.client.send(
527
599
  new CreateMultipartUploadCommand({
@@ -533,6 +605,9 @@ async function handleMultipartInit(config, input, request) {
533
605
  ACL: acl
534
606
  })
535
607
  );
608
+ if (!UploadId) {
609
+ throw errors.internalError();
610
+ }
536
611
  await runLifecycleHook(config.multipart?.onInit, {
537
612
  request,
538
613
  key,
@@ -572,14 +647,8 @@ async function handleListParts(config, input, request) {
572
647
  bucket,
573
648
  uploadId
574
649
  });
575
- const response = await config.client.send(
576
- new ListPartsCommand({
577
- Bucket: bucket,
578
- Key: key,
579
- UploadId: uploadId
580
- })
581
- );
582
- const parts = (response.Parts ?? []).map((p) => ({
650
+ const listed = await listAllParts(config.client, { bucket, key, uploadId });
651
+ const parts = listed.map((p) => ({
583
652
  partNumber: p.PartNumber ?? 0,
584
653
  size: p.Size ?? 0,
585
654
  eTag: (p.ETag ?? "").replace(/"/g, "")
@@ -609,7 +678,7 @@ async function handleSignPart(config, input, request) {
609
678
  });
610
679
  const uploadId = input.uploadId;
611
680
  const partNumber = input.partNumber;
612
- const expiresIn = normalizeExpiresIn(input.expiresIn);
681
+ const expiresIn = normalizeExpiresIn(input.expiresIn, config.maxExpiresIn);
613
682
  const partSize = typeof input.partSize === "number" && input.partSize > 0 ? Math.floor(input.partSize) : null;
614
683
  await runHook(config.multipart?.partGuard, {
615
684
  request,
@@ -650,6 +719,14 @@ var multipartPart = createS3Endpoint(
650
719
  return handleSignPart(ctx.context.config, ctx.body, ctx.context.request);
651
720
  }
652
721
  );
722
+ function putMetadataHeaders(metadata) {
723
+ if (!metadata) return {};
724
+ const headers = {};
725
+ for (const [k, v] of Object.entries(metadata)) {
726
+ headers[`x-amz-meta-${k}`] = v;
727
+ }
728
+ return headers;
729
+ }
653
730
  async function handleUpload(config, input, request) {
654
731
  const { key, bucket } = await resolveRequestTarget(config, config.upload, {
655
732
  request,
@@ -658,10 +735,13 @@ async function handleUpload(config, input, request) {
658
735
  fileName: input.fileName,
659
736
  contentType: input.contentType
660
737
  });
661
- const expiresIn = normalizeExpiresIn(input.expiresIn);
662
- const acl = input.acl === "public-read" ? "public-read" : "private";
738
+ const expiresIn = normalizeExpiresIn(input.expiresIn, config.maxExpiresIn);
739
+ const acl = resolveRequestAcl(config.upload, input.acl);
663
740
  const contentType = input.contentType ?? "application/octet-stream";
664
741
  const fileSize = typeof input.fileSize === "number" && input.fileSize > 0 ? Math.floor(input.fileSize) : null;
742
+ if (config.upload?.requireFileSize && fileSize === null) {
743
+ throw errors.fileSizeRequiredUpload();
744
+ }
665
745
  await runHook(config.upload?.guard, {
666
746
  request,
667
747
  key,
@@ -673,12 +753,10 @@ async function handleUpload(config, input, request) {
673
753
  fileName: input.fileName
674
754
  });
675
755
  const method = config.upload?.method ?? "POST";
676
- if (method === "PUT" && config.upload?.requireFileSize && fileSize === null) {
677
- throw errors.fileSizeRequiredUpload();
678
- }
679
756
  if (method === "PUT") {
680
757
  const putHeaders = {
681
- "Content-Type": contentType
758
+ "Content-Type": contentType,
759
+ ...putMetadataHeaders(input.metadata)
682
760
  };
683
761
  if (input.fileName) {
684
762
  putHeaders["Content-Disposition"] = buildContentDisposition(
@@ -692,6 +770,7 @@ async function handleUpload(config, input, request) {
692
770
  Key: key,
693
771
  ContentType: contentType,
694
772
  ACL: acl,
773
+ Metadata: input.metadata,
695
774
  ...input.fileName ? { ContentDisposition: buildContentDisposition(input.fileName) } : {},
696
775
  ...fileSize !== null ? { ContentLength: fileSize } : {}
697
776
  }),
@@ -737,7 +816,7 @@ async function handleUpload(config, input, request) {
737
816
  {
738
817
  Bucket: bucket,
739
818
  Key: key,
740
- Conditions: rangeMax !== void 0 ? [["content-length-range", rangeMin, rangeMax]] : [["content-length-range", rangeMin, Number.MAX_SAFE_INTEGER]],
819
+ Conditions: rangeMax !== void 0 ? [["content-length-range", rangeMin, rangeMax]] : [["content-length-range", rangeMin, S3_MAX_POST_OBJECT_BYTES]],
741
820
  Fields: fields,
742
821
  Expires: expiresIn
743
822
  }
@@ -939,6 +1018,7 @@ function applyPlugins(config) {
939
1018
  }
940
1019
  }
941
1020
  const { plugins: _omit, ...rest } = config;
1021
+ assertExclusiveBucketFlags(rest);
942
1022
  for (const plugin of plugins) {
943
1023
  plugin.init?.({ config: rest, getPlugin });
944
1024
  }
@@ -991,10 +1071,7 @@ function bindEndpoints(endpoints, context) {
991
1071
  ...input,
992
1072
  headers: headers ?? request.headers,
993
1073
  request,
994
- context: {
995
- ...context,
996
- ...typeof input.context === "object" && input.context !== null ? input.context : {}
997
- }
1074
+ context
998
1075
  });
999
1076
  });
1000
1077
  bound.path = endpoint.path;
@@ -1004,7 +1081,7 @@ function bindEndpoints(endpoints, context) {
1004
1081
  return api;
1005
1082
  }
1006
1083
 
1007
- // src/router.ts
1084
+ // src/api/router.ts
1008
1085
  var NETWORK_CODES = /* @__PURE__ */ new Set([
1009
1086
  "EAI_AGAIN",
1010
1087
  "ECONNREFUSED",
@@ -1080,6 +1157,6 @@ function dimahS3(config) {
1080
1157
  };
1081
1158
  }
1082
1159
 
1083
- export { createS3Endpoint, createS3Middleware, definePlugin, dimahS3, errors, resolveObjectAcl };
1160
+ export { createS3Endpoint, createS3Middleware, definePlugin, dimahS3, errors };
1084
1161
  //# sourceMappingURL=index.js.map
1085
1162
  //# sourceMappingURL=index.js.map