tigertag 1.0.6 → 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.
package/src/signature.js CHANGED
@@ -1,11 +1,24 @@
1
1
  'use strict';
2
2
 
3
- // TigerTag RFID Guide
4
- // Copyright (C) 2025 TigerTag
3
+ // SPDX-License-Identifier: Apache-2.0
5
4
  //
6
- // This program is free software: you can redistribute it and/or modify
7
- // it under the terms of the GNU General Public License as published by
8
- // the Free Software Foundation, either version 3 of the License.
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
9
22
 
10
23
  /**
11
24
  * ECDSA-P256 signature verification for TigerTag chips.
@@ -52,7 +65,7 @@ class SignatureResult {
52
65
  }
53
66
 
54
67
  toString() {
55
- const base = SignatureResult._ICONS[this.status] || `? ${this.status}`;
68
+ const base = SignatureResult._LABELS[this.status] || `? ${this.status}`;
56
69
  return this.detail ? `${base} ${this.detail}` : base;
57
70
  }
58
71
 
@@ -72,13 +85,13 @@ SignatureResult.NO_CRYPTO = 'no_crypto';
72
85
  SignatureResult.NO_KEY = 'no_key';
73
86
  SignatureResult.NO_UID = 'no_uid';
74
87
 
75
- SignatureResult._ICONS = {
76
- valid: '✅ VALID',
77
- invalid: '❌ INVALID',
78
- unsigned: '⬜ NOT SIGNED',
79
- no_crypto: '⚠️ crypto not available',
80
- no_key: '⚠️ public key not found in id_version.json',
81
- no_uid: '⚠️ UID unavailable — provide a full 180-byte chip dump',
88
+ SignatureResult._LABELS = {
89
+ valid: 'VALID',
90
+ invalid: 'INVALID',
91
+ unsigned: 'NOT SIGNED',
92
+ no_crypto: 'NO CRYPTO — crypto not available',
93
+ no_key: 'NO PUBLIC KEY — public key not found in id_version.json',
94
+ no_uid: 'NO UID — UID unavailable, provide a full 180-byte chip dump',
82
95
  };
83
96
 
84
97
  /**
package/src/tag.js CHANGED
@@ -1,17 +1,30 @@
1
1
  'use strict';
2
2
 
3
- // TigerTag RFID Guide
4
- // Copyright (C) 2025 TigerTag
3
+ // SPDX-License-Identifier: Apache-2.0
5
4
  //
6
- // This program is free software: you can redistribute it and/or modify
7
- // it under the terms of the GNU General Public License as published by
8
- // the Free Software Foundation, either version 3 of the License.
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
9
22
 
10
23
  /**
11
24
  * TigerTag class — binary parsing, serialization, CRUD, and cloud sync.
12
25
  */
13
26
 
14
- const { TigerTagDB, syncDatabases, _BUNDLED_DB_PATH } = require('./db');
27
+ const { TigerTagDB, syncDatabases } = require('./db');
15
28
  const { SignatureResult, verifySignature } = require('./signature');
16
29
 
17
30
  // ── Protocol constants ──────────────────────────────────────────────────────
@@ -48,6 +61,7 @@ const _PATCHABLE_FIELDS = new Set([
48
61
  'measure', 'idUnit', 'measureAvailable',
49
62
  'nozzleTempMin', 'nozzleTempMax', 'dryTemp', 'dryTime', 'bedTempMin', 'bedTempMax',
50
63
  'timestamp', 'customMessage', 'tdRaw',
64
+ 'tagInfo', 'tagCount', 'tagIndex',
51
65
  ]);
52
66
 
53
67
  // ── ApiDiff ─────────────────────────────────────────────────────────────────
