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.
- package/dist/assets/factory.js +4 -0
- package/dist/assets/store-s3.d.ts +20 -0
- package/dist/assets/store-s3.js +23 -1
- package/dist/cli/cli.js +512 -274
- package/dist/cli/generate-ai-content.js +143 -57
- package/dist/cli/init.js +140 -57
- package/dist/cli/template-files/cms-stack.ts.template +7 -4
- package/dist/config/schemas/config.d.ts +11 -6
- package/dist/config/schemas/media.d.ts +23 -6
- package/dist/config/schemas/media.js +38 -9
- package/dist/config/schemas/url.d.ts +38 -0
- package/dist/config/schemas/url.js +49 -0
- package/dist/config/types.d.ts +2 -0
- package/dist/editor/context/AssetContext.d.ts +12 -7
- package/dist/editor/context/AssetContext.js +12 -7
- package/dist/utils/sanitize-href.d.ts +41 -0
- package/dist/utils/sanitize-href.js +116 -0
- package/package.json +26 -14
package/dist/assets/factory.js
CHANGED
|
@@ -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>;
|
package/dist/assets/store-s3.js
CHANGED
|
@@ -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 {
|
|
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);
|