fundus 0.2.0 → 0.4.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/README.md +67 -2
- package/build/client/_app/immutable/assets/2.B5tQm8hR.css +1 -0
- package/build/client/_app/immutable/assets/2.B5tQm8hR.css.br +0 -0
- package/build/client/_app/immutable/assets/2.B5tQm8hR.css.gz +0 -0
- package/build/client/_app/immutable/assets/{3.ChSpCG-T.css → 3.CmXD8k9d.css} +1 -1
- package/build/client/_app/immutable/assets/3.CmXD8k9d.css.br +0 -0
- package/build/client/_app/immutable/assets/3.CmXD8k9d.css.gz +0 -0
- package/build/client/_app/immutable/assets/{LibraryContextMenu.BYvU4SY7.css → LibraryContextMenu.CVb-Lqdf.css} +1 -1
- package/build/client/_app/immutable/assets/LibraryContextMenu.CVb-Lqdf.css.br +0 -0
- package/build/client/_app/immutable/assets/{LibraryContextMenu.BYvU4SY7.css.gz → LibraryContextMenu.CVb-Lqdf.css.gz} +0 -0
- package/build/client/_app/immutable/chunks/{C8W3KwDq.js → BPiGyn-T.js} +1 -1
- package/build/client/_app/immutable/chunks/BPiGyn-T.js.br +0 -0
- package/build/client/_app/immutable/chunks/BPiGyn-T.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{CP-bnOxU.js → BoEHtxM8.js} +1 -1
- package/build/client/_app/immutable/chunks/BoEHtxM8.js.br +0 -0
- package/build/client/_app/immutable/chunks/BoEHtxM8.js.gz +0 -0
- package/build/client/_app/immutable/chunks/C-T1_s5P.js +1 -0
- package/build/client/_app/immutable/chunks/C-T1_s5P.js.br +0 -0
- package/build/client/_app/immutable/chunks/C-T1_s5P.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{ByVSyIj7.js → CeJsL6hj.js} +1 -1
- package/build/client/_app/immutable/chunks/CeJsL6hj.js.br +0 -0
- package/build/client/_app/immutable/chunks/CeJsL6hj.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{Db8LRH6X.js → Cq4Yaq35.js} +3 -3
- package/build/client/_app/immutable/chunks/Cq4Yaq35.js.br +0 -0
- package/build/client/_app/immutable/chunks/Cq4Yaq35.js.gz +0 -0
- package/build/client/_app/immutable/chunks/D5sQSsUv.js +1 -0
- package/build/client/_app/immutable/chunks/D5sQSsUv.js.br +0 -0
- package/build/client/_app/immutable/chunks/D5sQSsUv.js.gz +0 -0
- package/build/client/_app/immutable/entry/{app.nM6SgHKJ.js → app.BQ4TfPHG.js} +2 -2
- package/build/client/_app/immutable/entry/app.BQ4TfPHG.js.br +0 -0
- package/build/client/_app/immutable/entry/app.BQ4TfPHG.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.COP9tIRH.js +1 -0
- package/build/client/_app/immutable/entry/start.COP9tIRH.js.br +0 -0
- package/build/client/_app/immutable/entry/start.COP9tIRH.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.CftchwYV.js +2 -0
- package/build/client/_app/immutable/nodes/0.CftchwYV.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.CftchwYV.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{1.dMklHTc2.js → 1.C8DOqctV.js} +1 -1
- package/build/client/_app/immutable/nodes/1.C8DOqctV.js.br +1 -0
- package/build/client/_app/immutable/nodes/1.C8DOqctV.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2.BR8OVsS9.js +3 -0
- package/build/client/_app/immutable/nodes/2.BR8OVsS9.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.BR8OVsS9.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.BO5hcxIH.js +1 -0
- package/build/client/_app/immutable/nodes/3.BO5hcxIH.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.BO5hcxIH.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{4.Dgxx52FJ.js → 4.DTaH-k-c.js} +1 -1
- package/build/client/_app/immutable/nodes/4.DTaH-k-c.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.DTaH-k-c.js.gz +0 -0
- package/build/client/_app/version.json +1 -1
- package/build/client/_app/version.json.br +0 -0
- package/build/client/_app/version.json.gz +0 -0
- package/build/handler.js +7 -7
- package/build/index.js +7 -7
- package/build/server/chunks/chunks/asset-id.js-CFsW8jeL.js +48 -0
- package/build/server/chunks/chunks/asset-id.js-CFsW8jeL.js.map +1 -0
- package/build/server/chunks/chunks/{context.js-B77ZAVLi.js → context.js-CG2t-ufq.js} +568 -25
- package/build/server/chunks/chunks/context.js-CG2t-ufq.js.map +1 -0
- package/build/server/chunks/chunks/{library-store.js-DJ-JRE-g.js → contract.js-sHny3b-s.js} +133 -359
- package/build/server/chunks/chunks/contract.js-sHny3b-s.js.map +1 -0
- package/build/server/chunks/chunks/{core.js-EA_ysb1J.js → core.js-Clg1CHb6.js} +2 -48
- package/build/server/chunks/chunks/core.js-Clg1CHb6.js.map +1 -0
- package/build/server/chunks/chunks/{exports.js-DvhE7Ffd.js → exports.js-rOfFZumP.js} +2 -2
- package/build/server/chunks/chunks/{exports.js-DvhE7Ffd.js.map → exports.js-rOfFZumP.js.map} +1 -1
- package/build/server/chunks/chunks/{http.js-DyoR9XRI.js → http.js--6sDZFfm.js} +35 -3
- package/build/server/chunks/chunks/http.js--6sDZFfm.js.map +1 -0
- package/build/server/chunks/chunks/{internal.js-Dn37LrAW.js → internal.js-DnveDsYn.js} +3 -3
- package/build/server/chunks/chunks/{internal.js-Dn37LrAW.js.map → internal.js-DnveDsYn.js.map} +1 -1
- package/build/server/chunks/chunks/internal2.js-BlXt2rfE.js.map +1 -1
- package/build/server/chunks/chunks/raw-reconciliation.js-DOFtjkgu.js +343 -0
- package/build/server/chunks/chunks/raw-reconciliation.js-DOFtjkgu.js.map +1 -0
- package/build/server/chunks/chunks/raw-reconciliation2.js-DFwky2ou.js +11 -0
- package/build/server/chunks/chunks/raw-reconciliation2.js-DFwky2ou.js.map +1 -0
- package/build/server/chunks/chunks/{shared.js-Dob0TlTu.js → shared.js-DtvIbMLj.js} +2 -2
- package/build/server/chunks/chunks/{shared.js-Dob0TlTu.js.map → shared.js-DtvIbMLj.js.map} +1 -1
- package/build/server/chunks/chunks/{utils.js-B4YimMxv.js → utils.js-bP1j9RdM.js} +3 -3
- package/build/server/chunks/chunks/{utils.js-B4YimMxv.js.map → utils.js-bP1j9RdM.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/_...path_/{_server.ts.js-H4Pq1KAE.js → _server.ts.js-CkSErIdC.js} +7 -5
- package/build/server/chunks/entries/endpoints/api/_...path_/{_server.ts.js-H4Pq1KAE.js.map → _server.ts.js-CkSErIdC.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/assets/_id_/{_server.ts.js-BI3KHSfE.js → _server.ts.js-BfdsBPWo.js} +11 -6
- package/build/server/chunks/entries/endpoints/api/assets/_id_/{_server.ts.js-BI3KHSfE.js.map → _server.ts.js-BfdsBPWo.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/assets/_id_/raw/{_server.ts.js-BlNYUbXX.js → _server.ts.js-D0aKDq87.js} +11 -6
- package/build/server/chunks/entries/endpoints/api/assets/_id_/raw/{_server.ts.js-BlNYUbXX.js.map → _server.ts.js-D0aKDq87.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/assets/_id_/reimport/_server.ts.js-DsX_m2gc.js +31 -0
- package/build/server/chunks/entries/endpoints/api/assets/_id_/reimport/_server.ts.js-DsX_m2gc.js.map +1 -0
- package/build/server/chunks/entries/endpoints/api/assets/{_server.ts.js-j-_N540j.js → _server.ts.js-nfThjRxs.js} +11 -6
- package/build/server/chunks/entries/endpoints/api/assets/{_server.ts.js-j-_N540j.js.map → _server.ts.js-nfThjRxs.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/assets/delete/{_server.ts.js-BHKobzAg.js → _server.ts.js-CseHmsYB.js} +11 -6
- package/build/server/chunks/entries/endpoints/api/assets/delete/{_server.ts.js-BHKobzAg.js.map → _server.ts.js-CseHmsYB.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/assets/move/{_server.ts.js-BFKjIOCv.js → _server.ts.js-CHiICsyC.js} +11 -6
- package/build/server/chunks/entries/endpoints/api/assets/move/{_server.ts.js-BFKjIOCv.js.map → _server.ts.js-CHiICsyC.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/folders/_path_/{_server.ts.js-BxZeVw8y.js → _server.ts.js-Dgf46K5u.js} +11 -6
- package/build/server/chunks/entries/endpoints/api/folders/_path_/{_server.ts.js-BxZeVw8y.js.map → _server.ts.js-Dgf46K5u.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/folders/{_server.ts.js-CO36U_CT.js → _server.ts.js-DwQJUZFa.js} +11 -6
- package/build/server/chunks/entries/endpoints/api/folders/{_server.ts.js-CO36U_CT.js.map → _server.ts.js-DwQJUZFa.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/imports/_server.ts.js-BnKS8nVF.js +37 -0
- package/build/server/chunks/entries/endpoints/api/imports/_server.ts.js-BnKS8nVF.js.map +1 -0
- package/build/server/chunks/entries/endpoints/api/manifests/_name_/{_server.ts.js-CBlg1Bkl.js → _server.ts.js-BrDbLcyO.js} +11 -6
- package/build/server/chunks/entries/endpoints/api/manifests/_name_/{_server.ts.js-CBlg1Bkl.js.map → _server.ts.js-BrDbLcyO.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/manifests/{_server.ts.js-DcGLCjIs.js → _server.ts.js-DIUiQuyd.js} +11 -6
- package/build/server/chunks/entries/endpoints/api/manifests/{_server.ts.js-DcGLCjIs.js.map → _server.ts.js-DIUiQuyd.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/raw/adopt/{_server.ts.js-AqhtjOiL.js → _server.ts.js-BrReStaf.js} +11 -6
- package/build/server/chunks/entries/endpoints/api/raw/adopt/{_server.ts.js-AqhtjOiL.js.map → _server.ts.js-BrReStaf.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/raw/edited/accept/{_server.ts.js-CSa6-DcU.js → _server.ts.js-B31iGwJ-.js} +11 -6
- package/build/server/chunks/entries/endpoints/api/raw/edited/accept/{_server.ts.js-CSa6-DcU.js.map → _server.ts.js-B31iGwJ-.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/raw/edited/dismiss/{_server.ts.js-DnzZUoAo.js → _server.ts.js-DeZQFNdj.js} +11 -6
- package/build/server/chunks/entries/endpoints/api/raw/edited/dismiss/{_server.ts.js-DnzZUoAo.js.map → _server.ts.js-DeZQFNdj.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/reconcile/{_server.ts.js-B0KMprKq.js → _server.ts.js-DhDhUU7V.js} +11 -6
- package/build/server/chunks/entries/endpoints/api/reconcile/{_server.ts.js-B0KMprKq.js.map → _server.ts.js-DhDhUU7V.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/reveal/_server.ts.js-DGkPIMcq.js +80 -0
- package/build/server/chunks/entries/endpoints/api/reveal/_server.ts.js-DGkPIMcq.js.map +1 -0
- package/build/server/chunks/entries/endpoints/api/state/{_server.ts.js-B1N9o1Ne.js → _server.ts.js-BTQgWDH1.js} +11 -6
- package/build/server/chunks/entries/endpoints/api/state/{_server.ts.js-B1N9o1Ne.js.map → _server.ts.js-BTQgWDH1.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/files/_...path_/{_server.ts.js-B6HbnRqq.js → _server.ts.js-DB3i4juR.js} +7 -5
- package/build/server/chunks/entries/endpoints/files/_...path_/{_server.ts.js-B6HbnRqq.js.map → _server.ts.js-DB3i4juR.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/files/proxy/_...path_/{_server.ts.js-7wzPyG9v.js → _server.ts.js-kmc-gejQ.js} +11 -6
- package/build/server/chunks/entries/endpoints/files/proxy/_...path_/{_server.ts.js-7wzPyG9v.js.map → _server.ts.js-kmc-gejQ.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/files/raw/_...path_/{_server.ts.js-BWztxYge.js → _server.ts.js-B8EE1DD9.js} +11 -6
- package/build/server/chunks/entries/endpoints/files/raw/_...path_/{_server.ts.js-BWztxYge.js.map → _server.ts.js-B8EE1DD9.js.map} +1 -1
- package/build/server/chunks/entries/{hooks.server.js-CVKLksJ1.js → hooks.server.js-DbBA5yYQ.js} +10 -5
- package/build/server/chunks/entries/{hooks.server.js-CVKLksJ1.js.map → hooks.server.js-DbBA5yYQ.js.map} +1 -1
- package/build/server/chunks/{handler-DZ2Sps4a.js → handler-J6aKdov3.js} +5 -5
- package/build/server/chunks/{handler-DZ2Sps4a.js.map → handler-J6aKdov3.js.map} +1 -1
- package/build/server/chunks/{index.js-CdmTWQz8.js → index.js-CsvWJ7Qu.js} +5 -5
- package/build/server/chunks/{index.js-CdmTWQz8.js.map → index.js-CsvWJ7Qu.js.map} +1 -1
- package/build/server/chunks/{manifest.js-GhfNr35x.js → manifest.js-wHtZ9vlA.js} +46 -25
- package/build/server/chunks/manifest.js-wHtZ9vlA.js.map +1 -0
- package/build/server/chunks/nodes/0.js-By6-kbk6.js +11 -0
- package/build/server/chunks/nodes/0.js-By6-kbk6.js.map +1 -0
- package/build/server/chunks/nodes/1.js-CrPfmKpN.js +7 -0
- package/build/server/chunks/nodes/{1.js-DQgYreU0.js.map → 1.js-CrPfmKpN.js.map} +1 -1
- package/build/server/chunks/nodes/2.js-C-i3Y5t2.js +7 -0
- package/build/server/chunks/nodes/2.js-C-i3Y5t2.js.map +1 -0
- package/build/server/chunks/nodes/3.js-C1gcIGeV.js +7 -0
- package/build/server/chunks/nodes/3.js-C1gcIGeV.js.map +1 -0
- package/build/server/chunks/nodes/4.js-tgwl3VD3.js +7 -0
- package/build/server/chunks/nodes/{4.js-BebbMVXy.js.map → 4.js-tgwl3VD3.js.map} +1 -1
- package/dist/cli.js +2 -0
- package/dist/config.d.ts +1 -1
- package/dist/core/api-types.d.ts +12 -1
- package/dist/core/config.d.ts +12 -0
- package/dist/core/index.d.ts +3 -2
- package/dist/core/library.d.ts +8 -1
- package/dist/core/library.js +30 -6
- package/dist/server/cli-commands.d.ts +2 -1
- package/dist/server/cli-commands.js +155 -34
- package/dist/server/config-loader.d.ts +9 -1
- package/dist/server/config-loader.js +26 -1
- package/dist/server/file-manager.d.ts +2 -0
- package/dist/server/file-manager.js +17 -0
- package/dist/server/http.d.ts +1 -0
- package/dist/server/http.js +37 -0
- package/dist/server/importers/contract.d.ts +29 -0
- package/dist/server/importers/contract.js +13 -0
- package/dist/server/importers/figma.d.ts +21 -0
- package/dist/server/importers/figma.js +386 -0
- package/dist/server/importers/file.d.ts +6 -0
- package/dist/server/importers/file.js +33 -0
- package/dist/server/importers/pinned-https.d.ts +23 -0
- package/dist/server/importers/pinned-https.js +60 -0
- package/dist/server/importers/registry.d.ts +12 -0
- package/dist/server/importers/registry.js +31 -0
- package/dist/server/importers.d.ts +2 -0
- package/dist/server/importers.js +7 -0
- package/dist/server/library-store.d.ts +19 -1
- package/dist/server/library-store.js +110 -14
- package/dist/server/operations.d.ts +13 -1
- package/dist/server/operations.js +159 -23
- package/package.json +1 -1
- package/build/client/_app/immutable/assets/2.ohi-TClG.css +0 -1
- package/build/client/_app/immutable/assets/2.ohi-TClG.css.br +0 -0
- package/build/client/_app/immutable/assets/2.ohi-TClG.css.gz +0 -0
- package/build/client/_app/immutable/assets/3.ChSpCG-T.css.br +0 -0
- package/build/client/_app/immutable/assets/3.ChSpCG-T.css.gz +0 -0
- package/build/client/_app/immutable/assets/LibraryContextMenu.BYvU4SY7.css.br +0 -0
- package/build/client/_app/immutable/chunks/BAEZPv6c.js +0 -1
- package/build/client/_app/immutable/chunks/BAEZPv6c.js.br +0 -0
- package/build/client/_app/immutable/chunks/BAEZPv6c.js.gz +0 -0
- package/build/client/_app/immutable/chunks/ByVSyIj7.js.br +0 -1
- package/build/client/_app/immutable/chunks/ByVSyIj7.js.gz +0 -0
- package/build/client/_app/immutable/chunks/C8W3KwDq.js.br +0 -0
- package/build/client/_app/immutable/chunks/C8W3KwDq.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CP-bnOxU.js.br +0 -0
- package/build/client/_app/immutable/chunks/CP-bnOxU.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Db8LRH6X.js.br +0 -0
- package/build/client/_app/immutable/chunks/Db8LRH6X.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.nM6SgHKJ.js.br +0 -0
- package/build/client/_app/immutable/entry/app.nM6SgHKJ.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.DNXvkhtP.js +0 -1
- package/build/client/_app/immutable/entry/start.DNXvkhtP.js.br +0 -0
- package/build/client/_app/immutable/entry/start.DNXvkhtP.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.QeXQNCp2.js +0 -2
- package/build/client/_app/immutable/nodes/0.QeXQNCp2.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.QeXQNCp2.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.dMklHTc2.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.dMklHTc2.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2.CW4VMcIP.js +0 -3
- package/build/client/_app/immutable/nodes/2.CW4VMcIP.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.CW4VMcIP.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.CTiY_pbK.js +0 -1
- package/build/client/_app/immutable/nodes/3.CTiY_pbK.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.CTiY_pbK.js.gz +0 -0
- package/build/client/_app/immutable/nodes/4.Dgxx52FJ.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.Dgxx52FJ.js.gz +0 -0
- package/build/server/chunks/chunks/context.js-B77ZAVLi.js.map +0 -1
- package/build/server/chunks/chunks/core.js-EA_ysb1J.js.map +0 -1
- package/build/server/chunks/chunks/http.js-DyoR9XRI.js.map +0 -1
- package/build/server/chunks/chunks/library-store.js-DJ-JRE-g.js.map +0 -1
- package/build/server/chunks/manifest.js-GhfNr35x.js.map +0 -1
- package/build/server/chunks/nodes/0.js-DGT86_i-.js +0 -11
- package/build/server/chunks/nodes/0.js-DGT86_i-.js.map +0 -1
- package/build/server/chunks/nodes/1.js-DQgYreU0.js +0 -7
- package/build/server/chunks/nodes/2.js-DItreSrX.js +0 -7
- package/build/server/chunks/nodes/2.js-DItreSrX.js.map +0 -1
- package/build/server/chunks/nodes/3.js-bE1dJ_6v.js +0 -7
- package/build/server/chunks/nodes/3.js-bE1dJ_6v.js.map +0 -1
- package/build/server/chunks/nodes/4.js-BebbMVXy.js +0 -7
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { lookup } from 'node:dns/promises';
|
|
2
|
+
import { isIP } from 'node:net';
|
|
3
|
+
import sharp from 'sharp';
|
|
4
|
+
import { SourceImportError, invalidInput } from './contract.js';
|
|
5
|
+
import { requestPinnedHttps } from './pinned-https.js';
|
|
6
|
+
const MAX_DOWNLOAD_BYTES = 50 * 1024 * 1024;
|
|
7
|
+
const MAX_PIXELS = 32_000_000;
|
|
8
|
+
const MAX_REDIRECTS = 3;
|
|
9
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
10
|
+
const PNG_SIGNATURE = Uint8Array.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
11
|
+
function record(value) {
|
|
12
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
13
|
+
? value
|
|
14
|
+
: null;
|
|
15
|
+
}
|
|
16
|
+
export function parseFigmaSelectionUrl(value) {
|
|
17
|
+
let url;
|
|
18
|
+
try {
|
|
19
|
+
url = new URL(value);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return invalidInput('Enter a valid Figma selection link.');
|
|
23
|
+
}
|
|
24
|
+
if (url.protocol !== 'https:' ||
|
|
25
|
+
(url.hostname !== 'figma.com' && url.hostname !== 'www.figma.com')) {
|
|
26
|
+
return invalidInput('Figma links must use HTTPS on figma.com.');
|
|
27
|
+
}
|
|
28
|
+
const segments = url.pathname.split('/').filter(Boolean);
|
|
29
|
+
if (!['design', 'file', 'proto'].includes(segments[0] ?? '') || !segments[1]) {
|
|
30
|
+
return invalidInput('The Figma link must point to a design, file, or prototype.');
|
|
31
|
+
}
|
|
32
|
+
const nodeIds = url.searchParams.getAll('node-id');
|
|
33
|
+
if (nodeIds.length !== 1 || !/^\d+[-:]\d+$/.test(nodeIds[0])) {
|
|
34
|
+
return invalidInput('The Figma link must contain exactly one valid node-id.');
|
|
35
|
+
}
|
|
36
|
+
const [left, right] = nodeIds[0].split(/[-:]/);
|
|
37
|
+
return { fileKey: segments[1], nodeId: `${left}:${right}` };
|
|
38
|
+
}
|
|
39
|
+
function validateScale(value) {
|
|
40
|
+
if (value !== 1 && value !== 2 && value !== 3 && value !== 4) {
|
|
41
|
+
return invalidInput('Figma export scale must be 1, 2, 3, or 4.');
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
function validateInput(value) {
|
|
46
|
+
const input = record(value);
|
|
47
|
+
if (!input || typeof input.url !== 'string')
|
|
48
|
+
return invalidInput('Figma import requires a URL.');
|
|
49
|
+
if (input.format !== undefined && input.format !== 'png') {
|
|
50
|
+
return invalidInput('Figma import currently supports PNG only.');
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
url: input.url,
|
|
54
|
+
format: 'png',
|
|
55
|
+
...(input.scale !== undefined ? { scale: validateScale(input.scale) } : {})
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function validateFigmaSourceParameters(value) {
|
|
59
|
+
const source = record(value);
|
|
60
|
+
if (!source ||
|
|
61
|
+
typeof source.fileKey !== 'string' ||
|
|
62
|
+
source.fileKey.length === 0 ||
|
|
63
|
+
typeof source.nodeId !== 'string' ||
|
|
64
|
+
!/^\d+:\d+$/.test(source.nodeId) ||
|
|
65
|
+
source.format !== 'png') {
|
|
66
|
+
return invalidInput('Stored Figma source parameters are invalid.');
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
fileKey: source.fileKey,
|
|
70
|
+
nodeId: source.nodeId,
|
|
71
|
+
format: 'png',
|
|
72
|
+
scale: validateScale(source.scale)
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function retryAfter(response) {
|
|
76
|
+
const raw = response.headers.get('retry-after');
|
|
77
|
+
if (!raw || !/^\d+$/.test(raw.trim()))
|
|
78
|
+
return undefined;
|
|
79
|
+
const seconds = Number(raw);
|
|
80
|
+
return Number.isSafeInteger(seconds) ? seconds : undefined;
|
|
81
|
+
}
|
|
82
|
+
function upstreamStatusError(response) {
|
|
83
|
+
if (response.status === 401)
|
|
84
|
+
return new SourceImportError('unauthorized', 'Figma authentication failed.');
|
|
85
|
+
if (response.status === 403)
|
|
86
|
+
return new SourceImportError('forbidden', 'Figma authentication or access failed. The token may be invalid or expired, or it may not have access to this file.');
|
|
87
|
+
if (response.status === 404)
|
|
88
|
+
return new SourceImportError('not-found', 'The Figma file or node was not found.');
|
|
89
|
+
if (response.status === 429) {
|
|
90
|
+
return new SourceImportError('rate-limited', 'Figma rate-limited this request.', retryAfter(response));
|
|
91
|
+
}
|
|
92
|
+
return new SourceImportError('upstream-failed', `Figma export failed with status ${response.status}.`);
|
|
93
|
+
}
|
|
94
|
+
function isForbiddenIpv4(address) {
|
|
95
|
+
const parts = address.split('.').map(Number);
|
|
96
|
+
const [a, b] = parts;
|
|
97
|
+
return (a === 0 ||
|
|
98
|
+
a === 10 ||
|
|
99
|
+
a === 127 ||
|
|
100
|
+
(a === 100 && b >= 64 && b <= 127) ||
|
|
101
|
+
(a === 169 && b === 254) ||
|
|
102
|
+
(a === 172 && b >= 16 && b <= 31) ||
|
|
103
|
+
(a === 192 && b === 0) ||
|
|
104
|
+
(a === 192 && b === 0 && parts[2] === 2) ||
|
|
105
|
+
(a === 192 && b === 168) ||
|
|
106
|
+
(a === 198 && (b === 18 || b === 19)) ||
|
|
107
|
+
(a === 198 && b === 51 && parts[2] === 100) ||
|
|
108
|
+
(a === 203 && b === 0 && parts[2] === 113) ||
|
|
109
|
+
a >= 224);
|
|
110
|
+
}
|
|
111
|
+
function mappedIpv4Address(address) {
|
|
112
|
+
const normalized = address.toLowerCase().split('%')[0];
|
|
113
|
+
const dotted = /^(?:::ffff:|0:0:0:0:0:ffff:)(\d+\.\d+\.\d+\.\d+)$/.exec(normalized);
|
|
114
|
+
if (dotted && isIP(dotted[1]) === 4)
|
|
115
|
+
return dotted[1];
|
|
116
|
+
const halves = normalized.split('::');
|
|
117
|
+
if (halves.length > 2)
|
|
118
|
+
return null;
|
|
119
|
+
const left = halves[0] ? halves[0].split(':') : [];
|
|
120
|
+
const right = halves.length === 2 && halves[1] ? halves[1].split(':') : [];
|
|
121
|
+
const missing = 8 - left.length - right.length;
|
|
122
|
+
if (missing < 0 || (halves.length === 1 && missing !== 0))
|
|
123
|
+
return null;
|
|
124
|
+
const parts = [...left, ...Array.from({ length: missing }, () => '0'), ...right];
|
|
125
|
+
if (parts.length !== 8 ||
|
|
126
|
+
parts.slice(0, 5).some((part) => Number.parseInt(part || '0', 16) !== 0) ||
|
|
127
|
+
Number.parseInt(parts[5] || '0', 16) !== 0xffff) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const high = Number.parseInt(parts[6], 16);
|
|
131
|
+
const low = Number.parseInt(parts[7], 16);
|
|
132
|
+
if (!Number.isInteger(high) ||
|
|
133
|
+
!Number.isInteger(low) ||
|
|
134
|
+
high < 0 ||
|
|
135
|
+
high > 0xffff ||
|
|
136
|
+
low < 0 ||
|
|
137
|
+
low > 0xffff) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
return `${high >>> 8}.${high & 0xff}.${low >>> 8}.${low & 0xff}`;
|
|
141
|
+
}
|
|
142
|
+
function isForbiddenIp(address) {
|
|
143
|
+
if (isIP(address) === 4)
|
|
144
|
+
return isForbiddenIpv4(address);
|
|
145
|
+
const normalized = address.toLowerCase().split('%')[0];
|
|
146
|
+
const mapped = mappedIpv4Address(normalized);
|
|
147
|
+
if (mapped)
|
|
148
|
+
return isForbiddenIpv4(mapped);
|
|
149
|
+
return (normalized === '::' ||
|
|
150
|
+
normalized === '::1' ||
|
|
151
|
+
normalized.startsWith('fc') ||
|
|
152
|
+
normalized.startsWith('fd') ||
|
|
153
|
+
normalized.startsWith('fe8') ||
|
|
154
|
+
normalized.startsWith('fe9') ||
|
|
155
|
+
normalized.startsWith('fea') ||
|
|
156
|
+
normalized.startsWith('feb') ||
|
|
157
|
+
normalized.startsWith('ff') ||
|
|
158
|
+
normalized.startsWith('2001:db8:'));
|
|
159
|
+
}
|
|
160
|
+
export async function resolveSafeDownloadTarget(value) {
|
|
161
|
+
let url;
|
|
162
|
+
try {
|
|
163
|
+
url = value instanceof URL ? value : new URL(value);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
throw new SourceImportError('unsafe-download-url', 'Figma returned an unsafe download target.');
|
|
167
|
+
}
|
|
168
|
+
if (url.protocol !== 'https:' ||
|
|
169
|
+
url.username !== '' ||
|
|
170
|
+
url.password !== '' ||
|
|
171
|
+
(url.port !== '' && url.port !== '443') ||
|
|
172
|
+
url.hostname === '' ||
|
|
173
|
+
url.hostname === 'localhost' ||
|
|
174
|
+
url.hostname.endsWith('.localhost') ||
|
|
175
|
+
url.hostname.endsWith('.local') ||
|
|
176
|
+
url.hostname.startsWith('[') ||
|
|
177
|
+
isIP(url.hostname) !== 0) {
|
|
178
|
+
throw new SourceImportError('unsafe-download-url', 'Figma returned an unsafe download target.');
|
|
179
|
+
}
|
|
180
|
+
let resolvedAddresses;
|
|
181
|
+
try {
|
|
182
|
+
resolvedAddresses = await lookup(url.hostname, { all: true });
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
throw new SourceImportError('unsafe-download-url', 'Figma returned an unavailable download target.');
|
|
186
|
+
}
|
|
187
|
+
if (resolvedAddresses.length === 0 ||
|
|
188
|
+
resolvedAddresses.some(({ address }) => isIP(address) === 0 || isForbiddenIp(address))) {
|
|
189
|
+
throw new SourceImportError('unsafe-download-url', 'Figma returned an unsafe download target.');
|
|
190
|
+
}
|
|
191
|
+
const addresses = resolvedAddresses.map(({ address }) => ({
|
|
192
|
+
address,
|
|
193
|
+
family: isIP(address)
|
|
194
|
+
}));
|
|
195
|
+
return { url, addresses };
|
|
196
|
+
}
|
|
197
|
+
export async function assertSafeDownloadUrl(value) {
|
|
198
|
+
return (await resolveSafeDownloadTarget(value)).url;
|
|
199
|
+
}
|
|
200
|
+
async function fetchWithDeadline(fetcher, url, init, consume) {
|
|
201
|
+
const controller = new AbortController();
|
|
202
|
+
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
203
|
+
try {
|
|
204
|
+
const response = await fetcher(url, { ...init, signal: controller.signal });
|
|
205
|
+
if (!response.ok)
|
|
206
|
+
throw upstreamStatusError(response);
|
|
207
|
+
return await consume(response);
|
|
208
|
+
}
|
|
209
|
+
catch (cause) {
|
|
210
|
+
if (controller.signal.aborted || (cause instanceof Error && cause.name === 'AbortError')) {
|
|
211
|
+
throw new SourceImportError('download-timeout', 'Figma export timed out.');
|
|
212
|
+
}
|
|
213
|
+
if (cause instanceof SourceImportError)
|
|
214
|
+
throw cause;
|
|
215
|
+
throw new SourceImportError('upstream-failed', 'Figma export request failed.');
|
|
216
|
+
}
|
|
217
|
+
finally {
|
|
218
|
+
clearTimeout(timeout);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async function readLimitedBody(response, signal) {
|
|
222
|
+
let canceled = false;
|
|
223
|
+
const cancel = () => {
|
|
224
|
+
if (canceled)
|
|
225
|
+
return;
|
|
226
|
+
canceled = true;
|
|
227
|
+
response.cancel();
|
|
228
|
+
};
|
|
229
|
+
const declared = response.headers.get('content-length');
|
|
230
|
+
if (declared && /^\d+$/.test(declared) && Number(declared) > MAX_DOWNLOAD_BYTES) {
|
|
231
|
+
cancel();
|
|
232
|
+
throw new SourceImportError('download-too-large', 'Figma export exceeds the 50 MiB limit.');
|
|
233
|
+
}
|
|
234
|
+
const iterator = response.body[Symbol.asyncIterator]();
|
|
235
|
+
const chunks = [];
|
|
236
|
+
let size = 0;
|
|
237
|
+
let rejectAbort;
|
|
238
|
+
const aborted = new Promise((_, reject) => (rejectAbort = reject));
|
|
239
|
+
const onAbort = () => {
|
|
240
|
+
rejectAbort(new SourceImportError('download-timeout', 'Figma export download timed out.'));
|
|
241
|
+
cancel();
|
|
242
|
+
};
|
|
243
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
244
|
+
try {
|
|
245
|
+
for (;;) {
|
|
246
|
+
const { done, value } = await Promise.race([iterator.next(), aborted]);
|
|
247
|
+
if (signal.aborted) {
|
|
248
|
+
throw new SourceImportError('download-timeout', 'Figma export download timed out.');
|
|
249
|
+
}
|
|
250
|
+
if (done)
|
|
251
|
+
break;
|
|
252
|
+
size += value.byteLength;
|
|
253
|
+
if (size > MAX_DOWNLOAD_BYTES) {
|
|
254
|
+
cancel();
|
|
255
|
+
throw new SourceImportError('download-too-large', 'Figma export exceeds the 50 MiB limit.');
|
|
256
|
+
}
|
|
257
|
+
chunks.push(value);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
catch (cause) {
|
|
261
|
+
cancel();
|
|
262
|
+
throw cause;
|
|
263
|
+
}
|
|
264
|
+
finally {
|
|
265
|
+
signal.removeEventListener('abort', onAbort);
|
|
266
|
+
}
|
|
267
|
+
const bytes = new Uint8Array(size);
|
|
268
|
+
let offset = 0;
|
|
269
|
+
for (const chunk of chunks) {
|
|
270
|
+
bytes.set(chunk, offset);
|
|
271
|
+
offset += chunk.byteLength;
|
|
272
|
+
}
|
|
273
|
+
return bytes;
|
|
274
|
+
}
|
|
275
|
+
async function downloadExport(transport, initial) {
|
|
276
|
+
let target = await resolveSafeDownloadTarget(initial);
|
|
277
|
+
const controller = new AbortController();
|
|
278
|
+
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
279
|
+
try {
|
|
280
|
+
for (let redirects = 0;; redirects++) {
|
|
281
|
+
let response;
|
|
282
|
+
try {
|
|
283
|
+
response = await transport(target, controller.signal);
|
|
284
|
+
}
|
|
285
|
+
catch (cause) {
|
|
286
|
+
if (controller.signal.aborted || (cause instanceof Error && cause.name === 'AbortError')) {
|
|
287
|
+
throw new SourceImportError('download-timeout', 'Figma export download timed out.');
|
|
288
|
+
}
|
|
289
|
+
throw new SourceImportError('upstream-failed', 'Figma export download failed.');
|
|
290
|
+
}
|
|
291
|
+
if (response.status >= 300 && response.status < 400) {
|
|
292
|
+
if (redirects >= MAX_REDIRECTS) {
|
|
293
|
+
response.cancel();
|
|
294
|
+
throw new SourceImportError('unsafe-download-url', 'Figma export redirected too many times.');
|
|
295
|
+
}
|
|
296
|
+
const location = response.headers.get('location');
|
|
297
|
+
if (!location) {
|
|
298
|
+
response.cancel();
|
|
299
|
+
throw new SourceImportError('invalid-response', 'Figma export redirect was invalid.');
|
|
300
|
+
}
|
|
301
|
+
response.cancel();
|
|
302
|
+
target = await resolveSafeDownloadTarget(new URL(location, target.url));
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (response.status < 200 || response.status >= 300) {
|
|
306
|
+
response.cancel();
|
|
307
|
+
throw upstreamStatusError(response);
|
|
308
|
+
}
|
|
309
|
+
return await readLimitedBody(response, controller.signal);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
finally {
|
|
313
|
+
clearTimeout(timeout);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
async function validatePng(bytes) {
|
|
317
|
+
if (bytes.byteLength < PNG_SIGNATURE.byteLength ||
|
|
318
|
+
PNG_SIGNATURE.some((value, index) => bytes[index] !== value)) {
|
|
319
|
+
throw new SourceImportError('invalid-response', 'Figma export is not a valid PNG.');
|
|
320
|
+
}
|
|
321
|
+
let metadata;
|
|
322
|
+
try {
|
|
323
|
+
metadata = await sharp(bytes).metadata();
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
throw new SourceImportError('invalid-response', 'Figma export is not a readable PNG.');
|
|
327
|
+
}
|
|
328
|
+
if (!metadata.width || !metadata.height) {
|
|
329
|
+
throw new SourceImportError('invalid-response', 'Figma export has invalid dimensions.');
|
|
330
|
+
}
|
|
331
|
+
if (metadata.width * metadata.height > MAX_PIXELS) {
|
|
332
|
+
throw new SourceImportError('image-too-large', 'Figma export exceeds the 32 megapixel limit.');
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
async function materialize(source, context) {
|
|
336
|
+
const token = context.config.import.figma.token;
|
|
337
|
+
if (!token) {
|
|
338
|
+
throw new SourceImportError('not-configured', 'Figma access is not configured.');
|
|
339
|
+
}
|
|
340
|
+
const endpoint = new URL(`https://api.figma.com/v1/images/${encodeURIComponent(source.fileKey)}`);
|
|
341
|
+
endpoint.searchParams.set('ids', source.nodeId);
|
|
342
|
+
endpoint.searchParams.set('format', source.format);
|
|
343
|
+
endpoint.searchParams.set('scale', String(source.scale));
|
|
344
|
+
const jsonBytes = await fetchWithDeadline(context.fetch, endpoint, { redirect: 'error', headers: { 'X-Figma-Token': token } }, async (response) => new Uint8Array(await response.arrayBuffer()));
|
|
345
|
+
let payload;
|
|
346
|
+
try {
|
|
347
|
+
payload = JSON.parse(new TextDecoder().decode(jsonBytes));
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
throw new SourceImportError('invalid-response', 'Figma returned an invalid response.');
|
|
351
|
+
}
|
|
352
|
+
const body = record(payload);
|
|
353
|
+
if (!body || body.err || (typeof body.status === 'number' && body.status >= 400)) {
|
|
354
|
+
throw new SourceImportError('invalid-response', 'Figma could not render this node.');
|
|
355
|
+
}
|
|
356
|
+
const images = record(body.images);
|
|
357
|
+
if (!images || !Object.hasOwn(images, source.nodeId)) {
|
|
358
|
+
throw new SourceImportError('invalid-response', 'Figma did not return the requested node.');
|
|
359
|
+
}
|
|
360
|
+
const imageUrl = images[source.nodeId];
|
|
361
|
+
if (typeof imageUrl !== 'string' || imageUrl.length === 0) {
|
|
362
|
+
throw new SourceImportError('invalid-response', 'Figma could not render the requested node.');
|
|
363
|
+
}
|
|
364
|
+
const bytes = await downloadExport(context.httpsTransport ?? requestPinnedHttps, imageUrl);
|
|
365
|
+
await validatePng(bytes);
|
|
366
|
+
const importSource = { importer: 'figma', parameters: source };
|
|
367
|
+
return { bytes, extension: 'png', importSource };
|
|
368
|
+
}
|
|
369
|
+
export const figmaSourceImporter = {
|
|
370
|
+
kind: 'figma',
|
|
371
|
+
label: 'Figma',
|
|
372
|
+
validateInput,
|
|
373
|
+
async importRaw(input, context) {
|
|
374
|
+
const parsed = parseFigmaSelectionUrl(input.url);
|
|
375
|
+
return materialize({
|
|
376
|
+
fileKey: parsed.fileKey,
|
|
377
|
+
nodeId: parsed.nodeId,
|
|
378
|
+
format: 'png',
|
|
379
|
+
scale: input.scale ?? context.config.import.figma.defaults.scale
|
|
380
|
+
}, context);
|
|
381
|
+
},
|
|
382
|
+
validateSourceParameters: validateFigmaSourceParameters,
|
|
383
|
+
async reimportRaw(source, context) {
|
|
384
|
+
return materialize(source, context);
|
|
385
|
+
}
|
|
386
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { extname } from 'node:path';
|
|
2
|
+
import { SourceImportError } from './contract.js';
|
|
3
|
+
function validate(value) {
|
|
4
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
5
|
+
throw new SourceImportError('invalid-input', 'File import input must be an object.');
|
|
6
|
+
}
|
|
7
|
+
const input = value;
|
|
8
|
+
if (typeof input.fileName !== 'string' || input.fileName.length === 0) {
|
|
9
|
+
throw new SourceImportError('invalid-input', 'File import requires a file name.');
|
|
10
|
+
}
|
|
11
|
+
if (!(input.bytes instanceof Uint8Array)) {
|
|
12
|
+
throw new SourceImportError('invalid-input', 'File import requires byte data.');
|
|
13
|
+
}
|
|
14
|
+
return { fileName: input.fileName, bytes: input.bytes };
|
|
15
|
+
}
|
|
16
|
+
export const fileSourceImporter = {
|
|
17
|
+
kind: 'file',
|
|
18
|
+
label: 'File',
|
|
19
|
+
validateInput: validate,
|
|
20
|
+
async importRaw(input) {
|
|
21
|
+
const extension = extname(input.fileName).slice(1).toLowerCase();
|
|
22
|
+
if (!extension)
|
|
23
|
+
throw new SourceImportError('invalid-input', 'The file needs an extension.');
|
|
24
|
+
return {
|
|
25
|
+
bytes: input.bytes,
|
|
26
|
+
extension,
|
|
27
|
+
suggestedFileName: input.fileName
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
validateSourceParameters() {
|
|
31
|
+
throw new SourceImportError('invalid-input', 'File imports cannot be re-imported.');
|
|
32
|
+
}
|
|
33
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { IncomingMessage } from 'node:http';
|
|
2
|
+
import { type RequestOptions } from 'node:https';
|
|
3
|
+
import type { ClientRequest } from 'node:http';
|
|
4
|
+
import type { LookupFunction } from 'node:net';
|
|
5
|
+
export interface PinnedAddress {
|
|
6
|
+
address: string;
|
|
7
|
+
family: 4 | 6;
|
|
8
|
+
}
|
|
9
|
+
export interface PinnedHttpsTarget {
|
|
10
|
+
url: URL;
|
|
11
|
+
addresses: readonly PinnedAddress[];
|
|
12
|
+
}
|
|
13
|
+
export interface PinnedHttpsResponse {
|
|
14
|
+
status: number;
|
|
15
|
+
headers: Headers;
|
|
16
|
+
body: AsyncIterable<Uint8Array>;
|
|
17
|
+
cancel(): void;
|
|
18
|
+
}
|
|
19
|
+
export type PinnedHttpsTransport = (target: PinnedHttpsTarget, signal: AbortSignal) => Promise<PinnedHttpsResponse>;
|
|
20
|
+
export type HttpsRequester = (url: URL, options: RequestOptions, onResponse: (response: IncomingMessage) => void) => ClientRequest;
|
|
21
|
+
export declare function createPinnedLookup(target: PinnedHttpsTarget): LookupFunction;
|
|
22
|
+
export declare function createPinnedHttpsTransport(requester?: HttpsRequester): PinnedHttpsTransport;
|
|
23
|
+
export declare const requestPinnedHttps: PinnedHttpsTransport;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { request } from 'node:https';
|
|
2
|
+
function lookupError(hostname) {
|
|
3
|
+
const error = new Error(`No validated address is available for ${hostname}.`);
|
|
4
|
+
error.code = 'ENOTFOUND';
|
|
5
|
+
return error;
|
|
6
|
+
}
|
|
7
|
+
export function createPinnedLookup(target) {
|
|
8
|
+
return (hostname, options, callback) => {
|
|
9
|
+
if (hostname !== target.url.hostname) {
|
|
10
|
+
callback(lookupError(hostname), '', 0);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const requestedFamily = options.family === 4 || options.family === 6 ? options.family : 0;
|
|
14
|
+
const addresses = target.addresses.filter((candidate) => requestedFamily === 0 || candidate.family === requestedFamily);
|
|
15
|
+
if (addresses.length === 0) {
|
|
16
|
+
callback(lookupError(hostname), '', 0);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (options.all) {
|
|
20
|
+
callback(null, addresses.map(({ address, family }) => ({ address, family })));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
callback(null, addresses[0].address, addresses[0].family);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function responseHeaders(source) {
|
|
27
|
+
const headers = new Headers();
|
|
28
|
+
for (const [name, value] of Object.entries(source)) {
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
for (const item of value)
|
|
31
|
+
headers.append(name, item);
|
|
32
|
+
}
|
|
33
|
+
else if (value !== undefined) {
|
|
34
|
+
headers.set(name, value);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return headers;
|
|
38
|
+
}
|
|
39
|
+
export function createPinnedHttpsTransport(requester = request) {
|
|
40
|
+
return (target, signal) => new Promise((resolve, reject) => {
|
|
41
|
+
const request = requester(target.url, {
|
|
42
|
+
method: 'GET',
|
|
43
|
+
agent: false,
|
|
44
|
+
lookup: createPinnedLookup(target),
|
|
45
|
+
servername: target.url.hostname,
|
|
46
|
+
rejectUnauthorized: true,
|
|
47
|
+
signal
|
|
48
|
+
}, (response) => {
|
|
49
|
+
resolve({
|
|
50
|
+
status: response.statusCode ?? 0,
|
|
51
|
+
headers: responseHeaders(response.headers),
|
|
52
|
+
body: response,
|
|
53
|
+
cancel: () => response.destroy()
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
request.once('error', reject);
|
|
57
|
+
request.end();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
export const requestPinnedHttps = createPinnedHttpsTransport();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ImportSourceRecord } from '../../core/index.js';
|
|
2
|
+
import { type ServerSourceImporter } from './contract.js';
|
|
3
|
+
export declare class SourceImporterRegistry {
|
|
4
|
+
#private;
|
|
5
|
+
constructor(importers: readonly ServerSourceImporter[]);
|
|
6
|
+
require(kind: string): ServerSourceImporter;
|
|
7
|
+
has(kind: string): boolean;
|
|
8
|
+
requireReimport(source: ImportSourceRecord): {
|
|
9
|
+
importer: ServerSourceImporter;
|
|
10
|
+
parameters: unknown;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { SourceImportError } from './contract.js';
|
|
2
|
+
export class SourceImporterRegistry {
|
|
3
|
+
#byKind = new Map();
|
|
4
|
+
constructor(importers) {
|
|
5
|
+
for (const importer of importers) {
|
|
6
|
+
if (!importer.kind)
|
|
7
|
+
throw new Error('Source importer kind must not be empty.');
|
|
8
|
+
if (this.#byKind.has(importer.kind)) {
|
|
9
|
+
throw new Error(`Duplicate source importer kind "${importer.kind}".`);
|
|
10
|
+
}
|
|
11
|
+
this.#byKind.set(importer.kind, importer);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
require(kind) {
|
|
15
|
+
const importer = this.#byKind.get(kind);
|
|
16
|
+
if (!importer) {
|
|
17
|
+
throw new SourceImportError('invalid-input', `Unknown source importer "${kind}".`);
|
|
18
|
+
}
|
|
19
|
+
return importer;
|
|
20
|
+
}
|
|
21
|
+
has(kind) {
|
|
22
|
+
return this.#byKind.has(kind);
|
|
23
|
+
}
|
|
24
|
+
requireReimport(source) {
|
|
25
|
+
const importer = this.require(source.importer);
|
|
26
|
+
if (!importer.reimportRaw) {
|
|
27
|
+
throw new SourceImportError('invalid-input', `Source importer "${source.importer}" does not support re-import.`);
|
|
28
|
+
}
|
|
29
|
+
return { importer, parameters: importer.validateSourceParameters(source.parameters) };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { figmaSourceImporter } from './importers/figma.js';
|
|
2
|
+
import { fileSourceImporter } from './importers/file.js';
|
|
3
|
+
import { SourceImporterRegistry } from './importers/registry.js';
|
|
4
|
+
export const serverImporters = new SourceImporterRegistry([
|
|
5
|
+
fileSourceImporter,
|
|
6
|
+
figmaSourceImporter
|
|
7
|
+
]);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Library, type ValidationIssueDto } from '../core/index.js';
|
|
1
|
+
import { type ImportSourceRecord, type Library, type ValidationIssueDto } from '../core/index.js';
|
|
2
2
|
import type { ResolvedFundusConfig } from './config-loader.js';
|
|
3
3
|
import type { ServerPluginRegistry } from './plugins/contract.js';
|
|
4
4
|
export declare function assertSafeFileName(fileName: string): void;
|
|
@@ -23,6 +23,21 @@ export interface IngestOptions {
|
|
|
23
23
|
/** Registered destination folder; empty string means the implicit root. */
|
|
24
24
|
folder?: string;
|
|
25
25
|
}
|
|
26
|
+
export interface CreateAssetOptions extends IngestOptions {
|
|
27
|
+
manifests?: string[];
|
|
28
|
+
importSource?: ImportSourceRecord;
|
|
29
|
+
}
|
|
30
|
+
export interface CreateAssetStaging {
|
|
31
|
+
rawPath: string;
|
|
32
|
+
rawFileName: string;
|
|
33
|
+
parameters: unknown;
|
|
34
|
+
}
|
|
35
|
+
export interface ImportSourceExpectation {
|
|
36
|
+
raw: string;
|
|
37
|
+
rawHash: string;
|
|
38
|
+
type: string;
|
|
39
|
+
canonicalSource: string;
|
|
40
|
+
}
|
|
26
41
|
export interface ReplaceRawFileOptions {
|
|
27
42
|
/** Original file name of the replacement (sanitized before storing). */
|
|
28
43
|
fileName: string;
|
|
@@ -54,12 +69,15 @@ export declare class LibraryStore {
|
|
|
54
69
|
* the owning plugin's default parameters. Returns the updated library.
|
|
55
70
|
*/
|
|
56
71
|
ingest(options: IngestOptions): Promise<Library>;
|
|
72
|
+
createAsset(options: CreateAssetOptions, prepareParameters?: (staging: CreateAssetStaging) => Promise<unknown>): Promise<Library>;
|
|
57
73
|
/**
|
|
58
74
|
* Replace one asset's raw file while preserving its identity and authored
|
|
59
75
|
* metadata. The replacement stays in the current folder, receives a safe
|
|
60
76
|
* non-colliding name, and must be accepted by the asset's existing type.
|
|
61
77
|
*/
|
|
62
78
|
replaceRawFile(id: string, options: ReplaceRawFileOptions): Promise<Library>;
|
|
79
|
+
/** CAS-protected replacement used only by a repeatable import source. */
|
|
80
|
+
replaceRawFromImportSource(id: string, bytes: Uint8Array, expected: ImportSourceExpectation, nextSource?: ImportSourceRecord): Promise<Library>;
|
|
63
81
|
/** Replace an asset's parameters (validated by the owning plugin). */
|
|
64
82
|
updateParameters(id: string, parameters: unknown): Promise<Library>;
|
|
65
83
|
/** Replace an asset's manifest membership (every name must exist). */
|