@@ -142,6 +156,11 @@ class TigerTag {
142
156
  // HueForge
143
157
  this.tdRaw = fields.tdRaw || 0;
144
158
 
159
+ // Tag index / tag count (protocol v2.2) — u8 at payload offset +39.
160
+ // High nibble = which tag this one is, low nibble = tags on the item (0 = unknown),
161
+ // so the hex reads "index/count": 0x12 = tag 1 of 2.
162
+ this.tagInfo = fields.tagInfo || 0;
163
+
145
164
  // Signature (optional)
146
165
  this.signatureR = fields.signatureR instanceof Buffer ? fields.signatureR : Buffer.alloc(32);
147
166
  this.signatureS = fields.signatureS instanceof Buffer ? fields.signatureS : Buffer.alloc(32);
@@ -178,6 +197,21 @@ class TigerTag {
178
197
  /** HueForge TD as float. 0.0 = undefined, valid range 0.1–100.0. */
179
198
  get tdValue() { return this.tdRaw / 10.0; }
180
199
 
200
+ /**
201
+ * Number of TigerTags on the tagged item — a filament spool, a resin bottle… as given
202
+ * by idType (low nibble of tagInfo).
203
+ * 0 = unknown, 1 = single tag, 2 = twin tag, … up to 15.
204
+ * @returns {number}
205
+ */
206
+ get tagCount() { return this.tagInfo & 0x0F; }
207
+
208
+ /**
209
+ * Which of those tags this one is, from 1 (high nibble of tagInfo).
210
+ * 0 = unknown.
211
+ * @returns {number}
212
+ */
213
+ get tagIndex() { return this.tagInfo >> 4; }
214
+
181
215
  /** Manufacturing timestamp as UTC Date. */
182
216
  get manufacturingDate() {
183
217
  return new Date(_TIGERTAG_EPOCH_MS + this.timestamp * 1000);
@@ -366,6 +400,7 @@ class TigerTag {
366
400
  color2R: u8(36),
367
401
  color2G: u8(37),
368
402
  color2B: u8(38),
403
+ tagInfo: u8(39),
369
404
  color3R: u8(40),
370
405
  color3G: u8(41),
371
406
  color3B: u8(42),
@@ -418,6 +453,8 @@ class TigerTag {
418
453
  * @param {number} [options.timestamp] - Seconds since 2000-01-01 UTC. Defaults to now.
419
454
  * @param {string} [options.customMessage='']
420
455
  * @param {number} [options.tdRaw=0]
456
+ * @param {number} [options.tagCount=0] - TigerTags on the item (0 = unknown, 0–15).
457
+ * @param {number} [options.tagIndex=0] - Which tag this one is, from 1 (0 = unknown, 0–15).
421
458
  * @param {TigerTagDB} [options.db]
422
459
  * @returns {TigerTag}
423
460
  */
@@ -444,6 +481,8 @@ class TigerTag {
444
481
  timestamp = null,
445
482
  customMessage = '',
446
483
  tdRaw = 0,
484
+ tagCount = 0,
485
+ tagIndex = 0,
447
486
  measureAvailable = null,
448
487
  db = null,
449
488
  } = {}) {
@@ -492,6 +531,7 @@ class TigerTag {
492
531
  timestamp,
493
532
  customMessage,
494
533
  tdRaw,
534
+ tagInfo: TigerTag._packTagInfo(tagCount, tagIndex),
495
535
  uid,
496
536
  _db: db,
497
537
  });
@@ -521,6 +561,7 @@ class TigerTag {
521
561
  timestamp: ts,
522
562
  customMessage: '',
523
563
  tdRaw: 0,
564
+ tagInfo: 0,
524
565
  uid,
525
566
  });
526
567
  }
@@ -577,7 +618,7 @@ class TigerTag {
577
618
  buf[o++] = this.color2R & 0xFF;
578
619
  buf[o++] = this.color2G & 0xFF;
579
620
  buf[o++] = this.color2B & 0xFF;
580
- buf[o++] = 0x00;
621
+ buf[o++] = this.tagInfo & 0xFF;
581
622
 
582
623
  buf[o++] = this.color3R & 0xFF;
583
624
  buf[o++] = this.color3G & 0xFF;
@@ -618,8 +659,14 @@ class TigerTag {
618
659
  * Protected fields (idTigertag, idProduct, uid, signatureR, signatureS)
619
660
  * cannot be modified — they are covered by the ECDSA signature.
620
661
  *
662
+ * tagCount / tagIndex are accepted as shortcuts and re-encoded into
663
+ * tagInfo; the nibble that is not supplied keeps its current value.
664
+ * tagInfo is not covered by the signature, so patching it never
665
+ * invalidates a signed tag.
666
+ *
621
667
  * @param {object} kwargs - Field names (camelCase) and their new values.
622
668
  * @returns {TigerTag}
669
+ * @throws {RangeError} When tagCount / tagIndex is not an integer in 0–15.
623
670
  */
624
671
  patch(kwargs) {
625
672
  const protected_ = Object.keys(kwargs).filter((k) => _PROTECTED_FIELDS.has(k));
@@ -636,7 +683,38 @@ class TigerTag {
636
683
  + `Valid patchable fields: ${[..._PATCHABLE_FIELDS].sort().join(', ')}`,
637
684
  );
638
685
  }
639
- return new TigerTag(Object.assign({}, this, kwargs));
686
+ const updates = Object.assign({}, kwargs);
687
+ if ('tagCount' in updates || 'tagIndex' in updates) {
688
+ const base = 'tagInfo' in updates ? (updates.tagInfo & 0xFF) : this.tagInfo;
689
+ const count = 'tagCount' in updates ? updates.tagCount : base & 0x0F;
690
+ const index = 'tagIndex' in updates ? updates.tagIndex : base >> 4;
691
+ updates.tagInfo = TigerTag._packTagInfo(count, index);
692
+ delete updates.tagCount;
693
+ delete updates.tagIndex;
694
+ }
695
+ return new TigerTag(Object.assign({}, this, updates));
696
+ }
697
+
698
+ /**
699
+ * Pack a tag count and tag index into the tagInfo byte:
700
+ * (tagIndex << 4) | tagCount — the hex reads "index/count" (0x12 = tag 1 of 2).
701
+ *
702
+ * @param {number} tagCount - TigerTags on the item (0 = unknown, 0–15).
703
+ * @param {number} tagIndex - Which tag this one is, from 1 (0 = unknown, 0–15).
704
+ * @returns {number} u8 tagInfo value.
705
+ * @throws {RangeError} When either value is not an integer in 0–15
706
+ * (it would not fit in its 4-bit nibble).
707
+ * @private
708
+ */
709
+ static _packTagInfo(tagCount, tagIndex) {
710
+ const count = tagCount || 0;
711
+ const index = tagIndex || 0;
712
+ for (const [name, v] of [['tagCount', count], ['tagIndex', index]]) {
713
+ if (!Number.isInteger(v) || v < 0 || v > 15) {
714
+ throw new RangeError(`${name} must be an integer in 0–15, got ${v}`);
715
+ }
716
+ }
717
+ return (index << 4) | count;
640
718
  }
641
719
 
642
720
  // ── Validation ──────────────────────────────────────────────────────────
@@ -662,6 +740,25 @@ class TigerTag {
662
740
  `TD HueForge out of range: ${this.tdRaw} (valid: 10–1000 or 0=undefined)`,
663
741
  );
664
742
  }
743
+ const tagCount = this.tagCount;
744
+ const tagIndex = this.tagIndex;
745
+ if (!Number.isInteger(this.tagInfo) || this.tagInfo < 0 || this.tagInfo > 0xFF
746
+ || tagCount > 15 || tagIndex > 15) {
747
+ warnings.push(
748
+ `Tag index/count out of range: tag_info=${this.tagInfo} `
749
+ + `(count and index must each be 0–15, tag_info must be 0x00–0xFF)`,
750
+ );
751
+ }
752
+ if (tagCount > 0 && tagIndex > tagCount) {
753
+ warnings.push(
754
+ `Tag index (${tagIndex}) > tag count (${tagCount}) (valid index: 1–count or 0=unknown)`,
755
+ );
756
+ }
757
+ if (tagCount === 1 && tagIndex > 1) {
758
+ warnings.push(
759
+ `Single-tag item (count=1) but tag index is ${tagIndex} (expected 1 or 0=unknown)`,
760
+ );
761
+ }
665
762
  if (this.measure > 0 && this.measureAvailable > this.measure) {
666
763
  warnings.push(
667
764
  `measure_available (${this.measureAvailable}) > initial measure (${this.measure})`,
@@ -936,16 +1033,24 @@ class TigerTag {
936
1033
  }
937
1034
 
938
1035
  /**
939
- * Download or update reference databases.
1036
+ * Check for new reference tables now and reload this tag's database.
940
1037
  *
941
- * @param {string} [dbPath] - Target folder. Defaults to the bundled database directory.
1038
+ * Without dbPath: updates the data dir (see TigerTagDB.update()). With dbPath:
1039
+ * downloads into that custom folder, which is then used exclusively.
1040
+ *
1041
+ * @param {string} [dbPath] - Custom target folder. Default: the data dir.
942
1042
  * @param {boolean} [force=false] - Re-download all files even if up to date.
943
1043
  * @returns {Promise<string[]>} List of filenames that were downloaded/updated.
944
1044
  */
945
1045
  async syncDb(dbPath = null, force = false) {
946
- const targetPath = dbPath || _BUNDLED_DB_PATH;
947
- const updated = await syncDatabases(targetPath, { force, verbose: true });
948
- this._db = new TigerTagDB({ dbPath: targetPath });
1046
+ if (dbPath) {
1047
+ const updated = await syncDatabases(dbPath, { force, verbose: true });
1048
+ this._db = new TigerTagDB({ dbPath });
1049
+ return updated;
1050
+ }
1051
+ const db = new TigerTagDB({ autoUpdate: false });
1052
+ const updated = await db.update({ force });
1053
+ this._db = db;
949
1054
  return updated;
950
1055
  }
951
1056
 
@@ -1073,6 +1178,7 @@ class TigerTag {
1073
1178
  color_g3: numColors >= 3 ? this.color3G : 0,
1074
1179
  color_b3: numColors >= 3 ? this.color3B : 0,
1075
1180
  td_raw: this.tdRaw,
1181
+ tag_info: this.tagInfo,
1076
1182
  message: this.customMessage,
1077
1183
  measure_available: this.measureAvailable,
1078
1184
  ...TigerTag._baseUnitFields(this.measure, this.measureAvailable, this.idUnit),
@@ -1126,6 +1232,7 @@ class TigerTag {
1126
1232
  bed_max: 'bedTempMax',
1127
1233
  timestamp: 'timestamp',
1128
1234
  td_raw: 'tdRaw',
1235
+ tag_info: 'tagInfo',
1129
1236
  message: 'customMessage',
1130
1237
  };
1131
1238
  const kwargs = {};
@@ -1180,6 +1287,8 @@ class TigerTag {
1180
1287
  timestamp: raw.timestamp ?? null,
1181
1288
  customMessage: raw.message ?? '',
1182
1289
  tdRaw: raw.td_raw ?? 0,
1290
+ tagCount: (raw.tag_info ?? 0) & 0x0F,
1291
+ tagIndex: (raw.tag_info ?? 0) >> 4,
1183
1292
  db,
1184
1293
  });
1185
1294
  }
@@ -1230,6 +1339,101 @@ class TigerTag {
1230
1339
  });
1231
1340
  }
1232
1341
 
1342
+ /**
1343
+ * Build a ready-to-burn TigerTag+ from one entry of the official catalogue
1344
+ * (id_catalog.json). Pure and synchronous — see fromCatalog() to look the
1345
+ * product up by ID.
1346
+ *
1347
+ * RFID_Data mapping (same as the cloud document, see fromCloudDoc()):
1348
+ * data1 = idDiameter, data2/data3 = nozzle min/max, data4/data5 = dry temp/time,
1349
+ * data6/data7 = bed min/max; id_aspect2 null → 0 (none); missing values → 0.
1350
+ * Colours 2 and 3 come from RFID_Data color_r2…b3 when present, otherwise from
1351
+ * color_info.colors[1] / [2] ("#RRGGBBAA").
1352
+ *
1353
+ * @param {object} entry - Catalogue entry ({ id, RFID_Data, title, brand, color_info, … }).
1354
+ * @param {object} [options]
1355
+ * @param {Buffer} [options.uid] - 7-byte chip UID.
1356
+ * @param {number} [options.tagCount=0] - TigerTags on the item (twin tag: 2).
1357
+ * @param {number} [options.tagIndex=0] - Which tag this one is, from 1.
1358
+ * @param {number} [options.timestamp] - Seconds since 2000-01-01 UTC (default: now). Use the
1359
+ * same value on every tag of a twin tag.
1360
+ * @param {TigerTagDB} [options.db]
1361
+ * @returns {TigerTag} A TigerTag+ (idTigertag = ID_TIGERTAG_PLUS, idProduct = entry.id).
1362
+ * @throws {Error} When the entry has no RFID_Data (it cannot be written to a tag).
1363
+ */
1364
+ static fromCatalogEntry(entry, {
1365
+ uid = null, tagCount = 0, tagIndex = 0, timestamp = null, db = null,
1366
+ } = {}) {
1367
+ const r = entry && entry.RFID_Data;
1368
+ if (!r) {
1369
+ throw new Error(
1370
+ `Catalogue product ${entry && entry.id} ("${(entry && entry.title) || '?'}") has no RFID_Data `
1371
+ + '— it cannot be written to a tag.',
1372
+ );
1373
+ }
1374
+ const n = (v) => (v == null ? 0 : Number(v));
1375
+ const hex = (h) => {
1376
+ const m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i.exec(h || '');
1377
+ return m ? [parseInt(m[1], 16), parseInt(m[2], 16), parseInt(m[3], 16)] : [0, 0, 0];
1378
+ };
1379
+ const colors = (entry.color_info && entry.color_info.colors) || [];
1380
+ const c2 = r.color_r2 != null ? [n(r.color_r2), n(r.color_g2), n(r.color_b2)] : colors[1] ? hex(colors[1]) : [0, 0, 0];
1381
+ const c3 = r.color_r3 != null ? [n(r.color_r3), n(r.color_g3), n(r.color_b3)] : colors[2] ? hex(colors[2]) : [0, 0, 0];
1382
+ return TigerTag.create({
1383
+ productId: Number(entry.id),
1384
+ uid,
1385
+ idMaterial: n(r.id_material),
1386
+ idAspect1: n(r.id_aspect1),
1387
+ idAspect2: n(r.id_aspect2),
1388
+ idType: n(r.id_type),
1389
+ idDiameter: n(r.data1),
1390
+ idBrand: n(r.id_brand),
1391
+ color1R: n(r.color_r),
1392
+ color1G: n(r.color_g),
1393
+ color1B: n(r.color_b),
1394
+ color1A: r.color_a == null ? 255 : n(r.color_a),
1395
+ color2R: c2[0], color2G: c2[1], color2B: c2[2],
1396
+ color3R: c3[0], color3G: c3[1], color3B: c3[2],
1397
+ measure: n(r.measure),
1398
+ idUnit: n(r.id_unit),
1399
+ nozzleTempMin: n(r.data2),
1400
+ nozzleTempMax: n(r.data3),
1401
+ dryTemp: n(r.data4),
1402
+ dryTime: n(r.data5),
1403
+ bedTempMin: n(r.data6),
1404
+ bedTempMax: n(r.data7),
1405
+ timestamp,
1406
+ tagCount,
1407
+ tagIndex,
1408
+ db,
1409
+ });
1410
+ }
1411
+
1412
+ /**
1413
+ * Build a ready-to-burn TigerTag+ from a product ID of the official catalogue.
1414
+ * The catalogue is downloaded on first use and cached on disk (see loadCatalog()).
1415
+ * Use catalogEntry(productId) for the display metadata (title, brand, sku,
1416
+ * barcode, img_src).
1417
+ *
1418
+ * @param {number} productId - TigerTag+ product ID.
1419
+ * @param {object} [options] - fromCatalogEntry() options, plus:
1420
+ * @param {Map<number, object>} [options.catalog] - Already loaded catalogue (otherwise loaded with
1421
+ * the loadCatalog() options url / cacheDir / maxAge / force).
1422
+ * @returns {Promise<TigerTag>}
1423
+ * @throws {Error} When the ID is not in the catalogue, the product has no RFID_Data, or the
1424
+ * catalogue cannot be loaded (offline with no cache).
1425
+ */
1426
+ static async fromCatalog(productId, options = {}) {
1427
+ const { loadCatalog } = require('./catalog');
1428
+ const { catalog: given, uid, tagCount, tagIndex, timestamp, db, ...loadOpts } = options;
1429
+ const catalog = given || await loadCatalog(loadOpts);
1430
+ const entry = catalog.get(Number(productId));
1431
+ if (!entry) {
1432
+ throw new Error(`Product ID ${productId} is not in the TigerTag catalogue (${catalog.size} products).`);
1433
+ }
1434
+ return TigerTag.fromCatalogEntry(entry, { uid, tagCount, tagIndex, timestamp, db });
1435
+ }
1436
+
1233
1437
  /**
1234
1438
  * Apply a surgical patch using snake_case keys (toRawDict format).
1235
1439
  * Only the supplied keys are changed — all other fields are preserved.
@@ -1263,7 +1467,7 @@ class TigerTag {
1263
1467
  return {
1264
1468
  sdk: 'tigertag-sdk-js',
1265
1469
  sdk_mode: 'offline',
1266
- protocol: 'TigerTag Open Source v2.1',
1470
+ protocol: 'TigerTag Open Source v2.2',
1267
1471
  chip: 'NTAG213/215/216',
1268
1472
  uid: this.uidHex,
1269
1473
  version: {
@@ -1356,6 +1560,8 @@ class TigerTag {
1356
1560
  },
1357
1561
  manufacturing_date: this.manufacturingDate.toISOString(),
1358
1562
  twin_tag_pairing_id: this.timestamp,
1563
+ tag_count: this.tagCount || null,
1564
+ tag_index: this.tagIndex || null,
1359
1565
  custom_message: this.customMessage,
1360
1566
  authentication: {
1361
1567
  signed: this.isSigned,
@@ -1452,6 +1658,11 @@ class TigerTag {
1452
1658
  }
1453
1659
 
1454
1660
  if (this.tdRaw !== 0) parts.push(`HueForge TD: ${this.tdValue.toFixed(1)}.`);
1661
+ if (this.tagInfo !== 0) {
1662
+ // What the item is comes from idType (Filament, Resin, …); unknown → "item"
1663
+ const typeLabel = ((_db.type(this.idType) || {}).label || '').trim().toLowerCase();
1664
+ parts.push(`Tag ${this.tagIndex || '?'} of ${this.tagCount || '?'} on this ${typeLabel || 'item'}.`);
1665
+ }
1455
1666
 
1456
1667
  const dateStr = this.manufacturingDate.toISOString().slice(0, 10);
1457
1668
  parts.push(`Manufactured: ${dateStr}.`);
@@ -1496,7 +1707,7 @@ class TigerTag {
1496
1707
  const ul = TigerTagDB.label(_db.unit(this.idUnit));
1497
1708
  const sig = sigResult
1498
1709
  ? String(sigResult)
1499
- : (this.isSigned ? 'signed ✓' : 'not signed');
1710
+ : (this.isSigned ? 'signed (not verified)' : 'not signed');
1500
1711
 
1501
1712
  const recNote = (kMin, kMax, suffix = '°C') =>
1502
1713
  rec[kMin] != null ? ` (DB: ${rec[kMin]}–${rec[kMax]}${suffix})` : '';
@@ -1541,6 +1752,7 @@ class TigerTag {
1541
1752
  + `├─ Traceability ────────────────────────────────────────\n`
1542
1753
  + `│ Manufactured ${this.manufacturingDate.toISOString().replace('T', ' ').slice(0, 16)} UTC\n`
1543
1754
  + `│ Twin tag ID ${this.timestamp}\n`
1755
+ + `│ Tag ${this.tagIndex || '?'} of ${this.tagCount || '?'}${this.tagInfo === 0 ? ' (unknown)' : ''}\n`
1544
1756
  + `│ Message ${JSON.stringify(this.customMessage)}\n`
1545
1757
  + `├─ Signature ───────────────────────────────────────────\n`
1546
1758
  + `│ ECDSA ${sig}\n`
package/LICENSE.md DELETED
@@ -1,21 +0,0 @@
1
- # GNU General Public License v3.0
2
-
3
- TigerTag JavaScript SDK
4
- Copyright (C) 2025 TigerTag
5
-
6
- This program is free software: you can redistribute it and/or modify
7
- it under the terms of the GNU General Public License as published by
8
- the Free Software Foundation, either version 3 of the License, or
9
- (at your option) any later version.
10
-
11
- This program is distributed in the hope that it will be useful,
12
- but WITHOUT ANY WARRANTY; without even the implied warranty of
13
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
- GNU General Public License for more details.
15
-
16
- You should have received a copy of the GNU General Public License
17
- along with this program. If not, see <https://www.gnu.org/licenses/>.
18
-
19
- ---
20
-
21
- For commercial OEM licensing inquiries, contact: licensing@tigertag.io