@headless-lms/adapter-storage-s3 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,8 +1,10 @@
1
1
  # @headless-lms/adapter-storage-s3
2
2
 
3
3
  AWS S3 implementation of the `ObjectStorage` port from `@headless-lms/core/types`.
4
- The bucket is private and must already exist; access is via short-lived presigned
5
- URLs only. Also works against S3-compatible stores via `endpoint` + `forcePathStyle`.
4
+ The bucket must already exist. Everything in it is private except the `public/`
5
+ prefix, which holds the images the API serves publicly (profile photos and the
6
+ thumbnails and covers of published items); private files are reached through
7
+ short-lived presigned URLs. Also works against S3-compatible stores via `endpoint` + `forcePathStyle`.
6
8
 
7
9
  The adapter reads no environment itself — the installation's `config.ts` parses
8
10
  env into an `S3StorageConfig` and injects the constructed adapter:
@@ -24,11 +26,34 @@ The adapter never creates buckets — bucket creation needs `s3:CreateBucket`
24
26
  permissions and region constraints the app credentials should not have. Create
25
27
  the bucket once (console, CLI, or IaC); everything the app does is object-level.
26
28
 
27
- 1. Create a bucket and keep Block Public Access on (the default). No bucket
28
- policy is needed — access is via presigned URLs only.
29
+ 1. Create a bucket. Let anyone read `public/*` and nothing else, either with a
30
+ bucket policy (turn off "Block public access to buckets and objects granted
31
+ through new public bucket policies" for this bucket only):
32
+
33
+ ```json
34
+ {
35
+ "Version": "2012-10-17",
36
+ "Statement": [
37
+ {
38
+ "Effect": "Allow",
39
+ "Principal": "*",
40
+ "Action": "s3:GetObject",
41
+ "Resource": "arn:aws:s3:::your-bucket/public/*"
42
+ }
43
+ ]
44
+ }
45
+ ```
46
+
47
+ or by keeping the bucket private and serving `public/*` through a CDN such as
48
+ CloudFront with origin access control. Point `publicBaseUrl` at whichever
49
+ origin serves it. Public copies are written with
50
+ `Cache-Control: public, max-age=31536000, immutable`; a new image is a new key.
51
+
29
52
  2. Grant the app credentials `s3:PutObject`, `s3:GetObject`, `s3:HeadObject`,
30
- and `s3:DeleteObject` on `arn:aws:s3:::your-bucket/*`. Either an IAM user
31
- with access keys, or an IAM role when running on AWS (then omit the keys).
53
+ and `s3:DeleteObject` on `arn:aws:s3:::your-bucket/*`, and `s3:ListBucket` on
54
+ `arn:aws:s3:::your-bucket` (the hourly sweep lists stray member uploads).
55
+ Either an IAM user with access keys, or an IAM role when running on AWS
56
+ (then omit the keys).
32
57
  3. Add a CORS rule — browsers upload directly to the presigned URL:
33
58
 
34
59
  ```json
@@ -55,13 +80,14 @@ the bucket name cannot be a subdomain — they need path style. Set
55
80
 
56
81
  ## Environment variables (reference installation)
57
82
 
58
- | Variable | Required | Maps to | Notes |
59
- | -------------------------- | -------- | ----------------------- | ------------------------------------- |
60
- | `STORAGE_REGION` | yes | `region` | |
61
- | `STORAGE_BUCKET` | yes | `bucket` | Must exist; kept private. |
62
- | `STORAGE_ACCESS_KEY` | no | `accessKeyId` | Omit for the default credential chain.|
63
- | `STORAGE_SECRET_KEY` | no | `secretAccessKey` | Omit for the default credential chain.|
64
- | `STORAGE_ENDPOINT` | no | `endpoint` | Full URL, for S3-compatible stores. |
65
- | `STORAGE_FORCE_PATH_STYLE` | no | `forcePathStyle` | `"true"` / `"false"`. |
66
- | `STORAGE_UPLOAD_EXPIRY` | no | `uploadExpirySeconds` | Default 300. |
67
- | `STORAGE_DOWNLOAD_EXPIRY` | no | `downloadExpirySeconds` | Default 86400. |
83
+ | Variable | Required | Maps to | Notes |
84
+ | -------------------------- | -------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------ |
85
+ | `STORAGE_REGION` | yes | `region` | |
86
+ | `STORAGE_BUCKET` | yes | `bucket` | Must exist; kept private. |
87
+ | `STORAGE_ACCESS_KEY` | no | `accessKeyId` | Omit for the default credential chain. |
88
+ | `STORAGE_SECRET_KEY` | no | `secretAccessKey` | Omit for the default credential chain. |
89
+ | `STORAGE_ENDPOINT` | no | `endpoint` | Full URL, for S3-compatible stores. |
90
+ | `STORAGE_FORCE_PATH_STYLE` | no | `forcePathStyle` | `"true"` / `"false"`. |
91
+ | `STORAGE_UPLOAD_EXPIRY` | no | `uploadExpirySeconds` | Default 300. Stray member uploads older than this are swept. |
92
+ | `STORAGE_DOWNLOAD_EXPIRY` | no | `downloadExpirySeconds` | Download lifetime when the caller names none. Default 86400. The API names `DELIVERY_URL_EXPIRY` on every URL it serves. |
93
+ | `STORAGE_PUBLIC_URL` | yes | `publicBaseUrl` | Origin serving `public/*`, such as `https://cdn.example.com` or `https://your-bucket.s3.amazonaws.com`. |
package/dist/index.d.ts CHANGED
@@ -10,6 +10,7 @@ interface S3StorageConfig {
10
10
  endpoint?: string;
11
11
  /** Required by most S3-compatible stores when `endpoint` is set. */
12
12
  forcePathStyle?: boolean;
13
+ publicBaseUrl?: string;
13
14
  uploadExpirySeconds: number;
14
15
  downloadExpirySeconds: number;
15
16
  }
