uploaderkit 1.0.0 → 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.
- package/LICENSE +21 -0
- package/NOTICE +16 -0
- package/README.es.md +1140 -0
- package/README.md +1135 -0
- package/dist/FileViewer-ChTf7-uY.d.cts +49 -0
- package/dist/FileViewer-nuD-GEdJ.d.ts +49 -0
- package/dist/adapters/gcs.cjs +83 -0
- package/dist/adapters/gcs.cjs.map +1 -0
- package/dist/adapters/gcs.d.cts +31 -0
- package/dist/adapters/gcs.d.ts +31 -0
- package/dist/adapters/gcs.js +81 -0
- package/dist/adapters/gcs.js.map +1 -0
- package/dist/adapters/memory.cjs +27 -0
- package/dist/adapters/memory.cjs.map +1 -0
- package/dist/adapters/memory.d.cts +18 -0
- package/dist/adapters/memory.d.ts +18 -0
- package/dist/adapters/memory.js +25 -0
- package/dist/adapters/memory.js.map +1 -0
- package/dist/adapters/s3.cjs +75 -0
- package/dist/adapters/s3.cjs.map +1 -0
- package/dist/adapters/s3.d.cts +27 -0
- package/dist/adapters/s3.d.ts +27 -0
- package/dist/adapters/s3.js +73 -0
- package/dist/adapters/s3.js.map +1 -0
- package/dist/chunk-3FI44IOW.js +150 -0
- package/dist/chunk-3FI44IOW.js.map +1 -0
- package/dist/chunk-H7BRW5IY.js +308 -0
- package/dist/chunk-H7BRW5IY.js.map +1 -0
- package/dist/chunk-PDKAF4GX.js +707 -0
- package/dist/chunk-PDKAF4GX.js.map +1 -0
- package/dist/chunk-PTEX7F4R.js +309 -0
- package/dist/chunk-PTEX7F4R.js.map +1 -0
- package/dist/chunk-T5YZG6JW.js +511 -0
- package/dist/chunk-T5YZG6JW.js.map +1 -0
- package/dist/index.cjs +220 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +57 -170
- package/dist/index.d.ts +57 -170
- package/dist/index.js +1 -324
- package/dist/index.js.map +1 -1
- package/dist/presets.cjs +1665 -0
- package/dist/presets.cjs.map +1 -0
- package/dist/presets.d.cts +100 -0
- package/dist/presets.d.ts +100 -0
- package/dist/presets.js +405 -0
- package/dist/presets.js.map +1 -0
- package/dist/react.cjs +981 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +106 -0
- package/dist/react.d.ts +106 -0
- package/dist/react.js +5 -0
- package/dist/react.js.map +1 -0
- package/dist/server/express.cjs +179 -0
- package/dist/server/express.cjs.map +1 -0
- package/dist/server/express.d.cts +64 -0
- package/dist/server/express.d.ts +64 -0
- package/dist/server/express.js +111 -0
- package/dist/server/express.js.map +1 -0
- package/dist/server/next.cjs +165 -0
- package/dist/server/next.cjs.map +1 -0
- package/dist/server/next.d.cts +38 -0
- package/dist/server/next.d.ts +38 -0
- package/dist/server/next.js +107 -0
- package/dist/server/next.js.map +1 -0
- package/dist/server.cjs +550 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +19 -0
- package/dist/server.d.ts +19 -0
- package/dist/server.js +80 -0
- package/dist/server.js.map +1 -0
- package/dist/storage-CYkSHWZX.d.cts +133 -0
- package/dist/storage-Qc9epG0G.d.ts +133 -0
- package/dist/types-BSlJJwti.d.cts +341 -0
- package/dist/types-BSlJJwti.d.ts +341 -0
- package/dist/ui.cjs +2325 -0
- package/dist/ui.cjs.map +1 -0
- package/dist/ui.d.cts +331 -0
- package/dist/ui.d.ts +331 -0
- package/dist/ui.js +749 -0
- package/dist/ui.js.map +1 -0
- package/dist/useSlottedUploader-BzT5jV8c.d.cts +139 -0
- package/dist/useSlottedUploader-IkG5RhEO.d.ts +139 -0
- package/dist/useUploader-BiBdS-7y.d.cts +117 -0
- package/dist/useUploader-CQHpj_oI.d.ts +117 -0
- package/package.json +189 -3
- package/tailwind.css +80 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/constants.ts","../src/file.ts","../src/labels.ts","../src/validation.ts","../src/scopes.ts","../src/server/crypto.ts","../src/server/storage.ts"],"names":["Transform","createDecipheriv","randomBytes","createCipheriv","Readable"],"mappings":";;;;;;;;AAEO,IAAM,EAAA,GAAK,IAAA;AACX,IAAM,KAAK,IAAA,GAAO,EAAA;AAQlB,IAAM,aAAA,GAA0D;AAAA,EACtE,GAAA,EAAK,CAAC,GAAA,EAAM,EAAA,EAAM,IAAM,EAAA,EAAM,EAAA,EAAM,EAAA,EAAM,EAAA,EAAM,EAAI,CAAA;AAAA,EACpD,GAAA,EAAK,CAAC,GAAA,EAAM,GAAA,EAAM,GAAI,CAAA;AAAA,EACtB,IAAA,EAAM,CAAC,GAAA,EAAM,GAAA,EAAM,GAAI,CAAA;AAAA,EACvB,GAAA,EAAK,CAAC,EAAA,EAAM,EAAA,EAAM,IAAM,EAAI,CAAA;AAAA,EAC5B,IAAA,EAAM,CAAC,EAAA,EAAM,EAAA,EAAM,IAAM,EAAI,CAAA;AAAA;AAAA,EAC7B,GAAA,EAAK,CAAC,CAAA,EAAM,CAAA,EAAM,GAAM,CAAI,CAAA;AAAA,EAE5B,GAAA,EAAK,CAAC,EAAA,EAAM,EAAA,EAAM,IAAM,EAAI,CAAA;AAAA;AAAA;AAAA,EAG5B,IAAA,EAAM,CAAC,EAAA,EAAM,EAAA,EAAM,GAAM,CAAI,CAAA;AAAA,EAC7B,IAAA,EAAM,CAAC,EAAA,EAAM,EAAA,EAAM,GAAM,CAAI,CAAA;AAAA,EAC7B,GAAA,EAAK,CAAC,GAAA,EAAM,GAAA,EAAM,IAAM,GAAI,CAAA;AAAA,EAC5B,GAAA,EAAK,CAAC,GAAA,EAAM,GAAA,EAAM,IAAM,GAAI,CAAA;AAAA;AAAA,EAG5B,GAAA,EAAK,CAAC,EAAA,EAAM,GAAI,CAAA;AAAA,EAChB,GAAA,EAAK,CAAC,EAAA,EAAM,GAAI,CAAA;AAAA,EAChB,GAAA,EAAK,CAAC,EAAA,EAAM,GAAI,CAAA;AAAA,EAChB,KAAK,CAAC,EAAA,EAAM,EAAA,EAAM,EAAA,EAAM,IAAM,EAAI,CAAA;AAAA;AAAA,EAElC,IAAA,EAAM,CAAC,EAAA,EAAM,EAAA,EAAM,KAAM,GAAI,CAAA;AAAA,EAC7B,GAAA,EAAK,CAAC,EAAA,EAAM,EAAA,EAAM,IAAM,EAAI,CAAA;AAAA,EAC5B,GAAA,EAAK,CAAC,EAAA,EAAM,EAAA,EAAM,EAAI,CAAA;AAAA;AAAA,EACtB,GAAA,EAAK,CAAC,EAAA,EAAM,EAAA,EAAM,IAAM,EAAI,CAAA;AAAA,EAC5B,GAAA,EAAK,CAAC,EAAA,EAAM,GAAA,EAAM,KAAM,EAAI,CAAA;AAAA,EAC5B,GAAA,EAAK,CAAC,GAAA,EAAM,GAAI;AACjB,CAAA;AAEO,IAAM,UAAA,GAA4C;AAAA,EACxD,GAAA,EAAK,WAAA;AAAA,EACL,GAAA,EAAK,YAAA;AAAA,EACL,IAAA,EAAM,YAAA;AAAA,EACN,GAAA,EAAK,eAAA;AAAA,EACL,IAAA,EAAM,YAAA;AAAA,EACN,GAAA,EAAK,WAAA;AAAA,EACL,GAAA,EAAK,cAAA;AAAA,EACL,IAAA,EAAM,YAAA;AAAA,EAEN,GAAA,EAAK,4BAAA;AAAA,EACL,GAAA,EAAK,4BAAA;AAAA,EACL,GAAA,EAAK,wBAAA;AAAA,EACL,GAAA,EAAK,wBAAA;AAAA,EAEL,GAAA,EAAK,iBAAA;AAAA,EACL,GAAA,EAAK,oBAAA;AAAA,EACL,IAAA,EAAM,yEAAA;AAAA,EACN,GAAA,EAAK,0BAAA;AAAA,EACL,IAAA,EAAM,mEAAA;AAAA,EACN,GAAA,EAAK,UAAA;AAAA,EACL,GAAA,EAAK,YAAA;AAAA,EAEL,GAAA,EAAK,iBAAA;AAAA,EACL,IAAA,EAAM,kBAAA;AAAA,EAEN,GAAA,EAAK,WAAA;AAAA,EACL,GAAA,EAAK,iBAAA;AAAA,EACL,GAAA,EAAK,iBAAA;AAAA,EACL,IAAA,EAAM,YAAA;AAAA,EAEN,GAAA,EAAK,YAAA;AAAA,EACL,GAAA,EAAK,WAAA;AAAA,EACL,GAAA,EAAK,WAAA;AAAA,EACL,GAAA,EAAK;AACN,CAAA;AAMO,IAAM,oBAAA,GAAiE;AAAA,EAC7E,KAAA,EAAO;AAAA,IACN,YAAY,CAAC,KAAA,EAAO,OAAO,MAAA,EAAQ,KAAA,EAAO,QAAQ,KAAK,CAAA;AAAA,IACvD,UAAU,CAAA,GAAI,EAAA;AAAA,IACd,oBAAA,EAAsB,IAAA;AAAA,IACtB,KAAA,EAAO,OAAA;AAAA,IACP,MAAA,EAAQ;AAAA,GACT;AAAA,EACA,WAAA,EAAa;AAAA,IACZ,UAAA,EAAY,CAAC,KAAA,EAAO,KAAA,EAAO,KAAK,CAAA;AAAA,IAChC,UAAU,EAAA,GAAK,EAAA;AAAA,IACf,oBAAA,EAAsB,IAAA;AAAA,IACtB,KAAA,EAAO,aAAA;AAAA,IACP,MAAA,EAAQ;AAAA,GACT;AAAA,EACA,GAAA,EAAK;AAAA,IACJ,UAAA,EAAY,CAAC,KAAK,CAAA;AAAA,IAClB,UAAU,EAAA,GAAK,EAAA;AAAA,IACf,oBAAA,EAAsB,IAAA;AAAA,IACtB,KAAA,EAAO,aAAA;AAAA,IACP,MAAA,EAAQ;AAAA,GACT;AAAA,EACA,GAAA,EAAK;AAAA,IACJ,UAAA,EAAY,CAAC,KAAK,CAAA;AAAA,IAClB,UAAU,EAAA,GAAK,EAAA;AAAA,IACf,oBAAA,EAAsB,IAAA;AAAA,IACtB,KAAA,EAAO,KAAA;AAAA,IACP,MAAA,EAAQ;AAAA,GACT;AAAA,EACA,QAAA,EAAU;AAAA,IACT,UAAA,EAAY,CAAC,KAAA,EAAO,KAAA,EAAO,QAAQ,KAAA,EAAO,MAAA,EAAQ,OAAO,KAAK,CAAA;AAAA,IAC9D,UAAU,EAAA,GAAK,EAAA;AAAA,IACf,oBAAA,EAAsB,KAAA;AAAA,IACtB,KAAA,EAAO,UAAA;AAAA,IACP,MAAA,EAAQ;AAAA,GACT;AAAA,EACA,IAAA,EAAM;AAAA,IACL,UAAA,EAAY,CAAC,KAAA,EAAO,MAAM,CAAA;AAAA,IAC1B,UAAU,CAAA,GAAI,EAAA;AAAA,IACd,oBAAA,EAAsB,KAAA;AAAA,IACtB,KAAA,EAAO,WAAA;AAAA,IACP,MAAA,EAAQ;AAAA,GACT;AAAA,EACA,KAAA,EAAO;AAAA,IACN,UAAA,EAAY,CAAC,KAAA,EAAO,KAAA,EAAO,OAAO,MAAM,CAAA;AAAA,IACxC,UAAU,GAAA,GAAM,EAAA;AAAA,IAChB,oBAAA,EAAsB,KAAA;AAAA,IACtB,KAAA,EAAO,OAAA;AAAA,IACP,MAAA,EAAQ;AAAA,GACT;AAAA,EACA,KAAA,EAAO;AAAA,IACN,UAAA,EAAY,CAAC,KAAA,EAAO,KAAA,EAAO,OAAO,KAAK,CAAA;AAAA,IACvC,UAAU,EAAA,GAAK,EAAA;AAAA,IACf,oBAAA,EAAsB,KAAA;AAAA,IACtB,KAAA,EAAO,OAAA;AAAA,IACP,MAAA,EAAQ;AAAA,GACT;AAAA,EACA,GAAA,EAAK;AAAA,IACJ,YAAY,EAAC;AAAA,IACb,UAAU,EAAA,GAAK,EAAA;AAAA,IACf,oBAAA,EAAsB,KAAA;AAAA,IACtB,KAAA,EAAO,MAAA;AAAA,IACP,MAAA,EAAQ;AAAA;AAEV,CAAA;;;AC9IO,IAAM,gBAAA,GAAmB,CAAC,QAAA,KAA6B;AAC7D,EAAA,MAAM,GAAA,GAAM,QAAA,CAAS,WAAA,CAAY,GAAG,CAAA;AACpC,EAAA,IAAI,MAAM,CAAA,IAAK,GAAA,KAAQ,QAAA,CAAS,MAAA,GAAS,GAAG,OAAO,EAAA;AACnD,EAAA,OAAO,QAAA,CAAS,KAAA,CAAM,GAAA,GAAM,CAAC,EAAE,WAAA,EAAY;AAC5C,CAAA;AAOO,IAAM,gBAAA,GAAmB,CAAC,KAAA,KAChC,KAAA,IAAS,UAAA;AAEH,IAAM,cAAA,GAAiB,CAAC,KAAA,KAA0B;AACxD,EAAA,IAAI,KAAA,GAAQ,IAAA,EAAM,OAAO,CAAA,EAAG,KAAK,CAAA,EAAA,CAAA;AACjC,EAAA,MAAM,KAAA,GAAQ,CAAC,IAAA,EAAM,IAAA,EAAM,MAAM,IAAI,CAAA;AACrC,EAAA,IAAI,OAAO,KAAA,GAAQ,IAAA;AACnB,EAAA,IAAI,IAAA,GAAO,CAAA;AACX,EAAA,OAAO,IAAA,IAAQ,IAAA,IAAQ,IAAA,GAAO,KAAA,CAAM,SAAS,CAAA,EAAG;AAC/C,IAAA,IAAA,IAAQ,IAAA;AACR,IAAA,IAAA,EAAA;AAAA,EACD;AACA,EAAA,OAAO,CAAA,EAAG,IAAA,CAAK,OAAA,CAAQ,IAAA,IAAQ,EAAA,GAAK,CAAA,GAAI,CAAC,CAAC,CAAA,CAAA,EAAI,KAAA,CAAM,IAAI,CAAC,CAAA,CAAA;AAC1D,CAAA;;;AC6KO,IAAM,cAAA,GAAiC;AAAA,EAC7C,UAAA,EAAY,+BAAA;AAAA,EACZ,SAAA,EAAW,sBAAA;AAAA,EACX,cAAA,EAAgB,sDAAA;AAAA,EAChB,aAAA,EAAe,0BAAA;AAAA,EACf,SAAA,EAAW,cAAA;AAAA,EACX,IAAA,EAAM,MAAA;AAAA,EACN,MAAA,EAAQ,QAAA;AAAA,EACR,MAAA,EAAQ,QAAA;AAAA,EACR,KAAA,EAAO,MAAA;AAAA,EACP,MAAA,EAAQ,iBAAA;AAAA,EACR,MAAA,EAAQ,QAAA;AAAA,EACR,OAAA,EAAS,SAAA;AAAA,EACT,aAAA,EAAe,iBAAA;AAAA,EACf,QAAA,EAAU,UAAA;AAAA,EACV,IAAA,EAAM,MAAA;AAAA,EACN,YAAA,EAAc,gCAAA;AAAA,EACd,mBAAA,EAAqB,uDAAA;AAAA,EACrB,YAAA,EAAc,4CAAA;AAAA,EACd,UAAA,EAAY,8BAAA;AAAA,EACZ,eAAA,EAAiB,CAAA,GAAA,KAAO,CAAA,QAAA,EAAW,GAAG,CAAA,QAAA,CAAA;AAAA,EACtC,oBAAA,EAAsB,CAAC,SAAA,EAAW,UAAA,KACjC,CAAA,EAAG,SAAS,CAAA,gCAAA,EAAmC,UAAA,CAAW,IAAA,CAAK,IAAI,CAAC,CAAA,CAAA;AAAA,EACrE,eAAA,EAAiB,CAAA,QAAA,KAAY,CAAA,CAAA,EAAI,QAAQ,CAAA,6BAAA,CAAA;AAAA,EACzC,aAAA,EAAe,eAAA;AAAA,EACf,WAAA,EAAa,iCAAA;AAAA,EACb,eAAA,EACC,4EAAA;AAAA,EACD,WAAA,EAAa,OAAA;AAAA,EACb,SAAA,EAAW,aAAA;AAAA,EACX,aAAA,EAAe,UAAA;AAAA,EACf,KAAA,EAAO,OAAA;AAAA,EACP,SAAA,EAAW,4BAAA;AAAA,EACX,QAAA,EAAU,CAAA,QAAA,KAAY,CAAA,SAAA,EAAY,QAAQ,CAAA,CAAA;AAAA,EAC1C,WAAA,EAAa,MAAA;AAAA,EACb,OAAA,EAAS,SAAA;AAAA,EACT,kBAAA,EAAoB,aAAA;AAAA,EACpB,oBAAA,EAAsB,CAAA,QAAA,KACrB,CAAA,QAAA,EAAW,QAAQ,CAAA,oCAAA,CAAA;AAAA,EACpB,mBAAA,EAAqB,cAAA;AAAA,EACrB,uBAAuB,CAAC,OAAA,EAAS,aAChC,CAAA,SAAA,EAAY,OAAO,WAAW,QAAQ,CAAA,EAAA,CAAA;AAAA,EACvC,iBAAA,EAAmB,qBAAA;AAAA,EACnB,gBAAA,EAAkB,+BAAA;AAAA,EAClB,YAAA,EAAc,cAAA;AAAA,EACd,cAAA,EAAgB,WAAA;AAAA,EAChB,YAAA,EAAc,cAAA;AAAA,EACd,UAAA,EAAY,KAAA;AAAA,EAEZ,kBAAA,EAAoB,2BAAA;AAAA,EACpB,kBAAkB,CAAA,UAAA,KACjB,CAAA,8BAAA,EAAiC,UAAA,CAAW,IAAA,CAAK,IAAI,CAAC,CAAA,CAAA;AAAA,EACvD,WAAA,EAAa,mBAAA;AAAA,EACb,cAAc,CAAC,IAAA,EAAM,QACpB,CAAA,gBAAA,EAAmB,IAAI,uBAAuB,GAAG,CAAA,CAAA;AAAA,EAClD,cAAA,EAAgB,wBAAA;AAAA,EAChB,4BAAA,EAA8B,8CAAA;AAAA,EAE9B,iBAAA,EAAmB,oBAAA;AAAA,EACnB,YAAA,EAAc,cAAA;AAAA,EACd,YAAA,EAAc,oBAAA;AAAA,EACd,YAAA,EAAc,0BAAA;AAAA,EACd,gBAAA,EAAkB,iCAAA;AAAA,EAClB,WAAA,EAAa;AACd,CAAA;AAGO,IAAM,aAAA,GAAgB,CAC5B,MAAA,EACA,IAAA,GAAuB,cAAA,KACF,MAAA,GAAS,EAAE,GAAG,IAAA,EAAM,GAAG,MAAA,EAAO,GAAI,IAAA;CAMf,EAAE,GAAG,cAAA;;;AC3Q9C,IAAM,EAAA,GAAuB,EAAE,KAAA,EAAO,IAAA,EAAK;AAE3C,IAAM,IAAA,GAAO,CACZ,IAAA,EACA,OAAA,MACuB,EAAE,KAAA,EAAO,KAAA,EAAO,MAAM,OAAA,EAAQ,CAAA;AAE/C,IAAM,iBAAA,GAAoB,CAChC,QAAA,EACA,OAAA,EACA,MAAA,KACsB;AACtB,EAAA,IAAI,OAAA,CAAQ,MAAA,KAAW,CAAA,EAAG,OAAO,EAAA;AAEjC,EAAA,MAAM,IAAA,GAAO,cAAc,MAAM,CAAA;AACjC,EAAA,MAAM,SAAA,GAAY,iBAAiB,QAAQ,CAAA;AAC3C,EAAA,IAAI,CAAC,SAAA,EAAW;AACf,IAAA,OAAO,IAAA,CAAK,uBAAA,EAAyB,IAAA,CAAK,kBAAkB,CAAA;AAAA,EAC7D;AACA,EAAA,IAAI,CAAC,OAAA,CAAQ,QAAA,CAAS,SAA0B,CAAA,EAAG;AAClD,IAAA,OAAO,IAAA,CAAK,uBAAA,EAAyB,IAAA,CAAK,gBAAA,CAAiB,OAAO,CAAC,CAAA;AAAA,EACpE;AACA,EAAA,OAAO,EAAA;AACR,CAAA;AAEO,IAAM,YAAA,GAAe,CAC3B,IAAA,EACA,QAAA,EACA,MAAA,KACsB;AACtB,EAAA,MAAM,IAAA,GAAO,cAAc,MAAM,CAAA;AACjC,EAAA,IAAI,SAAS,CAAA,EAAG,OAAO,IAAA,CAAK,YAAA,EAAc,KAAK,WAAW,CAAA;AAC1D,EAAA,IAAI,OAAO,QAAA,EAAU;AACpB,IAAA,OAAO,IAAA;AAAA,MACN,WAAA;AAAA,MACA,KAAK,YAAA,CAAa,cAAA,CAAe,IAAI,CAAA,EAAG,cAAA,CAAe,QAAQ,CAAC;AAAA,KACjE;AAAA,EACD;AACA,EAAA,OAAO,EAAA;AACR,CAAA;AAOO,IAAM,oBAAA,GAAuB,OACnC,IAAA,EACA,MAAA,KAC+B;AAC/B,EAAA,MAAM,IAAA,GAAO,cAAc,MAAM,CAAA;AACjC,EAAA,MAAM,SAAA,GAAY,gBAAA,CAAiB,IAAA,CAAK,IAAI,CAAA;AAC5C,EAAA,IAAI,CAAC,gBAAA,CAAiB,SAAS,CAAA,EAAG,OAAO,EAAA;AAEzC,EAAA,MAAM,QAAA,GAAW,cAAc,SAAS,CAAA;AACxC,EAAA,IAAI,CAAC,UAAU,OAAO,EAAA;AAEtB,EAAA,MAAM,OAAO,IAAI,UAAA,CAAW,MAAM,IAAA,CAAK,WAAA,EAAa,CAAA,CAAE,KAAA;AAAA,IACrD,CAAA;AAAA,IACA,QAAA,CAAS;AAAA,GACV;AACA,EAAA,IAAI,IAAA,CAAK,MAAA,GAAS,QAAA,CAAS,MAAA,EAAQ;AAClC,IAAA,OAAO,IAAA,CAAK,uBAAA,EAAyB,IAAA,CAAK,cAAc,CAAA;AAAA,EACzD;AAEA,EAAA,MAAM,OAAA,GAAU,SAAS,KAAA,CAAM,CAAC,MAAM,KAAA,KAAU,IAAA,CAAK,KAAK,CAAA,KAAM,IAAI,CAAA;AACpE,EAAA,OAAO,OAAA,GACJ,EAAA,GACA,IAAA,CAAK,uBAAA,EAAyB,KAAK,4BAA4B,CAAA;AACnE,CAAA;AAMO,IAAM,YAAA,GAAe,OAC3B,IAAA,EACA,OAAA,GAA6B,EAAC,KACC;AAC/B,EAAA,MAAM;AAAA,IACL,QAAA,GAAW,QAAA;AAAA,IACX,oBAAoB,EAAC;AAAA,IACrB,sBAAsB,UAAA,GAAa,KAAA;AAAA,IACnC,MAAA;AAAA,IACA;AAAA,GACD,GAAI,OAAA;AAEJ,EAAA,MAAM,IAAA,GAAO,YAAA,CAAa,IAAA,CAAK,IAAA,EAAM,UAAU,MAAM,CAAA;AACrD,EAAA,IAAI,CAAC,IAAA,CAAK,KAAA,EAAO,OAAO,IAAA;AAExB,EAAA,MAAM,SAAA,GAAY,iBAAA,CAAkB,IAAA,CAAK,IAAA,EAAM,mBAAmB,MAAM,CAAA;AACxE,EAAA,IAAI,CAAC,SAAA,CAAU,KAAA,EAAO,OAAO,SAAA;AAE7B,EAAA,IAAI,UAAA,EAAY;AACf,IAAA,MAAM,KAAA,GAAQ,MAAM,oBAAA,CAAqB,IAAA,EAAM,MAAM,CAAA;AACrD,IAAA,IAAI,CAAC,KAAA,CAAM,KAAA,EAAO,OAAO,KAAA;AAAA,EAC1B;AAEA,EAAA,IAAI,gBAAA,EAAkB,OAAO,gBAAA,CAAiB,IAAI,CAAA;AAElD,EAAA,OAAO,EAAA;AACR,CAAA;;;ACjGO,IAAM,UAAA,GAAN,cAAyB,KAAA,CAAM;AAAA,EAC5B,IAAA,GAAO,YAAA;AACjB,CAAA;AAGA,IAAM,YAAA,GAAe,YAAA;AAErB,IAAM,SAAA,GAAY,CAAC,IAAA,MAA4B;AAAA,EAC9C,IAAA;AAAA,EACA,IAAA,EAAM,CAAA;AAAA,EACN,IAAA,EAAM,EAAA;AAAA,EACN,WAAA,EAAa,YAAY,IAAI,WAAA,CAAY,CAAC;AAC3C,CAAA,CAAA;AAGO,IAAM,YAAA,GAAe,CAAC,KAAA,KAC5B,KAAA,CAAM,KAAK,YAAA,EAAc,SAAA,CAAU,OAAO,CAAC,MAC3C,KAAA,CAAM,IAAA,CAAK,YAAA,EAAc,SAAA,CAAU,OAAO,CAAC,CAAA;AAOrC,IAAM,kBAAA,GAAqB,CAAC,KAAA,KAAoC;AACtE,EAAA,IAAI,KAAA,CAAM,OAAA,KAAY,MAAA,EAAW,OAAO,KAAA,CAAM,OAAA;AAC9C,EAAA,IAAA,CAAK,KAAA,CAAM,QAAA,IAAY,CAAA,IAAK,CAAA,EAAG,OAAO,KAAA;AACtC,EAAA,OAAO,YAAA,CAAa,KAAK,CAAA,GAAI,KAAA,GAAQ,QAAA;AACtC,CAAA;AAEA,IAAM,OAAA,GAAU,CAAC,GAAA,KAAgB;AAChC,EAAA,MAAM,GAAA,GAAM,GAAA,CAAI,WAAA,CAAY,GAAG,CAAA;AAC/B,EAAA,OAAO,QAAQ,EAAA,GAAK,EAAA,GAAK,GAAA,CAAI,KAAA,CAAM,GAAG,GAAG,CAAA;AAC1C,CAAA;AAGO,IAAM,kBAAA,GAAqB,CACjC,KAAA,EACA,QAAA,KAEA,MAAM,MAAA,GAAS,QAAQ,CAAA,IACvB,OAAA,CAAQ,MAAM,IAAA,CAAK,QAAA,EAAU,SAAA,CAAU,WAAW,CAAC,CAAC,CAAA;AAwJ9C,IAAM,gBAAA,GAAmB,OAC/B,QAAA,EACA,IAAA,EACA,MACA,MAAA,KAC+B;AAC/B,EAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,GAAA,CAAI,IAAI,CAAA;AAC/B,EAAA,MAAM,WAAW,KAAA,CAAM,QAAA,GACpB,oBAAA,CAAqB,KAAA,CAAM,QAAQ,CAAA,GACnC,MAAA;AAEH,EAAA,OAAO,aAAa,IAAA,EAAM;AAAA,IACzB,UAAU,KAAA,CAAM,QAAA;AAAA,IAChB,mBAAmB,KAAA,CAAM,MAAA;AAAA,IACzB,oBAAA,EAAsB,UAAU,oBAAA,IAAwB,IAAA;AAAA,IACxD;AAAA,GACA,CAAA;AACF,CAAA;AAMO,IAAM,sBAAA,GAAyB,CACrC,QAAA,EACA,QAAA,KACU;AACV,EAAA,MAAM,YAAA,GAAe,SAAS,KAAA,CAAM,MAAA;AAAA,IACnC,CAAA,IAAA,KAAQ,QAAA,CAAS,GAAA,CAAI,IAAI,EAAE,UAAA,KAAe;AAAA,GAC3C;AAEA,EAAA,IAAI,aAAa,MAAA,GAAS,CAAA,IAAK,CAAC,QAAA,CAAS,aAAa,SAAA,EAAW;AAChE,IAAA,MAAM,IAAI,UAAA;AAAA,MACT,aAAa,QAAA,CAAS,IAAI,qDAAqD,YAAA,CAAa,IAAA,CAAK,IAAI,CAAC,CAAA;AAAA,KACvG;AAAA,EACD;AACD,CAAA;AAWA,IAAM,gBAAgB,CAAC,GAAA,KACtB,GAAA,CAAI,UAAA,CAAW,GAAG,CAAA,IAClB,GAAA,CAAI,KAAA,CAAM,GAAG,EAAE,IAAA,CAAK,CAAA,OAAA,KAAW,OAAA,KAAY,IAAA,IAAQ,YAAY,GAAG,CAAA;AAU5D,IAAM,UAAA,GAAa,CACzB,QAAA,EACA,IAAA,EACA,UACA,IAAA,KACY;AACZ,EAAA,MAAM,MAAM,QAAA,CAAS,GAAA,CAAI,IAAI,CAAA,CAAE,IAAA,CAAK,UAAU,IAAI,CAAA;AAClD,EAAA,IAAI,aAAA,CAAc,GAAG,CAAA,EAAG;AACvB,IAAA,MAAM,IAAI,UAAA,CAAW,CAAA,OAAA,EAAU,IAAI,CAAA,0BAAA,EAA6B,GAAG,CAAA,CAAE,CAAA;AAAA,EACtE;AACA,EAAA,OAAO,GAAA;AACR,CAAA;;;AC1QA,IAAM,SAAA,GAAY,EAAA;AAClB,IAAM,UAAA,GAAa,EAAA;AACnB,IAAM,gBAAgB,SAAA,GAAY,UAAA;AAClC,IAAM,WAAA,GAAc,mBAAA;AAWpB,IAAM,gBAAA,GAAN,cAA+BA,gBAAA,CAAU;AAAA,EAIxC,YAA6B,GAAA,EAAa;AACzC,IAAA,KAAA,EAAM;AADsB,IAAA,IAAA,CAAA,GAAA,GAAA,GAAA;AAAA,EAE7B;AAAA,EAF6B,GAAA;AAAA,EAH7B,OAAA,GAAU,MAAA,CAAO,KAAA,CAAM,CAAC,CAAA;AAAA,EACxB,SAAA,GAAgC,IAAA;AAAA,EAMvB,UAAA,CACR,KAAA,EACA,SAAA,EACA,IAAA,EACO;AACP,IAAA,IAAI,IAAA,GAAO,KAAA;AACX,IAAA,IAAI,CAAC,KAAK,SAAA,EAAW;AACpB,MAAA,IAAA,CAAK,UAAU,MAAA,CAAO,MAAA,CAAO,CAAC,IAAA,CAAK,OAAA,EAAS,KAAK,CAAC,CAAA;AAClD,MAAA,IAAI,IAAA,CAAK,OAAA,CAAQ,MAAA,GAAS,aAAA,SAAsB,IAAA,EAAK;AAErD,MAAA,IAAA,CAAK,SAAA,GAAYC,uBAAA;AAAA,QAChB,aAAA;AAAA,QACA,IAAA,CAAK,GAAA;AAAA,QACL,IAAA,CAAK,OAAA,CAAQ,QAAA,CAAS,CAAA,EAAG,SAAS;AAAA,OACnC;AACA,MAAA,IAAA,CAAK,UAAU,UAAA,CAAW,IAAA,CAAK,QAAQ,QAAA,CAAS,SAAA,EAAW,aAAa,CAAC,CAAA;AACzE,MAAA,IAAA,GAAO,IAAA,CAAK,OAAA,CAAQ,QAAA,CAAS,aAAa,CAAA;AAC1C,MAAA,IAAI,IAAA,CAAK,MAAA,KAAW,CAAA,EAAG,OAAO,IAAA,EAAK;AAAA,IACpC;AAEA,IAAA,IAAI;AACH,MAAA,IAAA,CAAK,IAAA,CAAK,IAAA,CAAK,SAAA,CAAU,MAAA,CAAO,IAAI,CAAC,CAAA;AACrC,MAAA,IAAA,EAAK;AAAA,IACN,SAAS,KAAA,EAAO;AACf,MAAA,IAAA,CAAK,KAAc,CAAA;AAAA,IACpB;AAAA,EACD;AAAA,EAES,OAAO,IAAA,EAA+B;AAC9C,IAAA,IAAI,CAAC,KAAK,SAAA,EAAW;AACpB,MAAA,OAAO,IAAA,CAAK,IAAI,KAAA,CAAM,0CAA0C,CAAC,CAAA;AAAA,IAClE;AACA,IAAA,IAAI;AAIH,MAAA,IAAA,CAAK,IAAA,CAAK,IAAA,CAAK,SAAA,CAAU,KAAA,EAAO,CAAA;AAChC,MAAA,IAAA,EAAK;AAAA,IACN,SAAS,KAAA,EAAO;AACf,MAAA,IAAA,CAAK,KAAc,CAAA;AAAA,IACpB;AAAA,EACD;AACD,CAAA;AAaO,IAAM,kBAAA,GAAqB,CAAC,MAAA,KAAyC;AAC3E,EAAA,IAAI,CAAC,WAAA,CAAY,IAAA,CAAK,MAAM,CAAA,EAAG;AAC9B,IAAA,MAAM,IAAI,UAAA;AAAA,MACT;AAAA,KACD;AAAA,EACD;AACA,EAAA,MAAM,GAAA,GAAM,MAAA,CAAO,IAAA,CAAK,MAAA,EAAQ,KAAK,CAAA;AAErC,EAAA,OAAO;AAAA,IACN,SAAS,CAAA,IAAA,KAAQ;AAChB,MAAA,MAAM,EAAA,GAAKC,mBAAY,SAAS,CAAA;AAChC,MAAA,MAAM,MAAA,GAASC,qBAAA,CAAe,aAAA,EAAe,GAAA,EAAK,EAAE,CAAA;AACpD,MAAA,MAAM,SAAA,GAAY,MAAA,CAAO,MAAA,CAAO,CAAC,MAAA,CAAO,MAAA,CAAO,IAAI,CAAA,EAAG,MAAA,CAAO,KAAA,EAAO,CAAC,CAAA;AACrE,MAAA,OAAO,MAAA,CAAO,OAAO,CAAC,EAAA,EAAI,OAAO,UAAA,EAAW,EAAG,SAAS,CAAC,CAAA;AAAA,IAC1D,CAAA;AAAA,IACA,SAAS,CAAA,IAAA,KAAQ;AAChB,MAAA,MAAM,MAAA,GAAS,MAAA,CAAO,IAAA,CAAK,IAAI,CAAA;AAC/B,MAAA,MAAM,EAAA,GAAK,MAAA,CAAO,QAAA,CAAS,CAAA,EAAG,SAAS,CAAA;AACvC,MAAA,MAAM,GAAA,GAAM,MAAA,CAAO,QAAA,CAAS,SAAA,EAAW,YAAY,UAAU,CAAA;AAC7D,MAAA,MAAM,UAAA,GAAa,MAAA,CAAO,QAAA,CAAS,SAAA,GAAY,UAAU,CAAA;AACzD,MAAA,MAAM,QAAA,GAAWF,uBAAA,CAAiB,aAAA,EAAe,GAAA,EAAK,EAAE,CAAA;AACxD,MAAA,QAAA,CAAS,WAAW,GAAG,CAAA;AACvB,MAAA,OAAO,MAAA,CAAO,MAAA,CAAO,CAAC,QAAA,CAAS,MAAA,CAAO,UAAU,CAAA,EAAG,QAAA,CAAS,KAAA,EAAO,CAAC,CAAA;AAAA,IACrE,CAAA;AAAA,IACA,eAAe,CAAA,MAAA,KACb,MAAA,CAAiC,KAAK,IAAI,gBAAA,CAAiB,GAAG,CAAC;AAAA,GAClE;AACD;AC/FO,IAAM,mBAAA,GAAN,cAAkC,KAAA,CAAM;AAAA,EAE9C,WAAA,CACC,SACS,MAAA,EACR;AACD,IAAA,KAAA,CAAM,OAAO,CAAA;AAFJ,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAAA,EAGV;AAAA,EAHU,MAAA;AAAA,EAHD,IAAA,GAAO,qBAAA;AAOjB;AA4GA,IAAM,KAAA,GAAQ,CAAC,MAAA,KACd,CAAC,GAAG,IAAI,UAAA,CAAW,MAAM,CAAC,CAAA,CACxB,GAAA,CAAI,UAAQ,IAAA,CAAK,QAAA,CAAS,EAAE,CAAA,CAAE,QAAA,CAAS,GAAG,GAAG,CAAC,CAAA,CAC9C,IAAA,CAAK,EAAE,CAAA;AAOH,IAAM,gBAAgB,CAAwC;AAAA,EACpE,MAAA;AAAA,EACA,QAAA;AAAA,EACA,MAAA;AAAA,EACA,YAAA,GAAe,GAAA;AAAA,EACf,YAAA;AAAA,EACA;AACD,CAAA,KAAkD;AACjD,EAAA,MAAM,IAAA,GAAO,cAAc,MAAM,CAAA;AAEjC,EAAA,sBAAA,CAAuB,QAAQ,QAAQ,CAAA;AAEvC,EAAA,MAAM,SAAA,GAAY,OAAO,KAAA,CAAM,MAAA,CAAO,UAAQ,MAAA,CAAO,GAAA,CAAI,IAAI,CAAA,CAAE,OAAO,CAAA;AACtE,EAAA,IAAI,SAAA,CAAU,MAAA,GAAS,CAAA,IAAK,CAAC,MAAA,EAAQ;AACpC,IAAA,MAAM,IAAI,UAAA;AAAA,MACT,CAAA,OAAA,EAAU,SAAA,CAAU,IAAA,CAAK,IAAI,CAAC,CAAA,oEAAA;AAAA,KAC/B;AAAA,EACD;AACA,EAAA,IAAI,SAAA,CAAU,MAAA,GAAS,CAAA,IAAK,CAAC,YAAA,EAAc;AAC1C,IAAA,MAAM,IAAI,UAAA;AAAA,MACT,CAAA,OAAA,EAAU,SAAA,CAAU,IAAA,CAAK,IAAI,CAAC,CAAA,oJAAA;AAAA,KAC/B;AAAA,EACD;AAEA,EAAA,MAAM,QAAA,GAAW,CAAC,IAAA,KAA8B;AAC/C,IAAA,IAAI,CAAC,MAAA,CAAO,GAAA,CAAI,IAAI,CAAA,EAAG;AACtB,MAAA,MAAM,IAAI,mBAAA,CAAoB,IAAA,CAAK,YAAA,EAAc,GAAG,CAAA;AAAA,IACrD;AACA,IAAA,OAAO,MAAA,CAAO,IAAI,IAAI,CAAA;AAAA,EACvB,CAAA;AAEA,EAAA,MAAM,SAAS,OAAO;AAAA,IACrB,KAAA,EAAO,IAAA;AAAA,IACP,QAAA;AAAA,IACA,IAAA;AAAA,IACA;AAAA,GACD,KAA0C;AACzC,IAAA,MAAM,KAAA,GAAQ,SAAS,IAAI,CAAA;AAI3B,IAAA,MAAM,SAAS,MAAM,gBAAA,CAAiB,MAAA,EAAQ,IAAA,EAAM,MAAM,IAAI,CAAA;AAC9D,IAAA,IAAI,CAAC,OAAO,KAAA,EAAO,MAAM,IAAI,mBAAA,CAAoB,MAAA,CAAO,SAAS,GAAG,CAAA;AAEpE,IAAA,MAAM,GAAA,GAAM,UAAA,CAAW,MAAA,EAAQ,IAAA,EAAM,UAAU,IAAI,CAAA;AACnD,IAAA,MAAM,MAAM,IAAI,UAAA,CAAW,MAAM,IAAA,CAAK,aAAa,CAAA;AACnD,IAAA,MAAM,QAAA,GAAW,KAAA;AAAA,MAChB,MAAM,UAAA,CAAW,MAAA,CAAO,MAAA,CAAO,MAAA,CAAO,WAAW,GAAG;AAAA,KACrD;AACA,IAAA,MAAM,OAAO,KAAA,CAAM,OAAA,GAAU,MAAM,MAAA,CAAQ,OAAA,CAAQ,GAAG,CAAA,GAAI,GAAA;AAE1D,IAAA,MAAM,GAAA,GAAM,MAAM,QAAA,CAAS,GAAA,CAAI;AAAA,MAC9B,GAAA;AAAA,MACA,IAAA;AAAA;AAAA;AAAA,MAGA,WAAA,EAAa,KAAA,CAAM,OAAA,GAAU,0BAAA,GAA6B,IAAA,CAAK,IAAA;AAAA,MAC/D,YAAY,KAAA,CAAM,UAAA;AAAA,MAClB,UAAU,KAAA,CAAM;AAAA,KAChB,CAAA;AAOD,IAAA,MAAM,WAAqB,EAAC;AAC5B,IAAA,IAAI,kBAAA,CAAmB,KAAK,CAAA,KAAM,QAAA,EAAU;AAC3C,MAAA,MAAM,MAAA,GAAS,kBAAA,CAAmB,KAAA,EAAO,QAAQ,CAAA;AACjD,MAAA,MAAM,KAAA,GAAA,CAAS,MAAM,QAAA,CAAS,IAAA,CAAK,MAAM,CAAA,EAAG,MAAA;AAAA,QAC3C,CAAA,MAAA,KAAU,MAAA,CAAO,GAAA,KAAQ,GAAA,CAAI;AAAA,OAC9B;AACA,MAAA,MAAM,OAAA,CAAQ,GAAA;AAAA,QACb,KAAA,CAAM,GAAA;AAAA,UAAI,YACT,QAAA,CACE,MAAA,CAAO,OAAO,GAAG,CAAA,CAGjB,KAAK,CAAA,OAAA,KAAW;AAChB,YAAA,IAAI,OAAA,EAAS,QAAA,CAAS,IAAA,CAAK,MAAA,CAAO,GAAG,CAAA;AAAA,UACtC,CAAC,CAAA,CACA,KAAA,CAAM,MAAM,MAAS;AAAA;AACxB,OACD;AAAA,IACD;AAEA,IAAA,MAAM,eAAA,GAAkB,IAAI,QAAA,IAAY,QAAA;AACxC,IAAA,MAAM,SAAA,GAAY,IAAI,GAAA,GACnB,CAAA,EAAG,IAAI,GAAG,CAAA,EAAG,IAAI,GAAA,CAAI,QAAA,CAAS,GAAG,CAAA,GAAI,GAAA,GAAM,GAAG,CAAA,EAAA,EAAK,eAAA,CAAgB,MAAM,CAAA,EAAG,EAAE,CAAC,CAAA,CAAA,GAC/E,GAAA,CAAI,GAAA;AAEP,IAAA,OAAO;AAAA,MACN,KAAK,GAAA,CAAI,GAAA;AAAA,MACT,QAAA;AAAA,MACA,GAAA,EAAK,KAAA,CAAM,OAAA,GACR,YAAA,CAAc,EAAE,OAAO,IAAA,EAAM,QAAA,EAAU,GAAA,EAAK,GAAA,CAAI,GAAA,EAAK,IACrD,KAAA,CAAM,UAAA,KAAe,SAAA,GACpB,MAAM,QAAA,CAAS,SAAA,CAAW,GAAA,CAAI,GAAA,EAAK,EAAE,SAAA,EAAW,YAAA,EAAc,CAAA,GAC9D,SAAA;AAAA,MACJ,KAAA,EAAO,IAAA;AAAA,MACP,QAAA;AAAA,MACA,UAAU,IAAA,CAAK,IAAA;AAAA,MACf,UAAU,IAAA,CAAK,IAAA;AAAA,MACf,MAAM,IAAA,CAAK,IAAA;AAAA,MACX,QAAA,EAAU,eAAA;AAAA,MACV,UAAA,EAAY,KAAK,GAAA,EAAI;AAAA,MACrB,GAAI,UAAA,GAAa,EAAE,UAAA,KAAe;AAAC,KACpC;AAAA,EACD,CAAA;AAEA,EAAA,MAAM,OAAO,OAAO,EAAE,KAAA,EAAO,IAAA,EAAM,KAAI,KAAsC;AAC5E,IAAA,MAAM,KAAA,GAAQ,SAAS,IAAI,CAAA;AAC3B,IAAA,MAAM,IAAA,GAAO,MAAM,QAAA,CAAS,GAAA,CAAI,GAAG,CAAA;AACnC,IAAA,OAAO,KAAA,CAAM,OAAA,GAAU,MAAA,CAAQ,OAAA,CAAQ,IAAI,CAAA,GAAI,IAAA;AAAA,EAChD,CAAA;AAUA,EAAA,MAAM,aAAa,OAAO;AAAA,IACzB,KAAA,EAAO,IAAA;AAAA,IACP;AAAA,GACD,KAGyB;AACxB,IAAA,MAAM,KAAA,GAAQ,SAAS,IAAI,CAAA;AAC3B,IAAA,MAAM,YACL,QAAA,CAAS,SAAA,KAAc,CAAC,KAAA,CAAM,WAAW,MAAA,EAAQ,aAAA,CAAA;AAElD,IAAA,IAAI,CAAC,SAAA,EAAW;AACf,MAAA,OAAOG,eAAA,CAAS,IAAA,CAAK,CAAC,MAAA,CAAO,KAAK,MAAM,IAAA,CAAK,EAAE,KAAA,EAAO,IAAA,EAAM,GAAA,EAAK,CAAC,CAAC,CAAC,CAAA;AAAA,IACrE;AAEA,IAAA,MAAM,MAAA,GAAU,MAAM,QAAA,CAAS,SAAA,CAAW,GAAG,CAAA;AAC7C,IAAA,OAAO,KAAA,CAAM,OAAA,GAAW,MAAA,CAAQ,aAAA,CAAe,MAAM,CAAA,GAAiB,MAAA;AAAA,EACvE,CAAA;AAEA,EAAA,MAAM,SAAS,CAAC,EAAE,KAAA,EAAO,IAAA,EAAM,KAAI,KAAsC;AACxE,IAAA,MAAM,KAAA,GAAQ,SAAS,IAAI,CAAA;AAI3B,IAAA,IAAI,MAAM,YAAA,EAAc;AACvB,MAAA,OAAO,OAAA,CAAQ,QAAQ,KAAK,CAAA;AAAA,IAC7B;AACA,IAAA,OAAO,QAAA,CAAS,OAAO,GAAG,CAAA;AAAA,EAC3B,CAAA;AAEA,EAAA,MAAM,YAAY,CAAC;AAAA,IAClB,KAAA,EAAO,IAAA;AAAA,IACP,GAAA;AAAA,IACA,QAAA;AAAA,IACA;AAAA,GACD,KAKM;AACL,IAAA,MAAM,KAAA,GAAQ,SAAS,IAAI,CAAA;AAC3B,IAAA,IAAI,KAAA,CAAM,eAAe,SAAA,EAAW;AACnC,MAAA,MAAM,IAAI,UAAA;AAAA,QACT,UAAU,IAAI,CAAA,2EAAA;AAAA,OACf;AAAA,IACD;AACA,IAAA,MAAM,OAAA,GAA4B;AAAA,MACjC,WAAW,SAAA,IAAa,YAAA;AAAA,MACxB,GAAI,QAAA,KAAa,MAAA,GAAY,EAAE,QAAA,KAAa;AAAC,KAC9C;AACA,IAAA,OAAO,QAAA,CAAS,SAAA,CAAW,GAAA,EAAK,OAAO,CAAA;AAAA,EACxC,CAAA;AAEA,EAAA,OAAO;AAAA,IACN,MAAA;AAAA,IACA,IAAA;AAAA,IACA,UAAA;AAAA,IACA,MAAA;AAAA,IACA,SAAA;AAAA,IACA,IAAA,EAAM,CAAA,MAAA,KAAU,QAAA,CAAS,IAAA,CAAK,MAAM,CAAA;AAAA,IACpC,MAAA;AAAA,IACA,MAAA,EAAQ;AAAA,GACT;AACD","file":"server.cjs","sourcesContent":["import type { FileCategory, FileCategoryConfig, FileExtension } from './types'\n\nexport const KB = 1024\nexport const MB = 1024 * KB\nexport const GB = 1024 * MB\n\n/**\n * Leading bytes that identify a format regardless of its extension. The check\n * that uses them is the only defence against an executable renamed to `.pdf`,\n * since both the extension and the browser-reported MIME come from the client.\n */\nexport const MAGIC_NUMBERS: Partial<Record<FileExtension, number[]>> = {\n\tpng: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a],\n\tjpg: [0xff, 0xd8, 0xff],\n\tjpeg: [0xff, 0xd8, 0xff],\n\tgif: [0x47, 0x49, 0x46, 0x38],\n\twebp: [0x52, 0x49, 0x46, 0x46], // RIFF; bytes 8-11 spell WEBP\n\tico: [0x00, 0x00, 0x01, 0x00],\n\n\tpdf: [0x25, 0x50, 0x44, 0x46], // %PDF\n\n\t// OOXML files are ZIP containers, so docx/xlsx share the PK signature.\n\tdocx: [0x50, 0x4b, 0x03, 0x04],\n\txlsx: [0x50, 0x4b, 0x03, 0x04],\n\tdoc: [0xd0, 0xcf, 0x11, 0xe0],\n\txls: [0xd0, 0xcf, 0x11, 0xe0],\n\n\t// DER/ASN.1 sequence header.\n\tcer: [0x30, 0x82],\n\tcrt: [0x30, 0x82],\n\tkey: [0x30, 0x82],\n\tpem: [0x2d, 0x2d, 0x2d, 0x2d, 0x2d], // -----\n\n\twebm: [0x1a, 0x45, 0xdf, 0xa3],\n\tavi: [0x52, 0x49, 0x46, 0x46],\n\tmp3: [0x49, 0x44, 0x33], // ID3\n\twav: [0x52, 0x49, 0x46, 0x46],\n\togg: [0x4f, 0x67, 0x67, 0x53],\n\taac: [0xff, 0xf1],\n}\n\nexport const MIME_TYPES: Record<FileExtension, string> = {\n\tpng: 'image/png',\n\tjpg: 'image/jpeg',\n\tjpeg: 'image/jpeg',\n\tsvg: 'image/svg+xml',\n\twebp: 'image/webp',\n\tgif: 'image/gif',\n\tico: 'image/x-icon',\n\theic: 'image/heic',\n\n\tcer: 'application/x-x509-ca-cert',\n\tcrt: 'application/x-x509-ca-cert',\n\tpem: 'application/x-pem-file',\n\tkey: 'application/x-pem-file',\n\n\tpdf: 'application/pdf',\n\tdoc: 'application/msword',\n\tdocx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n\txls: 'application/vnd.ms-excel',\n\txlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n\tcsv: 'text/csv',\n\ttxt: 'text/plain',\n\n\txml: 'application/xml',\n\tjson: 'application/json',\n\n\tmp4: 'video/mp4',\n\tmov: 'video/quicktime',\n\tavi: 'video/x-msvideo',\n\twebm: 'video/webm',\n\n\tmp3: 'audio/mpeg',\n\twav: 'audio/wav',\n\togg: 'audio/ogg',\n\taac: 'audio/aac',\n}\n\n/**\n * Defaults per category. A scope always narrows these; it can never widen the\n * extension list, which is what keeps `any` from leaking into a strict scope.\n */\nexport const FILE_CATEGORY_CONFIG: Record<FileCategory, FileCategoryConfig> = {\n\timage: {\n\t\textensions: ['png', 'jpg', 'jpeg', 'svg', 'webp', 'gif'],\n\t\tmaxBytes: 5 * MB,\n\t\tvalidateMagicNumbers: true,\n\t\tlabel: 'image',\n\t\taccept: 'image/*',\n\t},\n\tcertificate: {\n\t\textensions: ['cer', 'crt', 'pem'],\n\t\tmaxBytes: 50 * KB,\n\t\tvalidateMagicNumbers: true,\n\t\tlabel: 'certificate',\n\t\taccept: '.cer,.crt,.pem',\n\t},\n\tkey: {\n\t\textensions: ['key'],\n\t\tmaxBytes: 50 * KB,\n\t\tvalidateMagicNumbers: true,\n\t\tlabel: 'private key',\n\t\taccept: '.key',\n\t},\n\tpdf: {\n\t\textensions: ['pdf'],\n\t\tmaxBytes: 20 * MB,\n\t\tvalidateMagicNumbers: true,\n\t\tlabel: 'PDF',\n\t\taccept: '.pdf',\n\t},\n\tdocument: {\n\t\textensions: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'csv', 'txt'],\n\t\tmaxBytes: 20 * MB,\n\t\tvalidateMagicNumbers: false,\n\t\tlabel: 'document',\n\t\taccept: '.pdf,.doc,.docx,.xls,.xlsx,.csv,.txt',\n\t},\n\tdata: {\n\t\textensions: ['xml', 'json'],\n\t\tmaxBytes: 5 * MB,\n\t\tvalidateMagicNumbers: false,\n\t\tlabel: 'data file',\n\t\taccept: '.xml,.json',\n\t},\n\tvideo: {\n\t\textensions: ['mp4', 'mov', 'avi', 'webm'],\n\t\tmaxBytes: 100 * MB,\n\t\tvalidateMagicNumbers: false,\n\t\tlabel: 'video',\n\t\taccept: 'video/*',\n\t},\n\taudio: {\n\t\textensions: ['mp3', 'wav', 'ogg', 'aac'],\n\t\tmaxBytes: 20 * MB,\n\t\tvalidateMagicNumbers: false,\n\t\tlabel: 'audio',\n\t\taccept: 'audio/*',\n\t},\n\tany: {\n\t\textensions: [],\n\t\tmaxBytes: 20 * MB,\n\t\tvalidateMagicNumbers: false,\n\t\tlabel: 'file',\n\t\taccept: '*/*',\n\t},\n}\n","import { MIME_TYPES } from './constants'\nimport type { FileExtension, FileLike } from './types'\n\n/** Lower-case extension without the dot, or `''` when the name has none. */\nexport const getFileExtension = (fileName: string): string => {\n\tconst dot = fileName.lastIndexOf('.')\n\tif (dot < 1 || dot === fileName.length - 1) return ''\n\treturn fileName.slice(dot + 1).toLowerCase()\n}\n\nexport const getMimeType = (fileName: string): string => {\n\tconst ext = getFileExtension(fileName) as FileExtension\n\treturn MIME_TYPES[ext] ?? 'application/octet-stream'\n}\n\nexport const isKnownExtension = (value: string): value is FileExtension =>\n\tvalue in MIME_TYPES\n\nexport const formatFileSize = (bytes: number): string => {\n\tif (bytes < 1024) return `${bytes} B`\n\tconst units = ['KB', 'MB', 'GB', 'TB']\n\tlet size = bytes / 1024\n\tlet unit = 0\n\twhile (size >= 1024 && unit < units.length - 1) {\n\t\tsize /= 1024\n\t\tunit++\n\t}\n\treturn `${size.toFixed(size >= 10 ? 0 : 1)} ${units[unit]}`\n}\n\n/** Longest stem kept. Object stores cap the whole key and the prefix spends part of it. */\nconst MAX_STEM_LENGTH = 80\n\n/** What an all-punctuation name degrades to, so a key never ends in a bare dot. */\nconst FALLBACK_STEM = 'file'\n\nconst slugSegment = (value: string): string =>\n\tvalue\n\t\t.normalize('NFD')\n\t\t.replace(/[\\u0300-\\u036f]/g, '')\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9]+/g, '-')\n\t\t.replace(/^-+|-+$/g, '')\n\n/**\n * Turns a user's file name into a safe key segment: ASCII, lower case, one\n * extension, no path syntax.\n *\n * Call it INSIDE your scope's `path()`, not around it:\n *\n * ```ts\n * path: (id, file) => `Docs/${id}/${sanitizeFileName(file.name)}`\n * ```\n *\n * `resolveKey` deliberately does not apply it for you. It would rewrite the key\n * your `path()` returned, and a client computing the same key to decide replace\n * mode would then disagree with the server. Inside `path()` both sides run the\n * one function and cannot drift.\n *\n * This exists because a macOS screenshot took an API down. `Screenshot\n * 2026-08-31 at 4.18.54 p.m..png` ends in `p.m.` + `.png`, so the raw name\n * carries `..`; the traversal guard rejected the key and the consumer dropped\n * the rejection, killing the process.\n *\n * Only the KEY is slugged — keep the original in `StoredFile.fileName`, which\n * is what a UI should render.\n */\nexport const sanitizeFileName = (fileName: string): string => {\n\t// A name arriving as a path is already an attempt, deliberate or not.\n\tconst base = fileName.split(/[/\\\\]/).pop() ?? ''\n\n\tconst dot = base.lastIndexOf('.')\n\t// A leading dot is a dotfile, not an extension: `.env` has no stem.\n\tconst hasExtension = dot > 0\n\tconst stem = slugSegment(hasExtension ? base.slice(0, dot) : base).slice(\n\t\t0,\n\t\tMAX_STEM_LENGTH\n\t)\n\tconst extension = hasExtension ? slugSegment(base.slice(dot + 1)) : ''\n\n\t// A trailing `-` can reappear when the length cut lands mid-separator.\n\tconst safeStem = stem.replace(/-+$/, '') || FALLBACK_STEM\n\n\treturn extension ? `${safeStem}.${extension}` : safeStem\n}\n\n/** Value for an `<input accept=\"…\">` built from an extension list. */\nexport const toAcceptAttribute = (extensions: FileExtension[]): string =>\n\textensions.length === 0\n\t\t? '*/*'\n\t\t: extensions.map(extension => `.${extension}`).join(',')\n\n/**\n * Adapts a multer memory-storage file to `FileLike` so server code validates\n * with the very same function the browser ran.\n */\nexport const fromMulterFile = (file: {\n\toriginalname: string\n\tsize: number\n\tmimetype: string\n\tbuffer: Uint8Array\n}): FileLike => ({\n\tname: file.originalname,\n\tsize: file.size,\n\ttype: file.mimetype,\n\tarrayBuffer: async () =>\n\t\tfile.buffer.buffer.slice(\n\t\t\tfile.buffer.byteOffset,\n\t\t\tfile.buffer.byteOffset + file.buffer.byteLength\n\t\t) as ArrayBuffer,\n})\n","/**\n * Every user-facing string the package renders, so no copy is hardcoded\n * inside a component and apps translate or reword without forking.\n *\n * English is the default so the kit ships globalized; `ES_LABELS` ships for\n * Spanish apps, selected once through `UploaderProvider language='es'`. Hooks\n * take `labels` in their options, components as a prop — always a `Partial`,\n * merged over the provider's base.\n */\nexport type UploaderLabels = {\n\t/** Prompt inside the single dropzone of `Uploader` (fine pointers). */\n\tdropPrompt: string\n\t/** Same zone on a touch device, where dragging barely exists. */\n\ttapPrompt: string\n\t/** Prompt inside the bulk dropzone of `SlottedUploader` (fine pointers). */\n\tbulkDropPrompt: string\n\t/** Same bulk zone on a touch device. */\n\tbulkTapPrompt: string\n\t/** Button that fires the batch when `uploadOn` is `'manual'`. */\n\tuploadAll: string\n\t/** Row action: preview a file. */\n\tview: string\n\t/** Row action: forget a file. */\n\tremove: string\n\t/** Row action: abort an in-flight upload. */\n\tcancel: string\n\t/** Suffix on a row the server confirmed. */\n\tready: string\n\t/**\n\t * Suffix on a row holding a picked file that has NOT been sent yet.\n\t * Distinct from {@link UploaderLabels.ready}, which means the server\n\t * confirmed it — two states, two strings.\n\t */\n\tstaged: string\n\t/** Slot action when the position is empty. */\n\tupload: string\n\t/** Slot action when the position is filled. */\n\treplace: string\n\t/** Pill shown while a file hovers over a filled slot row. */\n\tdropToReplace: string\n\t/** Viewer: aria-label of the previous-image arrow. */\n\tprevious: string\n\t/** Viewer: aria-label of the next-image arrow. */\n\tnext: string\n\t/** Fallback when a strategy rejects without a message. */\n\tuploadFailed: string\n\t/** The request never reached the server — offline, DNS, CORS preflight. */\n\tuploadNetworkFailed: string\n\t/** Message when the DELETE transport refused or failed. */\n\tremoveFailed: string\n\t/** A stored file could not be fetched for preview or for bytes. */\n\treadFailed: string\n\t/** A batch past `maxFiles`. Receives the cap. */\n\tmaxFilesReached: (max: number) => string\n\t/** A file picked for a slot whose extension the slot rejects. */\n\tslotFormatNotAllowed: (slotLabel: string, extensions: string[]) => string\n\t/** A bulk-dropped file no slot matched. */\n\tfileWithoutSlot: (fileName: string) => string\n\t/** Viewer: while the URL resolves. */\n\tviewerLoading: string\n\t/** Viewer: the URL could not be resolved (expired signature, network). */\n\tviewerError: string\n\t/** Second line of the error panel — what the reader can do about it. */\n\tviewerErrorHint: string\n\t/** Viewer: button that retries the resolution. */\n\tviewerRetry: string\n\t/** Viewer: open the file in a browser tab. */\n\topenInTab: string\n\t/** Header action; the long `download` names a file, this labels a button. */\n\tdownloadShort: string\n\t/** Viewer: close button. */\n\tclose: string\n\t/** Viewer: formats without an inline preview. */\n\tnoPreview: string\n\t/** Viewer: download button. Receives the file name. */\n\tdownload: (fileName: string) => string\n\t/** Viewer: fallback title when the file carries no name. */\n\tfilePreview: string\n\t/** Confirm dialog: affirmative button. */\n\tconfirm: string\n\t/** Confirm dialog heading before forgetting a file. */\n\tconfirmRemoveTitle: string\n\t/** Confirm dialog body before forgetting. Receives the file name. */\n\tconfirmRemoveMessage: (fileName: string) => string\n\t/** Confirm dialog heading before replacing a slot's file. */\n\tconfirmReplaceTitle: string\n\t/** Confirm dialog body before replacing. Receives current and incoming names. */\n\tconfirmReplaceMessage: (current: string, incoming: string) => string\n\t/** `DropAnywhereOverlay` heading while a drag is over the window. */\n\tdropAnywhereTitle: string\n\t/** `DropAnywhereOverlay` second line. */\n\tdropAnywhereHint: string\n\t/** `AvatarUploader`: hover label and aria-label of the picture. */\n\tavatarChange: string\n\t/** `AvatarUploader`: overlay while a file hovers the picture. */\n\tavatarDropHere: string\n\t/** `AvatarUploader`: the remove link under the picture. */\n\tavatarRemove: string\n\t/** `GalleryUploader`: caption of the add tile. */\n\tgalleryAdd: string\n\n\t// Validation — `validateFile` and everything that calls it. These run on\n\t// BOTH sides: the browser shows them, the server answers them as JSON.\n\t/** A name the extension check cannot read at all. */\n\tfileHasNoExtension: string\n\t/** The extension is not on the scope's list. Receives that list. */\n\tformatNotAllowed: (extensions: string[]) => string\n\t/** Zero bytes — a failed drag, a cancelled save. */\n\tfileIsEmpty: string\n\t/** Over the scope ceiling. Receives both sizes already formatted. */\n\tfileTooLarge: (size: string, max: string) => string\n\t/** Fewer bytes than the signature needs: a truncated transfer. */\n\tfileIncomplete: string\n\t/** The leading bytes contradict the extension — renamed, or corrupt. */\n\tcontentDoesNotMatchExtension: string\n\n\t// Server — the HTTP surface. `createStorage` and both framework adapters\n\t// resolve the same object, so one `labels` wires the whole round trip.\n\t/** 400: a route parameter or body field the handler needs is missing. */\n\trequestIncomplete: string\n\t/** 401: `getUser` answered null. */\n\tunauthorized: string\n\t/** 400: an upload arrived with no file part. */\n\tfileRequired: string\n\t/** 404: the URL named a scope the registry does not declare. */\n\tunknownScope: string\n\t/** 500: the catch-all when a handler throws something unrecognized. */\n\tprocessingFailed: string\n\t/** Download name for a key that ends in nothing usable. */\n\tunnamedFile: string\n}\n\n/** Spanish copy — select it once: `<UploaderProvider language='es'>`. */\nexport const ES_LABELS: UploaderLabels = {\n\tdropPrompt: 'Selecciona un archivo o arrástralo aquí',\n\ttapPrompt: 'Toca para elegir un archivo',\n\tbulkDropPrompt: 'Arrastra varios archivos — cada uno cae en su documento',\n\tbulkTapPrompt: 'Toca para agregar varios archivos',\n\tuploadAll: 'Subir archivos',\n\tview: 'Ver',\n\tremove: 'Quitar',\n\tcancel: 'Cancelar',\n\tready: 'Listo',\n\tstaged: 'Listo para subir',\n\tupload: 'Subir',\n\treplace: 'Reemplazar',\n\tdropToReplace: 'Suelta para reemplazar',\n\tprevious: 'Anterior',\n\tnext: 'Siguiente',\n\tuploadFailed: 'No se pudo subir el archivo',\n\tuploadNetworkFailed: 'No se pudo subir el archivo. Revisa tu conexión',\n\tremoveFailed: 'No se pudo borrar el archivo del almacenamiento',\n\treadFailed: 'No se pudo cargar el archivo',\n\tmaxFilesReached: max => `Máximo ${max} archivo(s)`,\n\tslotFormatNotAllowed: (slotLabel, extensions) =>\n\t\t`${slotLabel}: formato no permitido. Se aceptan: ${extensions.join(', ')}`,\n\tfileWithoutSlot: fileName =>\n\t\t`\"${fileName}\" no corresponde a ningún documento`,\n\tviewerLoading: 'Cargando…',\n\tviewerError: 'No se pudo cargar la vista previa',\n\tviewerErrorHint:\n\t\t'El archivo puede haberse movido o el enlace expiró. Puedes reintentar o descargarlo.',\n\tviewerRetry: 'Reintentar',\n\topenInTab: 'Abrir en pestaña',\n\tdownloadShort: 'Descargar',\n\tclose: 'Cerrar',\n\tnoPreview: 'Este formato no tiene vista previa',\n\tdownload: fileName => `Descargar ${fileName}`,\n\tfilePreview: 'Archivo',\n\tconfirm: 'Confirmar',\n\tconfirmRemoveTitle: 'Quitar archivo',\n\tconfirmRemoveMessage: fileName =>\n\t\t`¿Quitar \"${fileName}\"? Podrás subir otro después.`,\n\tconfirmReplaceTitle: 'Reemplazar archivo',\n\tconfirmReplaceMessage: (current, incoming) =>\n\t\t`¿Reemplazar \"${current}\" por \"${incoming}\"?`,\n\tdropAnywhereTitle: 'Suelta los archivos aquí',\n\tdropAnywhereHint: 'Cualquier lugar de la pantalla cuenta',\n\tavatarChange: 'Cambiar foto',\n\tavatarDropHere: 'Suelta aquí',\n\tavatarRemove: 'Quitar foto',\n\tgalleryAdd: 'Agregar',\n\n\tfileHasNoExtension: 'El archivo no tiene extensión',\n\tformatNotAllowed: extensions =>\n\t\t`Formato no permitido. Se aceptan: ${extensions.join(', ')}`,\n\tfileIsEmpty: 'El archivo está vacío',\n\tfileTooLarge: (size, max) => `El archivo pesa ${size} y el máximo es ${max}`,\n\tfileIncomplete: 'El archivo está incompleto',\n\tcontentDoesNotMatchExtension:\n\t\t'El contenido del archivo no corresponde a su extensión',\n\n\trequestIncomplete: 'Solicitud incompleta',\n\tunauthorized: 'No autorizado',\n\tfileRequired: 'Archivo requerido',\n\tunknownScope: 'Destino de archivo no válido',\n\tprocessingFailed: 'No se pudo procesar el archivo',\n\tunnamedFile: 'archivo',\n}\n\n/** English copy — the package default, so the kit ships globalized. */\nexport const DEFAULT_LABELS: UploaderLabels = {\n\tdropPrompt: 'Choose a file or drag it here',\n\ttapPrompt: 'Tap to choose a file',\n\tbulkDropPrompt: 'Drop several files — each lands on its document',\n\tbulkTapPrompt: 'Tap to add several files',\n\tuploadAll: 'Upload files',\n\tview: 'View',\n\tremove: 'Remove',\n\tcancel: 'Cancel',\n\tready: 'Done',\n\tstaged: 'Ready to upload',\n\tupload: 'Upload',\n\treplace: 'Replace',\n\tdropToReplace: 'Drop to replace',\n\tprevious: 'Previous',\n\tnext: 'Next',\n\tuploadFailed: 'The file could not be uploaded',\n\tuploadNetworkFailed: 'The file could not be uploaded. Check your connection',\n\tremoveFailed: 'The file could not be removed from storage',\n\treadFailed: 'The file could not be loaded',\n\tmaxFilesReached: max => `At most ${max} file(s)`,\n\tslotFormatNotAllowed: (slotLabel, extensions) =>\n\t\t`${slotLabel}: format not allowed. Accepted: ${extensions.join(', ')}`,\n\tfileWithoutSlot: fileName => `\"${fileName}\" does not match any document`,\n\tviewerLoading: 'Loading…',\n\tviewerError: 'The preview could not be loaded',\n\tviewerErrorHint:\n\t\t'The file may have moved or the link expired. You can retry or download it.',\n\tviewerRetry: 'Retry',\n\topenInTab: 'Open in tab',\n\tdownloadShort: 'Download',\n\tclose: 'Close',\n\tnoPreview: 'This format has no preview',\n\tdownload: fileName => `Download ${fileName}`,\n\tfilePreview: 'File',\n\tconfirm: 'Confirm',\n\tconfirmRemoveTitle: 'Remove file',\n\tconfirmRemoveMessage: fileName =>\n\t\t`Remove \"${fileName}\"? You can upload another one later.`,\n\tconfirmReplaceTitle: 'Replace file',\n\tconfirmReplaceMessage: (current, incoming) =>\n\t\t`Replace \"${current}\" with \"${incoming}\"?`,\n\tdropAnywhereTitle: 'Drop the files here',\n\tdropAnywhereHint: 'Anywhere on the screen counts',\n\tavatarChange: 'Change photo',\n\tavatarDropHere: 'Drop here',\n\tavatarRemove: 'Remove photo',\n\tgalleryAdd: 'Add',\n\n\tfileHasNoExtension: 'The file has no extension',\n\tformatNotAllowed: extensions =>\n\t\t`Format not allowed. Accepted: ${extensions.join(', ')}`,\n\tfileIsEmpty: 'The file is empty',\n\tfileTooLarge: (size, max) =>\n\t\t`The file weighs ${size} and the maximum is ${max}`,\n\tfileIncomplete: 'The file is incomplete',\n\tcontentDoesNotMatchExtension: 'The file contents do not match its extension',\n\n\trequestIncomplete: 'Incomplete request',\n\tunauthorized: 'Unauthorized',\n\tfileRequired: 'A file is required',\n\tunknownScope: 'Unknown file destination',\n\tprocessingFailed: 'The file could not be processed',\n\tunnamedFile: 'file',\n}\n\n/** The merge every entry point runs: partial overrides over the default. */\nexport const resolveLabels = (\n\tlabels?: Partial<UploaderLabels>,\n\tbase: UploaderLabels = DEFAULT_LABELS\n): UploaderLabels => (labels ? { ...base, ...labels } : base)\n\n/**\n * @deprecated English IS {@link DEFAULT_LABELS} now. A shallow copy, kept so\n * 1.x consumers passing it keep working.\n */\nexport const EN_LABELS: UploaderLabels = { ...DEFAULT_LABELS }\n","import { MAGIC_NUMBERS } from './constants'\nimport { formatFileSize, getFileExtension, isKnownExtension } from './file'\nimport { resolveLabels, type UploaderLabels } from './labels'\nimport type {\n\tFileExtension,\n\tFileLike,\n\tValidationOptions,\n\tValidationResult,\n} from './types'\n\nconst ok: ValidationResult = { valid: true }\n\nconst fail = (\n\tcode: Exclude<ValidationResult, { valid: true }>['code'],\n\tmessage: string\n): ValidationResult => ({ valid: false, code, message })\n\nexport const validateExtension = (\n\tfileName: string,\n\tallowed: FileExtension[],\n\tlabels?: Partial<UploaderLabels>\n): ValidationResult => {\n\tif (allowed.length === 0) return ok\n\n\tconst copy = resolveLabels(labels)\n\tconst extension = getFileExtension(fileName)\n\tif (!extension) {\n\t\treturn fail('extension-not-allowed', copy.fileHasNoExtension)\n\t}\n\tif (!allowed.includes(extension as FileExtension)) {\n\t\treturn fail('extension-not-allowed', copy.formatNotAllowed(allowed))\n\t}\n\treturn ok\n}\n\nexport const validateSize = (\n\tsize: number,\n\tmaxBytes: number,\n\tlabels?: Partial<UploaderLabels>\n): ValidationResult => {\n\tconst copy = resolveLabels(labels)\n\tif (size === 0) return fail('empty-file', copy.fileIsEmpty)\n\tif (size > maxBytes) {\n\t\treturn fail(\n\t\t\t'too-large',\n\t\t\tcopy.fileTooLarge(formatFileSize(size), formatFileSize(maxBytes))\n\t\t)\n\t}\n\treturn ok\n}\n\n/**\n * Compares the file's leading bytes against the signature its extension\n * claims. Extensions without a known signature pass — absence of a signature\n * is not evidence of tampering.\n */\nexport const validateMagicNumbers = async (\n\tfile: FileLike,\n\tlabels?: Partial<UploaderLabels>\n): Promise<ValidationResult> => {\n\tconst copy = resolveLabels(labels)\n\tconst extension = getFileExtension(file.name)\n\tif (!isKnownExtension(extension)) return ok\n\n\tconst expected = MAGIC_NUMBERS[extension]\n\tif (!expected) return ok\n\n\tconst head = new Uint8Array(await file.arrayBuffer()).slice(\n\t\t0,\n\t\texpected.length\n\t)\n\tif (head.length < expected.length) {\n\t\treturn fail('magic-number-mismatch', copy.fileIncomplete)\n\t}\n\n\tconst matches = expected.every((byte, index) => head[index] === byte)\n\treturn matches\n\t\t? ok\n\t\t: fail('magic-number-mismatch', copy.contentDoesNotMatchExtension)\n}\n\n/**\n * Runs the checks cheapest-first so a 2 GB file is rejected on its size before\n * anything reads its bytes.\n */\nexport const validateFile = async (\n\tfile: FileLike,\n\toptions: ValidationOptions = {}\n): Promise<ValidationResult> => {\n\tconst {\n\t\tmaxBytes = Infinity,\n\t\tallowedExtensions = [],\n\t\tvalidateMagicNumbers: checkMagic = false,\n\t\tlabels,\n\t\tcustomValidation,\n\t} = options\n\n\tconst size = validateSize(file.size, maxBytes, labels)\n\tif (!size.valid) return size\n\n\tconst extension = validateExtension(file.name, allowedExtensions, labels)\n\tif (!extension.valid) return extension\n\n\tif (checkMagic) {\n\t\tconst magic = await validateMagicNumbers(file, labels)\n\t\tif (!magic.valid) return magic\n\t}\n\n\tif (customValidation) return customValidation(file)\n\n\treturn ok\n}\n\nexport type FileValidation = {\n\tfile: FileLike\n\tresult: ValidationResult\n}\n\n/** Validates every file and reports each result; never short-circuits. */\nexport const validateFiles = async (\n\tfiles: FileLike[],\n\toptions: ValidationOptions = {}\n): Promise<FileValidation[]> =>\n\tPromise.all(\n\t\tfiles.map(async file => ({\n\t\t\tfile,\n\t\t\tresult: await validateFile(file, options),\n\t\t}))\n\t)\n","import { FILE_CATEGORY_CONFIG } from './constants'\nimport { toAcceptAttribute } from './file'\nimport type { UploaderLabels } from './labels'\nimport type {\n\tFileLike,\n\tReplaceMode,\n\tScopeConfig,\n\tScopeRegistry,\n\tStorageProvider,\n\tValidationResult,\n} from './types'\nimport { validateFile } from './validation'\n\n/** Thrown for a definition or wiring mistake — never for a user's bad file. */\nexport class ScopeError extends Error {\n\toverride name = 'ScopeError'\n}\n\n/** Stand-in entity used to compare the SHAPE of two `path` callbacks. */\nconst PROBE_ENTITY = '__entity__'\n\nconst probeFile = (name: string): FileLike => ({\n\tname,\n\tsize: 0,\n\ttype: '',\n\tarrayBuffer: async () => new ArrayBuffer(0),\n})\n\n/** True when `path` returns the same key whatever the file is called. */\nexport const hasStableKey = (scope: ScopeConfig): boolean =>\n\tscope.path(PROBE_ENTITY, probeFile('a.png')) ===\n\tscope.path(PROBE_ENTITY, probeFile('b.png'))\n\n/**\n * The mode a scope runs in when it does not name one.\n *\n * @see AGENTS.md §3 — why a stable key needs no sweep and a name-carrying one does\n */\nexport const resolveReplaceMode = (scope: ScopeConfig): ReplaceMode => {\n\tif (scope.replace !== undefined) return scope.replace\n\tif ((scope.maxFiles ?? 1) > 1) return 'key'\n\treturn hasStableKey(scope) ? 'key' : 'entity'\n}\n\nconst dirname = (key: string) => {\n\tconst cut = key.lastIndexOf('/')\n\treturn cut === -1 ? '' : key.slice(0, cut)\n}\n\n/** Folder an `'entity'` replace is allowed to sweep. */\nexport const resolveScopePrefix = (\n\tscope: ScopeConfig,\n\tentityId: string\n): string =>\n\tscope.prefix?.(entityId) ??\n\tdirname(scope.path(entityId, probeFile('probe.bin')))\n\n/** `a` contains `b`, counting only whole path segments. */\nconst containsPath = (a: string, b: string) => b === a || b.startsWith(`${a}/`)\n\nconst assertDefinition = (name: string, scope: ScopeConfig): void => {\n\tif (typeof scope.path !== 'function') {\n\t\tthrow new ScopeError(`Scope \"${name}\": \"path\" must be a function`)\n\t}\n\tif (!Number.isFinite(scope.maxBytes) || scope.maxBytes <= 0) {\n\t\tthrow new ScopeError(\n\t\t\t`Scope \"${name}\": \"maxBytes\" must be a positive number`\n\t\t)\n\t}\n\tif (scope.accept.length === 0) {\n\t\tthrow new ScopeError(\n\t\t\t`Scope \"${name}\": \"accept\" cannot be empty — list the extensions this scope takes`\n\t\t)\n\t}\n\n\t// A scope narrows its category, never widens it. Otherwise a 'pdf' scope\n\t// could quietly start taking executables.\n\tif (scope.category && scope.category !== 'any') {\n\t\tconst allowed = FILE_CATEGORY_CONFIG[scope.category].extensions\n\t\tconst outside = scope.accept.filter(\n\t\t\textension => !allowed.includes(extension)\n\t\t)\n\t\tif (outside.length > 0) {\n\t\t\tthrow new ScopeError(\n\t\t\t\t`Scope \"${name}\": ${outside.join(', ')} not allowed by category \"${scope.category}\"`\n\t\t\t)\n\t\t}\n\t}\n\n\tif (scope.compress) {\n\t\tconst imageOnly = FILE_CATEGORY_CONFIG.image.extensions\n\t\tconst notImages = scope.accept.filter(\n\t\t\textension => !imageOnly.includes(extension)\n\t\t)\n\t\tif (notImages.length > 0) {\n\t\t\tthrow new ScopeError(\n\t\t\t\t`Scope \"${name}\": \"compress\" only applies to images, but it accepts ${notImages.join(', ')}`\n\t\t\t)\n\t\t}\n\t}\n\n\tif (scope.maxFiles !== undefined) {\n\t\tif (!Number.isInteger(scope.maxFiles) || scope.maxFiles < 1) {\n\t\t\tthrow new ScopeError(\n\t\t\t\t`Scope \"${name}\": \"maxFiles\" must be a positive integer, received ${String(scope.maxFiles)}`\n\t\t\t)\n\t\t}\n\t}\n\n\t// The contradiction the derived default exists to prevent: a scope that\n\t// holds a collection cannot also erase the collection on every upload.\n\tif (scope.replace === 'entity' && (scope.maxFiles ?? 1) > 1) {\n\t\tthrow new ScopeError(\n\t\t\t`Scope \"${name}\": replace \"entity\" contradicts maxFiles ${scope.maxFiles} — ` +\n\t\t\t\t'an entity-wide replace would delete the other files on the next upload. ' +\n\t\t\t\t\"Use 'key' to overwrite one slot, or false to keep every version.\"\n\t\t)\n\t}\n\n\tif (\n\t\tresolveReplaceMode(scope) === 'entity' &&\n\t\tresolveScopePrefix(scope, PROBE_ENTITY) === ''\n\t) {\n\t\tthrow new ScopeError(\n\t\t\t`Scope \"${name}\": replace \"entity\" needs a folder to sweep, but \"path\" resolves to a ` +\n\t\t\t\t'bucket-root key. Nest the key under a folder, or declare \"prefix\".'\n\t\t)\n\t}\n}\n\n/**\n * Rejects a registry where sweeping one scope would reach another's objects.\n *\n * `path` shapes are compared through a placeholder entity, so this catches the\n * real hazard — two scopes sharing a folder for the SAME entity — while two\n * scopes rooted at different entity folders stay independent. A false positive\n * is answered with an explicit `prefix`, and the message says so.\n */\nconst assertNoSweepCollision = (scopes: Record<string, ScopeConfig>): void => {\n\tconst entries = Object.entries(scopes)\n\n\tfor (const [name, scope] of entries) {\n\t\tif (resolveReplaceMode(scope) !== 'entity') continue\n\t\tconst prefix = resolveScopePrefix(scope, PROBE_ENTITY)\n\n\t\tfor (const [otherName, other] of entries) {\n\t\t\tif (otherName === name) continue\n\t\t\tconst otherKey = other.path(PROBE_ENTITY, probeFile('probe.bin'))\n\t\t\tif (!containsPath(prefix, otherKey)) continue\n\n\t\t\tthrow new ScopeError(\n\t\t\t\t`Scope \"${name}\" replaces per entity and would sweep \"${prefix}\", which also holds ` +\n\t\t\t\t\t`scope \"${otherName}\" — an upload to one would delete the other's files. ` +\n\t\t\t\t\t'Give each its own folder, or declare a narrower \"prefix\".'\n\t\t\t)\n\t\t}\n\t}\n}\n\n/**\n * Declares every destination an app can write to. The returned registry is\n * imported by the client (to configure inputs and pre-validate) and by the\n * server (to authorize and re-validate), which is what keeps the two in sync.\n *\n * Definitions are checked eagerly: a malformed scope throws at import time,\n * not on the first upload. That includes the two replace mistakes no runtime\n * check could recover from — an entity-wide replace on a collection, and two\n * scopes whose folders overlap, where an avatar upload would delete the same\n * user's documents.\n */\nexport const defineScopes = <T extends Record<string, ScopeConfig>>(\n\tscopes: T\n): ScopeRegistry<T> => {\n\tfor (const [name, scope] of Object.entries(scopes)) {\n\t\tassertDefinition(name, scope)\n\t}\n\tassertNoSweepCollision(scopes)\n\n\tconst names = Object.keys(scopes) as (keyof T & string)[]\n\n\tconst get = (name: string): ScopeConfig => {\n\t\tconst scope = scopes[name as keyof T]\n\t\tif (!scope) {\n\t\t\tthrow new ScopeError(\n\t\t\t\t`Unknown scope \"${name}\". Declared: ${names.join(', ')}`\n\t\t\t)\n\t\t}\n\t\treturn scope\n\t}\n\n\treturn {\n\t\tscopes,\n\t\tnames,\n\t\tget,\n\t\thas: name => name in scopes,\n\t\taccept: name => toAcceptAttribute(get(name).accept),\n\t}\n}\n\n/**\n * The one validation call. Client runs it for feedback, server runs it for\n * safety, both against the same scope.\n *\n * `labels` is what keeps the two answers identical in wording as well as in\n * verdict: the hook passes the copy it renders with, `createStorage` passes\n * the copy it was configured with.\n */\nexport const validateForScope = async <T extends Record<string, ScopeConfig>>(\n\tregistry: ScopeRegistry<T>,\n\tname: string,\n\tfile: FileLike,\n\tlabels?: Partial<UploaderLabels>\n): Promise<ValidationResult> => {\n\tconst scope = registry.get(name)\n\tconst category = scope.category\n\t\t? FILE_CATEGORY_CONFIG[scope.category]\n\t\t: undefined\n\n\treturn validateFile(file, {\n\t\tmaxBytes: scope.maxBytes,\n\t\tallowedExtensions: scope.accept,\n\t\tvalidateMagicNumbers: category?.validateMagicNumbers ?? true,\n\t\tlabels,\n\t})\n}\n\n/**\n * Fails fast when a provider cannot honour what the scopes promise — a private\n * scope on a provider that cannot sign URLs is a runtime 403 waiting to happen.\n */\nexport const assertProviderSupports = <T extends Record<string, ScopeConfig>>(\n\tregistry: ScopeRegistry<T>,\n\tprovider: StorageProvider\n): void => {\n\tconst needsSigning = registry.names.filter(\n\t\tname => registry.get(name).visibility === 'private'\n\t)\n\n\tif (needsSigning.length > 0 && !provider.capabilities.signedUrl) {\n\t\tthrow new ScopeError(\n\t\t\t`Provider \"${provider.name}\" cannot sign URLs, but these scopes are private: ${needsSigning.join(', ')}`\n\t\t)\n\t}\n}\n\n/**\n * Whether a key would escape its own prefix.\n *\n * Judged per SEGMENT. A `key.includes('..')` test reads as the same check and\n * is not: `report..pdf` and `Screenshot at 4.18.54 p.m..png` carry two dots\n * without ever being traversal, and a macOS screenshot is the common case, not\n * a corner one. Traversal needs a segment that IS `..`, so that is what this\n * asks.\n */\nconst escapesPrefix = (key: string): boolean =>\n\tkey.startsWith('/') ||\n\tkey.split('/').some(segment => segment === '..' || segment === '.')\n\n/**\n * Resolves the storage key for an upload.\n *\n * The guard is a backstop, not a sanitizer: it refuses a key rather than\n * rewriting one, because a client computing the same key to decide replace\n * mode would then disagree with the server. Run {@link sanitizeFileName}\n * inside the scope's `path()` and both sides stay identical.\n */\nexport const resolveKey = <T extends Record<string, ScopeConfig>>(\n\tregistry: ScopeRegistry<T>,\n\tname: string,\n\tentityId: string,\n\tfile: FileLike\n): string => {\n\tconst key = registry.get(name).path(entityId, file)\n\tif (escapesPrefix(key)) {\n\t\tthrow new ScopeError(`Scope \"${name}\" produced an unsafe key: ${key}`)\n\t}\n\treturn key\n}\n","import {\n\tcreateCipheriv,\n\tcreateDecipheriv,\n\ttype DecipherGCM,\n\trandomBytes,\n} from 'node:crypto'\nimport { Transform, type TransformCallback } from 'node:stream'\n\nimport { ScopeError } from '../scopes'\nimport type { StreamingCryptoHooks } from './storage'\n\nconst IV_LENGTH = 12\nconst TAG_LENGTH = 16\nconst HEADER_LENGTH = IV_LENGTH + TAG_LENGTH\nconst KEY_PATTERN = /^[0-9a-fA-F]{64}$/\n\n/**\n * Decrypts as bytes arrive instead of after they all have.\n *\n * The `[iv][tag][ciphertext]` layout is what makes this possible at all: GCM\n * needs the tag before it can decrypt, and here it leads the object rather\n * than trailing it. The header may straddle several chunks, so it is\n * accumulated until complete and whatever followed it in that chunk is fed\n * onward — a 12-byte first chunk would otherwise be dropped.\n */\nclass GcmDecryptStream extends Transform {\n\t#header = Buffer.alloc(0)\n\t#decipher: DecipherGCM | null = null\n\n\tconstructor(private readonly key: Buffer) {\n\t\tsuper()\n\t}\n\n\toverride _transform(\n\t\tchunk: Buffer,\n\t\t_encoding: BufferEncoding,\n\t\tdone: TransformCallback\n\t): void {\n\t\tlet body = chunk\n\t\tif (!this.#decipher) {\n\t\t\tthis.#header = Buffer.concat([this.#header, chunk])\n\t\t\tif (this.#header.length < HEADER_LENGTH) return done()\n\n\t\t\tthis.#decipher = createDecipheriv(\n\t\t\t\t'aes-256-gcm',\n\t\t\t\tthis.key,\n\t\t\t\tthis.#header.subarray(0, IV_LENGTH)\n\t\t\t) as DecipherGCM\n\t\t\tthis.#decipher.setAuthTag(this.#header.subarray(IV_LENGTH, HEADER_LENGTH))\n\t\t\tbody = this.#header.subarray(HEADER_LENGTH)\n\t\t\tif (body.length === 0) return done()\n\t\t}\n\n\t\ttry {\n\t\t\tthis.push(this.#decipher.update(body))\n\t\t\tdone()\n\t\t} catch (error) {\n\t\t\tdone(error as Error)\n\t\t}\n\t}\n\n\toverride _flush(done: TransformCallback): void {\n\t\tif (!this.#decipher) {\n\t\t\treturn done(new Error('Encrypted object ended before its header'))\n\t\t}\n\t\ttry {\n\t\t\t// Throws when the tag does not match. By now the consumer already has\n\t\t\t// the plaintext, so tampering surfaces as a broken stream, not as a\n\t\t\t// clean rejection — the cost of streaming, stated in `CryptoHooks`.\n\t\t\tthis.push(this.#decipher.final())\n\t\t\tdone()\n\t\t} catch (error) {\n\t\t\tdone(error as Error)\n\t\t}\n\t}\n}\n\n/**\n * Reference cipher for `encrypt: true` scopes: AES-256-GCM, layout\n * `[iv 12][auth tag 16][ciphertext]`. Injecting different {@link CryptoHooks}\n * into `createStorage` stays fully supported.\n *\n * The key must be exactly 64 hex chars — no passphrase derivation, so two\n * instances can never run \"almost the same\" secret and silently write\n * mutually unreadable files. `openssl rand -hex 32`.\n *\n * @see AGENTS.md §4.4\n */\nexport const createAesGcmCrypto = (hexKey: string): StreamingCryptoHooks => {\n\tif (!KEY_PATTERN.test(hexKey)) {\n\t\tthrow new ScopeError(\n\t\t\t'createAesGcmCrypto: the key must be exactly 64 hex characters (32 bytes). Generate one with `openssl rand -hex 32`.'\n\t\t)\n\t}\n\tconst key = Buffer.from(hexKey, 'hex')\n\n\treturn {\n\t\tencrypt: data => {\n\t\t\tconst iv = randomBytes(IV_LENGTH)\n\t\t\tconst cipher = createCipheriv('aes-256-gcm', key, iv)\n\t\t\tconst encrypted = Buffer.concat([cipher.update(data), cipher.final()])\n\t\t\treturn Buffer.concat([iv, cipher.getAuthTag(), encrypted])\n\t\t},\n\t\tdecrypt: data => {\n\t\t\tconst buffer = Buffer.from(data)\n\t\t\tconst iv = buffer.subarray(0, IV_LENGTH)\n\t\t\tconst tag = buffer.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH)\n\t\t\tconst ciphertext = buffer.subarray(IV_LENGTH + TAG_LENGTH)\n\t\t\tconst decipher = createDecipheriv('aes-256-gcm', key, iv)\n\t\t\tdecipher.setAuthTag(tag)\n\t\t\treturn Buffer.concat([decipher.update(ciphertext), decipher.final()])\n\t\t},\n\t\tdecryptStream: source =>\n\t\t\t(source as NodeJS.ReadableStream).pipe(new GcmDecryptStream(key)),\n\t}\n}\n","import { Readable } from 'node:stream'\n\nimport { resolveLabels, type UploaderLabels } from '../labels'\nimport { resolveReplaceMode, resolveScopePrefix } from '../scopes'\nimport { resolveKey } from '../scopes'\nimport { assertProviderSupports, ScopeError, validateForScope } from '../scopes'\nimport type { ScopeConfig, ScopeRegistry } from '../types'\nimport type {\n\tCryptoHooks,\n\tFileLike,\n\tSignedUrlOptions,\n\tStorageProvider,\n\tStoredFile,\n} from '../types'\n\n/**\n * A request-level failure: bad file, unknown scope name from a URL, missing\n * part. Carries the HTTP status and a message safe to show the user — unlike\n * `ScopeError`, which flags wiring bugs and must stay dev-facing.\n */\nexport class StorageRequestError extends Error {\n\toverride name = 'StorageRequestError'\n\tconstructor(\n\t\tmessage: string,\n\t\treadonly status: number\n\t) {\n\t\tsuper(message)\n\t}\n}\n\nexport type CreateStorageOptions<T extends Record<string, ScopeConfig>> = {\n\tscopes: ScopeRegistry<T>\n\tprovider: StreamingStorageProvider\n\t/** Required when any scope declares `encrypt`. The app owns the cipher. */\n\tcrypto?: StreamingCryptoHooks\n\t/** Lifetime of signed URLs, in seconds. @defaultValue 300 */\n\tsignedUrlTtl?: number\n\t/**\n\t * `StoredFile.url` for encrypted scopes. Required alongside them: a signed\n\t * URL points at the bucket object, which is CIPHERTEXT — the only readable\n\t * route is the app's authenticated view endpoint (which runs `read` and\n\t * decrypts). Return that endpoint's URL, absolute or app-relative.\n\t */\n\tencryptedUrl?: (input: {\n\t\tscope: string\n\t\tentityId: string\n\t\tkey: string\n\t}) => string\n\t/**\n\t * Copy for every `StorageRequestError` this service words, and for the\n\t * validation messages it answers with. Omitted means English.\n\t *\n\t * Hand it the same object the client renders with: the server re-runs the\n\t * very validation the browser ran, so a mismatch here is the one place a\n\t * user sees the same rejection twice, in two languages.\n\t */\n\tlabels?: Partial<UploaderLabels>\n}\n\nexport type UploadInput = {\n\tscope: string\n\tentityId: string\n\tfile: FileLike\n\tuploadedBy?: string\n}\n\n/**\n * A stored file plus the keys the upload removed. `replaced` is empty unless\n * the scope sweeps per entity; when it is not, the app must drop those keys\n * from whatever it persisted, or it keeps rendering objects that are gone.\n */\nexport type UploadResult = StoredFile & { replaced: string[] }\n\n/**\n * A provider that can also read without buffering. Additive and optional, so\n * every existing provider still satisfies it — `readStream` falls back to\n * `get` when the method is absent.\n *\n * Declared here rather than in the core because the core's `StorageProvider`\n * is the published contract; widening it there would be a breaking change for\n * anyone implementing it.\n */\nexport type StreamingStorageProvider = StorageProvider & {\n\t/**\n\t * Reads without holding the object in memory. Serving a 20MB document\n\t * through `get` costs its full size in RAM per concurrent request.\n\t */\n\tgetStream?(key: string): Promise<Readable>\n}\n\n/** {@link CryptoHooks} that can also decrypt progressively. */\nexport type StreamingCryptoHooks = CryptoHooks & {\n\t/**\n\t * Decrypts as bytes arrive, so a read never materializes the plaintext.\n\t *\n\t * Weigh the cost: plaintext reaches the consumer BEFORE the authentication\n\t * tag is verified, so tampering surfaces as a stream that fails at the end,\n\t * mid-delivery. `decrypt` verifies before returning a single byte.\n\t *\n\t * There is no streaming counterpart for encryption on purpose — AES-GCM\n\t * only knows its tag once the input ends, and the layout writes it first.\n\t */\n\tdecryptStream?: (source: Readable) => Readable\n}\n\nexport type StorageService<\n\tT extends Record<string, ScopeConfig> = Record<string, ScopeConfig>,\n> = {\n\tupload(input: UploadInput): Promise<UploadResult>\n\t/** Raw bytes, decrypted when the scope is encrypted. */\n\tread(input: { scope: string; key: string }): Promise<Uint8Array>\n\t/**\n\t * The same content as a stream, so serving a file costs a buffer's worth of\n\t * memory instead of the whole object. Falls back to `read` when the provider\n\t * or the cipher cannot stream, so it is always safe to prefer.\n\t */\n\treadStream(input: { scope: string; key: string }): Promise<Readable>\n\tremove(input: { scope: string; key: string }): Promise<boolean>\n\t/** Fresh expiring URL for a private object. Throws on public scopes. */\n\tsignedUrl(input: {\n\t\tscope: string\n\t\tkey: string\n\t\tdownload?: boolean\n\t\texpiresIn?: number\n\t}): Promise<string>\n\tlist(prefix: string): Promise<{ key: string; size: number }[]>\n\tscopes: ScopeRegistry<T>\n\t/**\n\t * The resolved copy, exposed so the framework adapters word their own\n\t * failures with it. One `labels` on `createStorage` therefore covers the\n\t * whole round trip — the service and the route that wraps it cannot end up\n\t * answering in two languages.\n\t */\n\tlabels: UploaderLabels\n}\n\nconst toHex = (buffer: ArrayBuffer): string =>\n\t[...new Uint8Array(buffer)]\n\t\t.map(byte => byte.toString(16).padStart(2, '0'))\n\t\t.join('')\n\n/**\n * The server side of the contract. Boots defensively: a provider that cannot\n * honour the scopes, or an encrypted scope without an injected cipher, throws\n * here — before the first request, while a deploy can still fail loudly.\n */\nexport const createStorage = <T extends Record<string, ScopeConfig>>({\n\tscopes,\n\tprovider,\n\tcrypto,\n\tsignedUrlTtl = 300,\n\tencryptedUrl,\n\tlabels,\n}: CreateStorageOptions<T>): StorageService<T> => {\n\tconst copy = resolveLabels(labels)\n\n\tassertProviderSupports(scopes, provider)\n\n\tconst encrypted = scopes.names.filter(name => scopes.get(name).encrypt)\n\tif (encrypted.length > 0 && !crypto) {\n\t\tthrow new ScopeError(\n\t\t\t`Scopes ${encrypted.join(', ')} declare \"encrypt\" but no CryptoHooks were provided to createStorage`\n\t\t)\n\t}\n\tif (encrypted.length > 0 && !encryptedUrl) {\n\t\tthrow new ScopeError(\n\t\t\t`Scopes ${encrypted.join(', ')} declare \"encrypt\" but no \"encryptedUrl\" was provided — a signed URL would serve ciphertext. Point it at the app's authenticated view endpoint.`\n\t\t)\n\t}\n\n\tconst getScope = (name: string): ScopeConfig => {\n\t\tif (!scopes.has(name)) {\n\t\t\tthrow new StorageRequestError(copy.unknownScope, 404)\n\t\t}\n\t\treturn scopes.get(name)\n\t}\n\n\tconst upload = async ({\n\t\tscope: name,\n\t\tentityId,\n\t\tfile,\n\t\tuploadedBy,\n\t}: UploadInput): Promise<UploadResult> => {\n\t\tconst scope = getScope(name)\n\n\t\t// The client already validated; running the same function again here is\n\t\t// the part that makes the client check advisory instead of load-bearing.\n\t\tconst result = await validateForScope(scopes, name, file, copy)\n\t\tif (!result.valid) throw new StorageRequestError(result.message, 422)\n\n\t\tconst key = resolveKey(scopes, name, entityId, file)\n\t\tconst raw = new Uint8Array(await file.arrayBuffer())\n\t\tconst checksum = toHex(\n\t\t\tawait globalThis.crypto.subtle.digest('SHA-256', raw)\n\t\t)\n\t\tconst body = scope.encrypt ? await crypto!.encrypt(raw) : raw\n\n\t\tconst put = await provider.put({\n\t\t\tkey,\n\t\t\tbody,\n\t\t\t// Encrypted bytes are opaque on purpose: serving them with the real\n\t\t\t// MIME would let a browser try to render ciphertext.\n\t\t\tcontentType: scope.encrypt ? 'application/octet-stream' : file.type,\n\t\t\tvisibility: scope.visibility,\n\t\t\tmetadata: scope.metadata,\n\t\t})\n\n\t\t// Orphan sweep AFTER a successful put: the new object must exist before\n\t\t// anything is deleted, or a failure between the two leaves the entity\n\t\t// with nothing. Delete failures are swallowed on purpose — the upload\n\t\t// the caller asked for did happen, and a stale object is not worth\n\t\t// failing it over.\n\t\tconst replaced: string[] = []\n\t\tif (resolveReplaceMode(scope) === 'entity') {\n\t\t\tconst prefix = resolveScopePrefix(scope, entityId)\n\t\t\tconst stale = (await provider.list(prefix)).filter(\n\t\t\t\tobject => object.key !== put.key\n\t\t\t)\n\t\t\tawait Promise.all(\n\t\t\t\tstale.map(object =>\n\t\t\t\t\tprovider\n\t\t\t\t\t\t.delete(object.key)\n\t\t\t\t\t\t// Only what the provider confirmed gone: `replaced` is what\n\t\t\t\t\t\t// the app drops from its own records.\n\t\t\t\t\t\t.then(deleted => {\n\t\t\t\t\t\t\tif (deleted) replaced.push(object.key)\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.catch(() => undefined)\n\t\t\t\t)\n\t\t\t)\n\t\t}\n\n\t\tconst contentChecksum = put.checksum ?? checksum\n\t\tconst publicUrl = put.url\n\t\t\t? `${put.url}${put.url.includes('?') ? '&' : '?'}v=${contentChecksum.slice(0, 16)}`\n\t\t\t: put.url\n\n\t\treturn {\n\t\t\tkey: put.key,\n\t\t\treplaced,\n\t\t\turl: scope.encrypt\n\t\t\t\t? encryptedUrl!({ scope: name, entityId, key: put.key })\n\t\t\t\t: scope.visibility === 'private'\n\t\t\t\t\t? await provider.signedUrl!(put.key, { expiresIn: signedUrlTtl })\n\t\t\t\t\t: publicUrl,\n\t\t\tscope: name,\n\t\t\tentityId,\n\t\t\tfileName: file.name,\n\t\t\tmimeType: file.type,\n\t\t\tsize: file.size,\n\t\t\tchecksum: contentChecksum,\n\t\t\tuploadedAt: Date.now(),\n\t\t\t...(uploadedBy ? { uploadedBy } : {}),\n\t\t}\n\t}\n\n\tconst read = async ({ scope: name, key }: { scope: string; key: string }) => {\n\t\tconst scope = getScope(name)\n\t\tconst data = await provider.get(key)\n\t\treturn scope.encrypt ? crypto!.decrypt(data) : data\n\t}\n\n\t/**\n\t * The same read without holding the object in memory.\n\t *\n\t * Degrades honestly: a provider with no `getStream`, or an encrypted scope\n\t * whose crypto has no `decryptStream`, falls back to `read` and is wrapped\n\t * in a one-chunk stream. Callers get one shape either way and never have to\n\t * ask which capabilities are present.\n\t */\n\tconst readStream = async ({\n\t\tscope: name,\n\t\tkey,\n\t}: {\n\t\tscope: string\n\t\tkey: string\n\t}): Promise<Readable> => {\n\t\tconst scope = getScope(name)\n\t\tconst canStream =\n\t\t\tprovider.getStream && (!scope.encrypt || crypto?.decryptStream)\n\n\t\tif (!canStream) {\n\t\t\treturn Readable.from([Buffer.from(await read({ scope: name, key }))])\n\t\t}\n\n\t\tconst source = (await provider.getStream!(key)) as Readable\n\t\treturn scope.encrypt ? (crypto!.decryptStream!(source) as Readable) : source\n\t}\n\n\tconst remove = ({ scope: name, key }: { scope: string; key: string }) => {\n\t\tconst scope = getScope(name)\n\t\t// The history contract, enforced where it cannot be bypassed: a scope\n\t\t// marked `keepOnRemove` answers `false` instead of destroying the\n\t\t// trail, whatever any client asks.\n\t\tif (scope.keepOnRemove) {\n\t\t\treturn Promise.resolve(false)\n\t\t}\n\t\treturn provider.delete(key)\n\t}\n\n\tconst signedUrl = ({\n\t\tscope: name,\n\t\tkey,\n\t\tdownload,\n\t\texpiresIn,\n\t}: {\n\t\tscope: string\n\t\tkey: string\n\t\tdownload?: boolean\n\t\texpiresIn?: number\n\t}) => {\n\t\tconst scope = getScope(name)\n\t\tif (scope.visibility !== 'private') {\n\t\t\tthrow new ScopeError(\n\t\t\t\t`Scope \"${name}\" is public; its objects are reachable by their stored URL and never signed`\n\t\t\t)\n\t\t}\n\t\tconst options: SignedUrlOptions = {\n\t\t\texpiresIn: expiresIn ?? signedUrlTtl,\n\t\t\t...(download !== undefined ? { download } : {}),\n\t\t}\n\t\treturn provider.signedUrl!(key, options)\n\t}\n\n\treturn {\n\t\tupload,\n\t\tread,\n\t\treadStream,\n\t\tremove,\n\t\tsignedUrl,\n\t\tlist: prefix => provider.list(prefix),\n\t\tscopes,\n\t\tlabels: copy,\n\t}\n}\n"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { a as StreamingCryptoHooks } from './storage-CYkSHWZX.cjs';
|
|
2
|
+
export { C as CreateStorageOptions, b as StorageRequestError, c as StorageService, U as UploadInput, d as UploadResult, e as createStorage } from './storage-CYkSHWZX.cjs';
|
|
3
|
+
import 'node:stream';
|
|
4
|
+
import './types-BSlJJwti.cjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Reference cipher for `encrypt: true` scopes: AES-256-GCM, layout
|
|
8
|
+
* `[iv 12][auth tag 16][ciphertext]`. Injecting different {@link CryptoHooks}
|
|
9
|
+
* into `createStorage` stays fully supported.
|
|
10
|
+
*
|
|
11
|
+
* The key must be exactly 64 hex chars — no passphrase derivation, so two
|
|
12
|
+
* instances can never run "almost the same" secret and silently write
|
|
13
|
+
* mutually unreadable files. `openssl rand -hex 32`.
|
|
14
|
+
*
|
|
15
|
+
* @see AGENTS.md §4.4
|
|
16
|
+
*/
|
|
17
|
+
declare const createAesGcmCrypto: (hexKey: string) => StreamingCryptoHooks;
|
|
18
|
+
|
|
19
|
+
export { createAesGcmCrypto };
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { a as StreamingCryptoHooks } from './storage-Qc9epG0G.js';
|
|
2
|
+
export { C as CreateStorageOptions, b as StorageRequestError, c as StorageService, U as UploadInput, d as UploadResult, e as createStorage } from './storage-Qc9epG0G.js';
|
|
3
|
+
import 'node:stream';
|
|
4
|
+
import './types-BSlJJwti.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Reference cipher for `encrypt: true` scopes: AES-256-GCM, layout
|
|
8
|
+
* `[iv 12][auth tag 16][ciphertext]`. Injecting different {@link CryptoHooks}
|
|
9
|
+
* into `createStorage` stays fully supported.
|
|
10
|
+
*
|
|
11
|
+
* The key must be exactly 64 hex chars — no passphrase derivation, so two
|
|
12
|
+
* instances can never run "almost the same" secret and silently write
|
|
13
|
+
* mutually unreadable files. `openssl rand -hex 32`.
|
|
14
|
+
*
|
|
15
|
+
* @see AGENTS.md §4.4
|
|
16
|
+
*/
|
|
17
|
+
declare const createAesGcmCrypto: (hexKey: string) => StreamingCryptoHooks;
|
|
18
|
+
|
|
19
|
+
export { createAesGcmCrypto };
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export { StorageRequestError, createStorage } from './chunk-3FI44IOW.js';
|
|
2
|
+
import { ScopeError } from './chunk-T5YZG6JW.js';
|
|
3
|
+
import { createDecipheriv, randomBytes, createCipheriv } from 'crypto';
|
|
4
|
+
import { Transform } from 'stream';
|
|
5
|
+
|
|
6
|
+
var IV_LENGTH = 12;
|
|
7
|
+
var TAG_LENGTH = 16;
|
|
8
|
+
var HEADER_LENGTH = IV_LENGTH + TAG_LENGTH;
|
|
9
|
+
var KEY_PATTERN = /^[0-9a-fA-F]{64}$/;
|
|
10
|
+
var GcmDecryptStream = class extends Transform {
|
|
11
|
+
constructor(key) {
|
|
12
|
+
super();
|
|
13
|
+
this.key = key;
|
|
14
|
+
}
|
|
15
|
+
key;
|
|
16
|
+
#header = Buffer.alloc(0);
|
|
17
|
+
#decipher = null;
|
|
18
|
+
_transform(chunk, _encoding, done) {
|
|
19
|
+
let body = chunk;
|
|
20
|
+
if (!this.#decipher) {
|
|
21
|
+
this.#header = Buffer.concat([this.#header, chunk]);
|
|
22
|
+
if (this.#header.length < HEADER_LENGTH) return done();
|
|
23
|
+
this.#decipher = createDecipheriv(
|
|
24
|
+
"aes-256-gcm",
|
|
25
|
+
this.key,
|
|
26
|
+
this.#header.subarray(0, IV_LENGTH)
|
|
27
|
+
);
|
|
28
|
+
this.#decipher.setAuthTag(this.#header.subarray(IV_LENGTH, HEADER_LENGTH));
|
|
29
|
+
body = this.#header.subarray(HEADER_LENGTH);
|
|
30
|
+
if (body.length === 0) return done();
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
this.push(this.#decipher.update(body));
|
|
34
|
+
done();
|
|
35
|
+
} catch (error) {
|
|
36
|
+
done(error);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
_flush(done) {
|
|
40
|
+
if (!this.#decipher) {
|
|
41
|
+
return done(new Error("Encrypted object ended before its header"));
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
this.push(this.#decipher.final());
|
|
45
|
+
done();
|
|
46
|
+
} catch (error) {
|
|
47
|
+
done(error);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
var createAesGcmCrypto = (hexKey) => {
|
|
52
|
+
if (!KEY_PATTERN.test(hexKey)) {
|
|
53
|
+
throw new ScopeError(
|
|
54
|
+
"createAesGcmCrypto: the key must be exactly 64 hex characters (32 bytes). Generate one with `openssl rand -hex 32`."
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
const key = Buffer.from(hexKey, "hex");
|
|
58
|
+
return {
|
|
59
|
+
encrypt: (data) => {
|
|
60
|
+
const iv = randomBytes(IV_LENGTH);
|
|
61
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
62
|
+
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
|
|
63
|
+
return Buffer.concat([iv, cipher.getAuthTag(), encrypted]);
|
|
64
|
+
},
|
|
65
|
+
decrypt: (data) => {
|
|
66
|
+
const buffer = Buffer.from(data);
|
|
67
|
+
const iv = buffer.subarray(0, IV_LENGTH);
|
|
68
|
+
const tag = buffer.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH);
|
|
69
|
+
const ciphertext = buffer.subarray(IV_LENGTH + TAG_LENGTH);
|
|
70
|
+
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
71
|
+
decipher.setAuthTag(tag);
|
|
72
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
73
|
+
},
|
|
74
|
+
decryptStream: (source) => source.pipe(new GcmDecryptStream(key))
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export { createAesGcmCrypto };
|
|
79
|
+
//# sourceMappingURL=server.js.map
|
|
80
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server/crypto.ts"],"names":[],"mappings":"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","file":"server.js","sourcesContent":["import {\n\tcreateCipheriv,\n\tcreateDecipheriv,\n\ttype DecipherGCM,\n\trandomBytes,\n} from 'node:crypto'\nimport { Transform, type TransformCallback } from 'node:stream'\n\nimport { ScopeError } from '../scopes'\nimport type { StreamingCryptoHooks } from './storage'\n\nconst IV_LENGTH = 12\nconst TAG_LENGTH = 16\nconst HEADER_LENGTH = IV_LENGTH + TAG_LENGTH\nconst KEY_PATTERN = /^[0-9a-fA-F]{64}$/\n\n/**\n * Decrypts as bytes arrive instead of after they all have.\n *\n * The `[iv][tag][ciphertext]` layout is what makes this possible at all: GCM\n * needs the tag before it can decrypt, and here it leads the object rather\n * than trailing it. The header may straddle several chunks, so it is\n * accumulated until complete and whatever followed it in that chunk is fed\n * onward — a 12-byte first chunk would otherwise be dropped.\n */\nclass GcmDecryptStream extends Transform {\n\t#header = Buffer.alloc(0)\n\t#decipher: DecipherGCM | null = null\n\n\tconstructor(private readonly key: Buffer) {\n\t\tsuper()\n\t}\n\n\toverride _transform(\n\t\tchunk: Buffer,\n\t\t_encoding: BufferEncoding,\n\t\tdone: TransformCallback\n\t): void {\n\t\tlet body = chunk\n\t\tif (!this.#decipher) {\n\t\t\tthis.#header = Buffer.concat([this.#header, chunk])\n\t\t\tif (this.#header.length < HEADER_LENGTH) return done()\n\n\t\t\tthis.#decipher = createDecipheriv(\n\t\t\t\t'aes-256-gcm',\n\t\t\t\tthis.key,\n\t\t\t\tthis.#header.subarray(0, IV_LENGTH)\n\t\t\t) as DecipherGCM\n\t\t\tthis.#decipher.setAuthTag(this.#header.subarray(IV_LENGTH, HEADER_LENGTH))\n\t\t\tbody = this.#header.subarray(HEADER_LENGTH)\n\t\t\tif (body.length === 0) return done()\n\t\t}\n\n\t\ttry {\n\t\t\tthis.push(this.#decipher.update(body))\n\t\t\tdone()\n\t\t} catch (error) {\n\t\t\tdone(error as Error)\n\t\t}\n\t}\n\n\toverride _flush(done: TransformCallback): void {\n\t\tif (!this.#decipher) {\n\t\t\treturn done(new Error('Encrypted object ended before its header'))\n\t\t}\n\t\ttry {\n\t\t\t// Throws when the tag does not match. By now the consumer already has\n\t\t\t// the plaintext, so tampering surfaces as a broken stream, not as a\n\t\t\t// clean rejection — the cost of streaming, stated in `CryptoHooks`.\n\t\t\tthis.push(this.#decipher.final())\n\t\t\tdone()\n\t\t} catch (error) {\n\t\t\tdone(error as Error)\n\t\t}\n\t}\n}\n\n/**\n * Reference cipher for `encrypt: true` scopes: AES-256-GCM, layout\n * `[iv 12][auth tag 16][ciphertext]`. Injecting different {@link CryptoHooks}\n * into `createStorage` stays fully supported.\n *\n * The key must be exactly 64 hex chars — no passphrase derivation, so two\n * instances can never run \"almost the same\" secret and silently write\n * mutually unreadable files. `openssl rand -hex 32`.\n *\n * @see AGENTS.md §4.4\n */\nexport const createAesGcmCrypto = (hexKey: string): StreamingCryptoHooks => {\n\tif (!KEY_PATTERN.test(hexKey)) {\n\t\tthrow new ScopeError(\n\t\t\t'createAesGcmCrypto: the key must be exactly 64 hex characters (32 bytes). Generate one with `openssl rand -hex 32`.'\n\t\t)\n\t}\n\tconst key = Buffer.from(hexKey, 'hex')\n\n\treturn {\n\t\tencrypt: data => {\n\t\t\tconst iv = randomBytes(IV_LENGTH)\n\t\t\tconst cipher = createCipheriv('aes-256-gcm', key, iv)\n\t\t\tconst encrypted = Buffer.concat([cipher.update(data), cipher.final()])\n\t\t\treturn Buffer.concat([iv, cipher.getAuthTag(), encrypted])\n\t\t},\n\t\tdecrypt: data => {\n\t\t\tconst buffer = Buffer.from(data)\n\t\t\tconst iv = buffer.subarray(0, IV_LENGTH)\n\t\t\tconst tag = buffer.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH)\n\t\t\tconst ciphertext = buffer.subarray(IV_LENGTH + TAG_LENGTH)\n\t\t\tconst decipher = createDecipheriv('aes-256-gcm', key, iv)\n\t\t\tdecipher.setAuthTag(tag)\n\t\t\treturn Buffer.concat([decipher.update(ciphertext), decipher.final()])\n\t\t},\n\t\tdecryptStream: source =>\n\t\t\t(source as NodeJS.ReadableStream).pipe(new GcmDecryptStream(key)),\n\t}\n}\n"]}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
import { S as StorageProvider, d as ScopeConfig, e as ScopeRegistry, i as CryptoHooks, U as UploaderLabels, c as FileLike, o as StoredFile } from './types-BSlJJwti.cjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A request-level failure: bad file, unknown scope name from a URL, missing
|
|
6
|
+
* part. Carries the HTTP status and a message safe to show the user — unlike
|
|
7
|
+
* `ScopeError`, which flags wiring bugs and must stay dev-facing.
|
|
8
|
+
*/
|
|
9
|
+
declare class StorageRequestError extends Error {
|
|
10
|
+
readonly status: number;
|
|
11
|
+
name: string;
|
|
12
|
+
constructor(message: string, status: number);
|
|
13
|
+
}
|
|
14
|
+
type CreateStorageOptions<T extends Record<string, ScopeConfig>> = {
|
|
15
|
+
scopes: ScopeRegistry<T>;
|
|
16
|
+
provider: StreamingStorageProvider;
|
|
17
|
+
/** Required when any scope declares `encrypt`. The app owns the cipher. */
|
|
18
|
+
crypto?: StreamingCryptoHooks;
|
|
19
|
+
/** Lifetime of signed URLs, in seconds. @defaultValue 300 */
|
|
20
|
+
signedUrlTtl?: number;
|
|
21
|
+
/**
|
|
22
|
+
* `StoredFile.url` for encrypted scopes. Required alongside them: a signed
|
|
23
|
+
* URL points at the bucket object, which is CIPHERTEXT — the only readable
|
|
24
|
+
* route is the app's authenticated view endpoint (which runs `read` and
|
|
25
|
+
* decrypts). Return that endpoint's URL, absolute or app-relative.
|
|
26
|
+
*/
|
|
27
|
+
encryptedUrl?: (input: {
|
|
28
|
+
scope: string;
|
|
29
|
+
entityId: string;
|
|
30
|
+
key: string;
|
|
31
|
+
}) => string;
|
|
32
|
+
/**
|
|
33
|
+
* Copy for every `StorageRequestError` this service words, and for the
|
|
34
|
+
* validation messages it answers with. Omitted means English.
|
|
35
|
+
*
|
|
36
|
+
* Hand it the same object the client renders with: the server re-runs the
|
|
37
|
+
* very validation the browser ran, so a mismatch here is the one place a
|
|
38
|
+
* user sees the same rejection twice, in two languages.
|
|
39
|
+
*/
|
|
40
|
+
labels?: Partial<UploaderLabels>;
|
|
41
|
+
};
|
|
42
|
+
type UploadInput = {
|
|
43
|
+
scope: string;
|
|
44
|
+
entityId: string;
|
|
45
|
+
file: FileLike;
|
|
46
|
+
uploadedBy?: string;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* A stored file plus the keys the upload removed. `replaced` is empty unless
|
|
50
|
+
* the scope sweeps per entity; when it is not, the app must drop those keys
|
|
51
|
+
* from whatever it persisted, or it keeps rendering objects that are gone.
|
|
52
|
+
*/
|
|
53
|
+
type UploadResult = StoredFile & {
|
|
54
|
+
replaced: string[];
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* A provider that can also read without buffering. Additive and optional, so
|
|
58
|
+
* every existing provider still satisfies it — `readStream` falls back to
|
|
59
|
+
* `get` when the method is absent.
|
|
60
|
+
*
|
|
61
|
+
* Declared here rather than in the core because the core's `StorageProvider`
|
|
62
|
+
* is the published contract; widening it there would be a breaking change for
|
|
63
|
+
* anyone implementing it.
|
|
64
|
+
*/
|
|
65
|
+
type StreamingStorageProvider = StorageProvider & {
|
|
66
|
+
/**
|
|
67
|
+
* Reads without holding the object in memory. Serving a 20MB document
|
|
68
|
+
* through `get` costs its full size in RAM per concurrent request.
|
|
69
|
+
*/
|
|
70
|
+
getStream?(key: string): Promise<Readable>;
|
|
71
|
+
};
|
|
72
|
+
/** {@link CryptoHooks} that can also decrypt progressively. */
|
|
73
|
+
type StreamingCryptoHooks = CryptoHooks & {
|
|
74
|
+
/**
|
|
75
|
+
* Decrypts as bytes arrive, so a read never materializes the plaintext.
|
|
76
|
+
*
|
|
77
|
+
* Weigh the cost: plaintext reaches the consumer BEFORE the authentication
|
|
78
|
+
* tag is verified, so tampering surfaces as a stream that fails at the end,
|
|
79
|
+
* mid-delivery. `decrypt` verifies before returning a single byte.
|
|
80
|
+
*
|
|
81
|
+
* There is no streaming counterpart for encryption on purpose — AES-GCM
|
|
82
|
+
* only knows its tag once the input ends, and the layout writes it first.
|
|
83
|
+
*/
|
|
84
|
+
decryptStream?: (source: Readable) => Readable;
|
|
85
|
+
};
|
|
86
|
+
type StorageService<T extends Record<string, ScopeConfig> = Record<string, ScopeConfig>> = {
|
|
87
|
+
upload(input: UploadInput): Promise<UploadResult>;
|
|
88
|
+
/** Raw bytes, decrypted when the scope is encrypted. */
|
|
89
|
+
read(input: {
|
|
90
|
+
scope: string;
|
|
91
|
+
key: string;
|
|
92
|
+
}): Promise<Uint8Array>;
|
|
93
|
+
/**
|
|
94
|
+
* The same content as a stream, so serving a file costs a buffer's worth of
|
|
95
|
+
* memory instead of the whole object. Falls back to `read` when the provider
|
|
96
|
+
* or the cipher cannot stream, so it is always safe to prefer.
|
|
97
|
+
*/
|
|
98
|
+
readStream(input: {
|
|
99
|
+
scope: string;
|
|
100
|
+
key: string;
|
|
101
|
+
}): Promise<Readable>;
|
|
102
|
+
remove(input: {
|
|
103
|
+
scope: string;
|
|
104
|
+
key: string;
|
|
105
|
+
}): Promise<boolean>;
|
|
106
|
+
/** Fresh expiring URL for a private object. Throws on public scopes. */
|
|
107
|
+
signedUrl(input: {
|
|
108
|
+
scope: string;
|
|
109
|
+
key: string;
|
|
110
|
+
download?: boolean;
|
|
111
|
+
expiresIn?: number;
|
|
112
|
+
}): Promise<string>;
|
|
113
|
+
list(prefix: string): Promise<{
|
|
114
|
+
key: string;
|
|
115
|
+
size: number;
|
|
116
|
+
}[]>;
|
|
117
|
+
scopes: ScopeRegistry<T>;
|
|
118
|
+
/**
|
|
119
|
+
* The resolved copy, exposed so the framework adapters word their own
|
|
120
|
+
* failures with it. One `labels` on `createStorage` therefore covers the
|
|
121
|
+
* whole round trip — the service and the route that wraps it cannot end up
|
|
122
|
+
* answering in two languages.
|
|
123
|
+
*/
|
|
124
|
+
labels: UploaderLabels;
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* The server side of the contract. Boots defensively: a provider that cannot
|
|
128
|
+
* honour the scopes, or an encrypted scope without an injected cipher, throws
|
|
129
|
+
* here — before the first request, while a deploy can still fail loudly.
|
|
130
|
+
*/
|
|
131
|
+
declare const createStorage: <T extends Record<string, ScopeConfig>>({ scopes, provider, crypto, signedUrlTtl, encryptedUrl, labels, }: CreateStorageOptions<T>) => StorageService<T>;
|
|
132
|
+
|
|
133
|
+
export { type CreateStorageOptions as C, type StreamingStorageProvider as S, type UploadInput as U, type StreamingCryptoHooks as a, StorageRequestError as b, type StorageService as c, type UploadResult as d, createStorage as e };
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
import { S as StorageProvider, d as ScopeConfig, e as ScopeRegistry, i as CryptoHooks, U as UploaderLabels, c as FileLike, o as StoredFile } from './types-BSlJJwti.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A request-level failure: bad file, unknown scope name from a URL, missing
|
|
6
|
+
* part. Carries the HTTP status and a message safe to show the user — unlike
|
|
7
|
+
* `ScopeError`, which flags wiring bugs and must stay dev-facing.
|
|
8
|
+
*/
|
|
9
|
+
declare class StorageRequestError extends Error {
|
|
10
|
+
readonly status: number;
|
|
11
|
+
name: string;
|
|
12
|
+
constructor(message: string, status: number);
|
|
13
|
+
}
|
|
14
|
+
type CreateStorageOptions<T extends Record<string, ScopeConfig>> = {
|
|
15
|
+
scopes: ScopeRegistry<T>;
|
|
16
|
+
provider: StreamingStorageProvider;
|
|
17
|
+
/** Required when any scope declares `encrypt`. The app owns the cipher. */
|
|
18
|
+
crypto?: StreamingCryptoHooks;
|
|
19
|
+
/** Lifetime of signed URLs, in seconds. @defaultValue 300 */
|
|
20
|
+
signedUrlTtl?: number;
|
|
21
|
+
/**
|
|
22
|
+
* `StoredFile.url` for encrypted scopes. Required alongside them: a signed
|
|
23
|
+
* URL points at the bucket object, which is CIPHERTEXT — the only readable
|
|
24
|
+
* route is the app's authenticated view endpoint (which runs `read` and
|
|
25
|
+
* decrypts). Return that endpoint's URL, absolute or app-relative.
|
|
26
|
+
*/
|
|
27
|
+
encryptedUrl?: (input: {
|
|
28
|
+
scope: string;
|
|
29
|
+
entityId: string;
|
|
30
|
+
key: string;
|
|
31
|
+
}) => string;
|
|
32
|
+
/**
|
|
33
|
+
* Copy for every `StorageRequestError` this service words, and for the
|
|
34
|
+
* validation messages it answers with. Omitted means English.
|
|
35
|
+
*
|
|
36
|
+
* Hand it the same object the client renders with: the server re-runs the
|
|
37
|
+
* very validation the browser ran, so a mismatch here is the one place a
|
|
38
|
+
* user sees the same rejection twice, in two languages.
|
|
39
|
+
*/
|
|
40
|
+
labels?: Partial<UploaderLabels>;
|
|
41
|
+
};
|
|
42
|
+
type UploadInput = {
|
|
43
|
+
scope: string;
|
|
44
|
+
entityId: string;
|
|
45
|
+
file: FileLike;
|
|
46
|
+
uploadedBy?: string;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* A stored file plus the keys the upload removed. `replaced` is empty unless
|
|
50
|
+
* the scope sweeps per entity; when it is not, the app must drop those keys
|
|
51
|
+
* from whatever it persisted, or it keeps rendering objects that are gone.
|
|
52
|
+
*/
|
|
53
|
+
type UploadResult = StoredFile & {
|
|
54
|
+
replaced: string[];
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* A provider that can also read without buffering. Additive and optional, so
|
|
58
|
+
* every existing provider still satisfies it — `readStream` falls back to
|
|
59
|
+
* `get` when the method is absent.
|
|
60
|
+
*
|
|
61
|
+
* Declared here rather than in the core because the core's `StorageProvider`
|
|
62
|
+
* is the published contract; widening it there would be a breaking change for
|
|
63
|
+
* anyone implementing it.
|
|
64
|
+
*/
|
|
65
|
+
type StreamingStorageProvider = StorageProvider & {
|
|
66
|
+
/**
|
|
67
|
+
* Reads without holding the object in memory. Serving a 20MB document
|
|
68
|
+
* through `get` costs its full size in RAM per concurrent request.
|
|
69
|
+
*/
|
|
70
|
+
getStream?(key: string): Promise<Readable>;
|
|
71
|
+
};
|
|
72
|
+
/** {@link CryptoHooks} that can also decrypt progressively. */
|
|
73
|
+
type StreamingCryptoHooks = CryptoHooks & {
|
|
74
|
+
/**
|
|
75
|
+
* Decrypts as bytes arrive, so a read never materializes the plaintext.
|
|
76
|
+
*
|
|
77
|
+
* Weigh the cost: plaintext reaches the consumer BEFORE the authentication
|
|
78
|
+
* tag is verified, so tampering surfaces as a stream that fails at the end,
|
|
79
|
+
* mid-delivery. `decrypt` verifies before returning a single byte.
|
|
80
|
+
*
|
|
81
|
+
* There is no streaming counterpart for encryption on purpose — AES-GCM
|
|
82
|
+
* only knows its tag once the input ends, and the layout writes it first.
|
|
83
|
+
*/
|
|
84
|
+
decryptStream?: (source: Readable) => Readable;
|
|
85
|
+
};
|
|
86
|
+
type StorageService<T extends Record<string, ScopeConfig> = Record<string, ScopeConfig>> = {
|
|
87
|
+
upload(input: UploadInput): Promise<UploadResult>;
|
|
88
|
+
/** Raw bytes, decrypted when the scope is encrypted. */
|
|
89
|
+
read(input: {
|
|
90
|
+
scope: string;
|
|
91
|
+
key: string;
|
|
92
|
+
}): Promise<Uint8Array>;
|
|
93
|
+
/**
|
|
94
|
+
* The same content as a stream, so serving a file costs a buffer's worth of
|
|
95
|
+
* memory instead of the whole object. Falls back to `read` when the provider
|
|
96
|
+
* or the cipher cannot stream, so it is always safe to prefer.
|
|
97
|
+
*/
|
|
98
|
+
readStream(input: {
|
|
99
|
+
scope: string;
|
|
100
|
+
key: string;
|
|
101
|
+
}): Promise<Readable>;
|
|
102
|
+
remove(input: {
|
|
103
|
+
scope: string;
|
|
104
|
+
key: string;
|
|
105
|
+
}): Promise<boolean>;
|
|
106
|
+
/** Fresh expiring URL for a private object. Throws on public scopes. */
|
|
107
|
+
signedUrl(input: {
|
|
108
|
+
scope: string;
|
|
109
|
+
key: string;
|
|
110
|
+
download?: boolean;
|
|
111
|
+
expiresIn?: number;
|
|
112
|
+
}): Promise<string>;
|
|
113
|
+
list(prefix: string): Promise<{
|
|
114
|
+
key: string;
|
|
115
|
+
size: number;
|
|
116
|
+
}[]>;
|
|
117
|
+
scopes: ScopeRegistry<T>;
|
|
118
|
+
/**
|
|
119
|
+
* The resolved copy, exposed so the framework adapters word their own
|
|
120
|
+
* failures with it. One `labels` on `createStorage` therefore covers the
|
|
121
|
+
* whole round trip — the service and the route that wraps it cannot end up
|
|
122
|
+
* answering in two languages.
|
|
123
|
+
*/
|
|
124
|
+
labels: UploaderLabels;
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* The server side of the contract. Boots defensively: a provider that cannot
|
|
128
|
+
* honour the scopes, or an encrypted scope without an injected cipher, throws
|
|
129
|
+
* here — before the first request, while a deploy can still fail loudly.
|
|
130
|
+
*/
|
|
131
|
+
declare const createStorage: <T extends Record<string, ScopeConfig>>({ scopes, provider, crypto, signedUrlTtl, encryptedUrl, labels, }: CreateStorageOptions<T>) => StorageService<T>;
|
|
132
|
+
|
|
133
|
+
export { type CreateStorageOptions as C, type StreamingStorageProvider as S, type UploadInput as U, type StreamingCryptoHooks as a, StorageRequestError as b, type StorageService as c, type UploadResult as d, createStorage as e };
|