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,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Store orchestration around the (pure) finalize pipeline: dedup, write, and
|
|
3
|
+
* best-effort staging cleanup. Split from pipeline.ts so the pipeline itself
|
|
4
|
+
* stays store-agnostic and trivially unit-testable with plain byte fixtures.
|
|
5
|
+
*/
|
|
6
|
+
import { runFinalizePipeline } from './pipeline.js';
|
|
7
|
+
import { ASSET_PREFIXES } from './keys.js';
|
|
8
|
+
/**
|
|
9
|
+
* A client-supplied staging key must be exactly `asset-staging/{uuid}` — the
|
|
10
|
+
* shape beginUpload generates. Without this check, finalize's read + cleanup
|
|
11
|
+
* would operate on ANY key the store can reach (asset-meta/, asset-originals/,
|
|
12
|
+
* or in a shared bucket even builds/), letting a non-admin delete arbitrary
|
|
13
|
+
* objects via the best-effort deleteStaging. Stores with overridden staging
|
|
14
|
+
* prefixes must parameterize this check if that escape hatch is ever used.
|
|
15
|
+
*/
|
|
16
|
+
const STAGING_UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
17
|
+
export function isValidStagingKey(key) {
|
|
18
|
+
const prefix = `${ASSET_PREFIXES.staging}/`;
|
|
19
|
+
return key.startsWith(prefix) && STAGING_UUID_PATTERN.test(key.slice(prefix.length));
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Run the finalize pipeline over raw bytes and persist the result.
|
|
23
|
+
*
|
|
24
|
+
* Order matters here too: dedup FIRST (after hashing, inside the pipeline) -
|
|
25
|
+
* if an identical blob already exists, return the existing meta and skip all
|
|
26
|
+
* writes entirely (first-name-wins). Otherwise write the original, then the
|
|
27
|
+
* optional public object, then `putMetaIfAbsent` LAST: meta is the commit
|
|
28
|
+
* point for an asset's existence, so a crash between `putOriginal` and here
|
|
29
|
+
* leaves only orphaned (harmless, content-addressed) blobs, never a meta
|
|
30
|
+
* record pointing at a missing blob.
|
|
31
|
+
*/
|
|
32
|
+
export async function finalizeAsset(store, input) {
|
|
33
|
+
const result = await runFinalizePipeline(input);
|
|
34
|
+
if (!result.ok)
|
|
35
|
+
return result;
|
|
36
|
+
const { meta, data, publicObject } = result;
|
|
37
|
+
const existing = await store.getMeta(meta.hash32);
|
|
38
|
+
if (existing)
|
|
39
|
+
return { ok: true, meta: existing };
|
|
40
|
+
await store.putOriginal({ hash32: meta.hash32, ext: meta.ext, data, contentType: meta.mime });
|
|
41
|
+
if (publicObject) {
|
|
42
|
+
await store.putPublicObject(publicObject);
|
|
43
|
+
}
|
|
44
|
+
const putResult = await store.putMetaIfAbsent(meta.hash32, meta);
|
|
45
|
+
if (putResult === 'already-exists') {
|
|
46
|
+
// Lost a race against a concurrent identical upload - the winner's meta
|
|
47
|
+
// (already committed) is authoritative; return it rather than ours.
|
|
48
|
+
const winner = await store.getMeta(meta.hash32);
|
|
49
|
+
if (winner)
|
|
50
|
+
return { ok: true, meta: winner };
|
|
51
|
+
}
|
|
52
|
+
return { ok: true, meta };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Finalize a previously-staged upload: read the staged bytes, run
|
|
56
|
+
* `finalizeAsset`, and best-effort delete the staging object regardless of
|
|
57
|
+
* outcome (success OR pipeline rejection - a rejected upload's staged bytes
|
|
58
|
+
* are just as much litter as an accepted one's).
|
|
59
|
+
*/
|
|
60
|
+
export async function finalizeStagedUpload(store, stagingKey, filename, uploadedBy) {
|
|
61
|
+
if (!isValidStagingKey(stagingKey)) {
|
|
62
|
+
return { ok: false, status: 400, error: 'Invalid staging key' };
|
|
63
|
+
}
|
|
64
|
+
const staged = await store.readStaging(stagingKey);
|
|
65
|
+
if (!staged) {
|
|
66
|
+
return { ok: false, status: 404, error: 'Staged upload not found or expired' };
|
|
67
|
+
}
|
|
68
|
+
const result = await finalizeAsset(store, { data: staged, filename, uploadedBy });
|
|
69
|
+
await store.deleteStaging(stagingKey).catch(() => { });
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=finalize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"finalize.js","sourceRoot":"","sources":["../../src/assets/finalize.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,mBAAmB,EAA8C,MAAM,YAAY,CAAA;AAC5F,OAAO,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAGvC;;;;;;;GAOG;AACH,MAAM,oBAAoB,GAAG,gEAAgE,CAAA;AAE7F,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,MAAM,MAAM,GAAG,GAAG,cAAc,CAAC,OAAO,GAAG,CAAA;IAC3C,OAAO,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,oBAAoB,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAA;AACtF,CAAC;AAID;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,KAAiB,EACjB,KAAoB;IAEpB,MAAM,MAAM,GAAG,MAAM,mBAAmB,CAAC,KAAK,CAAC,CAAA;IAC/C,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,OAAO,MAAM,CAAA;IAE7B,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,GAAG,MAAM,CAAA;IAE3C,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IACjD,IAAI,QAAQ;QAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAA;IAEjD,MAAM,KAAK,CAAC,WAAW,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAA;IAC7F,IAAI,YAAY,EAAE,CAAC;QACjB,MAAM,KAAK,CAAC,eAAe,CAAC,YAAY,CAAC,CAAA;IAC3C,CAAC;IAED,MAAM,SAAS,GAAG,MAAM,KAAK,CAAC,eAAe,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;IAChE,IAAI,SAAS,KAAK,gBAAgB,EAAE,CAAC;QACnC,wEAAwE;QACxE,oEAAoE;QACpE,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QAC/C,IAAI,MAAM;YAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;IAC/C,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAA;AAC3B,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,KAAiB,EACjB,UAAkB,EAClB,QAAgB,EAChB,UAAmB;IAEnB,IAAI,CAAC,iBAAiB,CAAC,UAAU,CAAC,EAAE,CAAC;QACnC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAA;IACjE,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,UAAU,CAAC,CAAA;IAClD,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,oCAAoC,EAAE,CAAA;IAChF,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC,CAAA;IACjF,MAAM,KAAK,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;IACrD,OAAO,MAAM,CAAA;AACf,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal module barrel for the assets/ store implementation. NOT a package
|
|
3
|
+
* entrypoint — server code within canopycms imports from here or from the
|
|
4
|
+
* individual files; client-side code (the editor) must `import type` from
|
|
5
|
+
* ./types directly instead, so node:fs/S3-SDK imports never reach a browser
|
|
6
|
+
* bundle.
|
|
7
|
+
*/
|
|
8
|
+
export type { AssetMeta, AssetStore, BeginUploadInput, PublicObject, StagedUploadTarget, } from './types';
|
|
9
|
+
export { ASSET_PREFIXES, createKeyBuilders, hashBytes, metaKey, metaPrefix, originalKey, originalPrefix, publicKey, slugifyFilename, stagingKey, type AssetPrefixes, } from './keys';
|
|
10
|
+
export { LocalAssetStore, type LocalAssetStoreOptions } from './store-local';
|
|
11
|
+
export { S3AssetStore, type S3AssetStoreOptions } from './store-s3';
|
|
12
|
+
export { createAssetStore } from './factory';
|
|
13
|
+
export { ALLOWED_UPLOAD_CONTENT_TYPES, runFinalizePipeline, type FinalizeInput, type FinalizePublicObject, type FinalizeResult, type FinalizeRejection, type FinalizeSuccess, } from './pipeline';
|
|
14
|
+
export { finalizeAsset, finalizeStagedUpload, type FinalizeAssetResult } from './finalize';
|
|
15
|
+
export { sanitizeSvg } from './svg-sanitizer';
|
|
16
|
+
export { assetSrc, IDENTITY_TRANSFORM_DIRECTIVE } from './asset-src';
|
|
17
|
+
export { formatDirectives, isAllowedTransformWidth, isValidCropRect, parseTransformPath, type CropRect, type IdentityDirectives, type OutputFormat, type ParsedTransformPath, type ParseTransformPathResult, type ResizeDirectives, type TransformDirectives, } from './transform-directives';
|
|
18
|
+
export { applyTransform, type ApplyTransformInput, type TransformResult } from './transform';
|
|
19
|
+
export { assetUrl, assetSrcSet, type AssetRef, type AssetUrlOptions } from './asset-url';
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/assets/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,YAAY,EACV,SAAS,EACT,UAAU,EACV,gBAAgB,EAChB,YAAY,EACZ,kBAAkB,GACnB,MAAM,SAAS,CAAA;AAEhB,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,SAAS,EACT,OAAO,EACP,UAAU,EACV,WAAW,EACX,cAAc,EACd,SAAS,EACT,eAAe,EACf,UAAU,EACV,KAAK,aAAa,GACnB,MAAM,QAAQ,CAAA;AAEf,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,eAAe,CAAA;AAC5E,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,YAAY,CAAA;AACnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAA;AAE5C,OAAO,EACL,4BAA4B,EAC5B,mBAAmB,EACnB,KAAK,aAAa,EAClB,KAAK,oBAAoB,EACzB,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,eAAe,GACrB,MAAM,YAAY,CAAA;AAEnB,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,KAAK,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAE1F,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAE7C,OAAO,EAAE,QAAQ,EAAE,4BAA4B,EAAE,MAAM,aAAa,CAAA;AAEpE,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EACvB,eAAe,EACf,kBAAkB,EAClB,KAAK,QAAQ,EACb,KAAK,kBAAkB,EACvB,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,KAAK,wBAAwB,EAC7B,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,GACzB,MAAM,wBAAwB,CAAA;AAE/B,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAA;AAE5F,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,KAAK,QAAQ,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAA"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal module barrel for the assets/ store implementation. NOT a package
|
|
3
|
+
* entrypoint — server code within canopycms imports from here or from the
|
|
4
|
+
* individual files; client-side code (the editor) must `import type` from
|
|
5
|
+
* ./types directly instead, so node:fs/S3-SDK imports never reach a browser
|
|
6
|
+
* bundle.
|
|
7
|
+
*/
|
|
8
|
+
export { ASSET_PREFIXES, createKeyBuilders, hashBytes, metaKey, metaPrefix, originalKey, originalPrefix, publicKey, slugifyFilename, stagingKey, } from './keys.js';
|
|
9
|
+
export { LocalAssetStore } from './store-local.js';
|
|
10
|
+
export { S3AssetStore } from './store-s3.js';
|
|
11
|
+
export { createAssetStore } from './factory.js';
|
|
12
|
+
export { ALLOWED_UPLOAD_CONTENT_TYPES, runFinalizePipeline, } from './pipeline.js';
|
|
13
|
+
export { finalizeAsset, finalizeStagedUpload } from './finalize.js';
|
|
14
|
+
export { sanitizeSvg } from './svg-sanitizer.js';
|
|
15
|
+
export { assetSrc, IDENTITY_TRANSFORM_DIRECTIVE } from './asset-src.js';
|
|
16
|
+
export { formatDirectives, isAllowedTransformWidth, isValidCropRect, parseTransformPath, } from './transform-directives.js';
|
|
17
|
+
export { applyTransform } from './transform.js';
|
|
18
|
+
export { assetUrl, assetSrcSet } from './asset-url.js';
|
|
19
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/assets/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAUH,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,SAAS,EACT,OAAO,EACP,UAAU,EACV,WAAW,EACX,cAAc,EACd,SAAS,EACT,eAAe,EACf,UAAU,GAEX,MAAM,QAAQ,CAAA;AAEf,OAAO,EAAE,eAAe,EAA+B,MAAM,eAAe,CAAA;AAC5E,OAAO,EAAE,YAAY,EAA4B,MAAM,YAAY,CAAA;AACnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAA;AAE5C,OAAO,EACL,4BAA4B,EAC5B,mBAAmB,GAMpB,MAAM,YAAY,CAAA;AAEnB,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAA4B,MAAM,YAAY,CAAA;AAE1F,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAE7C,OAAO,EAAE,QAAQ,EAAE,4BAA4B,EAAE,MAAM,aAAa,CAAA;AAEpE,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EACvB,eAAe,EACf,kBAAkB,GAQnB,MAAM,wBAAwB,CAAA;AAE/B,OAAO,EAAE,cAAc,EAAkD,MAAM,aAAa,CAAA;AAE5F,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAuC,MAAM,aAAa,CAAA"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure key/hash/slug helpers for the asset store. No filesystem or network I/O
|
|
3
|
+
* here — keeps this file trivially unit-testable and safe to reason about for
|
|
4
|
+
* path-traversal / header-injection concerns.
|
|
5
|
+
*
|
|
6
|
+
* Bucket-prefix layout (see .claude/future-tasks/assets-media-system.md):
|
|
7
|
+
* asset-originals/{hash32}.{ext} private; full-fidelity originals
|
|
8
|
+
* asset-staging/{uuid} presigned-POST target
|
|
9
|
+
* asset-meta/{hash32}.json private; filename/uploader/dims/mime
|
|
10
|
+
* assets/t/{directives}/{hash32}/{slug} transform outputs (see transform-directives.ts)
|
|
11
|
+
* assets/{hash32}/{slug}.{ext} public static: sanitized SVG + PDF only
|
|
12
|
+
*
|
|
13
|
+
* Stores must build keys through these helpers rather than hand-rolling prefix
|
|
14
|
+
* strings, so the layout stays defined in exactly one place.
|
|
15
|
+
*/
|
|
16
|
+
import { ASSET_PREFIXES, type AssetPrefixes } from './asset-prefixes';
|
|
17
|
+
export { ASSET_PREFIXES, type AssetPrefixes };
|
|
18
|
+
/** sha-256 of the given bytes, truncated to 32 hex chars (128 bits). */
|
|
19
|
+
export declare function hashBytes(data: Uint8Array): string;
|
|
20
|
+
/**
|
|
21
|
+
* Slugify an (untrusted) original filename into a safe slug + normalized
|
|
22
|
+
* extension. Never trust the input for path or header use directly — this
|
|
23
|
+
* strips directory components, non-ASCII/unsafe characters, and caps length.
|
|
24
|
+
*
|
|
25
|
+
* - Directory components (`/`, `\`, including `..` segments) are dropped —
|
|
26
|
+
* only the final path segment is considered.
|
|
27
|
+
* - The slug charset is restricted to `[a-z0-9-]`; anything else (spaces,
|
|
28
|
+
* unicode, punctuation) collapses to a single `-`, with leading/trailing
|
|
29
|
+
* dashes stripped.
|
|
30
|
+
* - Diacritics are stripped (NFKD) before charset filtering, so an accented
|
|
31
|
+
* "e"-with-acute-accent becomes plain "e" rather than being dropped entirely.
|
|
32
|
+
* - The extension (text after the last `.` in the final segment) is
|
|
33
|
+
* lowercased and restricted to `[a-z0-9]`, capped at 10 chars.
|
|
34
|
+
* - An empty/unsafe result falls back to a fixed slug so callers never see
|
|
35
|
+
* an empty string.
|
|
36
|
+
*/
|
|
37
|
+
export declare function slugifyFilename(name: string): {
|
|
38
|
+
slug: string;
|
|
39
|
+
ext: string;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Build the key helpers bound to a given prefix set. Stores that support prefix
|
|
43
|
+
* overrides (currently S3AssetStore) call this with their configured prefixes;
|
|
44
|
+
* everyone else uses the default export instances below.
|
|
45
|
+
*/
|
|
46
|
+
export declare function createKeyBuilders(prefixes?: AssetPrefixes): {
|
|
47
|
+
originalKey: (hash32: string, ext: string) => string;
|
|
48
|
+
/** Prefix used to look up an original by hash32 alone (extension unknown ahead of time). */
|
|
49
|
+
originalPrefix: (hash32: string) => string;
|
|
50
|
+
stagingKey: (uuid: string) => string;
|
|
51
|
+
metaKey: (hash32: string) => string;
|
|
52
|
+
metaPrefix: () => string;
|
|
53
|
+
publicKey: (hash32: string, slug: string, ext: string) => string;
|
|
54
|
+
};
|
|
55
|
+
export declare const originalKey: (hash32: string, ext: string) => string, originalPrefix: (hash32: string) => string, stagingKey: (uuid: string) => string, metaKey: (hash32: string) => string, metaPrefix: () => string, publicKey: (hash32: string, slug: string, ext: string) => string;
|
|
56
|
+
//# sourceMappingURL=keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../../src/assets/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,OAAO,EAAE,cAAc,EAAE,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAErE,OAAO,EAAE,cAAc,EAAE,KAAK,aAAa,EAAE,CAAA;AAE7C,wEAAwE;AACxE,wBAAgB,SAAS,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAElD;AAMD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAkC3E;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,GAAE,aAA8B;0BAEhD,MAAM,OAAO,MAAM,KAAG,MAAM;IAClD,4FAA4F;6BACnE,MAAM,KAAG,MAAM;uBACrB,MAAM,KAAG,MAAM;sBAChB,MAAM,KAAG,MAAM;sBACjB,MAAM;wBACF,MAAM,QAAQ,MAAM,OAAO,MAAM,KAAG,MAAM;EAGjE;AAED,eAAO,MAAQ,WAAW,WAXA,MAAM,OAAO,MAAM,KAAG,MAAM,EAW1B,cAAc,WATb,MAAM,KAAG,MAAM,EASA,UAAU,SAR/B,MAAM,KAAG,MAAM,EAQkB,OAAO,WAPzC,MAAM,KAAG,MAAM,EAO4B,UAAU,QANvD,MAAM,EAMmD,SAAS,WAL9D,MAAM,QAAQ,MAAM,OAAO,MAAM,KAAG,MAMvC,CAAA"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure key/hash/slug helpers for the asset store. No filesystem or network I/O
|
|
3
|
+
* here — keeps this file trivially unit-testable and safe to reason about for
|
|
4
|
+
* path-traversal / header-injection concerns.
|
|
5
|
+
*
|
|
6
|
+
* Bucket-prefix layout (see .claude/future-tasks/assets-media-system.md):
|
|
7
|
+
* asset-originals/{hash32}.{ext} private; full-fidelity originals
|
|
8
|
+
* asset-staging/{uuid} presigned-POST target
|
|
9
|
+
* asset-meta/{hash32}.json private; filename/uploader/dims/mime
|
|
10
|
+
* assets/t/{directives}/{hash32}/{slug} transform outputs (see transform-directives.ts)
|
|
11
|
+
* assets/{hash32}/{slug}.{ext} public static: sanitized SVG + PDF only
|
|
12
|
+
*
|
|
13
|
+
* Stores must build keys through these helpers rather than hand-rolling prefix
|
|
14
|
+
* strings, so the layout stays defined in exactly one place.
|
|
15
|
+
*/
|
|
16
|
+
import { createHash } from 'node:crypto';
|
|
17
|
+
import { ASSET_PREFIXES } from './asset-prefixes.js';
|
|
18
|
+
export { ASSET_PREFIXES };
|
|
19
|
+
/** sha-256 of the given bytes, truncated to 32 hex chars (128 bits). */
|
|
20
|
+
export function hashBytes(data) {
|
|
21
|
+
return createHash('sha256').update(data).digest('hex').slice(0, 32);
|
|
22
|
+
}
|
|
23
|
+
const MAX_SLUG_LENGTH = 80;
|
|
24
|
+
const MAX_EXT_LENGTH = 10;
|
|
25
|
+
const FALLBACK_SLUG = 'file';
|
|
26
|
+
/**
|
|
27
|
+
* Slugify an (untrusted) original filename into a safe slug + normalized
|
|
28
|
+
* extension. Never trust the input for path or header use directly — this
|
|
29
|
+
* strips directory components, non-ASCII/unsafe characters, and caps length.
|
|
30
|
+
*
|
|
31
|
+
* - Directory components (`/`, `\`, including `..` segments) are dropped —
|
|
32
|
+
* only the final path segment is considered.
|
|
33
|
+
* - The slug charset is restricted to `[a-z0-9-]`; anything else (spaces,
|
|
34
|
+
* unicode, punctuation) collapses to a single `-`, with leading/trailing
|
|
35
|
+
* dashes stripped.
|
|
36
|
+
* - Diacritics are stripped (NFKD) before charset filtering, so an accented
|
|
37
|
+
* "e"-with-acute-accent becomes plain "e" rather than being dropped entirely.
|
|
38
|
+
* - The extension (text after the last `.` in the final segment) is
|
|
39
|
+
* lowercased and restricted to `[a-z0-9]`, capped at 10 chars.
|
|
40
|
+
* - An empty/unsafe result falls back to a fixed slug so callers never see
|
|
41
|
+
* an empty string.
|
|
42
|
+
*/
|
|
43
|
+
export function slugifyFilename(name) {
|
|
44
|
+
// Drop any directory components (handles both `/` and `\`, and `..` segments)
|
|
45
|
+
// so the untrusted original filename can never influence a constructed path.
|
|
46
|
+
const segments = name.split(/[/\\]+/);
|
|
47
|
+
const basename = segments[segments.length - 1] ?? '';
|
|
48
|
+
const lastDot = basename.lastIndexOf('.');
|
|
49
|
+
const hasExt = lastDot > 0 && lastDot < basename.length - 1;
|
|
50
|
+
const base = hasExt ? basename.slice(0, lastDot) : basename;
|
|
51
|
+
const extRaw = hasExt ? basename.slice(lastDot + 1) : '';
|
|
52
|
+
const ext = extRaw
|
|
53
|
+
.toLowerCase()
|
|
54
|
+
.replace(/[^a-z0-9]/g, '')
|
|
55
|
+
.slice(0, MAX_EXT_LENGTH);
|
|
56
|
+
// U+0300-U+036F is the combining-diacritical-marks block, stripped after NFKD
|
|
57
|
+
// decomposition. Built from fixed char codes (not a literal regex) so this
|
|
58
|
+
// file's source stays pure ASCII; the pattern is not derived from input, so
|
|
59
|
+
// there is no injection risk despite the non-literal RegExp constructor.
|
|
60
|
+
// eslint-disable-next-line security/detect-non-literal-regexp
|
|
61
|
+
const COMBINING_MARKS = new RegExp(`[${String.fromCharCode(0x0300)}-${String.fromCharCode(0x036f)}]`, 'g');
|
|
62
|
+
let slug = base
|
|
63
|
+
.normalize('NFKD')
|
|
64
|
+
.replace(COMBINING_MARKS, '')
|
|
65
|
+
.toLowerCase()
|
|
66
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
67
|
+
.replace(/^-+|-+$/g, '');
|
|
68
|
+
slug = slug.slice(0, MAX_SLUG_LENGTH).replace(/-+$/g, '');
|
|
69
|
+
return { slug: slug || FALLBACK_SLUG, ext };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Build the key helpers bound to a given prefix set. Stores that support prefix
|
|
73
|
+
* overrides (currently S3AssetStore) call this with their configured prefixes;
|
|
74
|
+
* everyone else uses the default export instances below.
|
|
75
|
+
*/
|
|
76
|
+
export function createKeyBuilders(prefixes = ASSET_PREFIXES) {
|
|
77
|
+
return {
|
|
78
|
+
originalKey: (hash32, ext) => `${prefixes.originals}/${hash32}.${ext}`,
|
|
79
|
+
/** Prefix used to look up an original by hash32 alone (extension unknown ahead of time). */
|
|
80
|
+
originalPrefix: (hash32) => `${prefixes.originals}/${hash32}.`,
|
|
81
|
+
stagingKey: (uuid) => `${prefixes.staging}/${uuid}`,
|
|
82
|
+
metaKey: (hash32) => `${prefixes.meta}/${hash32}.json`,
|
|
83
|
+
metaPrefix: () => `${prefixes.meta}/`,
|
|
84
|
+
publicKey: (hash32, slug, ext) => `${prefixes.public}/${hash32}/${slug}.${ext}`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export const { originalKey, originalPrefix, stagingKey, metaKey, metaPrefix, publicKey } = createKeyBuilders();
|
|
88
|
+
//# sourceMappingURL=keys.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.js","sourceRoot":"","sources":["../../src/assets/keys.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finalize pipeline: turns raw uploaded bytes into a validated AssetMeta
|
|
3
|
+
* (+ optional public object for svg/pdf), or a typed rejection. Pure
|
|
4
|
+
* transform - no store I/O here (see finalize.ts for the store orchestration
|
|
5
|
+
* that dedups, writes, and calls this).
|
|
6
|
+
*
|
|
7
|
+
* Server-only - never import this module from client/editor code. It pulls
|
|
8
|
+
* in `file-type`, `image-size`, and the SVG sanitizer, none of which belong
|
|
9
|
+
* in a browser bundle.
|
|
10
|
+
*/
|
|
11
|
+
import type { AssetMeta } from './types';
|
|
12
|
+
/** Client-declared content types accepted at presign time (UX only - never trusted at finalize). */
|
|
13
|
+
export declare const ALLOWED_UPLOAD_CONTENT_TYPES: Set<string>;
|
|
14
|
+
export interface FinalizeInput {
|
|
15
|
+
data: Uint8Array;
|
|
16
|
+
filename: string;
|
|
17
|
+
uploadedBy?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface FinalizePublicObject {
|
|
20
|
+
key: string;
|
|
21
|
+
data: Uint8Array;
|
|
22
|
+
contentType: string;
|
|
23
|
+
contentDisposition: string;
|
|
24
|
+
cacheControl: string;
|
|
25
|
+
}
|
|
26
|
+
export interface FinalizeSuccess {
|
|
27
|
+
ok: true;
|
|
28
|
+
meta: AssetMeta;
|
|
29
|
+
/** Canonical bytes to persist as the original - sanitized bytes for svg, untouched otherwise. */
|
|
30
|
+
data: Uint8Array;
|
|
31
|
+
publicObject?: FinalizePublicObject;
|
|
32
|
+
}
|
|
33
|
+
export interface FinalizeRejection {
|
|
34
|
+
ok: false;
|
|
35
|
+
status: 413 | 415;
|
|
36
|
+
error: string;
|
|
37
|
+
}
|
|
38
|
+
export type FinalizeResult = FinalizeSuccess | FinalizeRejection;
|
|
39
|
+
/**
|
|
40
|
+
* Run the finalize pipeline over freshly-uploaded bytes: sniff the real file
|
|
41
|
+
* type (never trust the client-declared contentType beyond presign-time UX),
|
|
42
|
+
* sanitize if svg, hash, extract dimensions, and build the AssetMeta + any
|
|
43
|
+
* public object (svg/pdf only) the caller should persist.
|
|
44
|
+
*
|
|
45
|
+
* Order matters: for SVG, sanitization happens BEFORE hashing - the sanitized
|
|
46
|
+
* bytes are what get hashed and stored, so a later re-sanitization (e.g. a
|
|
47
|
+
* stricter sanitizer version) cannot desync a stored hash from its content.
|
|
48
|
+
*/
|
|
49
|
+
export declare function runFinalizePipeline(input: FinalizeInput): Promise<FinalizeResult>;
|
|
50
|
+
//# sourceMappingURL=pipeline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/assets/pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AASH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAMxC,oGAAoG;AACpG,eAAO,MAAM,4BAA4B,aAIvC,CAAA;AAkBF,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,UAAU,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,UAAU,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,YAAY,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,IAAI,CAAA;IACR,IAAI,EAAE,SAAS,CAAA;IACf,iGAAiG;IACjG,IAAI,EAAE,UAAU,CAAA;IAChB,YAAY,CAAC,EAAE,oBAAoB,CAAA;CACpC;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,KAAK,CAAA;IACT,MAAM,EAAE,GAAG,GAAG,GAAG,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,MAAM,cAAc,GAAG,eAAe,GAAG,iBAAiB,CAAA;AAoEhE;;;;;;;;;GASG;AACH,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC,CA8FvF"}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finalize pipeline: turns raw uploaded bytes into a validated AssetMeta
|
|
3
|
+
* (+ optional public object for svg/pdf), or a typed rejection. Pure
|
|
4
|
+
* transform - no store I/O here (see finalize.ts for the store orchestration
|
|
5
|
+
* that dedups, writes, and calls this).
|
|
6
|
+
*
|
|
7
|
+
* Server-only - never import this module from client/editor code. It pulls
|
|
8
|
+
* in `file-type`, `image-size`, and the SVG sanitizer, none of which belong
|
|
9
|
+
* in a browser bundle.
|
|
10
|
+
*/
|
|
11
|
+
import { fileTypeFromBuffer } from 'file-type';
|
|
12
|
+
import { imageSize } from 'image-size';
|
|
13
|
+
import { create as createContentDisposition } from 'content-disposition';
|
|
14
|
+
import { hashBytes, publicKey, slugifyFilename } from './keys.js';
|
|
15
|
+
import { sanitizeSvg } from './svg-sanitizer.js';
|
|
16
|
+
import { MAX_INPUT_PIXELS } from './transform-directives.js';
|
|
17
|
+
const RASTER_MIME_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp', 'image/gif']);
|
|
18
|
+
const PDF_MIME_TYPE = 'application/pdf';
|
|
19
|
+
const SVG_MIME_TYPE = 'image/svg+xml';
|
|
20
|
+
/** Client-declared content types accepted at presign time (UX only - never trusted at finalize). */
|
|
21
|
+
export const ALLOWED_UPLOAD_CONTENT_TYPES = new Set([
|
|
22
|
+
...RASTER_MIME_TYPES,
|
|
23
|
+
PDF_MIME_TYPE,
|
|
24
|
+
SVG_MIME_TYPE,
|
|
25
|
+
]);
|
|
26
|
+
const PDF_MAX_BYTES = 25 * 1024 * 1024;
|
|
27
|
+
/**
|
|
28
|
+
* Defensive raster byte cap, independent of whatever the configured
|
|
29
|
+
* AssetStore's own upload-size limit happens to be (that limit lives at the
|
|
30
|
+
* store/API boundary, not here) - mirrors PDF_MAX_BYTES's role as a
|
|
31
|
+
* pipeline-level backstop rather than the sole enforcement point.
|
|
32
|
+
*/
|
|
33
|
+
const RASTER_MAX_BYTES = 50 * 1024 * 1024;
|
|
34
|
+
/** EXIF orientation values 5-8 are 90-degree rotations, which swap width/height. */
|
|
35
|
+
const isRotated90 = (orientation) => orientation !== undefined && orientation >= 5 && orientation <= 8;
|
|
36
|
+
const PUBLIC_CACHE_CONTROL = 'public, max-age=31536000, immutable';
|
|
37
|
+
/** U+FEFF (byte-order mark). Built from a char code, not a literal, so this file's source stays pure ASCII. */
|
|
38
|
+
const BOM = String.fromCharCode(0xfeff);
|
|
39
|
+
/**
|
|
40
|
+
* Strip anything that may legally precede the root `<svg>` element: a BOM,
|
|
41
|
+
* whitespace, XML comments, the `<?xml ... ?>` prolog, and a `<!DOCTYPE ...>`
|
|
42
|
+
* declaration - in any order, any number of times (some tools emit more than
|
|
43
|
+
* one comment). Loops until a pass makes no further change.
|
|
44
|
+
*/
|
|
45
|
+
function stripSvgPreamble(text) {
|
|
46
|
+
let stripped = text.startsWith(BOM) ? text.slice(BOM.length) : text;
|
|
47
|
+
let previous;
|
|
48
|
+
do {
|
|
49
|
+
previous = stripped;
|
|
50
|
+
stripped = stripped.replace(/^\s+/, '');
|
|
51
|
+
stripped = stripped.replace(/^<!--[\s\S]*?-->/, '');
|
|
52
|
+
stripped = stripped.replace(/^<\?xml[\s\S]*?\?>/i, '');
|
|
53
|
+
stripped = stripped.replace(/^<!DOCTYPE[\s\S]*?>/i, '');
|
|
54
|
+
} while (stripped !== previous);
|
|
55
|
+
return stripped;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Detect whether `data` is an SVG document. `file-type` cannot do this (SVG is
|
|
59
|
+
* a text format, not magic-byte-detectable) - and, discovered empirically,
|
|
60
|
+
* `file-type` does NOT simply return `undefined` for all SVGs either: an SVG
|
|
61
|
+
* with an `<?xml ?>` prolog gets positively sniffed as generic `application/xml`.
|
|
62
|
+
* So the pipeline's real branch condition is "not a recognized raster/pdf mime"
|
|
63
|
+
* (which covers both file-type returning undefined AND returning some other
|
|
64
|
+
* non-allowlisted guess like application/xml), and within that branch this
|
|
65
|
+
* function does the actual SVG determination: decode as UTF-8 (reject on
|
|
66
|
+
* failure), strip anything that may precede the root element, and require the
|
|
67
|
+
* first element to be `<svg` (case-insensitive).
|
|
68
|
+
*
|
|
69
|
+
* Returns the decoded text on success (so the caller doesn't have to decode
|
|
70
|
+
* twice), or `null` if this is not a decodable SVG document.
|
|
71
|
+
*/
|
|
72
|
+
function sniffSvg(data) {
|
|
73
|
+
let text;
|
|
74
|
+
try {
|
|
75
|
+
text = new TextDecoder('utf-8', { fatal: true }).decode(data);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const stripped = stripSvgPreamble(text);
|
|
81
|
+
return /^<svg[\s>/]/i.test(stripped) ? text : null;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Extract width/height (rasters and svg only - PDFs have no pixel dims).
|
|
85
|
+
* Non-fatal on failure: a corrupt-but-magic-byte-valid file still finalizes,
|
|
86
|
+
* just without dimensions (AssetMeta.width/height are optional).
|
|
87
|
+
*/
|
|
88
|
+
function computeDimensions(data) {
|
|
89
|
+
try {
|
|
90
|
+
const result = imageSize(data);
|
|
91
|
+
let { width, height } = result;
|
|
92
|
+
if (isRotated90(result.orientation)) {
|
|
93
|
+
;
|
|
94
|
+
[width, height] = [height, width];
|
|
95
|
+
}
|
|
96
|
+
return { width, height };
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return {};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Run the finalize pipeline over freshly-uploaded bytes: sniff the real file
|
|
104
|
+
* type (never trust the client-declared contentType beyond presign-time UX),
|
|
105
|
+
* sanitize if svg, hash, extract dimensions, and build the AssetMeta + any
|
|
106
|
+
* public object (svg/pdf only) the caller should persist.
|
|
107
|
+
*
|
|
108
|
+
* Order matters: for SVG, sanitization happens BEFORE hashing - the sanitized
|
|
109
|
+
* bytes are what get hashed and stored, so a later re-sanitization (e.g. a
|
|
110
|
+
* stricter sanitizer version) cannot desync a stored hash from its content.
|
|
111
|
+
*/
|
|
112
|
+
export async function runFinalizePipeline(input) {
|
|
113
|
+
const sniffed = await fileTypeFromBuffer(input.data);
|
|
114
|
+
let kind;
|
|
115
|
+
let mime;
|
|
116
|
+
let ext;
|
|
117
|
+
let canonicalData = input.data;
|
|
118
|
+
if (sniffed && RASTER_MIME_TYPES.has(sniffed.mime)) {
|
|
119
|
+
if (input.data.byteLength > RASTER_MAX_BYTES) {
|
|
120
|
+
return {
|
|
121
|
+
ok: false,
|
|
122
|
+
status: 413,
|
|
123
|
+
error: `Raster image exceeds the ${RASTER_MAX_BYTES}-byte limit`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
kind = 'raster';
|
|
127
|
+
mime = sniffed.mime;
|
|
128
|
+
ext = sniffed.ext;
|
|
129
|
+
}
|
|
130
|
+
else if (sniffed && sniffed.mime === PDF_MIME_TYPE) {
|
|
131
|
+
if (input.data.byteLength > PDF_MAX_BYTES) {
|
|
132
|
+
return { ok: false, status: 413, error: `PDF exceeds the ${PDF_MAX_BYTES}-byte limit` };
|
|
133
|
+
}
|
|
134
|
+
kind = 'pdf';
|
|
135
|
+
mime = PDF_MIME_TYPE;
|
|
136
|
+
ext = sniffed.ext;
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
// Not a recognized raster/pdf - could be a bare/prolog'd SVG, or genuine junk.
|
|
140
|
+
const svgText = sniffSvg(input.data);
|
|
141
|
+
if (!svgText) {
|
|
142
|
+
return { ok: false, status: 415, error: 'Unsupported or unrecognized file type' };
|
|
143
|
+
}
|
|
144
|
+
kind = 'svg';
|
|
145
|
+
mime = SVG_MIME_TYPE;
|
|
146
|
+
ext = 'svg';
|
|
147
|
+
canonicalData = new TextEncoder().encode(sanitizeSvg(svgText));
|
|
148
|
+
}
|
|
149
|
+
const hash32 = hashBytes(canonicalData);
|
|
150
|
+
const { slug } = slugifyFilename(input.filename);
|
|
151
|
+
const dims = kind === 'pdf' ? {} : computeDimensions(canonicalData);
|
|
152
|
+
// Decompression-bomb defense: a raster's on-disk size says nothing about
|
|
153
|
+
// its decoded pixel count (a solid-color 30000x30000 PNG compresses to a
|
|
154
|
+
// few KB) - reject on width*height before this asset is ever finalized, so
|
|
155
|
+
// the transform engine (transform.ts's own `limitInputPixels`) is never the
|
|
156
|
+
// only thing standing between an attacker and a huge decode. SVG is exempt:
|
|
157
|
+
// `computeDimensions` reads its declared viewBox/width/height attributes,
|
|
158
|
+
// not a decoded raster, so there is no analogous decode-time memory cost.
|
|
159
|
+
if (kind === 'raster' && dims.width !== undefined && dims.height !== undefined) {
|
|
160
|
+
const pixelCount = dims.width * dims.height;
|
|
161
|
+
if (pixelCount > MAX_INPUT_PIXELS) {
|
|
162
|
+
return {
|
|
163
|
+
ok: false,
|
|
164
|
+
status: 413,
|
|
165
|
+
error: `Image dimensions ${dims.width}x${dims.height} (${pixelCount} pixels) exceed the ${MAX_INPUT_PIXELS}-pixel limit`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const meta = {
|
|
170
|
+
hash32,
|
|
171
|
+
filename: input.filename,
|
|
172
|
+
slug,
|
|
173
|
+
ext,
|
|
174
|
+
mime,
|
|
175
|
+
size: canonicalData.byteLength,
|
|
176
|
+
...(dims.width !== undefined && { width: dims.width }),
|
|
177
|
+
...(dims.height !== undefined && { height: dims.height }),
|
|
178
|
+
kind,
|
|
179
|
+
...(input.uploadedBy && { uploadedBy: input.uploadedBy }),
|
|
180
|
+
uploadedAt: new Date().toISOString(),
|
|
181
|
+
};
|
|
182
|
+
let publicObject;
|
|
183
|
+
if (kind === 'svg' || kind === 'pdf') {
|
|
184
|
+
// svg: rendered inline by the browser. pdf: downloaded under its real
|
|
185
|
+
// original filename (never the slug) via Content-Disposition, built with
|
|
186
|
+
// the content-disposition package (RFC 5987) rather than string
|
|
187
|
+
// interpolation, so header-injection characters in the filename can never
|
|
188
|
+
// reach a raw HTTP header value.
|
|
189
|
+
const contentDisposition = createContentDisposition(input.filename, {
|
|
190
|
+
type: kind === 'svg' ? 'inline' : 'attachment',
|
|
191
|
+
});
|
|
192
|
+
publicObject = {
|
|
193
|
+
key: publicKey(hash32, slug, ext),
|
|
194
|
+
data: canonicalData,
|
|
195
|
+
contentType: mime,
|
|
196
|
+
contentDisposition,
|
|
197
|
+
cacheControl: PUBLIC_CACHE_CONTROL,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
return { ok: true, meta, data: canonicalData, publicObject };
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=pipeline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pipeline.js","sourceRoot":"","sources":["../../src/assets/pipeline.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,84 @@
|
|
|
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 type { AssetMeta, AssetStore, BeginUploadInput, PublicObject, StagedUploadTarget } from './types';
|
|
7
|
+
export interface LocalAssetStoreOptions {
|
|
8
|
+
/** Root directory. The five bucket prefixes are created as subdirectories under it. */
|
|
9
|
+
root: string;
|
|
10
|
+
}
|
|
11
|
+
export declare class LocalAssetStore implements AssetStore {
|
|
12
|
+
readonly capabilities: {
|
|
13
|
+
directUpload: boolean;
|
|
14
|
+
};
|
|
15
|
+
private readonly root;
|
|
16
|
+
constructor(options: LocalAssetStoreOptions);
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a store key to an absolute filesystem path, guarding against
|
|
19
|
+
* escape from the root. Keys are internally generated (never taken
|
|
20
|
+
* verbatim from user input), but this guard is kept as defense-in-depth
|
|
21
|
+
* (ported from the v1 LocalAssetStore).
|
|
22
|
+
*
|
|
23
|
+
* A naive `resolved.startsWith(root)` check would let a sibling directory
|
|
24
|
+
* that merely shares the root's name as a prefix (e.g. root `.../assets`,
|
|
25
|
+
* sibling `.../assets-evil`) pass. Comparing against `root + path.sep`
|
|
26
|
+
* closes that gap.
|
|
27
|
+
*/
|
|
28
|
+
private resolveKey;
|
|
29
|
+
private headersSidecarPath;
|
|
30
|
+
private writeHeadersSidecar;
|
|
31
|
+
private readHeadersSidecar;
|
|
32
|
+
private readFileOrNull;
|
|
33
|
+
private writeFile;
|
|
34
|
+
private deleteFileIfPresent;
|
|
35
|
+
beginUpload(_input: BeginUploadInput): Promise<StagedUploadTarget>;
|
|
36
|
+
/**
|
|
37
|
+
* Staging methods accept caller-influenced keys (finalize receives the key
|
|
38
|
+
* from the client), so they must never operate outside the staging prefix —
|
|
39
|
+
* otherwise a crafted key turns the staging cleanup into an arbitrary-object
|
|
40
|
+
* delete. The API layer validates too; this is defense-in-depth.
|
|
41
|
+
*/
|
|
42
|
+
private assertStagingKey;
|
|
43
|
+
writeStaging(key: string, data: Uint8Array, _contentType?: string): Promise<void>;
|
|
44
|
+
readStaging(key: string): Promise<Uint8Array | null>;
|
|
45
|
+
deleteStaging(key: string): Promise<void>;
|
|
46
|
+
putOriginal(input: {
|
|
47
|
+
hash32: string;
|
|
48
|
+
ext: string;
|
|
49
|
+
data: Uint8Array;
|
|
50
|
+
contentType: string;
|
|
51
|
+
}): Promise<void>;
|
|
52
|
+
readOriginal(hash32: string): Promise<{
|
|
53
|
+
data: Uint8Array;
|
|
54
|
+
ext: string;
|
|
55
|
+
contentType?: string;
|
|
56
|
+
} | null>;
|
|
57
|
+
putPublicObject(input: {
|
|
58
|
+
key: string;
|
|
59
|
+
data: Uint8Array;
|
|
60
|
+
contentType: string;
|
|
61
|
+
contentDisposition?: string;
|
|
62
|
+
cacheControl?: string;
|
|
63
|
+
}): Promise<void>;
|
|
64
|
+
readPublicObject(key: string): Promise<PublicObject | null>;
|
|
65
|
+
/**
|
|
66
|
+
* Atomic-exclusive create: opens with the `wx` flag so two concurrent
|
|
67
|
+
* writers racing on the same hash32 have exactly one winner ('created')
|
|
68
|
+
* and exactly one loser ('already-exists'). The rename-based
|
|
69
|
+
* `atomicWriteFile` helper is deliberately NOT used here — a rename
|
|
70
|
+
* always clobbers the destination, which would let the second writer
|
|
71
|
+
* silently overwrite the first instead of losing cleanly.
|
|
72
|
+
*/
|
|
73
|
+
putMetaIfAbsent(hash32: string, meta: AssetMeta): Promise<'created' | 'already-exists'>;
|
|
74
|
+
getMeta(hash32: string): Promise<AssetMeta | null>;
|
|
75
|
+
listMeta(input?: {
|
|
76
|
+
cursor?: string;
|
|
77
|
+
limit?: number;
|
|
78
|
+
}): Promise<{
|
|
79
|
+
items: AssetMeta[];
|
|
80
|
+
nextCursor?: string;
|
|
81
|
+
}>;
|
|
82
|
+
deleteMeta(hash32: string): Promise<void>;
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=store-local.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-local.d.ts","sourceRoot":"","sources":["../../src/assets/store-local.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AASH,OAAO,KAAK,EACV,SAAS,EACT,UAAU,EACV,gBAAgB,EAChB,YAAY,EACZ,kBAAkB,EACnB,MAAM,SAAS,CAAA;AAEhB,MAAM,WAAW,sBAAsB;IACrC,uFAAuF;IACvF,IAAI,EAAE,MAAM,CAAA;CACb;AAcD,qBAAa,eAAgB,YAAW,UAAU;IAChD,QAAQ,CAAC,YAAY;;MAA0B;IAC/C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAQ;gBAEjB,OAAO,EAAE,sBAAsB;IAI3C;;;;;;;;;;OAUG;IACH,OAAO,CAAC,UAAU;IASlB,OAAO,CAAC,kBAAkB;YAIZ,mBAAmB;YAInB,kBAAkB;YAUlB,cAAc;YAUd,SAAS;YAIT,mBAAmB;IAU3B,WAAW,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAQxE;;;;;OAKG;IACH,OAAO,CAAC,gBAAgB;IAMlB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKjF,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;IAMX,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;IAsBpE,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;IAUX,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAajE;;;;;;;OAOG;IACG,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,GAAG,gBAAgB,CAAC;IAYvF,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAWlD,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;IAkDI,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAShD"}
|