tigertag 1.1.0 → 1.2.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.
@@ -1 +1,18 @@
1
- [{"id":142,"label":"Filament"},{"id":173,"label":"Resin"}]
1
+ [
2
+ {
3
+ "id": 142,
4
+ "label": "Filament"
5
+ },
6
+ {
7
+ "id": 116,
8
+ "label": "Accessories"
9
+ },
10
+ {
11
+ "id": 41,
12
+ "label": "Spare Part"
13
+ },
14
+ {
15
+ "id": 173,
16
+ "label": "Resin"
17
+ }
18
+ ]
@@ -1 +1,30 @@
1
- [{"id":0,"version":"0.0","name":"RFID Empty","tag":"TIGER_TAG_UNINITIALIZED","public_key":""},{"id":1542820452,"version":"1.0","name":"TigerTag","tag":"TIGER_TAG_MAKER_V1.0","public_key":"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwtX8JRYMoAXTbkU7EXJYKX7g4Mf0\nZ3WUuuGzlfyiEiS5UseXT6l1t1ZbMgzsg5IVA0TB7+/w6eyTlgnz/HXONw==\n-----END PUBLIC KEY-----"},{"id":1816240865,"version":"1.0","name":"TigerTag Init","tag":"TIGER_TAG_INIT","public_key":"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwtX8JRYMoAXTbkU7EXJYKX7g4Mf0\nZ3WUuuGzlfyiEiS5UseXT6l1t1ZbMgzsg5IVA0TB7+/w6eyTlgnz/HXONw==\n-----END PUBLIC KEY-----"},{"id":3155151767,"version":"1.0","name":"TigerTag+","tag":"TIGER_TAG_PRO_V1.0","public_key":"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwtX8JRYMoAXTbkU7EXJYKX7g4Mf0\nZ3WUuuGzlfyiEiS5UseXT6l1t1ZbMgzsg5IVA0TB7+/w6eyTlgnz/HXONw==\n-----END PUBLIC KEY-----"}]
1
+ [
2
+ {
3
+ "id": 0,
4
+ "version": "0.0",
5
+ "name": "RFID Empty",
6
+ "tag": "TIGER_TAG_UNINITIALIZED",
7
+ "public_key": ""
8
+ },
9
+ {
10
+ "id": 1542820452,
11
+ "version": "1.0",
12
+ "name": "TigerTag",
13
+ "tag": "TIGER_TAG_V1.0",
14
+ "public_key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwtX8JRYMoAXTbkU7EXJYKX7g4Mf0\nZ3WUuuGzlfyiEiS5UseXT6l1t1ZbMgzsg5IVA0TB7+/w6eyTlgnz/HXONw==\n-----END PUBLIC KEY-----"
15
+ },
16
+ {
17
+ "id": 1816240865,
18
+ "version": "1.0",
19
+ "name": "TigerTag Init",
20
+ "tag": "TIGER_TAG_INIT",
21
+ "public_key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwtX8JRYMoAXTbkU7EXJYKX7g4Mf0\nZ3WUuuGzlfyiEiS5UseXT6l1t1ZbMgzsg5IVA0TB7+/w6eyTlgnz/HXONw==\n-----END PUBLIC KEY-----"
22
+ },
23
+ {
24
+ "id": 3155151767,
25
+ "version": "1.0",
26
+ "name": "TigerTag+",
27
+ "tag": "TIGER_TAG_PLUS_V1.0",
28
+ "public_key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwtX8JRYMoAXTbkU7EXJYKX7g4Mf0\nZ3WUuuGzlfyiEiS5UseXT6l1t1ZbMgzsg5IVA0TB7+/w6eyTlgnz/HXONw==\n-----END PUBLIC KEY-----"
29
+ }
30
+ ]
@@ -1 +1 @@
1
- {"versions":1763073059935,"types":1777884684291,"brands":1782652927786,"filament_diameters":1777895560487,"filament_materials":1781878743554,"aspects":1779219283884,"measure_units":1777896731691}
1
+ {"versions":1788636538646,"types":1783889853153,"brands":1790349246723,"filament_diameters":1777895560487,"filament_materials":1789508548480,"aspects":1785250220651,"measure_units":1777896731691}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tigertag",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "JavaScript SDK for TigerTag RFID material identification",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -33,7 +33,7 @@
33
33
  ],
