canopycms 0.0.57 → 0.0.59
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/ai/json-to-markdown.js +74 -3
- package/dist/ai/json-to-markdown.js.map +1 -1
- package/dist/api/__test__/mock-client.d.ts +9 -5
- package/dist/api/__test__/mock-client.d.ts.map +1 -1
- package/dist/api/__test__/mock-client.js +15 -7
- package/dist/api/__test__/mock-client.js.map +1 -1
- package/dist/api/assets.d.ts +100 -36
- package/dist/api/assets.d.ts.map +1 -1
- package/dist/api/assets.js +338 -51
- package/dist/api/assets.js.map +1 -1
- package/dist/api/client.d.ts +16 -5
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +24 -6
- package/dist/api/client.js.map +1 -1
- package/dist/api/entries.d.ts +4 -4
- package/dist/api/index.d.ts +1 -1
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js.map +1 -1
- package/dist/api/route-builder.d.ts +9 -0
- package/dist/api/route-builder.d.ts.map +1 -1
- package/dist/api/route-builder.js +1 -0
- package/dist/api/route-builder.js.map +1 -1
- package/dist/api/schema.d.ts +4 -4
- package/dist/api/types.d.ts +11 -1
- package/dist/api/types.d.ts.map +1 -1
- package/dist/assets/asset-prefixes.d.ts +31 -0
- package/dist/assets/asset-prefixes.d.ts.map +1 -0
- package/dist/assets/asset-prefixes.js +20 -0
- package/dist/assets/asset-prefixes.js.map +1 -0
- package/dist/assets/asset-src.d.ts +23 -0
- package/dist/assets/asset-src.d.ts.map +1 -0
- package/dist/assets/asset-src.js +28 -0
- package/dist/assets/asset-src.js.map +1 -0
- package/dist/assets/asset-url.d.ts +35 -0
- package/dist/assets/asset-url.d.ts.map +1 -0
- package/dist/assets/asset-url.js +77 -0
- package/dist/assets/asset-url.js.map +1 -0
- package/dist/assets/factory.d.ts +18 -0
- package/dist/assets/factory.d.ts.map +1 -0
- package/dist/assets/factory.js +42 -0
- package/dist/assets/factory.js.map +1 -0
- package/dist/assets/finalize.d.ts +36 -0
- package/dist/assets/finalize.d.ts.map +1 -0
- package/dist/assets/finalize.js +72 -0
- package/dist/assets/finalize.js.map +1 -0
- package/dist/assets/index.d.ts +20 -0
- package/dist/assets/index.d.ts.map +1 -0
- package/dist/assets/index.js +19 -0
- package/dist/assets/index.js.map +1 -0
- package/dist/assets/keys.d.ts +56 -0
- package/dist/assets/keys.d.ts.map +1 -0
- package/dist/assets/keys.js +88 -0
- package/dist/assets/keys.js.map +1 -0
- package/dist/assets/pipeline.d.ts +50 -0
- package/dist/assets/pipeline.d.ts.map +1 -0
- package/dist/assets/pipeline.js +202 -0
- package/dist/assets/pipeline.js.map +1 -0
- package/dist/assets/store-local.d.ts +84 -0
- package/dist/assets/store-local.d.ts.map +1 -0
- package/dist/assets/store-local.js +254 -0
- package/dist/assets/store-local.js.map +1 -0
- package/dist/assets/store-s3.d.ts +71 -0
- package/dist/assets/store-s3.d.ts.map +1 -0
- package/dist/assets/store-s3.js +183 -0
- package/dist/assets/store-s3.js.map +1 -0
- package/dist/assets/svg-sanitizer.d.ts +37 -0
- package/dist/assets/svg-sanitizer.d.ts.map +1 -0
- package/dist/assets/svg-sanitizer.js +222 -0
- package/dist/assets/svg-sanitizer.js.map +1 -0
- package/dist/assets/transform-directives.d.ts +97 -0
- package/dist/assets/transform-directives.d.ts.map +1 -0
- package/dist/assets/transform-directives.js +267 -0
- package/dist/assets/transform-directives.js.map +1 -0
- package/dist/assets/transform.d.ts +51 -0
- package/dist/assets/transform.d.ts.map +1 -0
- package/dist/assets/transform.js +190 -0
- package/dist/assets/transform.js.map +1 -0
- package/dist/assets/types.d.ts +82 -0
- package/dist/assets/types.d.ts.map +1 -0
- package/dist/assets/types.js +10 -0
- package/dist/assets/types.js.map +1 -0
- package/dist/authorization/groups/schema.d.ts +2 -2
- package/dist/authorization/permissions/schema.d.ts +2 -2
- package/dist/branch-workspace.d.ts.map +1 -1
- package/dist/branch-workspace.js +1 -0
- package/dist/branch-workspace.js.map +1 -1
- package/dist/cli/cli.d.ts +20 -0
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +9561 -246
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/generate-ai-content.js +175 -16
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +29 -5
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/template-files/Dockerfile.cms.template +33 -1
- package/dist/cli/template-files/deploy-cms.yml.template +9 -1
- package/dist/cli/template-files/dockerignore.template +13 -0
- package/dist/cli/template-files/middleware-clerk.ts.template +12 -5
- package/dist/cli/template-files/middleware.ts.template +10 -5
- package/dist/cli/templates.d.ts +1 -0
- package/dist/cli/templates.d.ts.map +1 -1
- package/dist/cli/templates.js +3 -0
- package/dist/cli/templates.js.map +1 -1
- package/dist/config/helpers.d.ts.map +1 -1
- package/dist/config/helpers.js +2 -1
- package/dist/config/helpers.js.map +1 -1
- package/dist/config/index.d.ts +2 -2
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +1 -1
- package/dist/config/index.js.map +1 -1
- package/dist/config/schemas/config.d.ts +23 -1
- package/dist/config/schemas/config.d.ts.map +1 -1
- package/dist/config/schemas/config.js +14 -1
- package/dist/config/schemas/config.js.map +1 -1
- package/dist/config/schemas/field.d.ts +38 -3
- package/dist/config/schemas/field.d.ts.map +1 -1
- package/dist/config/schemas/field.js +18 -0
- package/dist/config/schemas/field.js.map +1 -1
- package/dist/config/schemas/media.d.ts +8 -0
- package/dist/config/schemas/media.d.ts.map +1 -1
- package/dist/config/schemas/media.js +9 -0
- package/dist/config/schemas/media.js.map +1 -1
- package/dist/config/types.d.ts +63 -3
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +4 -1
- package/dist/config/types.js.map +1 -1
- package/dist/editor/CanopyEditor.d.ts.map +1 -1
- package/dist/editor/CanopyEditor.js +1 -1
- package/dist/editor/CanopyEditor.js.map +1 -1
- package/dist/editor/Editor.d.ts +2 -0
- package/dist/editor/Editor.d.ts.map +1 -1
- package/dist/editor/Editor.js +82 -80
- package/dist/editor/Editor.js.map +1 -1
- package/dist/editor/FormRenderer.d.ts.map +1 -1
- package/dist/editor/FormRenderer.js +9 -0
- package/dist/editor/FormRenderer.js.map +1 -1
- package/dist/editor/components/EditorSidebar.d.ts +6 -1
- package/dist/editor/components/EditorSidebar.d.ts.map +1 -1
- package/dist/editor/components/EditorSidebar.js +4 -3
- package/dist/editor/components/EditorSidebar.js.map +1 -1
- package/dist/editor/context/AssetContext.d.ts +25 -0
- package/dist/editor/context/AssetContext.d.ts.map +1 -0
- package/dist/editor/context/AssetContext.js +26 -0
- package/dist/editor/context/AssetContext.js.map +1 -0
- package/dist/editor/context/index.d.ts +1 -0
- package/dist/editor/context/index.d.ts.map +1 -1
- package/dist/editor/context/index.js +1 -0
- package/dist/editor/context/index.js.map +1 -1
- package/dist/editor/fields/ImageField.d.ts +22 -0
- package/dist/editor/fields/ImageField.d.ts.map +1 -0
- package/dist/editor/fields/ImageField.js +100 -0
- package/dist/editor/fields/ImageField.js.map +1 -0
- package/dist/editor/fields/MarkdownField.d.ts.map +1 -1
- package/dist/editor/fields/MarkdownField.js +30 -4
- package/dist/editor/fields/MarkdownField.js.map +1 -1
- package/dist/editor/fields/MdxImageDialog.d.ts +16 -0
- package/dist/editor/fields/MdxImageDialog.d.ts.map +1 -0
- package/dist/editor/fields/MdxImageDialog.js +73 -0
- package/dist/editor/fields/MdxImageDialog.js.map +1 -0
- package/dist/editor/media/AssetCard.d.ts +21 -0
- package/dist/editor/media/AssetCard.d.ts.map +1 -0
- package/dist/editor/media/AssetCard.js +41 -0
- package/dist/editor/media/AssetCard.js.map +1 -0
- package/dist/editor/media/CropStep.d.ts +26 -0
- package/dist/editor/media/CropStep.d.ts.map +1 -0
- package/dist/editor/media/CropStep.js +38 -0
- package/dist/editor/media/CropStep.js.map +1 -0
- package/dist/editor/media/MediaLibrary.d.ts +22 -0
- package/dist/editor/media/MediaLibrary.d.ts.map +1 -0
- package/dist/editor/media/MediaLibrary.js +17 -0
- package/dist/editor/media/MediaLibrary.js.map +1 -0
- package/dist/editor/media/MediaLibraryBody.d.ts +17 -0
- package/dist/editor/media/MediaLibraryBody.d.ts.map +1 -0
- package/dist/editor/media/MediaLibraryBody.js +133 -0
- package/dist/editor/media/MediaLibraryBody.js.map +1 -0
- package/dist/editor/media/crop-math.d.ts +37 -0
- package/dist/editor/media/crop-math.d.ts.map +1 -0
- package/dist/editor/media/crop-math.js +62 -0
- package/dist/editor/media/crop-math.js.map +1 -0
- package/dist/editor/media/upload-asset.d.ts +31 -0
- package/dist/editor/media/upload-asset.d.ts.map +1 -0
- package/dist/editor/media/upload-asset.js +56 -0
- package/dist/editor/media/upload-asset.js.map +1 -0
- package/dist/editor/media/upload-constants.d.ts +27 -0
- package/dist/editor/media/upload-constants.d.ts.map +1 -0
- package/dist/editor/media/upload-constants.js +40 -0
- package/dist/editor/media/upload-constants.js.map +1 -0
- package/dist/editor/media/useAssetUpload.d.ts +19 -0
- package/dist/editor/media/useAssetUpload.d.ts.map +1 -0
- package/dist/editor/media/useAssetUpload.js +53 -0
- package/dist/editor/media/useAssetUpload.js.map +1 -0
- package/dist/editor/media/xhr-upload.d.ts +20 -0
- package/dist/editor/media/xhr-upload.d.ts.map +1 -0
- package/dist/editor/media/xhr-upload.js +42 -0
- package/dist/editor/media/xhr-upload.js.map +1 -0
- package/dist/editor/theme.d.ts +2 -1
- package/dist/editor/theme.d.ts.map +1 -1
- package/dist/editor/theme.js +1 -0
- package/dist/editor/theme.js.map +1 -1
- package/dist/entry-schema.d.ts +19 -0
- package/dist/entry-schema.d.ts.map +1 -1
- package/dist/entry-schema.js.map +1 -1
- package/dist/git-manager.d.ts +39 -0
- package/dist/git-manager.d.ts.map +1 -1
- package/dist/git-manager.js +98 -8
- package/dist/git-manager.js.map +1 -1
- package/dist/http/handler.d.ts +5 -2
- package/dist/http/handler.d.ts.map +1 -1
- package/dist/http/handler.js +13 -4
- package/dist/http/handler.js.map +1 -1
- package/dist/http/index.d.ts +2 -2
- package/dist/http/index.d.ts.map +1 -1
- package/dist/http/index.js +1 -1
- package/dist/http/index.js.map +1 -1
- package/dist/http/router.d.ts +14 -1
- package/dist/http/router.d.ts.map +1 -1
- package/dist/http/router.js +4 -1
- package/dist/http/router.js.map +1 -1
- package/dist/http/types.d.ts +40 -0
- package/dist/http/types.d.ts.map +1 -1
- package/dist/http/types.js +9 -0
- package/dist/http/types.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/schema/schema-store.d.ts +2 -2
- package/dist/server.d.ts +45 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +41 -0
- package/dist/server.js.map +1 -1
- package/dist/settings-workspace.d.ts.map +1 -1
- package/dist/settings-workspace.js +1 -0
- package/dist/settings-workspace.js.map +1 -1
- package/dist/utils/git.d.ts +19 -0
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +47 -0
- package/dist/utils/git.js.map +1 -1
- package/dist/validation/entry-validator.d.ts.map +1 -1
- package/dist/validation/entry-validator.js +56 -1
- package/dist/validation/entry-validator.js.map +1 -1
- package/dist/worker/cms-worker.d.ts +28 -0
- package/dist/worker/cms-worker.d.ts.map +1 -1
- package/dist/worker/cms-worker.js +100 -20
- package/dist/worker/cms-worker.js.map +1 -1
- package/package.json +13 -1
- package/dist/asset-store.d.ts +0 -32
- package/dist/asset-store.d.ts.map +0 -1
- package/dist/asset-store.js +0 -85
- package/dist/asset-store.js.map +0 -1
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem-backed AssetStore for dev and tests. Mirrors the same
|
|
3
|
+
* bucket-prefix layout as S3AssetStore (see keys.ts) so content behaves
|
|
4
|
+
* identically across adapters.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { randomUUID } from 'node:crypto';
|
|
9
|
+
import { getErrorMessage, isFileExistsError, isNotFoundError } from '../utils/error.js';
|
|
10
|
+
import { atomicWriteFile } from '../utils/atomic-write.js';
|
|
11
|
+
import { ASSET_PREFIXES, metaKey, originalKey, stagingKey } from './keys.js';
|
|
12
|
+
/** Matches S3AssetStore's default; local has no separate configuration knob for it. */
|
|
13
|
+
const DEFAULT_MAX_UPLOAD_BYTES = 50 * 1024 * 1024;
|
|
14
|
+
const DEFAULT_LIST_LIMIT = 24;
|
|
15
|
+
const HEADERS_SIDECAR_SUFFIX = '.headers.json';
|
|
16
|
+
export class LocalAssetStore {
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.capabilities = { directUpload: false };
|
|
19
|
+
this.root = path.resolve(options.root);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a store key to an absolute filesystem path, guarding against
|
|
23
|
+
* escape from the root. Keys are internally generated (never taken
|
|
24
|
+
* verbatim from user input), but this guard is kept as defense-in-depth
|
|
25
|
+
* (ported from the v1 LocalAssetStore).
|
|
26
|
+
*
|
|
27
|
+
* A naive `resolved.startsWith(root)` check would let a sibling directory
|
|
28
|
+
* that merely shares the root's name as a prefix (e.g. root `.../assets`,
|
|
29
|
+
* sibling `.../assets-evil`) pass. Comparing against `root + path.sep`
|
|
30
|
+
* closes that gap.
|
|
31
|
+
*/
|
|
32
|
+
resolveKey(key) {
|
|
33
|
+
const resolved = path.resolve(this.root, key);
|
|
34
|
+
const rootWithSep = this.root.endsWith(path.sep) ? this.root : this.root + path.sep;
|
|
35
|
+
if (resolved !== this.root && !resolved.startsWith(rootWithSep)) {
|
|
36
|
+
throw new Error(`Path traversal detected for key: ${key}`);
|
|
37
|
+
}
|
|
38
|
+
return resolved;
|
|
39
|
+
}
|
|
40
|
+
headersSidecarPath(filePath) {
|
|
41
|
+
return `${filePath}${HEADERS_SIDECAR_SUFFIX}`;
|
|
42
|
+
}
|
|
43
|
+
async writeHeadersSidecar(filePath, headers) {
|
|
44
|
+
await atomicWriteFile(this.headersSidecarPath(filePath), JSON.stringify(headers));
|
|
45
|
+
}
|
|
46
|
+
async readHeadersSidecar(filePath) {
|
|
47
|
+
try {
|
|
48
|
+
const raw = await fs.readFile(this.headersSidecarPath(filePath), 'utf-8');
|
|
49
|
+
return JSON.parse(raw);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
if (isNotFoundError(err))
|
|
53
|
+
return {};
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async readFileOrNull(filePath) {
|
|
58
|
+
try {
|
|
59
|
+
const buf = await fs.readFile(filePath);
|
|
60
|
+
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
if (isNotFoundError(err))
|
|
64
|
+
return null;
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async writeFile(filePath, data) {
|
|
69
|
+
await atomicWriteFile(filePath, Buffer.from(data));
|
|
70
|
+
}
|
|
71
|
+
async deleteFileIfPresent(filePath) {
|
|
72
|
+
try {
|
|
73
|
+
await fs.unlink(filePath);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
if (!isNotFoundError(err))
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
// Best-effort sidecar cleanup; a missing sidecar is not an error.
|
|
80
|
+
await fs.unlink(this.headersSidecarPath(filePath)).catch(() => { });
|
|
81
|
+
}
|
|
82
|
+
async beginUpload(_input) {
|
|
83
|
+
return {
|
|
84
|
+
mode: 'proxied',
|
|
85
|
+
stagingKey: stagingKey(randomUUID()),
|
|
86
|
+
maxBytes: DEFAULT_MAX_UPLOAD_BYTES,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Staging methods accept caller-influenced keys (finalize receives the key
|
|
91
|
+
* from the client), so they must never operate outside the staging prefix —
|
|
92
|
+
* otherwise a crafted key turns the staging cleanup into an arbitrary-object
|
|
93
|
+
* delete. The API layer validates too; this is defense-in-depth.
|
|
94
|
+
*/
|
|
95
|
+
assertStagingKey(key) {
|
|
96
|
+
if (!key.startsWith(`${ASSET_PREFIXES.staging}/`)) {
|
|
97
|
+
throw new Error(`Not a staging key: ${key}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async writeStaging(key, data, _contentType) {
|
|
101
|
+
this.assertStagingKey(key);
|
|
102
|
+
await this.writeFile(this.resolveKey(key), data);
|
|
103
|
+
}
|
|
104
|
+
async readStaging(key) {
|
|
105
|
+
this.assertStagingKey(key);
|
|
106
|
+
return this.readFileOrNull(this.resolveKey(key));
|
|
107
|
+
}
|
|
108
|
+
async deleteStaging(key) {
|
|
109
|
+
this.assertStagingKey(key);
|
|
110
|
+
await this.deleteFileIfPresent(this.resolveKey(key));
|
|
111
|
+
}
|
|
112
|
+
async putOriginal(input) {
|
|
113
|
+
const filePath = this.resolveKey(originalKey(input.hash32, input.ext));
|
|
114
|
+
await this.writeFile(filePath, input.data);
|
|
115
|
+
await this.writeHeadersSidecar(filePath, { contentType: input.contentType });
|
|
116
|
+
}
|
|
117
|
+
async readOriginal(hash32) {
|
|
118
|
+
const dir = this.resolveKey(ASSET_PREFIXES.originals);
|
|
119
|
+
let entries;
|
|
120
|
+
try {
|
|
121
|
+
entries = await fs.readdir(dir);
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
if (isNotFoundError(err))
|
|
125
|
+
return null;
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
128
|
+
const prefix = `${hash32}.`;
|
|
129
|
+
const match = entries.find((name) => name.startsWith(prefix) && !name.endsWith(HEADERS_SIDECAR_SUFFIX));
|
|
130
|
+
if (!match)
|
|
131
|
+
return null;
|
|
132
|
+
const filePath = path.join(dir, match);
|
|
133
|
+
const data = await this.readFileOrNull(filePath);
|
|
134
|
+
if (!data)
|
|
135
|
+
return null;
|
|
136
|
+
const headers = await this.readHeadersSidecar(filePath);
|
|
137
|
+
return { data, ext: match.slice(prefix.length), contentType: headers.contentType };
|
|
138
|
+
}
|
|
139
|
+
async putPublicObject(input) {
|
|
140
|
+
const filePath = this.resolveKey(input.key);
|
|
141
|
+
await this.writeFile(filePath, input.data);
|
|
142
|
+
await this.writeHeadersSidecar(filePath, {
|
|
143
|
+
contentType: input.contentType,
|
|
144
|
+
contentDisposition: input.contentDisposition,
|
|
145
|
+
cacheControl: input.cacheControl,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
async readPublicObject(key) {
|
|
149
|
+
const filePath = this.resolveKey(key);
|
|
150
|
+
const data = await this.readFileOrNull(filePath);
|
|
151
|
+
if (!data)
|
|
152
|
+
return null;
|
|
153
|
+
const headers = await this.readHeadersSidecar(filePath);
|
|
154
|
+
return {
|
|
155
|
+
data,
|
|
156
|
+
contentType: headers.contentType,
|
|
157
|
+
contentDisposition: headers.contentDisposition,
|
|
158
|
+
cacheControl: headers.cacheControl,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Atomic-exclusive create: opens with the `wx` flag so two concurrent
|
|
163
|
+
* writers racing on the same hash32 have exactly one winner ('created')
|
|
164
|
+
* and exactly one loser ('already-exists'). The rename-based
|
|
165
|
+
* `atomicWriteFile` helper is deliberately NOT used here — a rename
|
|
166
|
+
* always clobbers the destination, which would let the second writer
|
|
167
|
+
* silently overwrite the first instead of losing cleanly.
|
|
168
|
+
*/
|
|
169
|
+
async putMetaIfAbsent(hash32, meta) {
|
|
170
|
+
const filePath = this.resolveKey(metaKey(hash32));
|
|
171
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
172
|
+
try {
|
|
173
|
+
await fs.writeFile(filePath, JSON.stringify(meta), { flag: 'wx' });
|
|
174
|
+
return 'created';
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
if (isFileExistsError(err))
|
|
178
|
+
return 'already-exists';
|
|
179
|
+
throw err;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async getMeta(hash32) {
|
|
183
|
+
const filePath = this.resolveKey(metaKey(hash32));
|
|
184
|
+
try {
|
|
185
|
+
const raw = await fs.readFile(filePath, 'utf-8');
|
|
186
|
+
return JSON.parse(raw);
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
if (isNotFoundError(err))
|
|
190
|
+
return null;
|
|
191
|
+
throw err;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async listMeta(input) {
|
|
195
|
+
const limit = input?.limit ?? DEFAULT_LIST_LIMIT;
|
|
196
|
+
const dir = this.resolveKey(ASSET_PREFIXES.meta);
|
|
197
|
+
let filenames;
|
|
198
|
+
try {
|
|
199
|
+
filenames = (await fs.readdir(dir)).filter((name) => name.endsWith('.json'));
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
if (isNotFoundError(err))
|
|
203
|
+
filenames = [];
|
|
204
|
+
else
|
|
205
|
+
throw err;
|
|
206
|
+
}
|
|
207
|
+
const all = [];
|
|
208
|
+
for (const name of filenames) {
|
|
209
|
+
try {
|
|
210
|
+
const raw = await fs.readFile(path.join(dir, name), 'utf-8');
|
|
211
|
+
all.push(JSON.parse(raw));
|
|
212
|
+
}
|
|
213
|
+
catch (err) {
|
|
214
|
+
// A meta file racing with deleteMeta is expected under concurrency;
|
|
215
|
+
// anything else is a real problem worth surfacing via getErrorMessage.
|
|
216
|
+
if (!isNotFoundError(err)) {
|
|
217
|
+
throw new Error(`Failed to read asset meta file '${name}': ${getErrorMessage(err)}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// Newest first; hash32 is the deterministic tiebreak for equal timestamps
|
|
222
|
+
// (common in fast test loops using Date.now()), which keeps pagination
|
|
223
|
+
// stable and duplicate/gap-free across calls.
|
|
224
|
+
all.sort((a, b) => {
|
|
225
|
+
if (a.uploadedAt !== b.uploadedAt)
|
|
226
|
+
return a.uploadedAt < b.uploadedAt ? 1 : -1;
|
|
227
|
+
return a.hash32 < b.hash32 ? 1 : -1;
|
|
228
|
+
});
|
|
229
|
+
let startIndex = 0;
|
|
230
|
+
if (input?.cursor) {
|
|
231
|
+
const afterHash32 = Buffer.from(input.cursor, 'base64').toString('utf-8');
|
|
232
|
+
const idx = all.findIndex((m) => m.hash32 === afterHash32);
|
|
233
|
+
startIndex = idx === -1 ? 0 : idx + 1;
|
|
234
|
+
}
|
|
235
|
+
const items = all.slice(startIndex, startIndex + limit);
|
|
236
|
+
const hasMore = startIndex + limit < all.length;
|
|
237
|
+
const nextCursor = hasMore
|
|
238
|
+
? Buffer.from(items[items.length - 1].hash32, 'utf-8').toString('base64')
|
|
239
|
+
: undefined;
|
|
240
|
+
return { items, nextCursor };
|
|
241
|
+
}
|
|
242
|
+
async deleteMeta(hash32) {
|
|
243
|
+
const filePath = this.resolveKey(metaKey(hash32));
|
|
244
|
+
try {
|
|
245
|
+
await fs.unlink(filePath);
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
if (isNotFoundError(err))
|
|
249
|
+
return;
|
|
250
|
+
throw err;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
//# sourceMappingURL=store-local.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-local.js","sourceRoot":"","sources":["../../src/assets/store-local.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S3-backed AssetStore. Same bucket-prefix layout as LocalAssetStore (see
|
|
3
|
+
* keys.ts). Assumes an EXISTING content bucket (versioning/SSE/replication
|
|
4
|
+
* already configured by the site's CDK stack) — this store only ever reads
|
|
5
|
+
* and writes objects under the five asset prefixes.
|
|
6
|
+
*/
|
|
7
|
+
import { type AssetPrefixes } from './keys';
|
|
8
|
+
import type { AssetMeta, AssetStore, BeginUploadInput, PublicObject, StagedUploadTarget } from './types';
|
|
9
|
+
export interface S3AssetStoreOptions {
|
|
10
|
+
bucket: string;
|
|
11
|
+
region: string;
|
|
12
|
+
publicBaseUrl?: string;
|
|
13
|
+
/** Defaults to 50 MiB. */
|
|
14
|
+
maxUploadBytes?: number;
|
|
15
|
+
/** Override the default bucket-prefix layout (rarely needed). */
|
|
16
|
+
prefixes?: AssetPrefixes;
|
|
17
|
+
}
|
|
18
|
+
export declare class S3AssetStore implements AssetStore {
|
|
19
|
+
readonly capabilities: {
|
|
20
|
+
directUpload: boolean;
|
|
21
|
+
};
|
|
22
|
+
private readonly client;
|
|
23
|
+
private readonly bucket;
|
|
24
|
+
private readonly maxUploadBytes;
|
|
25
|
+
private readonly keys;
|
|
26
|
+
private readonly stagingPrefix;
|
|
27
|
+
constructor(options: S3AssetStoreOptions);
|
|
28
|
+
/**
|
|
29
|
+
* Staging methods accept caller-influenced keys (finalize receives the key
|
|
30
|
+
* from the client), so they must never operate outside the staging prefix —
|
|
31
|
+
* in a shared content bucket an unguarded deleteStaging would reach deploy
|
|
32
|
+
* artifacts under builds/. The API layer validates too; defense-in-depth.
|
|
33
|
+
*/
|
|
34
|
+
private assertStagingKey;
|
|
35
|
+
beginUpload(input: BeginUploadInput): Promise<StagedUploadTarget>;
|
|
36
|
+
writeStaging(key: string, data: Uint8Array, contentType?: string): Promise<void>;
|
|
37
|
+
readStaging(key: string): Promise<Uint8Array | null>;
|
|
38
|
+
deleteStaging(key: string): Promise<void>;
|
|
39
|
+
putOriginal(input: {
|
|
40
|
+
hash32: string;
|
|
41
|
+
ext: string;
|
|
42
|
+
data: Uint8Array;
|
|
43
|
+
contentType: string;
|
|
44
|
+
}): Promise<void>;
|
|
45
|
+
readOriginal(hash32: string): Promise<{
|
|
46
|
+
data: Uint8Array;
|
|
47
|
+
ext: string;
|
|
48
|
+
contentType?: string;
|
|
49
|
+
} | null>;
|
|
50
|
+
putPublicObject(input: {
|
|
51
|
+
key: string;
|
|
52
|
+
data: Uint8Array;
|
|
53
|
+
contentType: string;
|
|
54
|
+
contentDisposition?: string;
|
|
55
|
+
cacheControl?: string;
|
|
56
|
+
}): Promise<void>;
|
|
57
|
+
readPublicObject(key: string): Promise<PublicObject | null>;
|
|
58
|
+
putMetaIfAbsent(hash32: string, meta: AssetMeta): Promise<'created' | 'already-exists'>;
|
|
59
|
+
getMeta(hash32: string): Promise<AssetMeta | null>;
|
|
60
|
+
listMeta(input?: {
|
|
61
|
+
cursor?: string;
|
|
62
|
+
limit?: number;
|
|
63
|
+
}): Promise<{
|
|
64
|
+
items: AssetMeta[];
|
|
65
|
+
nextCursor?: string;
|
|
66
|
+
}>;
|
|
67
|
+
deleteMeta(hash32: string): Promise<void>;
|
|
68
|
+
private getObject;
|
|
69
|
+
private getObjectBytes;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=store-s3.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-s3.d.ts","sourceRoot":"","sources":["../../src/assets/store-s3.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAYH,OAAO,EAAqC,KAAK,aAAa,EAAE,MAAM,QAAQ,CAAA;AAC9E,OAAO,KAAK,EACV,SAAS,EACT,UAAU,EACV,gBAAgB,EAChB,YAAY,EACZ,kBAAkB,EACnB,MAAM,SAAS,CAAA;AAEhB,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,0BAA0B;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,iEAAiE;IACjE,QAAQ,CAAC,EAAE,aAAa,CAAA;CACzB;AA2BD,qBAAa,YAAa,YAAW,UAAU;IAC7C,QAAQ,CAAC,YAAY;;MAAyB;IAC9C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAU;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAQ;IACvC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAsC;IAC3D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAQ;gBAE1B,OAAO,EAAE,mBAAmB;IASxC;;;;;OAKG;IACH,OAAO,CAAC,gBAAgB;IAMlB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAiBjE,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOhF,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAKpD,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKzC,WAAW,CAAC,KAAK,EAAE;QACvB,MAAM,EAAE,MAAM,CAAA;QACd,GAAG,EAAE,MAAM,CAAA;QACX,IAAI,EAAE,UAAU,CAAA;QAChB,WAAW,EAAE,MAAM,CAAA;KACpB,GAAG,OAAO,CAAC,IAAI,CAAC;IAWX,YAAY,CAChB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;QAAE,IAAI,EAAE,UAAU,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAepE,eAAe,CAAC,KAAK,EAAE;QAC3B,GAAG,EAAE,MAAM,CAAA;QACX,IAAI,EAAE,UAAU,CAAA;QAChB,WAAW,EAAE,MAAM,CAAA;QACnB,kBAAkB,CAAC,EAAE,MAAM,CAAA;QAC3B,YAAY,CAAC,EAAE,MAAM,CAAA;KACtB,GAAG,OAAO,CAAC,IAAI,CAAC;IAaX,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAa3D,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,GAAG,gBAAgB,CAAC;IAkBvF,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAMlD,QAAQ,CAAC,KAAK,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QACnE,KAAK,EAAE,SAAS,EAAE,CAAA;QAClB,UAAU,CAAC,EAAE,MAAM,CAAA;KACpB,CAAC;IA6BI,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAMjC,SAAS;YAST,cAAc;CAM7B"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S3-backed AssetStore. Same bucket-prefix layout as LocalAssetStore (see
|
|
3
|
+
* keys.ts). Assumes an EXISTING content bucket (versioning/SSE/replication
|
|
4
|
+
* already configured by the site's CDK stack) — this store only ever reads
|
|
5
|
+
* and writes objects under the five asset prefixes.
|
|
6
|
+
*/
|
|
7
|
+
import { randomUUID } from 'node:crypto';
|
|
8
|
+
import { DeleteObjectCommand, GetObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client, } from '@aws-sdk/client-s3';
|
|
9
|
+
import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
|
|
10
|
+
import { ASSET_PREFIXES, createKeyBuilders } from './keys.js';
|
|
11
|
+
const DEFAULT_MAX_UPLOAD_BYTES = 50 * 1024 * 1024;
|
|
12
|
+
const PRESIGN_EXPIRY_SECONDS = 15 * 60;
|
|
13
|
+
function matchesAwsError(err, name, httpStatusCode) {
|
|
14
|
+
if (!(err instanceof Error))
|
|
15
|
+
return false;
|
|
16
|
+
const shaped = err;
|
|
17
|
+
return shaped.name === name || shaped.$metadata?.httpStatusCode === httpStatusCode;
|
|
18
|
+
}
|
|
19
|
+
const isPreconditionFailed = (err) => matchesAwsError(err, 'PreconditionFailed', 412);
|
|
20
|
+
const isNoSuchKey = (err) => matchesAwsError(err, 'NoSuchKey', 404);
|
|
21
|
+
export class S3AssetStore {
|
|
22
|
+
constructor(options) {
|
|
23
|
+
this.capabilities = { directUpload: true };
|
|
24
|
+
this.bucket = options.bucket;
|
|
25
|
+
this.maxUploadBytes = options.maxUploadBytes ?? DEFAULT_MAX_UPLOAD_BYTES;
|
|
26
|
+
this.client = new S3Client({ region: options.region });
|
|
27
|
+
const prefixes = options.prefixes ?? ASSET_PREFIXES;
|
|
28
|
+
this.keys = createKeyBuilders(prefixes);
|
|
29
|
+
this.stagingPrefix = `${prefixes.staging}/`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Staging methods accept caller-influenced keys (finalize receives the key
|
|
33
|
+
* from the client), so they must never operate outside the staging prefix —
|
|
34
|
+
* in a shared content bucket an unguarded deleteStaging would reach deploy
|
|
35
|
+
* artifacts under builds/. The API layer validates too; defense-in-depth.
|
|
36
|
+
*/
|
|
37
|
+
assertStagingKey(key) {
|
|
38
|
+
if (!key.startsWith(this.stagingPrefix)) {
|
|
39
|
+
throw new Error(`Not a staging key: ${key}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async beginUpload(input) {
|
|
43
|
+
const key = this.keys.stagingKey(randomUUID());
|
|
44
|
+
const { url, fields } = await createPresignedPost(this.client, {
|
|
45
|
+
Bucket: this.bucket,
|
|
46
|
+
Key: key,
|
|
47
|
+
Conditions: [
|
|
48
|
+
['content-length-range', 1, this.maxUploadBytes],
|
|
49
|
+
{ 'Content-Type': input.contentType },
|
|
50
|
+
],
|
|
51
|
+
Fields: {
|
|
52
|
+
'Content-Type': input.contentType,
|
|
53
|
+
},
|
|
54
|
+
Expires: PRESIGN_EXPIRY_SECONDS,
|
|
55
|
+
});
|
|
56
|
+
return { mode: 'direct', url, fields, stagingKey: key, maxBytes: this.maxUploadBytes };
|
|
57
|
+
}
|
|
58
|
+
async writeStaging(key, data, contentType) {
|
|
59
|
+
this.assertStagingKey(key);
|
|
60
|
+
await this.client.send(new PutObjectCommand({ Bucket: this.bucket, Key: key, Body: data, ContentType: contentType }));
|
|
61
|
+
}
|
|
62
|
+
async readStaging(key) {
|
|
63
|
+
this.assertStagingKey(key);
|
|
64
|
+
return this.getObjectBytes(key);
|
|
65
|
+
}
|
|
66
|
+
async deleteStaging(key) {
|
|
67
|
+
this.assertStagingKey(key);
|
|
68
|
+
await this.client.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: key }));
|
|
69
|
+
}
|
|
70
|
+
async putOriginal(input) {
|
|
71
|
+
await this.client.send(new PutObjectCommand({
|
|
72
|
+
Bucket: this.bucket,
|
|
73
|
+
Key: this.keys.originalKey(input.hash32, input.ext),
|
|
74
|
+
Body: input.data,
|
|
75
|
+
ContentType: input.contentType,
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
async readOriginal(hash32) {
|
|
79
|
+
const prefix = this.keys.originalPrefix(hash32);
|
|
80
|
+
const listed = await this.client.send(new ListObjectsV2Command({ Bucket: this.bucket, Prefix: prefix, MaxKeys: 1 }));
|
|
81
|
+
const foundKey = listed.Contents?.[0]?.Key;
|
|
82
|
+
if (!foundKey)
|
|
83
|
+
return null;
|
|
84
|
+
const result = await this.getObject(foundKey);
|
|
85
|
+
if (!result)
|
|
86
|
+
return null;
|
|
87
|
+
const data = await result.Body?.transformToByteArray();
|
|
88
|
+
if (!data)
|
|
89
|
+
return null;
|
|
90
|
+
return { data, ext: foundKey.slice(prefix.length), contentType: result.ContentType };
|
|
91
|
+
}
|
|
92
|
+
async putPublicObject(input) {
|
|
93
|
+
await this.client.send(new PutObjectCommand({
|
|
94
|
+
Bucket: this.bucket,
|
|
95
|
+
Key: input.key,
|
|
96
|
+
Body: input.data,
|
|
97
|
+
ContentType: input.contentType,
|
|
98
|
+
ContentDisposition: input.contentDisposition,
|
|
99
|
+
CacheControl: input.cacheControl,
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
async readPublicObject(key) {
|
|
103
|
+
const result = await this.getObject(key);
|
|
104
|
+
if (!result)
|
|
105
|
+
return null;
|
|
106
|
+
const data = await result.Body?.transformToByteArray();
|
|
107
|
+
if (!data)
|
|
108
|
+
return null;
|
|
109
|
+
return {
|
|
110
|
+
data,
|
|
111
|
+
contentType: result.ContentType,
|
|
112
|
+
contentDisposition: result.ContentDisposition,
|
|
113
|
+
cacheControl: result.CacheControl,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
async putMetaIfAbsent(hash32, meta) {
|
|
117
|
+
try {
|
|
118
|
+
await this.client.send(new PutObjectCommand({
|
|
119
|
+
Bucket: this.bucket,
|
|
120
|
+
Key: this.keys.metaKey(hash32),
|
|
121
|
+
Body: JSON.stringify(meta),
|
|
122
|
+
ContentType: 'application/json',
|
|
123
|
+
IfNoneMatch: '*',
|
|
124
|
+
}));
|
|
125
|
+
return 'created';
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
if (isPreconditionFailed(err))
|
|
129
|
+
return 'already-exists';
|
|
130
|
+
throw err;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async getMeta(hash32) {
|
|
134
|
+
const bytes = await this.getObjectBytes(this.keys.metaKey(hash32));
|
|
135
|
+
if (!bytes)
|
|
136
|
+
return null;
|
|
137
|
+
return JSON.parse(Buffer.from(bytes).toString('utf-8'));
|
|
138
|
+
}
|
|
139
|
+
async listMeta(input) {
|
|
140
|
+
const listed = await this.client.send(new ListObjectsV2Command({
|
|
141
|
+
Bucket: this.bucket,
|
|
142
|
+
Prefix: this.keys.metaPrefix(),
|
|
143
|
+
MaxKeys: input?.limit,
|
|
144
|
+
ContinuationToken: input?.cursor,
|
|
145
|
+
}));
|
|
146
|
+
const objectKeys = (listed.Contents ?? [])
|
|
147
|
+
.map((obj) => obj.Key)
|
|
148
|
+
.filter((key) => Boolean(key));
|
|
149
|
+
const fetched = await Promise.all(objectKeys.map(async (key) => {
|
|
150
|
+
const bytes = await this.getObjectBytes(key);
|
|
151
|
+
// A meta object deleted between LIST and GET (deleteMeta race) reads
|
|
152
|
+
// as null; skip it rather than failing the whole page.
|
|
153
|
+
if (!bytes)
|
|
154
|
+
return null;
|
|
155
|
+
return JSON.parse(Buffer.from(bytes).toString('utf-8'));
|
|
156
|
+
}));
|
|
157
|
+
return {
|
|
158
|
+
items: fetched.filter((meta) => meta !== null),
|
|
159
|
+
nextCursor: listed.IsTruncated ? listed.NextContinuationToken : undefined,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
async deleteMeta(hash32) {
|
|
163
|
+
await this.client.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: this.keys.metaKey(hash32) }));
|
|
164
|
+
}
|
|
165
|
+
async getObject(key) {
|
|
166
|
+
try {
|
|
167
|
+
return await this.client.send(new GetObjectCommand({ Bucket: this.bucket, Key: key }));
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
if (isNoSuchKey(err))
|
|
171
|
+
return null;
|
|
172
|
+
throw err;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async getObjectBytes(key) {
|
|
176
|
+
const result = await this.getObject(key);
|
|
177
|
+
if (!result)
|
|
178
|
+
return null;
|
|
179
|
+
const bytes = await result.Body?.transformToByteArray();
|
|
180
|
+
return bytes ?? null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=store-s3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-s3.js","sourceRoot":"","sources":["../../src/assets/store-s3.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SVG sanitization. Server-only - never import from client/editor code.
|
|
3
|
+
*
|
|
4
|
+
* Sanitizer choice: the design record (.claude/future-tasks/assets-media-system.md)
|
|
5
|
+
* suggested `dompurify` + a lightweight DOM shim (e.g. `linkedom`) in place of the
|
|
6
|
+
* heavier `jsdom`. In practice that pairing is unsafe: DOMPurify's own README warns
|
|
7
|
+
* that immature DOM shims can cause it to fail in ways that produce an XSS hole even
|
|
8
|
+
* when DOMPurify itself behaves correctly, and that is exactly what was reproduced
|
|
9
|
+
* here - `createDOMPurify(linkedomWindow).sanitize(dirtySvg)` returned the INPUT
|
|
10
|
+
* UNCHANGED (a `<script>`, `onload=`, and `<foreignObject>` all survived), because
|
|
11
|
+
* linkedom's window has no `NodeFilter` global and DOMPurify silently no-ops when it
|
|
12
|
+
* doesn't recognize the environment as supported. That is a dangerous failure mode
|
|
13
|
+
* for a security boundary, so this module uses `sanitize-html` instead: it works
|
|
14
|
+
* directly on the parser tree (htmlparser2, no DOM shim to be incomplete), is
|
|
15
|
+
* actively maintained, and - verified empirically - correctly strips scripts, event
|
|
16
|
+
* handler attributes, and `foreignObject` while preserving a valid, still-parseable
|
|
17
|
+
* SVG document.
|
|
18
|
+
*
|
|
19
|
+
* Allowlist-based (not blocklist): only elements/attributes known to be safe survive.
|
|
20
|
+
* `href`/`xlink:href` are further restricted to local fragment references (`#...`) so
|
|
21
|
+
* `<use>`/`<a>` can reference in-document defs but can never point at an external URL
|
|
22
|
+
* (tracking pixels, `javascript:` URLs, etc.) - sanitize-html's tag-level scheme
|
|
23
|
+
* allowlisting does not cover attribute name patterns like this, so it is enforced
|
|
24
|
+
* directly in `transformTags` below.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Sanitize an SVG document (as text). Strips scripts, event-handler attributes,
|
|
28
|
+
* `<foreignObject>` (and its content), inline `<style>` (CSS injection surface),
|
|
29
|
+
* any `href`/`xlink:href` that isn't a local fragment reference, and any
|
|
30
|
+
* `url(...)` reference on a paint/filter/marker attribute that isn't a local
|
|
31
|
+
* fragment reference (external-fetch tracking beacon).
|
|
32
|
+
*
|
|
33
|
+
* Input must already be confirmed to be an SVG document (see `sniffSvg` in
|
|
34
|
+
* pipeline.ts) - this function does not re-validate the root element.
|
|
35
|
+
*/
|
|
36
|
+
export declare function sanitizeSvg(svgText: string): string;
|
|
37
|
+
//# sourceMappingURL=svg-sanitizer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"svg-sanitizer.d.ts","sourceRoot":"","sources":["../../src/assets/svg-sanitizer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAuKH;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAwBnD"}
|