@rathnasgala2/template 2.0.0

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.
Files changed (43) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +515 -0
  3. package/contracts/render-policy.jcs +1 -0
  4. package/contracts/theme-styling-contract.jcs +1 -0
  5. package/package.json +96 -0
  6. package/src/core/errors.js +136 -0
  7. package/src/core/index.js +758 -0
  8. package/src/core/internal/appearance/bootstrap-script.js +159 -0
  9. package/src/core/internal/appearance/contract.js +193 -0
  10. package/src/core/internal/appearance/controller-markup.js +80 -0
  11. package/src/core/internal/appearance/styling-contract.js +509 -0
  12. package/src/core/internal/canonical-jcs.js +0 -0
  13. package/src/core/internal/content-security.js +0 -0
  14. package/src/core/internal/eleventy-render.js +220 -0
  15. package/src/core/internal/feeds.js +161 -0
  16. package/src/core/internal/fs-walk.js +38 -0
  17. package/src/core/internal/media/avif-probe.js +169 -0
  18. package/src/core/internal/media/font-policy.js +56 -0
  19. package/src/core/internal/media/jpeg-codec.js +0 -0
  20. package/src/core/internal/media/limits.js +129 -0
  21. package/src/core/internal/media/pipeline.js +353 -0
  22. package/src/core/internal/media/png-codec.js +406 -0
  23. package/src/core/internal/media/resize.js +147 -0
  24. package/src/core/internal/media/sniff.js +109 -0
  25. package/src/core/internal/media/webp-probe.js +139 -0
  26. package/src/core/internal/messages.js +78 -0
  27. package/src/core/internal/page-kinds.js +709 -0
  28. package/src/core/internal/provenance.js +49 -0
  29. package/src/core/internal/render-policy-content.js +295 -0
  30. package/src/core/internal/route-labels.js +68 -0
  31. package/src/core/internal/route.js +167 -0
  32. package/src/core/internal/search-index.js +119 -0
  33. package/src/core/internal/seo.js +114 -0
  34. package/src/core/internal/sitemap.js +77 -0
  35. package/src/core/internal/skeleton.js +413 -0
  36. package/src/core/internal/source-inventory.js +145 -0
  37. package/src/core/internal/text-direction.js +59 -0
  38. package/src/core/internal/theme-assets.js +387 -0
  39. package/src/core/internal/uuid.js +51 -0
  40. package/src/core/manifest.js +0 -0
  41. package/types/eleventy-plugin-syntaxhighlight.d.ts +26 -0
  42. package/types/eleventy.d.ts +38 -0
  43. package/types/index.d.ts +347 -0