34
34
  "author": "TigerTag <tigertag@tigertag.io> (https://tigertag.io)",
35
35
  "license": "Apache-2.0",
36
- "homepage": "https://tigertag.io",
36
+ "homepage": "https://tigersystem.io",
37
37
  "repository": {
38
38
  "type": "git",
39
39
  "url": "git+https://github.com/TigerTag-Project/TigerTag-SDK-JS.git"
@@ -53,5 +53,14 @@
53
53
  "optionalDependencies": {
54
54
  "nfc-pcsc": "^0.8.1",
55
55
  "ws": "^8.20.1"
56
+ },
57
+ "jest": {
58
+ "setupFiles": [
59
+ "<rootDir>/test/setup-env.js"
60
+ ],
61
+ "testPathIgnorePatterns": [
62
+ "/node_modules/",
63
+ "<rootDir>/test/setup-env.js"
64
+ ]
56
65
  }
57
66
  }
package/src/catalog.js ADDED
@@ -0,0 +1,285 @@
1
+ 'use strict';
2
+
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ //
5
+ // TigerTag SDK
6
+ // Copyright (c) 2025-2026 TigerTag Corp.
7
+ //
8
+ // Licensed under the Apache License, Version 2.0 (the "License");
9
+ // you may not use this file except in compliance with the License.
10
+ // You may obtain a copy of the License at
11
+ //
12
+ // http://www.apache.org/licenses/LICENSE-2.0
13
+ //
14
+ // Unless required by applicable law or agreed to in writing, software
15
+ // distributed under the License is distributed on an "AS IS" BASIS,
16
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ // See the License for the specific language governing permissions and
18
+ // limitations under the License.
19
+ //
20
+ // Implementing the TigerTag protocol requires no licence and no payment.
21
+ // https://github.com/TigerTag-Project/TigerTag-RFID-Guide/blob/main/LICENSING.md
22
+
23
+ /**
24
+ * TigerTag product catalogue (id_catalog.json) — loader, cache and lookup.
25
+ *
26
+ * The official catalogue lists every TigerTag+ product with the exact data to
27
+ * write on the chip (`RFID_Data`) plus display metadata (title, brand, SKU,
28
+ * barcode, image). A gzip copy ships in the npm package
29
+ * (database/id_catalog.json.gz, refreshed at every release) so it works offline;
30
+ * a fresher copy is downloaded into the data dir (shared with the reference
31
+ * tables) once the local copy is older than `maxAge` (1 day).
32
+ *
33
+ * @example
34
+ * const { loadCatalog, catalogEntry, TigerTag } = require('tigertag');
35
+ * const tag = await TigerTag.fromCatalog(3527039449); // ready to burn
36
+ * const entry = await catalogEntry(3527039449); // title, brand, sku, img_src…
37
+ */
38
+
39
+ const fs = require('fs');
40
+ const path = require('path');
41
+ const zlib = require('zlib');
42
+ const { defaultDataDir, resolveOffline, BUNDLED_DIR } = require('./datadir');
43
+
44
+ /** Official catalogue URL (GitHub raw, CORS *). */
45
+ const CATALOG_URL =
46
+ 'https://raw.githubusercontent.com/TigerTag-Project/TigerTag-RFID-Guide/refs/heads/main/database/id_catalog.json';
47
+
48
+ /** Default cache lifetime: 1 day (24 h) — the catalogue changes every day. */
49
+ const DEFAULT_MAX_AGE_MS = 24 * 3600 * 1000;
50
+
51
+ const _CACHE_FILE = 'id_catalog.json';
52
+ const _META_FILE = 'id_catalog.meta.json';
53
+ const _BUNDLED_FILE = 'id_catalog.json.gz';
54
+ const _memo = new Map(); // key → Promise<Map> (one parse per process)
55
+
56
+ /**
57
+ * Default folder for the downloaded catalogue: the SDK data dir
58
+ * (TIGERTAG_DATA_DIR, TIGERTAG_CACHE_DIR, or the per-user cache folder).
59
+ *
60
+ * @returns {string}
61
+ */
62
+ function defaultCacheDir() { return defaultDataDir(); }
63
+
64
+ function _toMap(list) {
65
+ if (!Array.isArray(list)) throw new Error('TigerTag catalogue: expected a JSON list of products');
66
+ const map = new Map();
67
+ for (const entry of list) {
68
+ if (entry && entry.id != null) map.set(Number(entry.id), entry);
69
+ }
70
+ return map;
71
+ }
72
+
73
+ function _readMeta(dir) {
74
+ try { return JSON.parse(fs.readFileSync(path.join(dir, _META_FILE), 'utf8')); } catch { return null; }
75
+ }
76
+
77
+ function _writeMeta(dir, meta) {
78
+ try { fs.writeFileSync(path.join(dir, _META_FILE), JSON.stringify(meta, null, 2)); } catch { /* read-only */ }
79
+ }
80
+
81
+ // The local copies: downloaded (data dir) and bundled (.gz in the package), with their dates
82
+ function _localCopies(dataDir, bundledDir) {
83
+ const out = { downloaded: null, bundled: null };
84
+ const file = path.join(dataDir, _CACHE_FILE);
85
+ try {
86
+ const st = fs.statSync(file);
87
+ const meta = _readMeta(dataDir) || {};
88
+ out.downloaded = {
89
+ source: 'downloaded', file, meta,
90
+ fetchedAt: meta.fetchedAt || st.mtime.toISOString(),
91
+ checkedMs: st.mtimeMs,
92
+ read: () => fs.readFileSync(file, 'utf8'),
93
+ };
94
+ } catch { /* none */ }
95
+ if (bundledDir) {
96
+ const gz = path.join(bundledDir, _BUNDLED_FILE);
97
+ try {
98
+ const st = fs.statSync(gz);
99
+ const meta = _readMeta(bundledDir) || {};
100
+ const fetchedAt = meta.fetchedAt || st.mtime.toISOString();
101
+ out.bundled = {
102
+ source: 'bundled', file: gz, meta, fetchedAt,
103
+ checkedMs: Date.parse(fetchedAt),
104
+ read: () => zlib.gunzipSync(fs.readFileSync(gz)).toString('utf8'),
105
+ };
106
+ } catch { /* none */ }
107
+ }
108
+ return out;
109
+ }
110
+
111
+ function _newest(copies) {
112
+ const list = [copies.downloaded, copies.bundled].filter(Boolean);
113
+ if (!list.length) return null;
114
+ return list.reduce((a, b) => (Date.parse(b.fetchedAt) > Date.parse(a.fetchedAt) ? b : a));
115
+ }
116
+
117
+ // Conditional GET: ETag / Last-Modified from the previous download → 304 skips the 12 MB body
118
+ async function _download(url, fetchImpl, timeoutMs, prev) {
119
+ const f = fetchImpl || globalThis.fetch;
120
+ if (typeof f !== 'function') throw new Error('fetch() is not available (Node.js 18+ required)');
121
+ const headers = {};
122
+ if (prev && prev.etag) headers['If-None-Match'] = prev.etag;
123
+ if (prev && prev.lastModified) headers['If-Modified-Since'] = prev.lastModified;
124
+ const controller = new AbortController();
125
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
126
+ try {
127
+ const resp = await f(url, { signal: controller.signal, headers });
128
+ const h = (name) => (resp.headers && typeof resp.headers.get === 'function' ? resp.headers.get(name) : null) || null;
129
+ if (resp.status === 304) return { notModified: true, etag: h('etag') || prev.etag, lastModified: h('last-modified') || prev.lastModified };
130
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
131
+ return { text: await resp.text(), etag: h('etag'), lastModified: h('last-modified') };
132
+ } finally {
133
+ clearTimeout(timer);
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Load the TigerTag catalogue as a Map productId → entry.
139
+ *
140
+ * Copies: the downloaded one in the data dir and the gzip one bundled in the
141
+ * package; the newest is used. When it is older than `maxAge` (or `force`), a
142
+ * conditional download checks for a new version (an unchanged catalogue answers
143
+ * 304 — nothing is re-downloaded). Offline (`offline: true` or
144
+ * TIGERTAG_OFFLINE=1), or when the download fails, the newest local copy is used
145
+ * with zero / no further network calls. Throws only when there is no copy at all.
146
+ *
147
+ * @param {object} [options]
148
+ * @param {string} [options.url=CATALOG_URL] - Catalogue URL.
149
+ * @param {string} [options.dataDir] - Folder for the downloaded copy (default: the SDK data dir).
150
+ * @param {string} [options.cacheDir] - Alias of dataDir.
151
+ * @param {boolean} [options.offline] - true = zero network calls (default: TIGERTAG_OFFLINE env).
152
+ * @param {number} [options.maxAge=1 day] - Max age in ms of the local copy before checking for a new version.
153
+ * @param {boolean} [options.force=false] - Check for a new version now (ignore memory and age).
154
+ * @param {number} [options.timeout=60000] - Download timeout in ms.
155
+ * @param {Function} [options.fetch] - fetch implementation (default: global fetch).
156
+ * @param {string|null} [options.bundledDir] - Folder of the bundled .gz (default: the package database/;
157
+ * null = ignore the bundled copy).
158
+ * @returns {Promise<Map<number, object>>}
159
+ */
160
+ function loadCatalog({
161
+ url = CATALOG_URL, dataDir, cacheDir, offline, maxAge = DEFAULT_MAX_AGE_MS,
162
+ force = false, timeout = 60000, fetch: fetchImpl = null, bundledDir = BUNDLED_DIR,
163
+ } = {}) {
164
+ const dir = path.resolve(dataDir || cacheDir || defaultDataDir());
165
+ const isOffline = resolveOffline(offline);
166
+ const key = `${url}|${dir}|${bundledDir}|${isOffline}`;
167
+ if (!force && _memo.has(key)) return _memo.get(key);
168
+ const p = (async () => {
169
+ const copies = _localCopies(dir, bundledDir);
170
+ const newest = _newest(copies);
171
+ const readNewest = () => _toMap(JSON.parse(newest.read()));
172
+ if (isOffline) {
173
+ if (newest) return readNewest();
174
+ throw new Error(`TigerTag catalogue unavailable offline: no local copy in ${dir} and no bundled copy.`);
175
+ }
176
+ const lastChecked = Math.max(copies.downloaded ? copies.downloaded.checkedMs : 0, copies.bundled ? copies.bundled.checkedMs : 0);
177
+ if (!force && newest && maxAge > 0 && Date.now() - lastChecked < maxAge) return readNewest();
178
+
179
+ const prevMeta = copies.downloaded && copies.downloaded.meta.url === url ? copies.downloaded.meta
180
+ : copies.bundled && (!copies.downloaded || newest === copies.bundled) ? copies.bundled.meta : null;
181
+ let res;
182
+ try {
183
+ res = await _download(url, fetchImpl, timeout, prevMeta);
184
+ } catch (err) {
185
+ if (newest) return readNewest(); // stale but usable
186
+ throw new Error(
187
+ `TigerTag catalogue unavailable: could not download ${url} (${err.message}) `
188
+ + `and there is no local copy in ${dir}. An internet connection is needed the first time.`,
189
+ );
190
+ }
191
+ const now = new Date().toISOString();
192
+ const file = path.join(dir, _CACHE_FILE);
193
+ let text = res.text;
194
+ let fetchedAt = now;
195
+ if (res.notModified) {
196
+ // Unchanged: keep (or materialise) the copy the ETag came from, record the check
197
+ const from = prevMeta === (copies.downloaded && copies.downloaded.meta) ? copies.downloaded : copies.bundled;
198
+ text = from.read();
199
+ fetchedAt = from.fetchedAt;
200
+ }
201
+ const map = _toMap(JSON.parse(text));
202
+ try {
203
+ fs.mkdirSync(dir, { recursive: true });
204
+ if (!(res.notModified && copies.downloaded && prevMeta === copies.downloaded.meta)) fs.writeFileSync(file, text);
205
+ else fs.utimesSync(file, new Date(), new Date());
206
+ _writeMeta(dir, {
207
+ url, count: map.size, fetchedAt, checkedAt: now,
208
+ etag: res.etag || null, lastModified: res.lastModified || null, bytes: Buffer.byteLength(text),
209
+ });
210
+ } catch { /* read-only data dir: keep the in-memory copy */ }
211
+ return map;
212
+ })();
213
+ _memo.set(key, p);
214
+ p.catch(() => _memo.delete(key)); // a failed load is retried next time
215
+ return p;
216
+ }
217
+
218
+ /**
219
+ * Check for a new catalogue version now (conditional download) and reload it.
220
+ * Same options as loadCatalog(), with `force: true`.
221
+ *
222
+ * @param {object} [options]
223
+ * @returns {Promise<Map<number, object>>}
224
+ */
225
+ function refreshCatalog(options = {}) {
226
+ return loadCatalog({ ...options, force: true });
227
+ }
228
+
229
+ /**
230
+ * Which catalogue copy is used, without loading it.
231
+ *
232
+ * @param {object} [options]
233
+ * @param {string} [options.dataDir] - Data dir (default: the SDK data dir). `cacheDir` is an alias.
234
+ * @param {string|null} [options.bundledDir] - Bundled folder (default: package database/; null = ignore).
235
+ * @returns {{ downloaded: boolean, source: 'downloaded'|'bundled'|null, count: number|null,
236
+ * fetchedAt: string|null, checkedAt: string|null, url: string|null, etag: string|null,
237
+ * lastModified: string|null, cacheFile: string, bundled: object|null }}
238
+ * `source` = the copy in use (the newest), `fetchedAt` = when that copy was downloaded,
239
+ * `checkedAt` = last check for a new version, `downloaded` = a copy exists in the data dir.
240
+ */
241
+ function catalogInfo({ dataDir, cacheDir, bundledDir = BUNDLED_DIR } = {}) {
242
+ const dir = path.resolve(dataDir || cacheDir || defaultDataDir());
243
+ const copies = _localCopies(dir, bundledDir);
244
+ const newest = _newest(copies);
245
+ const cacheFile = path.join(dir, _CACHE_FILE);
246
+ const b = copies.bundled ? { file: copies.bundled.file, fetchedAt: copies.bundled.fetchedAt, count: copies.bundled.meta.count ?? null } : null;
247
+ if (!newest) {
248
+ return { downloaded: false, source: null, count: null, fetchedAt: null, checkedAt: null, url: null, etag: null, lastModified: null, cacheFile, bundled: b };
249
+ }
250
+ const m = newest.meta || {};
251
+ const dm = copies.downloaded ? copies.downloaded.meta : {};
252
+ return {
253
+ downloaded: !!copies.downloaded,
254
+ source: newest.source,
255
+ count: m.count ?? null,
256
+ fetchedAt: newest.fetchedAt,
257
+ checkedAt: dm.checkedAt || (copies.downloaded ? new Date(copies.downloaded.checkedMs).toISOString() : null),
258
+ url: m.url || null,
259
+ etag: m.etag || null,
260
+ lastModified: m.lastModified || null,
261
+ cacheFile,
262
+ bundled: b,
263
+ };
264
+ }
265
+
266
+ /**
267
+ * Look up one catalogue product.
268
+ *
269
+ * @param {number} productId - TigerTag+ product ID.
270
+ * @param {object} [options]
271
+ * @param {Map<number, object>} [options.catalog] - Already loaded catalogue; otherwise loadCatalog(options).
272
+ * @returns {Promise<object|null>} The catalogue entry (title, brand, sku, barcode, img_src, RFID_Data…) or null.
273
+ */
274
+ async function catalogEntry(productId, options = {}) {
275
+ const catalog = options.catalog || await loadCatalog(options);
276
+ return catalog.get(Number(productId)) || null;
277
+ }
278
+
279
+ /** Clear the in-memory catalogue copies (the disk cache is kept). */
280
+ function clearCatalogMemo() { _memo.clear(); }
281
+
282
+ module.exports = {
283
+ CATALOG_URL, DEFAULT_MAX_AGE_MS, defaultCacheDir,
284
+ loadCatalog, refreshCatalog, catalogInfo, catalogEntry, clearCatalogMemo,
285
+ };
package/src/datadir.js ADDED
@@ -0,0 +1,65 @@
1
+ 'use strict';
2
+
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ //
5
+ // TigerTag SDK
6
+ // Copyright (c) 2025-2026 TigerTag Corp.
7
+ //
8
+ // Licensed under the Apache License, Version 2.0 (the "License");
9
+ // you may not use this file except in compliance with the License.
10
+ // You may obtain a copy of the License at
11
+ //
12
+ // http://www.apache.org/licenses/LICENSE-2.0
13
+ //
14
+ // Unless required by applicable law or agreed to in writing, software
15
+ // distributed under the License is distributed on an "AS IS" BASIS,
16
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ // See the License for the specific language governing permissions and
18
+ // limitations under the License.
19
+ //
20
+ // Implementing the TigerTag protocol requires no licence and no payment.
21
+ // https://github.com/TigerTag-Project/TigerTag-RFID-Guide/blob/main/LICENSING.md
22
+
23
+ /**
24
+ * Where the SDK keeps downloaded reference data (tables + catalogue), and the
25
+ * global offline switch.
26
+ */
27
+
28
+ const os = require('os');
29
+ const path = require('path');
30
+
31
+ /**
32
+ * Folder holding the downloaded fresh copies of the reference tables and the
33
+ * catalogue. Precedence: `TIGERTAG_DATA_DIR`, then `TIGERTAG_CACHE_DIR`, then the
34
+ * platform cache folder (`~/Library/Caches`, `%LOCALAPPDATA%`, `$XDG_CACHE_HOME`
35
+ * or `~/.cache`) + `/tigertag`.
36
+ *
37
+ * @returns {string}
38
+ */
39
+ function defaultDataDir() {
40
+ if (process.env.TIGERTAG_DATA_DIR) return process.env.TIGERTAG_DATA_DIR;
41
+ if (process.env.TIGERTAG_CACHE_DIR) return process.env.TIGERTAG_CACHE_DIR;
42
+ const home = os.homedir();
43
+ let base;
44
+ if (process.platform === 'darwin') base = path.join(home, 'Library', 'Caches');
45
+ else if (process.platform === 'win32') base = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
46
+ else base = process.env.XDG_CACHE_HOME || path.join(home, '.cache');
47
+ return path.join(base, 'tigertag');
48
+ }
49
+
50
+ /**
51
+ * Resolve the offline switch: an explicit option wins, otherwise the
52
+ * `TIGERTAG_OFFLINE` environment variable (`1`, `true`, `yes`, `on`).
53
+ *
54
+ * @param {boolean} [option]
55
+ * @returns {boolean}
56
+ */
57
+ function resolveOffline(option) {
58
+ if (option === true || option === false) return option;
59
+ return /^(1|true|yes|on)$/i.test(String(process.env.TIGERTAG_OFFLINE || ''));
60
+ }
61
+
62
+ /** Folder of the copies shipped inside the npm package. */
63
+ const BUNDLED_DIR = path.join(__dirname, '..', 'database');
64
+
65
+ module.exports = { defaultDataDir, resolveOffline, BUNDLED_DIR };