canopycms 0.0.66-int.85 → 0.0.66-int.86

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.
@@ -18,10 +18,14 @@ export function createAssetStore(media, opts = {}) {
18
18
  }
19
19
  switch (media.adapter) {
20
20
  case 's3':
21
+ // Every field here is optional on S3AssetStoreOptions, so dropping one is legal
22
+ // TypeScript and silently reverts a configured behavior to its default. factory.test.ts
23
+ // asserts each reaches the store for that reason.
21
24
  return new S3AssetStore({
22
25
  bucket: media.bucket,
23
26
  region: media.region,
24
27
  maxUploadBytes: media.maxUploadBytes,
28
+ uploadUrl: media.uploadUrl,
25
29
  });
26
30
  case 'local': {
27
31
  const root = media.directory ?? opts.devAssetsDir;
@@ -11,6 +11,17 @@ export interface S3AssetStoreOptions {
11
11
  region: string;
12
12
  /** Defaults to 50 MiB. */
13
13
  maxUploadBytes?: number;
14
+ /**
15
+ * POST target for presigned direct uploads, replacing the S3 REST endpoint the AWS SDK
16
+ * returns. Absolute http(s) URL or a site-relative path. See `mediaSchema`'s s3 branch for
17
+ * what this is for; see `isHttpUrlOrSameOriginPath` for what is accepted.
18
+ *
19
+ * This is NOT the `publicBaseUrl` that used to live on this interface and was deleted for
20
+ * looking like the store emitted prefixed URLs. Nothing is joined onto this value and it is
21
+ * never rendered or stored — it is the endpoint the browser posts to, and it is returned to
22
+ * the client inside the presign response rather than in the client config.
23
+ */
24
+ uploadUrl?: string;
14
25
  /** Override the default bucket-prefix layout (rarely needed). */
15
26
  prefixes?: AssetPrefixes;
16
27
  }
@@ -21,6 +32,7 @@ export declare class S3AssetStore implements AssetStore {
21
32
  private readonly client;
22
33
  private readonly bucket;
23
34
  private readonly maxUploadBytes;
35
+ private readonly uploadUrl;
24
36
  private readonly keys;
25
37
  private readonly stagingPrefix;
26
38
  constructor(options: S3AssetStoreOptions);
@@ -31,6 +43,14 @@ export declare class S3AssetStore implements AssetStore {
31
43
  * artifacts under builds/. The API layer validates too; defense-in-depth.
32
44
  */
33
45
  private assertStagingKey;
46
+ /**
47
+ * `uploadUrl` replaces only the returned `url`. `createPresignedPost` must still run — we
48
+ * need its `fields`, which carry the policy and signature — and it must still be called
49
+ * against the real bucket endpoint: a presigned POST's string-to-sign is the base64 policy
50
+ * alone, so the host is not signed and does not belong in the presign's input. Routing
51
+ * `uploadUrl` into the S3 client's `endpoint` instead would also mangle it, prepending the
52
+ * bucket as a subdomain (or, with forcePathStyle, exposing it in the path).
53
+ */
34
54
  beginUpload(input: BeginUploadInput): Promise<StagedUploadTarget>;
35
55
  writeStaging(key: string, data: Uint8Array, contentType?: string): Promise<void>;
36
56
  readStaging(key: string): Promise<Uint8Array | null>;
@@ -7,6 +7,7 @@
7
7
  import { randomUUID } from 'node:crypto';
8
8
  import { DeleteObjectCommand, GetObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client, } from '@aws-sdk/client-s3';
9
9
  import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
10
+ import { isHttpUrlOrSameOriginPath } from '../utils/sanitize-href.js';
10
11
  import { ASSET_PREFIXES, createKeyBuilders } from './keys.js';
11
12
  const DEFAULT_MAX_UPLOAD_BYTES = 50 * 1024 * 1024;
12
13
  const PRESIGN_EXPIRY_SECONDS = 15 * 60;
@@ -23,6 +24,13 @@ export class S3AssetStore {
23
24
  this.capabilities = { directUpload: true };
24
25
  this.bucket = options.bucket;
25
26
  this.maxUploadBytes = options.maxUploadBytes ?? DEFAULT_MAX_UPLOAD_BYTES;
27
+ // Re-validated here even though mediaSchema already checks it: this class is exported and
28
+ // constructible directly, so config validation is not on every path that reaches it. Same
29
+ // defense-in-depth reasoning as assertStagingKey below.
30
+ if (options.uploadUrl !== undefined && !isHttpUrlOrSameOriginPath(options.uploadUrl)) {
31
+ throw new Error(`Invalid uploadUrl: ${options.uploadUrl}`);
32
+ }
33
+ this.uploadUrl = options.uploadUrl;
26
34
  this.client = new S3Client({ region: options.region });
27
35
  const prefixes = options.prefixes ?? ASSET_PREFIXES;
28
36
  this.keys = createKeyBuilders(prefixes);
@@ -39,6 +47,14 @@ export class S3AssetStore {
39
47
  throw new Error(`Not a staging key: ${key}`);
40
48
  }
41
49
  }
50
+ /**
51
+ * `uploadUrl` replaces only the returned `url`. `createPresignedPost` must still run — we
52
+ * need its `fields`, which carry the policy and signature — and it must still be called
53
+ * against the real bucket endpoint: a presigned POST's string-to-sign is the base64 policy
54
+ * alone, so the host is not signed and does not belong in the presign's input. Routing
55
+ * `uploadUrl` into the S3 client's `endpoint` instead would also mangle it, prepending the
56
+ * bucket as a subdomain (or, with forcePathStyle, exposing it in the path).
57
+ */
42
58
  async beginUpload(input) {
43
59
  const key = this.keys.stagingKey(randomUUID());
44
60
  const { url, fields } = await createPresignedPost(this.client, {
@@ -53,7 +69,13 @@ export class S3AssetStore {
53
69
  },
54
70
  Expires: PRESIGN_EXPIRY_SECONDS,
55
71
  });
56
- return { mode: 'direct', url, fields, stagingKey: key, maxBytes: this.maxUploadBytes };
72
+ return {
73
+ mode: 'direct',
74
+ url: this.uploadUrl ?? url,
75
+ fields,
76
+ stagingKey: key,
77
+ maxBytes: this.maxUploadBytes,
78
+ };
57
79
  }
58
80
  async writeStaging(key, data, contentType) {
59
81
  this.assertStagingKey(key);