@cardinali-studio/adminkit 2.2.37 → 2.2.38

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/README.md CHANGED
@@ -196,6 +196,14 @@ It is opinionated about the useful narrow case: Hugo content, React 19, Zod 4, C
196
196
 
197
197
  This narrowness is intentional. A smaller system with explicit failure modes is easier to trust around content that people have edited by hand.
198
198
 
199
+ ## Delivery ZIP compatibility (2.2.38)
200
+
201
+ New delivery archives use STORE members with their CRC and sizes in each local header, without data descriptors. This prevents streaming extractors from mistaking descriptor-shaped bytes inside a photograph for the end of the file. CRC preparation reads bounded 4 MiB slices and remains pausable and cancellable; source changes fail closed. ZIP64 sizes and offsets are promoted independently.
202
+
203
+ Persistent checkpoints record `store-header-v1`. Checkpoints from earlier releases continue with `store-descriptor-v1`, preserving confirmed multipart bytes and derivative retries. The update affects newly generated archives; it does not rewrite existing stored ZIPs. Consumers must update the dependency, rebuild and redeploy.
204
+
205
+ The regression includes 208 synthetic members with the collision at member 53, streaming and indexed readers, ZIP64 offsets, and byte-identical resume against fixtures from 2.2.37.
206
+
199
207
  ## Development
200
208
 