@@ -0,0 +1,129 @@
1
+ import { MediaPipelineError } from '../../errors.js';
2
+
3
+ /**
4
+ * Media pipeline resource ceilings (brief S2 section 3, S2-T05: "bounded
5
+ * decoding worker", "decompression and resource limits").
6
+ *
7
+ * The raster-specific numbers below are DEC-097's own closed
8
+ * `gala-theme-binary-assets-v2` bounds (decision document section titled
9
+ * "Binary theme assets"): non-animated raster, width/height each `1..8192`,
10
+ * at most 16,777,216 pixels, at most 67,108,864 decoded RGBA8 buffer bytes,
11
+ * source raster at most 16,777,216 bytes, source font at most 4,194,304
12
+ * bytes. DEC-097 states those bounds for the closed theme *passive-asset*
13
+ * decoder profile; the brief's author-media pipeline (S2-T05) gives no
14
+ * separate numeric profile of its own, so this renderer reuses the same
15
+ * numbers for the author-media decode-bound worker rather than inventing an
16
+ * unrelated set — the physical decode-bomb risk (integer overflow, unbounded
17
+ * allocation) is identical for both call sites. This is an explicit,
18
+ * documented implementer decision, not a value taken from author-media text.
19
+ *
20
+ * DEC-097's separate "document-22 operational release" MVP launch gate also
21
+ * fixes a smaller general per-source-file ceiling (10,485,760 bytes) that
22
+ * applies to *every* repository source file regardless of kind, with "the
23
+ * smaller declared per-file/package budget still wins." Applied here: the
24
+ * effective per-image source-byte ceiling is `min(16,777,216, 10,485,760)`.
25
+ * The font ceiling (4,194,304) is already smaller than the general cap, so
26
+ * it is unaffected.
27
+ *
28
+ * `MAX_IMAGES_PER_PUBLICATION` and `MAX_TOTAL_MEDIA_BYTES_PER_PUBLICATION`
29
+ * are not fixed anywhere in the brief or DEC-097; there is no cited
30
+ * per-publication *media-specific* aggregate. These two values are this
31
+ * renderer's own conservative, documented ceilings (well under DEC-097's
32
+ * unrelated 50,000-file / 1,073,741,824-byte whole-artifact ceiling), so a
33
+ * publication cannot exhaust the media pipeline's bounded decode budget by
34
+ * sheer reference count even though each individual file is itself bounded.
35
+ */
36
+
37
+ /** Maximum admitted raster width or height, in pixels, inclusive. */
38
+ export const MAX_IMAGE_DIMENSION = 8192;
39
+
40
+ /** Maximum admitted decoded pixel count (`width * height`). */
41
+ export const MAX_IMAGE_PIXELS = 16_777_216;
42
+
43
+ /** Maximum admitted decoded RGBA8 output buffer size, in bytes. */
44
+ export const MAX_DECODED_RGBA_BYTES = 67_108_864;
45
+
46
+ /**
47
+ * Maximum admitted raster source byte length: the smaller of DEC-097's
48
+ * raster-specific cap (16,777,216) and its general per-source-file cap
49
+ * (10,485,760).
50
+ */
51
+ export const MAX_IMAGE_SOURCE_BYTES = 10_485_760;
52
+
53
+ /**
54
+ * Maximum admitted font source byte length (DEC-097's binary font cap,
55
+ * already smaller than the general per-source-file cap).
56
+ */
57
+ export const MAX_FONT_SOURCE_BYTES = 4_194_304;
58
+
59
+ /**
60
+ * Maximum distinct image references a single publication's media pipeline
61
+ * will process (implementer-chosen; see module documentation).
62
+ */
63
+ export const MAX_IMAGES_PER_PUBLICATION = 2048;
64
+
65
+ /**
66
+ * Maximum distinct font references a single publication's media pipeline
67
+ * will process. `appearance.fontAssets` is already schema-bounded to at
68
+ * most 8 entries (`resolvedFile[0..8]`); this constant documents that same
69
+ * number as the media pipeline's own font-count ceiling rather than
70
+ * re-deriving it from the schema at runtime.
71
+ */
72
+ export const MAX_FONTS_PER_PUBLICATION = 8;
73
+
74
+ /**
75
+ * Maximum aggregate decoded-plus-derivative byte budget the media pipeline
76
+ * will spend for one publication (implementer-chosen; see module
77
+ * documentation). Comfortably under DEC-097's unrelated 1,073,741,824-byte
78
+ * whole-artifact ceiling, which this constant does not replace or relax.
79
+ */
80
+ export const MAX_TOTAL_MEDIA_BYTES_PER_PUBLICATION = 268_435_456;
81
+
82
+ /**
83
+ * Assert that a decoded or declared raster's dimensions are within the
84
+ * shared decode ceilings, shared by every format-specific decoder/probe in
85
+ * this pipeline so the same check (and the same jscpd-flagged duplication
86
+ * it would otherwise create across `png-codec.js`, `webp-probe.js` and
87
+ * `avif-probe.js`) exists exactly once.
88
+ *
89
+ * @param {number} width declared or decoded width, in pixels
90
+ * @param {number} height declared or decoded height, in pixels
91
+ * @param {string} referencePath the source path, for error messages
92
+ * @param {string} formatLabel a short format name for the error message
93
+ * (for example `"PNG"`, `"WebP"`, `"AVIF"`)
94
+ * @returns {void}
95
+ */
96
+ export function assertDimensionsWithinCeiling(
97
+ width,
98
+ height,
99
+ referencePath,
100
+ formatLabel,
101
+ ) {
102
+ if (
103
+ width < 1 ||
104
+ height < 1 ||
105
+ width > MAX_IMAGE_DIMENSION ||
106
+ height > MAX_IMAGE_DIMENSION ||
107
+ width * height > MAX_IMAGE_PIXELS
108
+ ) {
109
+ throw new MediaPipelineError(
110
+ 'MEDIA_RESOURCE_EXCEEDED',
111
+ `${formatLabel} dimensions ${width}x${height} exceed the admitted decode ceiling`,
112
+ referencePath,
113
+ );
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Non-resetting wall-clock budget for one file's bounded decode, in
119
+ * milliseconds. DEC-097's own closed binary-asset decoder uses a ten-second
120
+ * non-resetting deadline; this renderer reuses that figure for the same
121
+ * documented reason as the size/pixel ceilings above.
122
+ */
123
+ export const DECODE_DEADLINE_MS = 10_000;
124
+
125
+ /** Deterministic responsive derivative widths, in pixels, ascending. */
126
+ export const DERIVATIVE_WIDTHS = Object.freeze([320, 640, 960, 1280, 1920]);
127
+
128
+ /** Fixed JPEG derivative encode quality (jpeg-js `quality` option, 0..100). */
129
+ export const DERIVATIVE_JPEG_QUALITY = 82;
@@ -0,0 +1,353 @@
1
+ /**
2
+ * Media pipeline orchestration (brief S2 section 3, S2-T05).
3
+ *
4
+ * `build-input:2.0.0` never carries raw asset bytes for a `resolvedFile`
5
+ * reference — only its repository-relative `path` and a `sourceDigest`
6
+ * (DEC-097 section 6: "`build` exposes this decoded set as a read-only
7
+ * source mount"). This pipeline therefore reads actual bytes from
8
+ * `options.sourceDirectory`, the caller-mounted read-only source tree, and
9
+ * fails closed (`MEDIA_SOURCE_DIGEST_MISMATCH`) if what it reads there does
10
+ * not hash to the digest `build-input` declared — the same fail-closed
11
+ * posture as every other adapter boundary in this renderer.
12
+ *
13
+ * Every distinct image/font reference in the publication (publication
14
+ * default image, each author's avatar, each content record's hero and
15
+ * social image, the appearance package's brand mark, wordmark and font
16
+ * assets) is collected once, deduplicated by path, processed independently,
17
+ * and turned into one or more content-addressed output files under
18
+ * `assets/media/` in the candidate output directory plus one
19
+ * `manifestAsset`-shaped row per output file. `manifestAsset` has no field
20
+ * for a separate source/transform/output digest triple or for width/height
21
+ * (`grep`-verified against the published schema); this pipeline expresses
22
+ * that provenance the only way the closed shape allows: the source digest a
23
+ * derivative was produced from is the same path's `manifestIncludedSource`
24
+ * row (already emitted by `collectIncludedSources`), the output digest is
25
+ * the asset row's own `sha256`, and the width/format transform is encoded
26
+ * in the content-addressed path and `mediaType` themselves.
27
+ */
28
+
29
+ import { createHash } from 'node:crypto';
30
+ import { readFile } from 'node:fs/promises';
31
+ import path from 'node:path';
32
+
33
+ import { MediaPipelineError } from '../../errors.js';
34
+ import { decodeJpeg, encodeJpeg } from './jpeg-codec.js';
35
+ import {
36
+ MAX_FONTS_PER_PUBLICATION,
37
+ MAX_IMAGES_PER_PUBLICATION,
38
+ MAX_IMAGE_SOURCE_BYTES,
39
+ MAX_TOTAL_MEDIA_BYTES_PER_PUBLICATION,
40
+ DERIVATIVE_WIDTHS,
41
+ } from './limits.js';
42
+ import { assertFontPolicy } from './font-policy.js';
43
+ import { decodePng, encodePng } from './png-codec.js';
44
+ import { applyExifOrientation, resizeRaster } from './resize.js';
45
+ import { MEDIA_TYPE_BY_FORMAT, sniffMediaFormat } from './sniff.js';
46
+ import { probeAvif } from './avif-probe.js';
47
+ import { probeWebp } from './webp-probe.js';
48
+
49
+ /** @type {Readonly<Record<string, string>>} media type -> file extension */
50
+ const EXTENSION_BY_MEDIA_TYPE = Object.freeze({
51
+ 'image/png': 'png',
52
+ 'image/jpeg': 'jpg',
53
+ 'image/webp': 'webp',
54
+ 'image/avif': 'avif',
55
+ 'font/woff2': 'woff2',
56
+ });
57
+
58
+ /**
59
+ * @typedef {{path: string, sourceDigest: string}} ImageReference
60
+ */
61
+
62
+ /**
63
+ * Collect every distinct image reference in `buildInput`, deduplicated by
64
+ * path (rejecting a path reused with a conflicting digest).
65
+ *
66
+ * @param {import('../../../../types/index.d.ts').NormalizedBuildInput} buildInput
67
+ * the validated build input
68
+ * @returns {ImageReference[]} the distinct image references, in
69
+ * first-seen order
70
+ */
71
+ function collectImageReferences(buildInput) {
72
+ /** @type {Map<string, ImageReference>} */
73
+ const byPath = new Map();
74
+ /** @type {(ref: ImageReference | undefined) => void} */
75
+ const add = (ref) => {
76
+ if (!ref) return;
77
+ const existing = byPath.get(ref.path);
78
+ if (existing && existing.sourceDigest !== ref.sourceDigest) {
79
+ throw new MediaPipelineError(
80
+ 'MEDIA_SOURCE_DIGEST_MISMATCH',
81
+ 'the same path is referenced with two different declared digests',
82
+ ref.path,
83
+ );
84
+ }
85
+ byPath.set(ref.path, ref);
86
+ };
87
+
88
+ add(buildInput.publication.defaultImage);
89
+ for (const author of buildInput.authors) add(author.avatar);
90
+ for (const record of buildInput.content) {
91
+ if (record.frontmatter.hero) add(record.frontmatter.hero.file);
92
+ add(record.frontmatter.socialImage);
93
+ }
94
+ add(buildInput.appearance.brandMark);
95
+ add(buildInput.appearance.wordmark);
96
+
97
+ return [...byPath.values()];
98
+ }
99
+
100
+ /**
101
+ * Collect every distinct font reference in `buildInput`, deduplicated by
102
+ * path.
103
+ *
104
+ * @param {import('../../../../types/index.d.ts').NormalizedBuildInput} buildInput
105
+ * the validated build input
106
+ * @returns {ImageReference[]} the distinct font references
107
+ */
108
+ function collectFontReferences(buildInput) {
109
+ /** @type {Map<string, ImageReference>} */
110
+ const byPath = new Map();
111
+ for (const font of buildInput.appearance.fontAssets ?? []) {
112
+ byPath.set(font.path, font);
113
+ }
114
+ return [...byPath.values()];
115
+ }
116
+
117
+ /**
118
+ * Read one referenced source file and verify its bytes match the declared
119
+ * digest, failing closed on any mismatch, traversal attempt, or oversize
120
+ * file.
121
+ *
122
+ * @param {string} sourceDirectory the caller-mounted read-only source root
123
+ * @param {ImageReference} reference the reference to read and verify
124
+ * @returns {Promise<Buffer>} the verified file bytes
125
+ */
126
+ async function readAndVerify(sourceDirectory, reference) {
127
+ const resolved = path.resolve(sourceDirectory, reference.path);
128
+ const root = path.resolve(sourceDirectory) + path.sep;
129
+ if (!resolved.startsWith(root)) {
130
+ throw new MediaPipelineError(
131
+ 'MEDIA_FORMAT_INVALID',
132
+ 'reference path escapes the source directory',
133
+ reference.path,
134
+ );
135
+ }
136
+ let bytes;
137
+ try {
138
+ bytes = await readFile(resolved);
139
+ } catch {
140
+ throw new MediaPipelineError(
141
+ 'MEDIA_FORMAT_INVALID',
142
+ 'referenced source file is missing or unreadable',
143
+ reference.path,
144
+ );
145
+ }
146
+ if (bytes.length > MAX_IMAGE_SOURCE_BYTES) {
147
+ throw new MediaPipelineError(
148
+ 'MEDIA_RESOURCE_EXCEEDED',
149
+ `source is ${bytes.length} bytes, exceeding the ${MAX_IMAGE_SOURCE_BYTES}-byte ceiling`,
150
+ reference.path,
151
+ );
152
+ }
153
+ const actualDigest = `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
154
+ if (actualDigest !== reference.sourceDigest) {
155
+ throw new MediaPipelineError(
156
+ 'MEDIA_SOURCE_DIGEST_MISMATCH',
157
+ `source bytes hash to ${actualDigest} but build-input declared ${reference.sourceDigest}`,
158
+ reference.path,
159
+ );
160
+ }
161
+ return bytes;
162
+ }
163
+
164
+ /**
165
+ * @param {Buffer} bytes derivative file bytes
166
+ * @returns {string} `sha256:` plus 64 lowercase hexadecimal characters
167
+ */
168
+ function digest(bytes) {
169
+ return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
170
+ }
171
+
172
+ /**
173
+ * Build the content-addressed output path for one media variant.
174
+ *
175
+ * @param {string} sourceDigest the reference's declared `sourceDigest`
176
+ * @param {string} variant `'original'` or `'<width>w'`
177
+ * @param {string} extension the file extension (no leading dot)
178
+ * @returns {string} a `repoRelativePath`-shaped output path
179
+ */
180
+ function derivativePath(sourceDigest, variant, extension) {
181
+ const hex = sourceDigest.replace(/^sha256:/, '');
182
+ return `assets/media/${hex}/${variant}.${extension}`;
183
+ }
184
+
185
+ /**
186
+ * @typedef {{path: string, bytes: Buffer}} PipelineOutputFile
187
+ */
188
+
189
+ /**
190
+ * Process one image reference into its canonical original plus every
191
+ * admitted responsive derivative.
192
+ *
193
+ * @param {Buffer} bytes verified source bytes
194
+ * @param {ImageReference} reference the reference these bytes back
195
+ * @returns {PipelineOutputFile[]} the output files this reference produces
196
+ */
197
+ function processImage(bytes, reference) {
198
+ const format = sniffMediaFormat(bytes);
199
+ if (format === 'svg') {
200
+ throw new MediaPipelineError(
201
+ 'MEDIA_SVG_REJECTED',
202
+ 'SVG is never processed as author media',
203
+ reference.path,
204
+ );
205
+ }
206
+ if (format === 'unknown') {
207
+ throw new MediaPipelineError(
208
+ 'MEDIA_FORMAT_INVALID',
209
+ 'unrecognized image format (checked from decoded bytes, not extension)',
210
+ reference.path,
211
+ );
212
+ }
213
+
214
+ if (format === 'webp' || format === 'avif') {
215
+ // Bounded container/dimension validation only; see webp-probe.js and
216
+ // avif-probe.js module documentation for the documented scope decision.
217
+ if (format === 'webp') {
218
+ probeWebp(bytes, reference.path);
219
+ } else {
220
+ probeAvif(bytes, reference.path);
221
+ }
222
+ const extension = EXTENSION_BY_MEDIA_TYPE[MEDIA_TYPE_BY_FORMAT[format]];
223
+ return [
224
+ {
225
+ path: derivativePath(reference.sourceDigest, 'original', extension),
226
+ bytes,
227
+ },
228
+ ];
229
+ }
230
+
231
+ /** @type {{width: number, height: number, rgba: Buffer}} */
232
+ let oriented;
233
+ /** @type {(raster: {width: number, height: number, rgba: Buffer}) => Buffer} */
234
+ let encode;
235
+ /** @type {'png' | 'jpg'} */
236
+ let extension;
237
+ if (format === 'png') {
238
+ oriented = decodePng(bytes, reference.path);
239
+ encode = encodePng;
240
+ extension = 'png';
241
+ } else {
242
+ const decodedJpeg = decodeJpeg(bytes, reference.path);
243
+ oriented = applyExifOrientation(decodedJpeg, decodedJpeg.orientation);
244
+ encode = encodeJpeg;
245
+ extension = 'jpg';
246
+ }
247
+
248
+ /** @type {PipelineOutputFile[]} */
249
+ const outputs = [
250
+ {
251
+ path: derivativePath(reference.sourceDigest, 'original', extension),
252
+ bytes: encode(oriented),
253
+ },
254
+ ];
255
+
256
+ const widths = DERIVATIVE_WIDTHS.filter((w) => w < oriented.width);
257
+ for (const width of widths) {
258
+ const resized = resizeRaster(oriented, width, reference.path);
259
+ outputs.push({
260
+ path: derivativePath(reference.sourceDigest, `${width}w`, extension),
261
+ bytes: encode(resized),
262
+ });
263
+ }
264
+ return outputs;
265
+ }
266
+
267
+ /**
268
+ * Process every image and font reference in `buildInput` into content-
269
+ * addressed output files plus their `manifestAsset` rows.
270
+ *
271
+ * @param {import('../../../../types/index.d.ts').NormalizedBuildInput} buildInput
272
+ * the validated build input
273
+ * @param {{sourceDirectory: string}} options the media pipeline's own
274
+ * options
275
+ * @returns {Promise<{assets: import('../../../../types/index.d.ts').ManifestAssetEntry[], files: PipelineOutputFile[]}>}
276
+ * the asset manifest rows and the output files backing them
277
+ */
278
+ export async function processMedia(buildInput, { sourceDirectory }) {
279
+ const imageReferences = collectImageReferences(buildInput);
280
+ if (imageReferences.length > MAX_IMAGES_PER_PUBLICATION) {
281
+ throw new MediaPipelineError(
282
+ 'MEDIA_RESOURCE_EXCEEDED',
283
+ `publication references ${imageReferences.length} distinct images, exceeding the ${MAX_IMAGES_PER_PUBLICATION} ceiling`,
284
+ 'publication',
285
+ );
286
+ }
287
+ const fontReferences = collectFontReferences(buildInput);
288
+ if (fontReferences.length > MAX_FONTS_PER_PUBLICATION) {
289
+ throw new MediaPipelineError(
290
+ 'MEDIA_RESOURCE_EXCEEDED',
291
+ `publication references ${fontReferences.length} distinct fonts, exceeding the ${MAX_FONTS_PER_PUBLICATION} ceiling`,
292
+ 'appearance',
293
+ );
294
+ }
295
+
296
+ /** @type {PipelineOutputFile[]} */
297
+ const files = [];
298
+ let totalBytes = 0;
299
+
300
+ for (const reference of imageReferences) {
301
+ const bytes = await readAndVerify(sourceDirectory, reference);
302
+ for (const output of processImage(bytes, reference)) {
303
+ totalBytes += output.bytes.byteLength;
304
+ files.push(output);
305
+ }
306
+ }
307
+
308
+ for (const reference of fontReferences) {
309
+ const bytes = await readAndVerify(sourceDirectory, reference);
310
+ assertFontPolicy(bytes, reference.path);
311
+ totalBytes += bytes.byteLength;
312
+ files.push({
313
+ path: derivativePath(reference.sourceDigest, 'original', 'woff2'),
314
+ bytes,
315
+ });
316
+ }
317
+
318
+ if (totalBytes > MAX_TOTAL_MEDIA_BYTES_PER_PUBLICATION) {
319
+ throw new MediaPipelineError(
320
+ 'MEDIA_RESOURCE_EXCEEDED',
321
+ `media pipeline output is ${totalBytes} bytes, exceeding the ${MAX_TOTAL_MEDIA_BYTES_PER_PUBLICATION}-byte publication ceiling`,
322
+ 'publication',
323
+ );
324
+ }
325
+
326
+ // Deterministic path order: derivativePath is a pure function of
327
+ // (sourceDigest, variant, extension), so sorting the finished file list by
328
+ // its own output path — rather than by reference-discovery order — is
329
+ // what makes this pipeline's manifest.assets ordering independent of
330
+ // `build-input`'s own field ordering.
331
+ files.sort((a, b) =>
332
+ Buffer.compare(Buffer.from(a.path, 'utf8'), Buffer.from(b.path, 'utf8')),
333
+ );
334
+
335
+ const assets = files.map((file) => {
336
+ const extension = file.path.slice(file.path.lastIndexOf('.') + 1);
337
+ const mediaType =
338
+ extension === 'woff2'
339
+ ? 'font/woff2'
340
+ : extension === 'jpg'
341
+ ? 'image/jpeg'
342
+ : `image/${extension}`;
343
+ return /** @type {import('../../../../types/index.d.ts').ManifestAssetEntry} */ ({
344
+ path: file.path,
345
+ mediaType,
346
+ byteLength: String(file.bytes.byteLength),
347
+ sha256: digest(file.bytes),
348
+ immutable: true,
349
+ });
350
+ });
351
+
352
+ return { assets, files };
353
+ }