pandoc_wasm 1.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.
data/SECURITY.md ADDED
@@ -0,0 +1,343 @@
1
+ # SECURITY.md - Aspects de sécurité des patches
2
+
3
+ > Documentation des modifications apportées aux packages Haskell pour la compilation WASM et leurs implications de sécurité.
4
+
5
+ ---
6
+
7
+ ## Introduction
8
+
9
+ Ce document détaille les modifications de sécurité appliquées aux dépendances de Pandoc pour permettre la compilation en WebAssembly. Ces patches ont été nécessaires car :
10
+
11
+ 1. **Architecture 32-bit** : WASM utilise une architecture 32-bit, causant des incompatibilités avec du code optimisé pour 64-bit
12
+ 2. **APIs système limitées** : WASI (WebAssembly System Interface) ne fournit pas toutes les APIs POSIX
13
+ 3. **Pas de threading** : Le runtime WASM n'a pas de support threading natif
14
+ 4. **Pas de réseau** : WASI Preview 1 a un support réseau très limité
15
+
16
+ **Important** : Ces patches n'ont pas été audités de manière exhaustive. Utilisez ce projet en connaissance de cause.
17
+
18
+ ---
19
+
20
+ ## Vue d'ensemble des modifications
21
+
22
+ | Package | Type de modification | Risque | Impact |
23
+ |---------|---------------------|--------|--------|
24
+ | basement | Conversions de types | Faible | Calculs numériques |
25
+ | memory | Désactivation fonctionnalités | Faible | Pas de memory-mapping |
26
+ | network | Stubs retournant erreurs | Moyen | Pas de réseau |
27
+ | cborg | Corrections 32-bit | Faible | Sérialisation CBOR |
28
+ | crypton | Désactivation threading | Faible | Crypto mono-thread |
29
+ | xml-conduit | Changement build-type | Aucun | Build uniquement |
30
+ | pandoc-cli | Suppression -threaded | Aucun | Runtime uniquement |
31
+
32
+ ---
33
+
34
+ ## Détail par package
35
+
36
+ ### 1. basement-0.0.16
37
+
38
+ **Fichiers modifiés** :
39
+ - `cbits/foundation_system.h`
40
+ - `Basement/Numerical/Conversion.hs`
41
+ - `Basement/Numerical/Additive.hs`
42
+ - `Basement/Types/OffsetSize.hs`
43
+ - `Basement/From.hs`
44
+ - `Basement/PrimType.hs`
45
+ - `Basement/Bits.hs`
46
+
47
+ **Nature des modifications** :
48
+
49
+ ```haskell
50
+ -- AVANT (GHC < 9.4)
51
+ import GHC.IntWord64
52
+
53
+ -- APRÈS (GHC >= 9.4)
54
+ import GHC.Prim (int64ToInt#, word64ToWord#, ...)
55
+ ```
56
+
57
+ **Raison** : Le module `GHC.IntWord64` a été supprimé dans GHC 9.4+. Les primitives sont maintenant dans `GHC.Prim`.
58
+
59
+ **Conversions 32-bit** :
60
+
61
+ ```haskell
62
+ -- Conversions explicites Word32# <-> Word# pour architecture 32-bit
63
+ word32ToWord# :: Word32# -> Word#
64
+ wordToWord32# :: Word# -> Word32#
65
+ ```
66
+
67
+ **Implications de sécurité** :
68
+ - ✅ Pas d'impact sur la sécurité cryptographique
69
+ - ✅ Conversions de types standards
70
+ - ⚠️ Vérifier la correction des calculs sur grands nombres si critique
71
+
72
+ ---
73
+
74
+ ### 2. memory-0.18.0
75
+
76
+ **Fichiers modifiés** :
77
+ - `Data/Memory/Internal/CompatPrim64.hs`
78
+ - `Data/Memory/MemMap/Posix.hsc`
79
+ - `Data/ByteArray/Mapping.hs`
80
+ - `Data/Memory/PtrMethods.hs`
81
+ - `memory.cabal`
82
+
83
+ **Nature des modifications** :
84
+
85
+ 1. **Désactivation de mmap** :
86
+ ```haskell
87
+ -- Module MemMap.Posix désactivé sur wasm32
88
+ #if !defined(wasm32_HOST_ARCH)
89
+ -- code mmap original
90
+ #endif
91
+ ```
92
+
93
+ 2. **Corrections FFI** :
94
+ ```haskell
95
+ -- Signatures FFI corrigées pour memcpy/memset
96
+ foreign import ccall unsafe "string.h memset"
97
+ c_memset :: Ptr Word8 -> Word8 -> CSize -> IO (Ptr Word8)
98
+ ```
99
+
100
+ **Implications de sécurité** :
101
+ - ✅ `mmap` n'est pas disponible en WASI de toute façon
102
+ - ✅ Les opérations mémoire utilisent l'allocateur standard
103
+ - ✅ Pas d'impact sur la sécurité des données en mémoire
104
+
105
+ ---
106
+
107
+ ### 3. network-3.2.8.0
108
+
109
+ **Fichiers modifiés** :
110
+ - `cbits/HsNet.c`
111
+ - `include/HsNet.h`
112
+
113
+ **Nature des modifications** :
114
+
115
+ Ajout de stubs pour les fonctions socket non fournies par WASI :
116
+
117
+ ```c
118
+ /* cbits/HsNet.c - Stubs WASI */
119
+ #if defined(__wasi__) || defined(__wasm__)
120
+
121
+ int socket(int domain, int type, int protocol) {
122
+ errno = ENOSYS;
123
+ return -1;
124
+ }
125
+
126
+ int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen) {
127
+ errno = ENOSYS;
128
+ return -1;
129
+ }
130
+
131
+ int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen) {
132
+ errno = ENOSYS;
133
+ return -1;
134
+ }
135
+
136
+ // ... autres stubs similaires
137
+ #endif
138
+ ```
139
+
140
+ **Fonctions stubées (retournent ENOSYS)** :
141
+ | Fonction | Comportement |
142
+ |----------|--------------|
143
+ | `socket()` | Retourne -1, errno=ENOSYS |
144
+ | `bind()` | Retourne -1, errno=ENOSYS |
145
+ | `listen()` | Retourne -1, errno=ENOSYS |
146
+ | `connect()` | Retourne -1, errno=ENOSYS |
147
+ | `setsockopt()` | Retourne 0 (succès factice) |
148
+ | `getsockopt()` | Retourne -1, errno=ENOPROTOOPT |
149
+ | `getpeername()` | Retourne -1, errno=ENOSYS |
150
+ | `getsockname()` | Retourne -1, errno=ENOSYS |
151
+ | `sendto()` | Retourne -1, errno=ENOSYS |
152
+ | `recvfrom()` | Retourne -1, errno=ENOSYS |
153
+
154
+ **Fonctions NON stubées** (fournies par WASI libc) :
155
+ - `accept()`, `send()`, `recv()`, `shutdown()`
156
+
157
+ **Implications de sécurité** :
158
+ - ✅ **Isolation réseau** : Aucune connexion réseau possible
159
+ - ✅ **Pas d'exfiltration de données** par réseau
160
+ - ⚠️ **Fonctionnalités cassées** : Tout code utilisant le réseau échouera
161
+ - ⚠️ `setsockopt()` retourne succès sans rien faire - code appelant peut avoir des attentes non respectées
162
+
163
+ ---
164
+
165
+ ### 4. cborg-0.2.10.0
166
+
167
+ **Fichiers modifiés** :
168
+ - `src/Codec/CBOR/Magic.hs`
169
+ - `src/Codec/CBOR/Decoding.hs`
170
+ - `src/Codec/CBOR/Read.hs`
171
+
172
+ **Nature des modifications** :
173
+
174
+ Corrections pour architecture 32-bit :
175
+
176
+ ```haskell
177
+ -- Corrections de conversions Word64#/Int64# sur 32-bit
178
+ -- Suppression des imports GHC.IntWord64 dépréciés
179
+ -- Corrections de syntaxe dans isWord64Canonical, isInt64Canonical
180
+ ```
181
+
182
+ **Implications de sécurité** :
183
+ - ✅ CBOR est utilisé pour la sérialisation de données
184
+ - ✅ Pas d'impact sur l'intégrité des données sérialisées
185
+ - ✅ Les tests de validation CBOR passent
186
+
187
+ ---
188
+
189
+ ### 5. crypton-1.0.5
190
+
191
+ **Fichiers modifiés** :
192
+ - `cbits/argon2/thread.h`
193
+ - `cbits/argon2/thread.c`
194
+
195
+ **Nature des modifications** :
196
+
197
+ ```c
198
+ /* cbits/argon2/thread.h */
199
+ #if defined(__wasi__) || defined(__wasm__) || defined(__wasm32__)
200
+ #define ARGON2_NO_THREADS 1
201
+ #endif
202
+ ```
203
+
204
+ **Raison** : WASI ne supporte pas `pthread_exit()`. Argon2 doit fonctionner en mode mono-thread.
205
+
206
+ **Implications de sécurité** :
207
+ - ✅ **Argon2 reste fonctionnel** en mode single-thread
208
+ - ⚠️ **Performance réduite** : Pas de parallélisation du hachage
209
+ - ✅ **Sécurité cryptographique préservée** : Argon2 mono-thread est toujours sécurisé
210
+ - ℹ️ Les paramètres de coût peuvent nécessiter ajustement pour compenser
211
+
212
+ **Note sur Argon2** : Argon2 est un algorithme de hachage de mots de passe. Le mode mono-thread est plus lent mais tout aussi sécurisé cryptographiquement.
213
+
214
+ ---
215
+
216
+ ### 6. xml-conduit-1.10.1.0
217
+
218
+ **Fichiers modifiés** :
219
+ - `xml-conduit.cabal`
220
+ - `Setup.hs`
221
+
222
+ **Nature des modifications** :
223
+
224
+ ```cabal
225
+ -- AVANT
226
+ build-type: Custom
227
+ custom-setup
228
+ setup-depends: base, Cabal, cabal-doctest
229
+
230
+ -- APRÈS
231
+ build-type: Simple
232
+ ```
233
+
234
+ **Raison** : Les packages avec `build-type: Custom` échouent en cross-compilation car le `Setup.hs` est compilé pour l'hôte, pas la cible WASM.
235
+
236
+ **Implications de sécurité** :
237
+ - ✅ **Aucun impact** : Changement de build uniquement
238
+ - ✅ Les doctests sont désactivés (tests uniquement)
239
+
240
+ ---
241
+
242
+ ### 7. pandoc-cli-3.8.3
243
+
244
+ **Fichiers modifiés** :
245
+ - `pandoc-cli.cabal`
246
+
247
+ **Nature des modifications** :
248
+
249
+ ```cabal
250
+ -- AVANT
251
+ ghc-options: -threaded -rtsopts -with-rtsopts=-A8m
252
+
253
+ -- APRÈS
254
+ ghc-options: -rtsopts -with-rtsopts=-A8m
255
+ ```
256
+
257
+ **Raison** : GHC WASM n'a pas de runtime threadé (`-lHSrts_thr` n'existe pas).
258
+
259
+ **Implications de sécurité** :
260
+ - ✅ **Aucun impact sur la sécurité**
261
+ - ℹ️ L'exécution est mono-thread (ce qui est la norme pour WASM)
262
+
263
+ ---
264
+
265
+ ## Implications globales de sécurité
266
+
267
+ ### Isolation WASI
268
+
269
+ Le binaire `pandoc.wasm` s'exécute dans un environnement WASI isolé :
270
+
271
+ ```
272
+ ┌─────────────────────────────────────────┐
273
+ │ Host System │
274
+ │ ┌───────────────────────────────────┐ │
275
+ │ │ WASI Sandbox │ │
276
+ │ │ ┌─────────────────────────────┐ │ │
277
+ │ │ │ pandoc.wasm │ │ │
278
+ │ │ │ │ │ │
279
+ │ │ │ ✗ Pas de réseau │ │ │
280
+ │ │ │ ✗ Pas de processus │ │ │
281
+ │ │ │ ✓ Fichiers (--dir .) │ │ │
282
+ │ │ │ ✓ stdin/stdout │ │ │
283
+ │ │ └─────────────────────────────┘ │ │
284
+ │ └───────────────────────────────────┘ │
285
+ └─────────────────────────────────────────┘
286
+ ```
287
+
288
+ **Garanties de sécurité WASI** :
289
+ - ✅ Accès fichiers limité aux répertoires explicitement montés (`--dir`)
290
+ - ✅ Pas d'accès réseau
291
+ - ✅ Pas de création de processus
292
+ - ✅ Isolation mémoire du runtime WASM
293
+
294
+ ### Points d'attention
295
+
296
+ | Aspect | Statut | Notes |
297
+ |--------|--------|-------|
298
+ | Exécution de code arbitraire | ✅ Protégé | Sandbox WASI |
299
+ | Accès fichiers | ⚠️ Contrôlé | Uniquement via `--dir` |
300
+ | Réseau | ✅ Bloqué | Stubs ENOSYS |
301
+ | Fuite mémoire | ℹ️ Possible | Comme tout programme |
302
+ | DoS (CPU) | ⚠️ Possible | Pas de limite de temps par défaut |
303
+
304
+ ### Recommandations
305
+
306
+ 1. **Limiter l'accès fichiers** : N'utilisez `--dir` que sur les répertoires nécessaires
307
+ 2. **Timeout** : Utilisez `wasmtime run --wasm timeout=30s` pour limiter le temps d'exécution
308
+ 3. **Mémoire** : Utilisez `--wasm max-memory=512MiB` pour limiter la mémoire
309
+ 4. **Entrées** : Validez les fichiers d'entrée avant conversion
310
+
311
+ ```bash
312
+ # Exemple d'exécution sécurisée
313
+ wasmtime run \
314
+ --dir ./input:readonly \
315
+ --dir ./output \
316
+ --wasm timeout=60s \
317
+ --wasm max-memory=1GiB \
318
+ pandoc.wasm -o ./output/result.html ./input/document.md
319
+ ```
320
+
321
+ ---
322
+
323
+ ## Audit et contributions
324
+
325
+ Ces patches n'ont pas fait l'objet d'un audit de sécurité formel. Si vous identifiez des problèmes de sécurité :
326
+
327
+ 1. **Ne pas créer d'issue publique** pour les vulnérabilités
328
+ 2. Contacter les mainteneurs en privé
329
+ 3. Fournir une description détaillée et un PoC si possible
330
+
331
+ ---
332
+
333
+ ## Changelog des patches
334
+
335
+ | Date | Package | Modification |
336
+ |------|---------|--------------|
337
+ | 2026-01-28 | basement | Conversions 32-bit, suppression GHC.IntWord64 |
338
+ | 2026-01-28 | memory | Désactivation mmap, corrections FFI |
339
+ | 2026-01-28 | network | Stubs socket WASI |
340
+ | 2026-01-28 | cborg | Corrections 32-bit |
341
+ | 2026-01-28 | crypton | ARGON2_NO_THREADS |
342
+ | 2026-01-28 | xml-conduit | build-type: Simple |
343
+ | 2026-01-28 | pandoc-cli | Suppression -threaded |
data/cabal.project ADDED
@@ -0,0 +1,45 @@
1
+ -- Pandoc WASM build configuration
2
+ -- Targets Pandoc 3.8.3 for WASM compilation
3
+
4
+ -- Use Hackage packages
5
+ index-state: 2026-01-20T00:00:00Z
6
+
7
+ -- pandoc-cli is now in patches, pandoc will be pulled as dependency
8
+
9
+ -- Use patched packages for WASM compatibility
10
+ packages: patches/basement-0.0.16
11
+ patches/memory-0.18.0
12
+ patches/network-3.2.8.0
13
+ patches/cborg-0.2.10.0
14
+ patches/crypton-1.0.5
15
+ patches/xml-conduit-1.10.1.0
16
+ patches/pandoc-cli-3.8.3
17
+
18
+ -- Disable problematic features for WASM
19
+ package pandoc-cli
20
+ flags: -lua -server
21
+
22
+ -- Disable pkg-config for digest (use bundled zlib from zlib-clib)
23
+ package digest
24
+ flags: -pkg-config
25
+
26
+ -- Embed data files for standalone WASM binary
27
+ package pandoc
28
+ flags: +embed_data_files
29
+
30
+ -- Force constraints for WASM-compatible versions
31
+ constraints:
32
+ pandoc == 3.8.3,
33
+ pandoc-cli == 3.8.3
34
+
35
+ -- Enable _GNU_SOURCE for network package C code on WASI
36
+ package network
37
+ cc-options: -D_GNU_SOURCE
38
+
39
+ -- Disable threading in crypton's argon2 (WASI doesn't support pthread_exit)
40
+ package crypton
41
+ cc-options: -DARGON2_NO_THREADS
42
+
43
+ -- Optimization
44
+ program-options
45
+ ghc-options: -O2
data/index.js ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @nathanhimpens/pandoc-wasm
3
+ *
4
+ * Exports the path to the pandoc.wasm binary.
5
+ * The binary is automatically downloaded from GitHub Releases during npm install.
6
+ */
7
+
8
+ const path = require('path');
9
+ const fs = require('fs');
10
+
11
+ const wasmPath = path.join(__dirname, 'pandoc.wasm');
12
+
13
+ // Check if the file exists, if not, provide helpful error message
14
+ if (!fs.existsSync(wasmPath)) {
15
+ console.warn(
16
+ 'Warning: pandoc.wasm not found. It should be downloaded automatically during installation.\n' +
17
+ 'If you see this message, try running: npm run postinstall'
18
+ );
19
+ }
20
+
21
+ module.exports = wasmPath;
22
+
23
+ // Also export as default for ES modules compatibility
24
+ module.exports.default = wasmPath;
data/lib/download.js ADDED
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Downloads pandoc.wasm from GitHub Releases
5
+ * This script runs automatically after npm install via the postinstall hook
6
+ */
7
+
8
+ const https = require('https');
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ const REPO_OWNER = 'NathanHimpens';
13
+ const REPO_NAME = 'pandoc-wasm';
14
+ const ASSET_NAME = 'pandoc.wasm';
15
+ const WASM_PATH = path.join(__dirname, '..', 'pandoc.wasm');
16
+
17
+ // Get version from package.json
18
+ const packageJson = require('../package.json');
19
+ const version = packageJson.version;
20
+
21
+ /**
22
+ * Get the latest release tag from GitHub API
23
+ */
24
+ function getLatestReleaseTag() {
25
+ return new Promise((resolve, reject) => {
26
+ const options = {
27
+ hostname: 'api.github.com',
28
+ path: `/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`,
29
+ headers: {
30
+ 'User-Agent': 'pandoc-wasm-downloader',
31
+ 'Accept': 'application/vnd.github.v3+json'
32
+ }
33
+ };
34
+
35
+ https.get(options, (res) => {
36
+ let data = '';
37
+
38
+ res.on('data', (chunk) => {
39
+ data += chunk;
40
+ });
41
+
42
+ res.on('end', () => {
43
+ if (res.statusCode === 200) {
44
+ try {
45
+ const release = JSON.parse(data);
46
+ resolve(release.tag_name);
47
+ } catch (e) {
48
+ reject(new Error(`Failed to parse release data: ${e.message}`));
49
+ }
50
+ } else if (res.statusCode === 404) {
51
+ // No releases yet, try to use version from package.json
52
+ console.log(`No GitHub release found. Using version ${version} from package.json.`);
53
+ resolve(`v${version}`);
54
+ } else {
55
+ reject(new Error(`GitHub API returned status ${res.statusCode}: ${data}`));
56
+ }
57
+ });
58
+ }).on('error', (e) => {
59
+ reject(new Error(`Failed to fetch release info: ${e.message}`));
60
+ });
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Download the asset from GitHub Releases
66
+ */
67
+ function downloadAsset(tag) {
68
+ return new Promise((resolve, reject) => {
69
+ // Try to get the release by tag first to find the asset URL
70
+ const options = {
71
+ hostname: 'api.github.com',
72
+ path: `/repos/${REPO_OWNER}/${REPO_NAME}/releases/tags/${tag}`,
73
+ headers: {
74
+ 'User-Agent': 'pandoc-wasm-downloader',
75
+ 'Accept': 'application/vnd.github.v3+json'
76
+ }
77
+ };
78
+
79
+ https.get(options, (res) => {
80
+ let data = '';
81
+
82
+ res.on('data', (chunk) => {
83
+ data += chunk;
84
+ });
85
+
86
+ res.on('end', () => {
87
+ if (res.statusCode === 404) {
88
+ // Release doesn't exist yet, skip download
89
+ console.log(`Release ${tag} not found on GitHub. Skipping download.`);
90
+ console.log('You can manually download pandoc.wasm from the repository or build it yourself.');
91
+ resolve(false);
92
+ return;
93
+ }
94
+
95
+ if (res.statusCode !== 200) {
96
+ reject(new Error(`GitHub API returned status ${res.statusCode}: ${data}`));
97
+ return;
98
+ }
99
+
100
+ try {
101
+ const release = JSON.parse(data);
102
+ const asset = release.assets.find(a => a.name === ASSET_NAME);
103
+
104
+ if (!asset) {
105
+ console.log(`Asset ${ASSET_NAME} not found in release ${tag}.`);
106
+ console.log('You can manually download pandoc.wasm from the repository or build it yourself.');
107
+ resolve(false);
108
+ return;
109
+ }
110
+
111
+ // Download the asset
112
+ console.log(`Downloading ${ASSET_NAME} from release ${tag}...`);
113
+ console.log(`Size: ${(asset.size / 1024 / 1024).toFixed(2)} MB`);
114
+
115
+ const file = fs.createWriteStream(WASM_PATH);
116
+ const downloadUrl = new URL(asset.browser_download_url);
117
+
118
+ const downloadOptions = {
119
+ hostname: downloadUrl.hostname,
120
+ path: downloadUrl.pathname + downloadUrl.search,
121
+ headers: {
122
+ 'User-Agent': 'pandoc-wasm-downloader',
123
+ 'Accept': 'application/octet-stream'
124
+ }
125
+ };
126
+
127
+ https.get(downloadOptions, (downloadRes) => {
128
+ if (downloadRes.statusCode !== 200) {
129
+ file.close();
130
+ fs.unlinkSync(WASM_PATH);
131
+ reject(new Error(`Failed to download asset: ${downloadRes.statusCode}`));
132
+ return;
133
+ }
134
+
135
+ downloadRes.pipe(file);
136
+
137
+ file.on('finish', () => {
138
+ file.close();
139
+ // Make executable
140
+ fs.chmodSync(WASM_PATH, 0o755);
141
+ console.log(`✓ Successfully downloaded ${ASSET_NAME}`);
142
+ resolve(true);
143
+ });
144
+ }).on('error', (e) => {
145
+ file.close();
146
+ fs.unlinkSync(WASM_PATH);
147
+ reject(new Error(`Download failed: ${e.message}`));
148
+ });
149
+ } catch (e) {
150
+ reject(new Error(`Failed to parse release data: ${e.message}`));
151
+ }
152
+ });
153
+ }).on('error', (e) => {
154
+ reject(new Error(`Failed to fetch release: ${e.message}`));
155
+ });
156
+ });
157
+ }
158
+
159
+ /**
160
+ * Main function
161
+ */
162
+ async function main() {
163
+ // Check if file already exists
164
+ if (fs.existsSync(WASM_PATH)) {
165
+ console.log('pandoc.wasm already exists. Skipping download.');
166
+ return;
167
+ }
168
+
169
+ try {
170
+ const tag = await getLatestReleaseTag();
171
+ const downloaded = await downloadAsset(tag);
172
+
173
+ if (!downloaded) {
174
+ console.warn('\n⚠️ pandoc.wasm was not downloaded automatically.');
175
+ console.warn('This is normal if no GitHub release exists yet.\n');
176
+ console.warn('To use this package, you need to:');
177
+ console.warn('1. Build pandoc.wasm yourself (see README.md)');
178
+ console.warn('2. Create a GitHub release with pandoc.wasm attached');
179
+ console.warn('3. Or manually copy pandoc.wasm to this directory\n');
180
+ // Don't exit with error - allow the package to be installed
181
+ // The user can manually add the file later
182
+ return;
183
+ }
184
+ } catch (error) {
185
+ console.error('Error downloading pandoc.wasm:', error.message);
186
+ console.error('\nYou can:');
187
+ console.error('1. Build it yourself following the instructions in README.md');
188
+ console.error('2. Manually download it from a GitHub release');
189
+ console.error('3. Copy it from the build directory after compilation');
190
+ // Don't exit with error - allow installation to continue
191
+ // The user can manually add the file later
192
+ console.warn('\n⚠️ Installation will continue, but pandoc.wasm must be added manually.');
193
+ }
194
+ }
195
+
196
+ // Run if called directly
197
+ if (require.main === module) {
198
+ main();
199
+ }
200
+
201
+ module.exports = { main, downloadAsset, getLatestReleaseTag };