@@ -17,7 +18,8 @@ declare class S3StorageAdapter implements ObjectStorage {
17
18
  private readonly logger?;
18
19
  private readonly client;
19
20
  private readonly bucket;
20
- private readonly uploadExpiry;
21
+ private readonly publicBase;
22
+ readonly uploadExpirySeconds: number;
21
23
  private readonly downloadExpiry;
22
24
  constructor(config: S3StorageConfig, logger?: Logger | undefined);
23
25
  presignUpload(input: {
@@ -27,6 +29,16 @@ declare class S3StorageAdapter implements ObjectStorage {
27
29
  }): Promise<PresignedUpload>;
28
30
  presignDownload(input: PresignDownloadInput): Promise<string>;
29
31
  stat(key: string): Promise<StoredObjectInfo | null>;
32
+ list(prefix: string): Promise<StoredObjectInfo[]>;
33
+ copy(input: {
34
+ from: string;
35
+ to: string;
36
+ }): Promise<void>;
37
+ publish(input: {
38
+ from: string;
39
+ to: string;
40
+ }): Promise<void>;
41
+ publicUrl(key: string): string;
30
42
  remove(key: string): Promise<void>;
31
43
  }
32
44
  //#endregion
package/dist/index.js CHANGED
@@ -1,11 +1,13 @@
1
- import { DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
1
+ import { CopyObjectCommand, DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
2
2
  import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
3
+ import { PUBLIC_CACHE_CONTROL } from "@headless-lms/core/types";
3
4
  //#region src/index.ts
4
5
  var S3StorageAdapter = class {
5
6
  logger;
6
7
  client;
7
8
  bucket;
8
- uploadExpiry;
9
+ publicBase;
10
+ uploadExpirySeconds;
9
11
  downloadExpiry;
10
12
  constructor(config, logger) {
11
13
  this.logger = logger;
@@ -19,11 +21,12 @@ var S3StorageAdapter = class {
19
21
  } } : {}
20
22
  });
21
23
  this.bucket = config.bucket;
22
- this.uploadExpiry = config.uploadExpirySeconds;
24
+ this.uploadExpirySeconds = config.uploadExpirySeconds;
23
25
  this.downloadExpiry = config.downloadExpirySeconds;
26
+ this.publicBase = config.publicBaseUrl ? config.publicBaseUrl.replace(/\/+$/, "") : null;
24
27
  }
25
28
  async presignUpload(input) {
26
- const expiresInSeconds = input.expiresInSeconds ?? this.uploadExpiry;
29
+ const expiresInSeconds = input.expiresInSeconds ?? this.uploadExpirySeconds;
27
30
  const command = new PutObjectCommand({
28
31
  Bucket: this.bucket,
29
32
  Key: input.key,
@@ -63,6 +66,52 @@ var S3StorageAdapter = class {
63
66
  throw err;
64
67
  }
65
68
  }
69
+ async list(prefix) {
70
+ const objects = [];
71
+ let token;
72
+ do {
73
+ const page = await this.client.send(new ListObjectsV2Command({
74
+ Bucket: this.bucket,
75
+ Prefix: prefix,
76
+ ...token ? { ContinuationToken: token } : {}
77
+ }));
78
+ for (const item of page.Contents ?? []) {
79
+ if (!item.Key) continue;
80
+ objects.push({
81
+ key: item.Key,
82
+ size: item.Size ?? 0,
83
+ ...item.LastModified ? { lastModified: item.LastModified.toISOString() } : {}
84
+ });
85
+ }
86
+ token = page.IsTruncated ? page.NextContinuationToken : void 0;
87
+ } while (token);
88
+ return objects;
89
+ }
90
+ async copy(input) {
91
+ await this.client.send(new CopyObjectCommand({
92
+ Bucket: this.bucket,
93
+ Key: input.to,
94
+ CopySource: encodeURI(`${this.bucket}/${input.from}`)
95
+ }));
96
+ }
97
+ async publish(input) {
98
+ const source = await this.client.send(new HeadObjectCommand({
99
+ Bucket: this.bucket,
100
+ Key: input.from
101
+ }));
102
+ await this.client.send(new CopyObjectCommand({
103
+ Bucket: this.bucket,
104
+ Key: input.to,
105
+ CopySource: encodeURI(`${this.bucket}/${input.from}`),
106
+ MetadataDirective: "REPLACE",
107
+ CacheControl: PUBLIC_CACHE_CONTROL,
108
+ ...source.ContentType ? { ContentType: source.ContentType } : {}
109
+ }));
110
+ }
111
+ publicUrl(key) {
112
+ if (!this.publicBase) throw new Error("S3 storage has no publicBaseUrl, so it cannot serve public files");
113
+ return `${this.publicBase}/${key.split("/").map(encodeURIComponent).join("/")}`;
114
+ }
66
115
  async remove(key) {
67
116
  await this.client.send(new DeleteObjectCommand({
68
117
  Bucket: this.bucket,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["// AWS S3 implementation of the ObjectStorage port. Private bucket; access via\n// short-lived presigned URLs only. The bucket must already exist.\nimport {\n DeleteObjectCommand,\n GetObjectCommand,\n HeadObjectCommand,\n PutObjectCommand,\n S3Client,\n} from \"@aws-sdk/client-s3\";\nimport { getSignedUrl } from \"@aws-sdk/s3-request-presigner\";\nimport type {\n Logger,\n ObjectStorage,\n PresignDownloadInput,\n PresignedUpload,\n StoredObjectInfo,\n} from \"@headless-lms/core/types\";\n\nexport interface S3StorageConfig {\n region: string;\n bucket: string;\n /** Omit both keys to use the default AWS credential chain (IAM role, env, profile). */\n accessKeyId?: string;\n secretAccessKey?: string;\n /** Custom endpoint for S3-compatible stores. */\n endpoint?: string;\n /** Required by most S3-compatible stores when `endpoint` is set. */\n forcePathStyle?: boolean;\n uploadExpirySeconds: number;\n downloadExpirySeconds: number;\n}\n\nexport class S3StorageAdapter implements ObjectStorage {\n private readonly client: S3Client;\n private readonly bucket: string;\n private readonly uploadExpiry: number;\n private readonly downloadExpiry: number;\n\n constructor(\n config: S3StorageConfig,\n private readonly logger?: Logger,\n ) {\n this.client = new S3Client({\n region: config.region,\n ...(config.endpoint ? { endpoint: config.endpoint } : {}),\n ...(config.forcePathStyle !== undefined ? { forcePathStyle: config.forcePathStyle } : {}),\n ...(config.accessKeyId && config.secretAccessKey\n ? {\n credentials: {\n accessKeyId: config.accessKeyId,\n secretAccessKey: config.secretAccessKey,\n },\n }\n : {}),\n });\n this.bucket = config.bucket;\n this.uploadExpiry = config.uploadExpirySeconds;\n this.downloadExpiry = config.downloadExpirySeconds;\n }\n\n async presignUpload(input: {\n key: string;\n contentType?: string;\n expiresInSeconds?: number;\n }): Promise<PresignedUpload> {\n const expiresInSeconds = input.expiresInSeconds ?? this.uploadExpiry;\n const command = new PutObjectCommand({\n Bucket: this.bucket,\n Key: input.key,\n ...(input.contentType ? { ContentType: input.contentType } : {}),\n });\n const url = await getSignedUrl(this.client, command, { expiresIn: expiresInSeconds });\n return {\n url,\n method: \"PUT\",\n key: input.key,\n expiresInSeconds,\n headers: input.contentType ? { \"Content-Type\": input.contentType } : {},\n };\n }\n\n async presignDownload(input: PresignDownloadInput): Promise<string> {\n const expiresInSeconds = input.expiresInSeconds ?? this.downloadExpiry;\n const command = new GetObjectCommand({\n Bucket: this.bucket,\n Key: input.key,\n ...(input.downloadFilename\n ? {\n ResponseContentDisposition: `attachment; filename=\"${input.downloadFilename.replace(/\"/g, \"\")}\"`,\n }\n : {}),\n });\n return getSignedUrl(this.client, command, { expiresIn: expiresInSeconds });\n }\n\n async stat(key: string): Promise<StoredObjectInfo | null> {\n try {\n const s = await this.client.send(\n new HeadObjectCommand({ Bucket: this.bucket, Key: key }),\n );\n return {\n key,\n size: s.ContentLength ?? 0,\n ...(s.ContentType ? { contentType: s.ContentType } : {}),\n ...(s.LastModified ? { lastModified: s.LastModified.toISOString() } : {}),\n };\n } catch (err) {\n if (isNotFound(err)) {\n return null;\n }\n throw err;\n }\n }\n\n async remove(key: string): Promise<void> {\n await this.client.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: key }));\n }\n}\n\nfunction isNotFound(err: unknown): boolean {\n const e = err as { name?: string; $metadata?: { httpStatusCode?: number } };\n return e?.name === \"NotFound\" || e?.name === \"NoSuchKey\" || e?.$metadata?.httpStatusCode === 404;\n}\n"],"mappings":";;;AAgCA,IAAa,mBAAb,MAAuD;CAQlC;CAPnB;CACA;CACA;CACA;CAEA,YACE,QACA,QACA;EADiB,KAAA,SAAA;EAEjB,KAAK,SAAS,IAAI,SAAS;GACzB,QAAQ,OAAO;GACf,GAAI,OAAO,WAAW,EAAE,UAAU,OAAO,SAAS,IAAI,CAAC;GACvD,GAAI,OAAO,mBAAmB,KAAA,IAAY,EAAE,gBAAgB,OAAO,eAAe,IAAI,CAAC;GACvF,GAAI,OAAO,eAAe,OAAO,kBAC7B,EACE,aAAa;IACX,aAAa,OAAO;IACpB,iBAAiB,OAAO;GAC1B,EACF,IACA,CAAC;EACP,CAAC;EACD,KAAK,SAAS,OAAO;EACrB,KAAK,eAAe,OAAO;EAC3B,KAAK,iBAAiB,OAAO;CAC/B;CAEA,MAAM,cAAc,OAIS;EAC3B,MAAM,mBAAmB,MAAM,oBAAoB,KAAK;EACxD,MAAM,UAAU,IAAI,iBAAiB;GACnC,QAAQ,KAAK;GACb,KAAK,MAAM;GACX,GAAI,MAAM,cAAc,EAAE,aAAa,MAAM,YAAY,IAAI,CAAC;EAChE,CAAC;EAED,OAAO;GACL,KAAA,MAFgB,aAAa,KAAK,QAAQ,SAAS,EAAE,WAAW,iBAAiB,CAAC;GAGlF,QAAQ;GACR,KAAK,MAAM;GACX;GACA,SAAS,MAAM,cAAc,EAAE,gBAAgB,MAAM,YAAY,IAAI,CAAC;EACxE;CACF;CAEA,MAAM,gBAAgB,OAA8C;EAClE,MAAM,mBAAmB,MAAM,oBAAoB,KAAK;EACxD,MAAM,UAAU,IAAI,iBAAiB;GACnC,QAAQ,KAAK;GACb,KAAK,MAAM;GACX,GAAI,MAAM,mBACN,EACE,4BAA4B,yBAAyB,MAAM,iBAAiB,QAAQ,MAAM,EAAE,EAAE,GAChG,IACA,CAAC;EACP,CAAC;EACD,OAAO,aAAa,KAAK,QAAQ,SAAS,EAAE,WAAW,iBAAiB,CAAC;CAC3E;CAEA,MAAM,KAAK,KAA+C;EACxD,IAAI;GACF,MAAM,IAAI,MAAM,KAAK,OAAO,KAC1B,IAAI,kBAAkB;IAAE,QAAQ,KAAK;IAAQ,KAAK;GAAI,CAAC,CACzD;GACA,OAAO;IACL;IACA,MAAM,EAAE,iBAAiB;IACzB,GAAI,EAAE,cAAc,EAAE,aAAa,EAAE,YAAY,IAAI,CAAC;IACtD,GAAI,EAAE,eAAe,EAAE,cAAc,EAAE,aAAa,YAAY,EAAE,IAAI,CAAC;GACzE;EACF,SAAS,KAAK;GACZ,IAAI,WAAW,GAAG,GAChB,OAAO;GAET,MAAM;EACR;CACF;CAEA,MAAM,OAAO,KAA4B;EACvC,MAAM,KAAK,OAAO,KAAK,IAAI,oBAAoB;GAAE,QAAQ,KAAK;GAAQ,KAAK;EAAI,CAAC,CAAC;CACnF;AACF;AAEA,SAAS,WAAW,KAAuB;CACzC,MAAM,IAAI;CACV,OAAO,GAAG,SAAS,cAAc,GAAG,SAAS,eAAe,GAAG,WAAW,mBAAmB;AAC/F"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["// AWS S3 implementation of the ObjectStorage port. Private bucket except the\n// public/ prefix; private files are reached by short-lived presigned URLs. The\n// bucket must already exist.\nimport {\n CopyObjectCommand,\n DeleteObjectCommand,\n GetObjectCommand,\n HeadObjectCommand,\n ListObjectsV2Command,\n PutObjectCommand,\n S3Client,\n} from \"@aws-sdk/client-s3\";\nimport { getSignedUrl } from \"@aws-sdk/s3-request-presigner\";\nimport {\n PUBLIC_CACHE_CONTROL,\n type Logger,\n type ObjectStorage,\n type PresignDownloadInput,\n type PresignedUpload,\n type StoredObjectInfo,\n} from \"@headless-lms/core/types\";\n\nexport interface S3StorageConfig {\n region: string;\n bucket: string;\n /** Omit both keys to use the default AWS credential chain (IAM role, env, profile). */\n accessKeyId?: string;\n secretAccessKey?: string;\n /** Custom endpoint for S3-compatible stores. */\n endpoint?: string;\n /** Required by most S3-compatible stores when `endpoint` is set. */\n forcePathStyle?: boolean;\n publicBaseUrl?: string;\n uploadExpirySeconds: number;\n downloadExpirySeconds: number;\n}\n\nexport class S3StorageAdapter implements ObjectStorage {\n private readonly client: S3Client;\n private readonly bucket: string;\n private readonly publicBase: string | null;\n readonly uploadExpirySeconds: number;\n private readonly downloadExpiry: number;\n\n constructor(\n config: S3StorageConfig,\n private readonly logger?: Logger,\n ) {\n this.client = new S3Client({\n region: config.region,\n ...(config.endpoint ? { endpoint: config.endpoint } : {}),\n ...(config.forcePathStyle !== undefined ? { forcePathStyle: config.forcePathStyle } : {}),\n ...(config.accessKeyId && config.secretAccessKey\n ? {\n credentials: {\n accessKeyId: config.accessKeyId,\n secretAccessKey: config.secretAccessKey,\n },\n }\n : {}),\n });\n this.bucket = config.bucket;\n this.uploadExpirySeconds = config.uploadExpirySeconds;\n this.downloadExpiry = config.downloadExpirySeconds;\n this.publicBase = config.publicBaseUrl ? config.publicBaseUrl.replace(/\\/+$/, \"\") : null;\n }\n\n async presignUpload(input: {\n key: string;\n contentType?: string;\n expiresInSeconds?: number;\n }): Promise<PresignedUpload> {\n const expiresInSeconds = input.expiresInSeconds ?? this.uploadExpirySeconds;\n const command = new PutObjectCommand({\n Bucket: this.bucket,\n Key: input.key,\n ...(input.contentType ? { ContentType: input.contentType } : {}),\n });\n const url = await getSignedUrl(this.client, command, { expiresIn: expiresInSeconds });\n return {\n url,\n method: \"PUT\",\n key: input.key,\n expiresInSeconds,\n headers: input.contentType ? { \"Content-Type\": input.contentType } : {},\n };\n }\n\n async presignDownload(input: PresignDownloadInput): Promise<string> {\n const expiresInSeconds = input.expiresInSeconds ?? this.downloadExpiry;\n const command = new GetObjectCommand({\n Bucket: this.bucket,\n Key: input.key,\n ...(input.downloadFilename\n ? {\n ResponseContentDisposition: `attachment; filename=\"${input.downloadFilename.replace(/\"/g, \"\")}\"`,\n }\n : {}),\n });\n return getSignedUrl(this.client, command, { expiresIn: expiresInSeconds });\n }\n\n async stat(key: string): Promise<StoredObjectInfo | null> {\n try {\n const s = await this.client.send(new HeadObjectCommand({ Bucket: this.bucket, Key: key }));\n return {\n key,\n size: s.ContentLength ?? 0,\n ...(s.ContentType ? { contentType: s.ContentType } : {}),\n ...(s.LastModified ? { lastModified: s.LastModified.toISOString() } : {}),\n };\n } catch (err) {\n if (isNotFound(err)) {\n return null;\n }\n throw err;\n }\n }\n\n async list(prefix: string): Promise<StoredObjectInfo[]> {\n const objects: StoredObjectInfo[] = [];\n let token: string | undefined;\n do {\n const page = await this.client.send(\n new ListObjectsV2Command({\n Bucket: this.bucket,\n Prefix: prefix,\n ...(token ? { ContinuationToken: token } : {}),\n }),\n );\n for (const item of page.Contents ?? []) {\n if (!item.Key) {\n continue;\n }\n objects.push({\n key: item.Key,\n size: item.Size ?? 0,\n ...(item.LastModified ? { lastModified: item.LastModified.toISOString() } : {}),\n });\n }\n token = page.IsTruncated ? page.NextContinuationToken : undefined;\n } while (token);\n return objects;\n }\n\n async copy(input: { from: string; to: string }): Promise<void> {\n await this.client.send(\n new CopyObjectCommand({\n Bucket: this.bucket,\n Key: input.to,\n CopySource: encodeURI(`${this.bucket}/${input.from}`),\n }),\n );\n }\n\n async publish(input: { from: string; to: string }): Promise<void> {\n const source = await this.client.send(\n new HeadObjectCommand({ Bucket: this.bucket, Key: input.from }),\n );\n await this.client.send(\n new CopyObjectCommand({\n Bucket: this.bucket,\n Key: input.to,\n CopySource: encodeURI(`${this.bucket}/${input.from}`),\n MetadataDirective: \"REPLACE\",\n CacheControl: PUBLIC_CACHE_CONTROL,\n ...(source.ContentType ? { ContentType: source.ContentType } : {}),\n }),\n );\n }\n\n publicUrl(key: string): string {\n if (!this.publicBase) {\n throw new Error(\"S3 storage has no publicBaseUrl, so it cannot serve public files\");\n }\n return `${this.publicBase}/${key.split(\"/\").map(encodeURIComponent).join(\"/\")}`;\n }\n\n async remove(key: string): Promise<void> {\n await this.client.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: key }));\n }\n}\n\nfunction isNotFound(err: unknown): boolean {\n const e = err as { name?: string; $metadata?: { httpStatusCode?: number } };\n return e?.name === \"NotFound\" || e?.name === \"NoSuchKey\" || e?.$metadata?.httpStatusCode === 404;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@headless-lms/adapter-storage-s3",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -13,7 +13,7 @@
13
13
  "dependencies": {
14
14
  "@aws-sdk/client-s3": "^3.712.0",
15
15
  "@aws-sdk/s3-request-presigner": "^3.712.0",
16
- "@headless-lms/core": "0.1.0"
16
+ "@headless-lms/core": "0.5.0"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@types/node": "^22.10.2",