canopycms 0.0.56 → 0.0.58
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/branch.d.ts +6 -0
- package/dist/api/branch.d.ts.map +1 -1
- package/dist/api/branch.js +63 -19
- package/dist/api/branch.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/content.d.ts +2 -2
- package/dist/api/entries.d.ts +4 -4
- package/dist/api/entries.d.ts.map +1 -1
- package/dist/api/entries.js +10 -2
- package/dist/api/entries.js.map +1 -1
- 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-metadata.d.ts +71 -6
- package/dist/branch-metadata.d.ts.map +1 -1
- package/dist/branch-metadata.js +132 -116
- package/dist/branch-metadata.js.map +1 -1
- package/dist/branch-registry.d.ts +78 -9
- package/dist/branch-registry.d.ts.map +1 -1
- package/dist/branch-registry.js +155 -29
- package/dist/branch-registry.js.map +1 -1
- package/dist/branch-schema-cache.d.ts +126 -11
- package/dist/branch-schema-cache.d.ts.map +1 -1
- package/dist/branch-schema-cache.js +208 -58
- package/dist/branch-schema-cache.js.map +1 -1
- package/dist/cli/generate-ai-content.js +1993 -1506
- package/dist/cli/init.js +16 -4
- package/dist/cli/migrate.d.ts.map +1 -1
- package/dist/cli/migrate.js +25 -0
- package/dist/cli/migrate.js.map +1 -1
- package/dist/cli/sync.js +5 -5
- package/dist/cli/sync.js.map +1 -1
- package/dist/comment-store.d.ts +45 -15
- package/dist/comment-store.d.ts.map +1 -1
- package/dist/comment-store.js +80 -101
- package/dist/comment-store.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 +10 -0
- package/dist/config/schemas/config.d.ts.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 +43 -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/content-index-generation.d.ts +40 -4
- package/dist/content-index-generation.d.ts.map +1 -1
- package/dist/content-index-generation.js +70 -73
- package/dist/content-index-generation.js.map +1 -1
- package/dist/content-store.d.ts +63 -0
- package/dist/content-store.d.ts.map +1 -1
- package/dist/content-store.js +421 -194
- package/dist/content-store.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 +15 -10
- package/dist/git-manager.d.ts.map +1 -1
- package/dist/git-manager.js +16 -11
- 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/resource-generation.d.ts +54 -0
- package/dist/resource-generation.d.ts.map +1 -0
- package/dist/resource-generation.js +156 -0
- package/dist/resource-generation.js.map +1 -0
- package/dist/schema/schema-store.d.ts +31 -4
- package/dist/schema/schema-store.d.ts.map +1 -1
- package/dist/schema/schema-store.js +47 -8
- package/dist/schema/schema-store.js.map +1 -1
- 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/sync-core.js +2 -2
- package/dist/sync-core.js.map +1 -1
- package/dist/utils/occ-json-write.d.ts +100 -0
- package/dist/utils/occ-json-write.d.ts.map +1 -0
- package/dist/utils/occ-json-write.js +234 -0
- package/dist/utils/occ-json-write.js.map +1 -0
- 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.map +1 -1
- package/dist/worker/cms-worker.js +21 -3
- 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,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"}
|
|
@@ -0,0 +1,222 @@
|
|
|
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
|
+
import sanitizeHtml from 'sanitize-html';
|
|
27
|
+
const ALLOWED_SVG_TAGS = [
|
|
28
|
+
'svg',
|
|
29
|
+
'g',
|
|
30
|
+
'defs',
|
|
31
|
+
'symbol',
|
|
32
|
+
'use',
|
|
33
|
+
'title',
|
|
34
|
+
'desc',
|
|
35
|
+
'rect',
|
|
36
|
+
'circle',
|
|
37
|
+
'ellipse',
|
|
38
|
+
'line',
|
|
39
|
+
'polyline',
|
|
40
|
+
'polygon',
|
|
41
|
+
'path',
|
|
42
|
+
'text',
|
|
43
|
+
'tspan',
|
|
44
|
+
'textPath',
|
|
45
|
+
'linearGradient',
|
|
46
|
+
'radialGradient',
|
|
47
|
+
'stop',
|
|
48
|
+
'clipPath',
|
|
49
|
+
'mask',
|
|
50
|
+
'pattern',
|
|
51
|
+
'filter',
|
|
52
|
+
'feGaussianBlur',
|
|
53
|
+
'feOffset',
|
|
54
|
+
'feBlend',
|
|
55
|
+
'feColorMatrix',
|
|
56
|
+
'feComponentTransfer',
|
|
57
|
+
'feFuncA',
|
|
58
|
+
'feFuncR',
|
|
59
|
+
'feFuncG',
|
|
60
|
+
'feFuncB',
|
|
61
|
+
'feComposite',
|
|
62
|
+
'feFlood',
|
|
63
|
+
'feMerge',
|
|
64
|
+
'feMergeNode',
|
|
65
|
+
'feMorphology',
|
|
66
|
+
'feTile',
|
|
67
|
+
'feTurbulence',
|
|
68
|
+
'feDropShadow',
|
|
69
|
+
'feDisplacementMap',
|
|
70
|
+
'image',
|
|
71
|
+
'a',
|
|
72
|
+
'marker',
|
|
73
|
+
'metadata',
|
|
74
|
+
];
|
|
75
|
+
const ALLOWED_SVG_ATTRS = [
|
|
76
|
+
'id',
|
|
77
|
+
'class',
|
|
78
|
+
'style',
|
|
79
|
+
'transform',
|
|
80
|
+
'fill',
|
|
81
|
+
'fill-rule',
|
|
82
|
+
'fill-opacity',
|
|
83
|
+
'stroke',
|
|
84
|
+
'stroke-width',
|
|
85
|
+
'stroke-linecap',
|
|
86
|
+
'stroke-linejoin',
|
|
87
|
+
'stroke-dasharray',
|
|
88
|
+
'stroke-opacity',
|
|
89
|
+
'opacity',
|
|
90
|
+
'd',
|
|
91
|
+
'x',
|
|
92
|
+
'y',
|
|
93
|
+
'width',
|
|
94
|
+
'height',
|
|
95
|
+
'cx',
|
|
96
|
+
'cy',
|
|
97
|
+
'r',
|
|
98
|
+
'rx',
|
|
99
|
+
'ry',
|
|
100
|
+
'x1',
|
|
101
|
+
'y1',
|
|
102
|
+
'x2',
|
|
103
|
+
'y2',
|
|
104
|
+
'points',
|
|
105
|
+
'viewBox',
|
|
106
|
+
'preserveAspectRatio',
|
|
107
|
+
'xmlns',
|
|
108
|
+
'xmlns:xlink',
|
|
109
|
+
'version',
|
|
110
|
+
'offset',
|
|
111
|
+
'stop-color',
|
|
112
|
+
'stop-opacity',
|
|
113
|
+
'gradientUnits',
|
|
114
|
+
'gradientTransform',
|
|
115
|
+
'patternUnits',
|
|
116
|
+
'patternTransform',
|
|
117
|
+
'href',
|
|
118
|
+
'xlink:href',
|
|
119
|
+
'clip-path',
|
|
120
|
+
'mask',
|
|
121
|
+
'filter',
|
|
122
|
+
'font-family',
|
|
123
|
+
'font-size',
|
|
124
|
+
'font-weight',
|
|
125
|
+
'text-anchor',
|
|
126
|
+
'dx',
|
|
127
|
+
'dy',
|
|
128
|
+
'marker-start',
|
|
129
|
+
'marker-mid',
|
|
130
|
+
'marker-end',
|
|
131
|
+
];
|
|
132
|
+
/**
|
|
133
|
+
* Elements whose entire subtree (tags AND text) is discarded when they are not
|
|
134
|
+
* in the allowlist. Without this, sanitize-html's default `disallowedTagsMode:
|
|
135
|
+
* 'discard'` removes the tag itself but leaves loose text content behind (e.g.
|
|
136
|
+
* a stripped `<foreignObject><div>evil</div></foreignObject>` would otherwise
|
|
137
|
+
* leave a bare "evil" text node in the sanitized output).
|
|
138
|
+
*/
|
|
139
|
+
const NON_TEXT_TAGS = ['script', 'style', 'textarea', 'option', 'foreignObject'];
|
|
140
|
+
/**
|
|
141
|
+
* Presentation attributes (beyond `style`) whose SVG grammar accepts a
|
|
142
|
+
* `<funciri>` (`url(...)`) value - `fill`/`stroke` reference paint servers
|
|
143
|
+
* (gradients/patterns), `filter`/`mask`/`clip-path` reference filter-effect
|
|
144
|
+
* elements, and the three `marker-*` attributes reference `<marker>`
|
|
145
|
+
* elements. Every one of these is in `ALLOWED_SVG_ATTRS`, so every one of
|
|
146
|
+
* them is a route for the same external-fetch beacon `style`'s `url(...)`
|
|
147
|
+
* hardening (below) already closes: `<rect fill="url(https://evil/beacon)">`
|
|
148
|
+
* fetches externally the moment the SVG is opened as a document, exactly
|
|
149
|
+
* like a `style="background:url(...)"` would.
|
|
150
|
+
*/
|
|
151
|
+
const URL_BEARING_ATTRS = new Set([
|
|
152
|
+
'style',
|
|
153
|
+
'fill',
|
|
154
|
+
'stroke',
|
|
155
|
+
'filter',
|
|
156
|
+
'mask',
|
|
157
|
+
'clip-path',
|
|
158
|
+
'marker-start',
|
|
159
|
+
'marker-mid',
|
|
160
|
+
'marker-end',
|
|
161
|
+
]);
|
|
162
|
+
/**
|
|
163
|
+
* True when `value` is safe for a URL-bearing presentation attribute: either
|
|
164
|
+
* it has no `url(...)` funciri and no backslash at all (the common case -
|
|
165
|
+
* plain colors, `none`, etc.), or its only `url(...)` reference is a local
|
|
166
|
+
* fragment (`url(#foo)`) - the same in-document-only allowance `href`/
|
|
167
|
+
* `xlink:href` get, and exactly what gradients/filters/markers legitimately
|
|
168
|
+
* need to reference document-local `<linearGradient>`/`<filter>`/`<marker>`
|
|
169
|
+
* defs. Backslashes are rejected unconditionally (even alongside an
|
|
170
|
+
* otherwise-local `url(#foo)`): CSS escapes (e.g. `\75 rl(...)` -> `url(...)`)
|
|
171
|
+
* let a crafted value slip past a plain `url(` text match yet still resolve
|
|
172
|
+
* to an external fetch in the browser's CSS/SVG value parser, and
|
|
173
|
+
* backslashes have no legitimate use in any of these attribute values.
|
|
174
|
+
*/
|
|
175
|
+
function isSafeUrlBearingValue(value) {
|
|
176
|
+
if (value.includes('\\'))
|
|
177
|
+
return false;
|
|
178
|
+
const urlMatches = value.matchAll(/url\s*\(\s*(['"]?)\s*([^)'"]*?)\s*\1\s*\)/gi);
|
|
179
|
+
let sawUrl = false;
|
|
180
|
+
for (const match of urlMatches) {
|
|
181
|
+
sawUrl = true;
|
|
182
|
+
if (!match[2].startsWith('#'))
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
return sawUrl ? true : !/url\s*\(/i.test(value);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Sanitize an SVG document (as text). Strips scripts, event-handler attributes,
|
|
189
|
+
* `<foreignObject>` (and its content), inline `<style>` (CSS injection surface),
|
|
190
|
+
* any `href`/`xlink:href` that isn't a local fragment reference, and any
|
|
191
|
+
* `url(...)` reference on a paint/filter/marker attribute that isn't a local
|
|
192
|
+
* fragment reference (external-fetch tracking beacon).
|
|
193
|
+
*
|
|
194
|
+
* Input must already be confirmed to be an SVG document (see `sniffSvg` in
|
|
195
|
+
* pipeline.ts) - this function does not re-validate the root element.
|
|
196
|
+
*/
|
|
197
|
+
export function sanitizeSvg(svgText) {
|
|
198
|
+
return sanitizeHtml(svgText, {
|
|
199
|
+
allowedTags: ALLOWED_SVG_TAGS,
|
|
200
|
+
allowedAttributes: { '*': ALLOWED_SVG_ATTRS },
|
|
201
|
+
nonTextTags: NON_TEXT_TAGS,
|
|
202
|
+
// xmlMode makes htmlparser2 treat this as XML (self-closing tags, no HTML
|
|
203
|
+
// element-specific parsing quirks) rather than HTML, which matters for SVG.
|
|
204
|
+
parser: { xmlMode: true, decodeEntities: true },
|
|
205
|
+
transformTags: {
|
|
206
|
+
'*': (tagName, attribs) => {
|
|
207
|
+
const filtered = {};
|
|
208
|
+
for (const [key, value] of Object.entries(attribs)) {
|
|
209
|
+
if ((key === 'href' || key === 'xlink:href') && !value.startsWith('#')) {
|
|
210
|
+
continue; // drop external/non-local references
|
|
211
|
+
}
|
|
212
|
+
if (URL_BEARING_ATTRS.has(key) && !isSafeUrlBearingValue(value)) {
|
|
213
|
+
continue; // drop external/non-local url(...) references (or any backslash)
|
|
214
|
+
}
|
|
215
|
+
filtered[key] = value;
|
|
216
|
+
}
|
|
217
|
+
return { tagName, attribs: filtered };
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=svg-sanitizer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"svg-sanitizer.js","sourceRoot":"","sources":["../../src/assets/svg-sanitizer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure parser/formatter for `/assets/t/{directives}/{hash32}/{slug}.{ext}`
|
|
3
|
+
* transform URLs. Isomorphic and dependency-free by design: this module has
|
|
4
|
+
* NO imports (not even other files in this directory) so it can be imported
|
|
5
|
+
* from client bundles (via `assetUrl`/`assetSrcSet` in asset-url.ts, exported
|
|
6
|
+
* off the package's main entry) as well as from the server-only transform
|
|
7
|
+
* engine (transform.ts) and the future prod transform Lambda (PR 7), without
|
|
8
|
+
* ever pulling in node:crypto, sharp, or any other server-only dependency.
|
|
9
|
+
*
|
|
10
|
+
* `{directives}` is either the literal identity token (`orig`) or a
|
|
11
|
+
* comma-separated list of `key=value` pairs drawn from:
|
|
12
|
+
* w={int} output width - allowlisted to multiples of 160 in [160, 4096]
|
|
13
|
+
* (bounds cache-stuffing; upscaling is rejected at transform
|
|
14
|
+
* time via `withoutEnlargement`, not here)
|
|
15
|
+
* f={fmt} output format: webp | jpeg | png - when present, the URL's
|
|
16
|
+
* `{ext}` must equal it exactly
|
|
17
|
+
* q={int} quality 1..100 (encoder-dependent)
|
|
18
|
+
* c={rect} normalized crop rect `x:y:w:h`, four floats in [0,1] with
|
|
19
|
+
* x+w<=1 and y+h<=1, w>0 and h>0 (colon-separated - commas are
|
|
20
|
+
* the directive separator)
|
|
21
|
+
*
|
|
22
|
+
* Duplicate keys, unknown keys, and empty directive strings are rejected.
|
|
23
|
+
* `orig` must appear alone.
|
|
24
|
+
*/
|
|
25
|
+
/** The identity transform directive name - EXIF-strip only, no other change. */
|
|
26
|
+
export declare const IDENTITY_TRANSFORM_DIRECTIVE = "orig";
|
|
27
|
+
export type OutputFormat = 'webp' | 'jpeg' | 'png';
|
|
28
|
+
export interface CropRect {
|
|
29
|
+
readonly x: number;
|
|
30
|
+
readonly y: number;
|
|
31
|
+
readonly w: number;
|
|
32
|
+
readonly h: number;
|
|
33
|
+
}
|
|
34
|
+
/** Identity ('orig'): EXIF-strip only, no resize/format/quality change. */
|
|
35
|
+
export interface IdentityDirectives {
|
|
36
|
+
readonly identity: true;
|
|
37
|
+
}
|
|
38
|
+
/** Non-identity directive set - every field optional, but at least one is present. */
|
|
39
|
+
export interface ResizeDirectives {
|
|
40
|
+
readonly identity: false;
|
|
41
|
+
readonly width?: number;
|
|
42
|
+
readonly format?: OutputFormat;
|
|
43
|
+
readonly quality?: number;
|
|
44
|
+
readonly crop?: CropRect;
|
|
45
|
+
}
|
|
46
|
+
export type TransformDirectives = IdentityDirectives | ResizeDirectives;
|
|
47
|
+
export interface ParsedTransformPath {
|
|
48
|
+
readonly directives: TransformDirectives;
|
|
49
|
+
readonly hash32: string;
|
|
50
|
+
readonly slug: string;
|
|
51
|
+
readonly ext: string;
|
|
52
|
+
}
|
|
53
|
+
export type ParseTransformPathResult = ({
|
|
54
|
+
readonly ok: true;
|
|
55
|
+
} & ParsedTransformPath) | {
|
|
56
|
+
readonly ok: false;
|
|
57
|
+
readonly error: string;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Decompression-bomb cap: the max `width * height` (post-decode pixel count)
|
|
61
|
+
* any raster the pipeline handles may have - checked both at upload finalize
|
|
62
|
+
* time (pipeline.ts, against the raw source dims) and at transform decode
|
|
63
|
+
* time (transform.ts, via sharp's `limitInputPixels`). Shared here (rather
|
|
64
|
+
* than duplicated in each server-only module) so it stays a single number,
|
|
65
|
+
* and because this file is dependency-free/isomorphic it can be imported by
|
|
66
|
+
* both without pulling sharp or file-type into anything. 4096x4096 (16.7 MP)
|
|
67
|
+
* comfortably covers the largest output this system ever serves (MAX_WIDTH)
|
|
68
|
+
* while still bounding decode-time memory for a small/compressible-but-huge
|
|
69
|
+
* source (e.g. a 30000x30000 solid-color PNG).
|
|
70
|
+
*/
|
|
71
|
+
export declare const MAX_INPUT_PIXELS: number;
|
|
72
|
+
/** True if `width` is on the allowlist: a multiple of 160 in [160, 4096]. */
|
|
73
|
+
export declare function isAllowedTransformWidth(width: number): boolean;
|
|
74
|
+
/** True if `quality` is on the allowlist: a multiple of 5 in [30, 95]. */
|
|
75
|
+
export declare function isAllowedTransformQuality(quality: number): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* True when x, y, w, h describe a valid normalized crop rect: all four values
|
|
78
|
+
* finite and in [0,1], w and h strictly positive, and the rect fits within
|
|
79
|
+
* bounds (x+w<=1, y+h<=1). Shared by the URL-directive parser (`parseCrop`,
|
|
80
|
+
* below, from a `x:y:w:h` string) and image field value validation (from a
|
|
81
|
+
* `{x,y,w,h}` object, in validation/entry-validator.ts) so both enforce
|
|
82
|
+
* identical constraints.
|
|
83
|
+
*/
|
|
84
|
+
export declare function isValidCropRect(x: number, y: number, w: number, h: number): boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Parse the three path segments after the `assets/t/` prefix:
|
|
87
|
+
* `[directivesRaw, hash32, "{slug}.{ext}"]`. The raw route strips the
|
|
88
|
+
* `assets/t/` prefix and splits the remaining key by `/` before calling this.
|
|
89
|
+
*/
|
|
90
|
+
export declare function parseTransformPath(segments: readonly string[]): ParseTransformPathResult;
|
|
91
|
+
/**
|
|
92
|
+
* Canonical string form of a directive set, so equivalent directive sets
|
|
93
|
+
* (different key order, different float formatting) always map to the same
|
|
94
|
+
* cache key. Order is fixed alphabetically by key: c, f, q, w.
|
|
95
|
+
*/
|
|
96
|
+
export declare function formatDirectives(directives: TransformDirectives): string;
|
|
97
|
+
//# sourceMappingURL=transform-directives.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transform-directives.d.ts","sourceRoot":"","sources":["../../src/assets/transform-directives.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,gFAAgF;AAChF,eAAO,MAAM,4BAA4B,SAAS,CAAA;AAElD,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,CAAA;AAElD,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,2EAA2E;AAC3E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAA;CACxB;AAED,sFAAsF;AACtF,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAA;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,CAAA;IAC9B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAA;CACzB;AAED,MAAM,MAAM,mBAAmB,GAAG,kBAAkB,GAAG,gBAAgB,CAAA;AAEvE,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,UAAU,EAAE,mBAAmB,CAAA;IACxC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,MAAM,wBAAwB,GAChC,CAAC;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG,mBAAmB,CAAC,GAC7C;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAMlD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gBAAgB,QAAc,CAAA;AAgC3C,6EAA6E;AAC7E,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAI9D;AAQD,0EAA0E;AAC1E,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAOlE;AAcD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAOnF;AA0FD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,GAAG,wBAAwB,CAkCxF;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,mBAAmB,GAAG,MAAM,CA4BxE"}
|