201
209
  ```sh
@@ -1,5 +1,8 @@
1
1
  import { type UploadControl } from "./delivery-network.js";
2
2
  export { UploadCancelled, type UploadControl } from "./delivery-network.js";
3
+ declare const ZIP_FORMAT = "store-header-v1";
4
+ declare const LEGACY_ZIP_FORMAT = "store-descriptor-v1";
5
+ type ZipFormat = typeof ZIP_FORMAT | typeof LEGACY_ZIP_FORMAT;
3
6
  export interface DeliveryUploadProgress {
4
7
  phase: "originals" | "derivatives" | "finalizing";
5
8
  file: string;
@@ -57,6 +60,8 @@ interface MemberCheckpoint {
57
60
  dataOffset: number;
58
61
  time: number;
59
62
  date: number;
63
+ zipFormat?: ZipFormat;
64
+ headerCrc?: number;
60
65
  }
61
66
  export declare function uploadDeliveryPackage(deliveryId: string, packageId: string, selected: File[], options: UploadOptions, onProgress: (progress: DeliveryUploadProgress) => void, control?: UploadControl): Promise<UploadResult>;
62
67
  interface ZipSink {
@@ -69,9 +74,10 @@ export declare class ZipStream {
69
74
  private sink;
70
75
  private offset;
71
76
  private entries;
72
- constructor(sink: ZipSink, utc?: boolean);
77
+ private format;
78
+ constructor(sink: ZipSink, utc?: boolean, format?: ZipFormat);
73
79
  restore(entries: DeliveryManifestEntry[], files: File[], offset: number): void;
74
- add(nameValue: string, blob: Blob, progress?: (read: number) => void, modified?: Date, checkpoint?: (current: MemberCheckpoint, offset: number) => Promise<void>, resume?: MemberCheckpoint): Promise<ZipMember>;
80
+ add(nameValue: string, blob: Blob, progress?: (read: number, preparing?: boolean) => void, modified?: Date, checkpoint?: (current: MemberCheckpoint, offset: number) => Promise<void>, resume?: MemberCheckpoint): Promise<ZipMember>;
75
81
  close(): Promise<void>;
76
82
  private sinkGate;
77
83
  private push;
@@ -14,6 +14,8 @@ const READ = 4 * 1024 * 1024;
14
14
  const UPLOAD_CONCURRENCY = 4;
15
15
  const IMAGE = /\.(jpe?g|png|webp)$/i;
16
16
  const enc = new TextEncoder();
17
+ const ZIP_FORMAT = "store-header-v1";
18
+ const LEGACY_ZIP_FORMAT = "store-descriptor-v1";
17
19
  const droppedPaths = new WeakMap();
18
20
  /* Il cancello di pausa e annullamento.
19
21
  *
@@ -154,7 +156,9 @@ async function buildDeliveryPackage(deliveryId, packageId, selected, options, on
154
156
  let currentOriginal = "";
155
157
  const originalsSink = new B2Sink(deliveryId, packageId, "originals", reportOriginals, control, options.manifestVersion === 2);
156
158
  sinks.push(originalsSink);
157
- const originals = new ZipStream(originalsSink, options.manifestVersion === 2);
159
+ // Confirmed multipart prefixes and derivative retries keep their original layout.
160
+ const zipFormat = checkpoint ? (checkpoint.current?.zipFormat || LEGACY_ZIP_FORMAT) : ZIP_FORMAT;
161
+ const originals = new ZipStream(originalsSink, options.manifestVersion === 2, zipFormat);
158
162
  if (checkpoint) {
159
163
  if (checkpoint.complete) {
160
164
  originalsSink.totalBytes = checkpoint.offset;
@@ -175,12 +179,13 @@ async function buildDeliveryPackage(deliveryId, packageId, selected, options, on
175
179
  checkpoint: { version: 1, fileIndex: processed, offset, sequence, current } }, control);
176
180
  lastCheckpoint = offset;
177
181
  }
178
- function reportOriginals() {
182
+ function reportOriginals(_read, preparing = false) {
179
183
  onProgress({
180
184
  phase: "originals", file: currentOriginal, doneFiles: processed, totalFiles: files.length,
181
185
  sourceBytes, uploadedBytes: originalsSink.uploadedBytes,
182
186
  transferredBytes: originalsSink.transferredBytes, startedAt,
183
187
  transferStartedAt: originalsSink.transferStartedAt || undefined,
188
+ note: preparing ? "Verifico il file prima del caricamento…" : undefined,
184
189
  });
185
190
  }
186
191
  for (const file of files.slice(processed)) {
@@ -188,7 +193,7 @@ async function buildDeliveryPackage(deliveryId, packageId, selected, options, on
188
193
  await control.gate();
189
194
  const name = safePath(deliveryRelativeName(file));
190
195
  currentOriginal = name;
191
- const member = await originals.add(name, file, reportOriginals, new Date(file.lastModified), options.manifestVersion === 2 ? saveCheckpoint : undefined, checkpoint && processed === checkpoint.fileIndex ? checkpoint.current : undefined);
196
+ const member = await originals.add(name, file, reportOriginals, new Date(file.lastModified), options.manifestVersion === 2 ? saveCheckpoint : undefined, checkpoint && !checkpoint.complete && processed === checkpoint.fileIndex ? checkpoint.current : undefined);
192
197
  entryByName.set(name, entries.length);
193
198
  entries.push({ name, type: file.type || mimeFromName(name), original: member });
194
199
  await saveMetadata(entries.length - 1, entries[entries.length - 1]);
@@ -200,7 +205,7 @@ async function buildDeliveryPackage(deliveryId, packageId, selected, options, on
200
205
  await flushMetadata();
201
206
  if (options.manifestVersion === 2 && !checkpoint?.complete) {
202
207
  await deliveryJson("/admin/api/deliveries", { action: "manifestCheckpoint", id: deliveryId, packageId,
203
- checkpoint: { version: 1, fileIndex: files.length, offset: originalsSink.totalBytes, sequence, complete: true } }, control);
208
+ checkpoint: { version: 1, fileIndex: files.length, offset: originalsSink.totalBytes, sequence, complete: true, current: { zipFormat } } }, control);
204
209
  }
205
210
  phase = "variants";
206
211
  sequence = 0;
@@ -213,12 +218,12 @@ async function buildDeliveryPackage(deliveryId, packageId, selected, options, on
213
218
  if (wantWebArchive) {
214
219
  webSink = new B2Sink(deliveryId, packageId, "web", undefined, control, options.manifestVersion === 2);
215
220
  sinks.push(webSink);
216
- webZip = new ZipStream(webSink, options.manifestVersion === 2);
221
+ webZip = new ZipStream(webSink, options.manifestVersion === 2, zipFormat);
217
222
  }
218
223
  if (options.galleryEnabled) {
219
224
  thumbsSink = new B2Sink(deliveryId, packageId, "thumbs", undefined, control, options.manifestVersion === 2);
220
225
  sinks.push(thumbsSink);
221
- thumbsZip = new ZipStream(thumbsSink, options.manifestVersion === 2);
226
+ thumbsZip = new ZipStream(thumbsSink, options.manifestVersion === 2, zipFormat);
222
227
  }
223
228
  if (webZip) {
224
229
  let imageDone = 0;
@@ -494,9 +499,13 @@ export class ZipStream {
494
499
  sink;
495
500
  offset = 0;
496
501
  entries = [];
497
- constructor(sink, utc = false) {
502
+ format;
503
+ constructor(sink, utc = false, format = ZIP_FORMAT) {
504
+ if (![ZIP_FORMAT, LEGACY_ZIP_FORMAT].includes(format))
505
+ throw new Error("Formato del checkpoint ZIP non supportato.");
498
506
  this.utc = utc;
499
507
  this.sink = sink;
508
+ this.format = format;
500
509
  }
501
510
  restore(entries, files, offset) {
502
511
  this.offset = offset;
@@ -509,22 +518,46 @@ export class ZipStream {
509
518
  });
510
519
  }
511
520
  async add(nameValue, blob, progress, modified = new Date(), checkpoint, resume) {
521
+ await this.sinkGate();
512
522
  const name = enc.encode(safePath(nameValue));
513
523
  const size = blob.size;
514
524
  const localOffset = resume?.localOffset ?? this.offset;
515
525
  const zip64 = size >= 0xffffffff;
526
+ const legacy = this.format === LEGACY_ZIP_FORMAT;
527
+ if (resume && (resume.zipFormat || LEGACY_ZIP_FORMAT) !== this.format)
528
+ throw new Error("Formato del checkpoint ZIP diverso dal caricamento.");
529
+ // STORE has a known size, but needs a CRC before its local header can
530
+ // be self-contained. Two bounded reads avoid buffering whole photos.
531
+ let headerCrc = 0;
532
+ if (!legacy) {
533
+ let value = 0xffffffff;
534
+ progress?.(0, true);
535
+ for (let offset = 0; offset < size; offset += READ) {
536
+ await this.sinkGate();
537
+ const length = Math.min(READ, size - offset);
538
+ const chunk = new Uint8Array(await blob.slice(offset, offset + length).arrayBuffer());
539
+ if (chunk.length !== length)
540
+ throw new Error("Lettura sorgente incompleta durante la verifica.");
541
+ value = crc32Update(value, chunk);
542
+ progress?.(offset + length, true);
543
+ }
544
+ headerCrc = (value ^ 0xffffffff) >>> 0;
545
+ if (resume && resume.headerCrc !== headerCrc)
546
+ throw new Error("Il file è cambiato: il checksum non corrisponde al checkpoint.");
547
+ }
516
548
  const extra = zip64 ? zip64Extra([size, size]) : new Uint8Array();
517
549
  const local = bytes(30 + name.length + extra.length);
518
550
  const v = view(local);
519
551
  v.setUint32(0, 0x04034b50, true);
520
552
  v.setUint16(4, zip64 ? 45 : 20, true);
521
- v.setUint16(6, 0x0808, true); // descriptor + nomi UTF-8
553
+ v.setUint16(6, legacy ? 0x0808 : 0x0800, true); // UTF-8; descriptor only for legacy checkpoints
522
554
  v.setUint16(8, 0, true); // STORE
523
555
  const [time, date] = dosDate(modified, this.utc);
524
556
  v.setUint16(10, time, true);
525
557
  v.setUint16(12, date, true);
526
- v.setUint32(18, zip64 ? 0xffffffff : 0, true);
527
- v.setUint32(22, zip64 ? 0xffffffff : 0, true);
558
+ v.setUint32(14, legacy ? 0 : headerCrc, true);
559
+ v.setUint32(18, zip64 ? 0xffffffff : legacy ? 0 : size, true);
560
+ v.setUint32(22, zip64 ? 0xffffffff : legacy ? 0 : size, true);
528
561
  v.setUint16(26, name.length, true);
529
562
  v.setUint16(28, extra.length, true);
530
563
  local.set(name, 30);
@@ -564,46 +597,56 @@ export class ZipStream {
564
597
  read += chunk.byteLength;
565
598
  await this.push(chunk);
566
599
  if (checkpoint && this.offset % PART === 0)
567
- await checkpoint({ name: nameValue, size, read, crc, chain, localOffset, dataOffset, time, date }, this.offset);
600
+ await checkpoint({ name: nameValue, size, read, crc, chain, localOffset, dataOffset, time, date,
601
+ zipFormat: this.format, ...(!legacy ? { headerCrc } : {}) }, this.offset);
568
602
  progress?.(read);
569
603
  }
570
604
  crc = (crc ^ 0xffffffff) >>> 0;
571
- const descriptor = bytes(zip64 ? 24 : 16);
572
- const d = view(descriptor);
573
- d.setUint32(0, 0x08074b50, true);
574
- d.setUint32(4, crc, true);
575
- if (zip64) {
576
- setU64(d, 8, size);
577
- setU64(d, 16, size);
578
- }
579
- else {
580
- d.setUint32(8, size, true);
581
- d.setUint32(12, size, true);
605
+ if (!legacy && crc !== headerCrc)
606
+ throw new Error("Il file è cambiato durante il caricamento. Riseleziona i file e riprova.");
607
+ if (legacy) {
608
+ const descriptor = bytes(zip64 ? 24 : 16);
609
+ const d = view(descriptor);
610
+ d.setUint32(0, 0x08074b50, true);
611
+ d.setUint32(4, crc, true);
612
+ if (zip64) {
613
+ setU64(d, 8, size);
614
+ setU64(d, 16, size);
615
+ }
616
+ else {
617
+ d.setUint32(8, size, true);
618
+ d.setUint32(12, size, true);
619
+ }
620
+ await this.push(descriptor);
582
621
  }
583
- await this.push(descriptor);
584
622
  this.entries.push({ name, size, crc, localOffset, time, date });
585
623
  return { offset: dataOffset, length: size, crc32: crc };
586
624
  }
587
625
  async close() {
588
626
  const centralOffset = this.offset;
589
627
  for (const entry of this.entries) {
590
- const large = entry.size >= 0xffffffff || entry.localOffset >= 0xffffffff;
591
- const extra = large ? zip64Extra([entry.size, entry.size, entry.localOffset]) : new Uint8Array();
628
+ const legacy = this.format === LEGACY_ZIP_FORMAT;
629
+ const largeSize = entry.size >= 0xffffffff;
630
+ const largeOffset = entry.localOffset >= 0xffffffff;
631
+ const large = largeSize || largeOffset;
632
+ const values = legacy && large ? [entry.size, entry.size, entry.localOffset]
633
+ : [...(largeSize ? [entry.size, entry.size] : []), ...(largeOffset ? [entry.localOffset] : [])];
634
+ const extra = values.length ? zip64Extra(values) : new Uint8Array();
592
635
  const central = bytes(46 + entry.name.length + extra.length);
593
636
  const v = view(central);
594
637
  v.setUint32(0, 0x02014b50, true);
595
638
  v.setUint16(4, 45, true);
596
639
  v.setUint16(6, large ? 45 : 20, true);
597
- v.setUint16(8, 0x0808, true);
640
+ v.setUint16(8, legacy ? 0x0808 : 0x0800, true);
598
641
  v.setUint16(10, 0, true);
599
642
  v.setUint16(12, entry.time, true);
600
643
  v.setUint16(14, entry.date, true);
601
644
  v.setUint32(16, entry.crc, true);
602
- v.setUint32(20, large ? 0xffffffff : entry.size, true);
603
- v.setUint32(24, large ? 0xffffffff : entry.size, true);
645
+ v.setUint32(20, largeSize || (legacy && large) ? 0xffffffff : entry.size, true);
646
+ v.setUint32(24, largeSize || (legacy && large) ? 0xffffffff : entry.size, true);
604
647
  v.setUint16(28, entry.name.length, true);
605
648
  v.setUint16(30, extra.length, true);
606
- v.setUint32(42, large ? 0xffffffff : entry.localOffset, true);
649
+ v.setUint32(42, largeOffset || (legacy && large) ? 0xffffffff : entry.localOffset, true);
607
650
  central.set(entry.name, 46);
608
651
  central.set(extra, 46 + entry.name.length);
609
652
  await this.push(central);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cardinali-studio/adminkit",
3
- "version": "2.2.37",
3
+ "version": "2.2.38",
4
4
  "type": "module",
5
5
  "description": "Motore riusabile di un pannello di amministrazione per siti Hugo su Cloudflare Pages: codec del front matter guidato dallo schema, form React, libreria media su R2. NIENTE di specifico di un sito qui dentro.",
6
6
  "license": "0BSD",
@@ -104,6 +104,7 @@
104
104
  "react-dom": "^19.2.8",
105
105
  "react-hook-form": "^7.85.0",
106
106
  "typescript": "^5.9.3",
107
+ "unzipper": "0.12.5",
107
108
  "wrangler": "4.114.0",
108
109
  "zod": "^4.4.3"
109
110
  },