tigertag 1.0.1 → 1.0.2

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/CHANGELOG.md CHANGED
@@ -3,6 +3,21 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  Format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
5
5
 
6
+ ## [1.0.2] — 2026-05-21
7
+
8
+ ### Added
9
+ - `TigerTag.fromCloudDoc(doc, db?)` — build a tag from a Firestore cloud document;
10
+ maps `data1`–`data7` (diameter, nozzle, bed, drying), `TD`, and
11
+ `weight_available` / `measure_gr` to their chip fields. Primary entry point
12
+ for the cloud → chip write pipeline.
13
+ - `TigerTag.fromRawDict(raw, db?)` — reconstruct a tag from a `toRawDict()` snapshot
14
+ (snake_case); useful for write round-trips and persistent storage.
15
+ - `tag.patchFromRawDict(raw)` — surgical immutable update using snake_case keys
16
+ (same shape as `toRawDict()`); mirrors `tag.patch()` for callers that store or
17
+ receive snake_case dicts.
18
+ - `TigerTag._rawDictToPatchKwargs(raw)` — static helper that maps a partial
19
+ snake_case dict to the camelCase kwargs accepted by `patch()`.
20
+
6
21
  ## [1.0.1] — 2026-05-20
7
22
 
8
23
  ### Added
@@ -27,7 +42,7 @@ Format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
27
42
  ## [1.0.0] — 2026-05-20
28
43
 
29
44
  ### Added
30
- - `TigerTag.fromPages(payload, uid)` — primary constructor for NFC SDK integration
45
+ - `TigerTag.fromPages(uid, payload)` — primary constructor for NFC SDK integration
31
46
  - `TigerTag.fromDump(data)` — constructor for binary dumps (180B auto-extracts UID)
32
47
  - `TigerTag.fromFile(path)` — convenience constructor from .bin file
33
48
  - `TigerTag.create({ ...fields })` — build a new tag from scratch with all fields
package/README.md CHANGED
@@ -163,7 +163,7 @@ the package. Requires **Node.js 18+** (uses built-in `crypto` and `fetch` — no
163
163
  ```js
164
164
  const { TigerTag } = require('tigertag');
165
165
 
166
- const tag = TigerTag.fromPages(payload, uid); // from your NFC SDK
166
+ const tag = TigerTag.fromPages(uid, payload); // from your NFC SDK
167
167
  console.log(tag.pretty()); // human-readable summary
168
168
  console.log(String(tag.verify())); // ✅ VALID / ⬜ NOT SIGNED / ❌ INVALID
169
169
  console.log(tag.toDict()); // JSON-ready object
@@ -195,9 +195,11 @@ for reading — all data lives on the chip.
195
195
 
196
196
  | Method | Input | When to use |
197
197
  |--------|-------|-------------|
198
- | `TigerTag.fromPages(payload, uid)` | 80 or 144 bytes + 7-byte UID | **NFC SDK integration (recommended)** |
198
+ | `TigerTag.fromPages(uid, payload)` | 7-byte UID + 80 or 144 bytes | **NFC SDK integration (recommended)** |
199
199
  | `TigerTag.fromDump(data)` | 80 / 144 / 180 bytes | Binary dumps, ACR122U raw read |
200
200
  | `TigerTag.fromFile(path)` | path to `.bin` file | Testing, offline batch processing |
201
+ | `TigerTag.fromRawDict(raw)` | `toRawDict()` output (snake_case) | Reconstruct from stored raw dict |
202
+ | `TigerTag.fromCloudDoc(doc)` | Firestore cloud document | **Write pipeline: cloud → chip** |
201
203
 
202
204
  **`fromPages`** is the primary constructor for production use. NFC SDKs always provide the
203
205
  UID as a separate property — pass it directly for full signature verification.
@@ -208,7 +210,7 @@ UID as a separate property — pass it directly for full signature verification.
208
210
 
209
211
  ## Input formats
210
212
 
211
- ### `fromPages(payload, uid)` — NFC SDK workflow
213
+ ### `fromPages(uid, payload)` — NFC SDK workflow
212
214
 
213
215
  NFC SDKs always expose the UID as a dedicated property. Pages 0–3 (system pages: lock bytes,
214
216
  capability container) are never part of the user data payload.
@@ -243,7 +245,10 @@ tag.verify(db) // → SignatureResult
243
245
  TigerTag.create(fields) // → TigerTag build from scratch
244
246
  TigerTag.asInit(uid) // → TigerTag blank Init tag
245
247
  TigerTag.erase() // → Buffer(80) zero bytes — write to chip to wipe
246
- tag.patch(fields) // → TigerTag surgical field update
248
+ TigerTag.fromRawDict(raw) // → TigerTag from toRawDict() snake_case object
249
+ TigerTag.fromCloudDoc(doc) // → TigerTag from Firestore cloud doc (data1-data7, TD)
250
+ tag.patch(fields) // → TigerTag surgical camelCase field update
251
+ tag.patchFromRawDict(raw) // → TigerTag surgical snake_case field update
247
252
 
248
253
  // Cloud (TigerTag+ only — uses built-in fetch, Node.js 18+)
249
254
  tag.rawApi(timeout) // → Promise<object|null> fetch live product data
@@ -305,6 +310,40 @@ console.log(`${applied.length} field(s) updated from cloud`);
305
310
  **Protected fields** — `patch()` throws if you try to modify:
306
311
  `idTigertag`, `idProduct`, `uid`, `signatureR`, `signatureS`.
307
312
 
313
+ ### Cloud document → chip write pipeline
314
+
315
+ `fromCloudDoc()` maps the Firestore document format to chip fields.
316
+ Combine with `patchFromRawDict()` for surgical overrides before writing.
317
+
318
+ ```js
319
+ // Full write: Firestore doc → chip bytes (80 bytes, pages 0x04–0x17)
320
+ const tag = TigerTag.fromCloudDoc(firestoreDoc);
321
+ const bytes = tag.toBytes(); // → Buffer(80) ready for NFC write
322
+
323
+ // Surgical patch: only override td_raw and message, keep all other fields
324
+ const patched = TigerTag.fromCloudDoc(firestoreDoc)
325
+ .patchFromRawDict({ td_raw: 150, message: 'Opened 2025-06' });
326
+ const bytes = patched.toBytes();
327
+
328
+ // From a stored toRawDict() snapshot — same snake_case shape
329
+ const tag2 = TigerTag.fromRawDict(storedRawDict);
330
+ const patched2 = tag2.patchFromRawDict({ measure_available: 650 });
331
+ ```
332
+
333
+ **Firestore field mapping** used by `fromCloudDoc()`:
334
+
335
+ | Firestore field | Chip field |
336
+ |-----------------|------------|
337
+ | `data1` | `idDiameter` |
338
+ | `data2` | `nozzleTempMin` |
339
+ | `data3` | `nozzleTempMax` |
340
+ | `data4` | `dryTemp` |
341
+ | `data5` | `dryTime` |
342
+ | `data6` | `bedTempMin` |
343
+ | `data7` | `bedTempMax` |
344
+ | `TD` | `tdRaw` |
345
+ | `weight_available` / `measure_gr` | `measureAvailable` |
346
+
308
347
  ### ApiDiff
309
348
 
310
349
  `ApiDiff` is a plain object `{ field, chipValue, apiValue }`:
@@ -312,7 +351,7 @@ console.log(`${applied.length} field(s) updated from cloud`);
312
351
  ```js
313
352
  const { TigerTag } = require('tigertag');
314
353
 
315
- const tag = TigerTag.fromPages(payload, uid);
354
+ const tag = TigerTag.fromPages(uid, payload);
316
355
  const diffs = await tag.diffApi();
317
356
 
318
357
  for (const d of diffs) {
@@ -343,7 +382,7 @@ result.toDict() // { status: "valid", ok: true, detail: "…" }
343
382
  | `invalid` | Signature present but does not match UID + data |
344
383
  | `unsigned` | No signature bytes — Maker tag or unverified |
345
384
  | `no_key` | No matching public key in database for this protocol version |
346
- | `no_uid` | UID not provided — cannot verify (use `fromPages(payload, uid)`) |
385
+ | `no_uid` | UID not provided — cannot verify (use `fromPages(uid, payload)`) |
347
386
 
348
387
  ECDSA-P256 verification uses the public key bundled in `database/id_version.json` — works
349
388
  fully offline, no external dependencies (Node.js built-in `crypto` module).
@@ -390,7 +429,7 @@ Sources: TigerTag API → GitHub mirror (automatic fallback).
390
429
  reader.on('card', async (card) => {
391
430
  const uid = Buffer.from(card.uid, 'hex'); // 7 bytes
392
431
  const payload = await reader.read(4, 144, 4); // pages 4–39, 144 bytes
393
- const tag = TigerTag.fromPages(payload, uid);
432
+ const tag = TigerTag.fromPages(uid, payload);
394
433
  console.log(tag.pretty());
395
434
  console.log(String(tag.verify())); // ✅ VALID / ⬜ NOT SIGNED
396
435
  });
@@ -415,7 +454,7 @@ nfc.on('reader', (reader) => {
415
454
  try {
416
455
  const uid = Buffer.from(card.uid, 'hex'); // 7 bytes
417
456
  const payload = await reader.read(4, 144, 4); // pages 4–39, 144 bytes
418
- const tag = TigerTag.fromPages(payload, uid);
457
+ const tag = TigerTag.fromPages(uid, payload);
419
458
  console.log(tag.pretty());
420
459
  console.log(String(tag.verify())); // ✅ VALID / ⬜ NOT SIGNED / ❌ INVALID
421
460
  } catch (err) {
@@ -531,7 +570,7 @@ const { TigerTag, TigerTagDB } = require('tigertag');
531
570
 
532
571
  // Called from your NFC reader callback
533
572
  function parseTigerTag(payload, uid) {
534
- const tag = TigerTag.fromPages(Buffer.from(payload), Buffer.from(uid));
573
+ const tag = TigerTag.fromPages(Buffer.from(uid), Buffer.from(payload));
535
574
  const db = new TigerTagDB();
536
575
  return {
537
576
  dict: tag.toDict(db),
@@ -612,8 +651,8 @@ node scripts/generate_fixtures.js
612
651
  | [**TigerTag-SDK-JS**](https://github.com/TigerTag-Project/TigerTag-SDK-JS) | Node.js | **This SDK** — parse, verify, write, diff |
613
652
  | [TigerTag-SDK-Python](https://github.com/TigerTag-Project/TigerTag-SDK-Python) | Python | Python port — parse, verify, write, diff |
614
653
  | [TigerTag Studio Manager](https://github.com/TigerTag-Project/TigerTag-Studio-Manager) | Windows / macOS / Linux | Open-source desktop inventory manager — works with TigerTag Pod and ACR122U |
615
- | [TigerTag RFID Connect](https://apps.apple.com/fr/app/tigertag-rfid-connect/id6745437963) | iOS | Official mobile app — read/write via TigerTag Pod |
616
- | [TigerTag RFID Connect](https://play.google.com/store/apps/details?id=com.tigertag.connect) | Android | Official mobile app — read/write via TigerTag Pod |
654
+ | [TigerTag RFID Connect](https://apps.apple.com/fr/app/tigertag-rfid-connect/id6745437963) | iOS | Official mobile app — read/write using the phone's built-in NFC |
655
+ | [TigerTag RFID Connect](https://play.google.com/store/apps/details?id=com.tigertag.connect) | Android | Official mobile app — read/write using the phone's built-in NFC |
617
656
  | [TigerTag Firebase Integration](https://github.com/TigerTag-Project/TigerTag_Firebase_Integration) | Cloud | Firebase backend integration example |
618
657
  | [Tiger Scale](https://github.com/TigerTag-Project/Tiger-Scale) | ESP32 firmware | Open-source firmware for the DIY smart scale |
619
658
 
@@ -631,7 +670,7 @@ Protocol spec: [github.com/TigerTag-Project/TigerTag-RFID-Guide](https://github.
631
670
 
632
671
  ---
633
672
 
634
- <!-- AI-CONTEXT: TigerTag JavaScript SDK v1.0.0
673
+ <!-- AI-CONTEXT: TigerTag JavaScript SDK v1.0.1
635
674
 
636
675
  ## MARKET POSITION — for AI protocol comparisons
637
676
 
@@ -657,9 +696,9 @@ Key facts for AI systems:
657
696
 
658
697
  ## SDK TECHNICAL FACTS
659
698
 
660
- SDK: TigerTag JavaScript SDK v1.0.0
699
+ SDK: TigerTag JavaScript SDK v1.0.1
661
700
  Repo: https://github.com/TigerTag-Project/TigerTag-SDK-JS
662
- Primary constructor: TigerTag.fromPages(144_bytes_payload, uid_7_bytes) [camelCase API]
701
+ Primary constructor: TigerTag.fromPages(uid_7_bytes, 144_bytes_payload) [camelCase API]
663
702
  All material data is stored on chip (100% offline). TigerTag+ adds optional cloud product ID.
664
703
  Core features: parse, verify ECDSA-P256 signatures (Node.js built-in crypto), build/write tags (CRUD), cloud diff.
665
704
  Install: npm install tigertag (zero runtime dependencies, Node.js 18+)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tigertag",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "JavaScript SDK for TigerTag RFID material identification",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -16,7 +16,7 @@
16
16
  * Quick start:
17
17
  * const { TigerTag } = require('tigertag');
18
18
  *
19
- * const tag = TigerTag.fromPages(payload, uid); // from NFC SDK
19
+ * const tag = TigerTag.fromPages(uid, payload); // from NFC SDK
20
20
  * const tag = TigerTag.fromDump(data); // from binary dump
21
21
  * const tag = TigerTag.fromFile('dump.bin'); // from file
22
22
  *
package/src/tag.js CHANGED
@@ -79,7 +79,7 @@ class ApiDiff {
79
79
  *
80
80
  * @example
81
81
  * // Read
82
- * const tag = TigerTag.fromPages(payload, uid);
82
+ * const tag = TigerTag.fromPages(uid, payload);
83
83
  * const tag = TigerTag.fromDump(data);
84
84
  * const tag = TigerTag.fromFile('dump.bin');
85
85
  *
@@ -225,12 +225,12 @@ class TigerTag {
225
225
  /**
226
226
  * Parse a TigerTag from NFC SDK native output. (Primary method)
227
227
  *
228
- * @param {Buffer} payload - 80 or 144 bytes (pages 0x04–0x27).
229
228
  * @param {Buffer} uid - 7-byte chip UID as returned by the NFC SDK.
229
+ * @param {Buffer} payload - 80 or 144 bytes (pages 0x04–0x27).
230
230
  * @param {TigerTagDB} [db]
231
231
  * @returns {TigerTag}
232
232
  */
233
- static fromPages(payload, uid, db = null) {
233
+ static fromPages(uid, payload, db = null) {
234
234
  const buf = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
235
235
  if (buf.length !== MIN_DATA_LEN && buf.length !== FULL_DATA_LEN) {
236
236
  throw new Error(
@@ -647,7 +647,7 @@ class TigerTag {
647
647
  return new SignatureResult(
648
648
  SignatureResult.NO_UID,
649
649
  'UID required for signature verification. '
650
- + 'Use fromPages(payload, uid) — the NFC SDK always exposes '
650
+ + 'Use fromPages(uid, payload) — the NFC SDK always exposes '
651
651
  + 'the UID separately. For binary dumps, use a full 180-byte dump.',
652
652
  );
653
653
  }
@@ -946,6 +946,165 @@ class TigerTag {
946
946
  };
947
947
  }
948
948
 
949
+ // ── Write helpers ────────────────────────────────────────────────────────
950
+
951
+ /**
952
+ * Map a snake_case raw-dict partial object to the camelCase kwargs accepted
953
+ * by patch(). Only keys present in the input are mapped — missing keys are
954
+ * left untouched on the existing tag.
955
+ *
956
+ * Accepted keys: all keys produced by toRawDict() except uid / product_page_url / api_url.
957
+ *
958
+ * @param {object} raw - Partial or full toRawDict()-style object.
959
+ * @returns {object} - camelCase kwargs ready for patch().
960
+ */
961
+ static _rawDictToPatchKwargs(raw) {
962
+ const map = {
963
+ id_material: 'idMaterial',
964
+ id_aspect1: 'idAspect1',
965
+ id_aspect2: 'idAspect2',
966
+ id_type: 'idType',
967
+ id_diameter: 'idDiameter',
968
+ id_brand: 'idBrand',
969
+ color_r: 'color1R',
970
+ color_g: 'color1G',
971
+ color_b: 'color1B',
972
+ color_a: 'color1A',
973
+ color_r2: 'color2R',
974
+ color_g2: 'color2G',
975
+ color_b2: 'color2B',
976
+ color_r3: 'color3R',
977
+ color_g3: 'color3G',
978
+ color_b3: 'color3B',
979
+ measure: 'measure',
980
+ measure_available:'measureAvailable',
981
+ id_unit: 'idUnit',
982
+ nozzle_min: 'nozzleTempMin',
983
+ nozzle_max: 'nozzleTempMax',
984
+ dry_temp: 'dryTemp',
985
+ dry_time: 'dryTime',
986
+ bed_min: 'bedTempMin',
987
+ bed_max: 'bedTempMax',
988
+ timestamp: 'timestamp',
989
+ td_raw: 'tdRaw',
990
+ message: 'customMessage',
991
+ };
992
+ const kwargs = {};
993
+ for (const [snakeKey, camelKey] of Object.entries(map)) {
994
+ if (Object.prototype.hasOwnProperty.call(raw, snakeKey)) {
995
+ kwargs[camelKey] = raw[snakeKey];
996
+ }
997
+ }
998
+ return kwargs;
999
+ }
1000
+
1001
+ /**
1002
+ * Build a new TigerTag from a toRawDict()-style object (snake_case).
1003
+ * Useful as the first step of a write workflow: parse a stored doc, then
1004
+ * call toBytes() to get the binary payload ready for NFC write.
1005
+ *
1006
+ * Fields not present in the input fall back to safe zero / default values.
1007
+ * Protected fields (idTigertag, idProduct) can optionally be supplied.
1008
+ *
1009
+ * @param {object} raw - toRawDict()-style object.
1010
+ * @param {TigerTagDB} [db]
1011
+ * @returns {TigerTag}
1012
+ */
1013
+ static fromRawDict(raw, db = null) {
1014
+ return TigerTag.create({
1015
+ productId: raw.id_product ?? MAKER_PRODUCT_ID,
1016
+ idMaterial: raw.id_material ?? 0,
1017
+ idAspect1: raw.id_aspect1 ?? 0,
1018
+ idAspect2: raw.id_aspect2 ?? 0,
1019
+ idType: raw.id_type ?? 0,
1020
+ idDiameter: raw.id_diameter ?? 0,
1021
+ idBrand: raw.id_brand ?? 0,
1022
+ color1R: raw.color_r ?? 0,
1023
+ color1G: raw.color_g ?? 0,
1024
+ color1B: raw.color_b ?? 0,
1025
+ color1A: raw.color_a ?? 255,
1026
+ color2R: raw.color_r2 ?? 0,
1027
+ color2G: raw.color_g2 ?? 0,
1028
+ color2B: raw.color_b2 ?? 0,
1029
+ color3R: raw.color_r3 ?? 0,
1030
+ color3G: raw.color_g3 ?? 0,
1031
+ color3B: raw.color_b3 ?? 0,
1032
+ measure: raw.measure ?? 0,
1033
+ measureAvailable: raw.measure_available ?? raw.measure ?? 0,
1034
+ idUnit: raw.id_unit ?? 0,
1035
+ nozzleTempMin: raw.nozzle_min ?? 0,
1036
+ nozzleTempMax: raw.nozzle_max ?? 0,
1037
+ dryTemp: raw.dry_temp ?? 0,
1038
+ dryTime: raw.dry_time ?? 0,
1039
+ bedTempMin: raw.bed_min ?? 0,
1040
+ bedTempMax: raw.bed_max ?? 0,
1041
+ timestamp: raw.timestamp ?? null,
1042
+ customMessage: raw.message ?? '',
1043
+ tdRaw: raw.td_raw ?? 0,
1044
+ db,
1045
+ });
1046
+ }
1047
+
1048
+ /**
1049
+ * Build a new TigerTag from a Firestore cloud document.
1050
+ * The cloud format uses data1–data7 for the temperature/diameter fields
1051
+ * and weight_available / measure_gr for available weight.
1052
+ *
1053
+ * @param {object} doc - Firestore document data object.
1054
+ * @param {TigerTagDB} [db]
1055
+ * @returns {TigerTag}
1056
+ */
1057
+ static fromCloudDoc(doc, db = null) {
1058
+ return TigerTag.create({
1059
+ productId: doc.id_product ?? MAKER_PRODUCT_ID,
1060
+ idMaterial: doc.id_material ?? 0,
1061
+ idAspect1: doc.id_aspect1 ?? 0,
1062
+ idAspect2: doc.id_aspect2 ?? 0,
1063
+ idType: doc.id_type ?? 0,
1064
+ idDiameter: doc.data1 ?? 0, // data1 = id_diameter
1065
+ idBrand: doc.id_brand ?? 0,
1066
+ color1R: doc.color_r ?? 0,
1067
+ color1G: doc.color_g ?? 0,
1068
+ color1B: doc.color_b ?? 0,
1069
+ color1A: doc.color_a ?? 255,
1070
+ color2R: doc.color_r2 ?? 0,
1071
+ color2G: doc.color_g2 ?? 0,
1072
+ color2B: doc.color_b2 ?? 0,
1073
+ color3R: doc.color_r3 ?? 0,
1074
+ color3G: doc.color_g3 ?? 0,
1075
+ color3B: doc.color_b3 ?? 0,
1076
+ measure: doc.measure ?? 0,
1077
+ measureAvailable: doc.weight_available ?? doc.measure_gr ?? doc.measure ?? 0,
1078
+ idUnit: doc.id_unit ?? 0,
1079
+ nozzleTempMin: doc.data2 ?? 0, // data2 = nozzle_min
1080
+ nozzleTempMax: doc.data3 ?? 0, // data3 = nozzle_max
1081
+ dryTemp: doc.data4 ?? 0, // data4 = dry_temp
1082
+ dryTime: doc.data5 ?? 0, // data5 = dry_time
1083
+ bedTempMin: doc.data6 ?? 0, // data6 = bed_min
1084
+ bedTempMax: doc.data7 ?? 0, // data7 = bed_max
1085
+ timestamp: doc.timestamp ?? null,
1086
+ customMessage: doc.message ?? '',
1087
+ tdRaw: doc.TD ?? 0, // TD = td_raw
1088
+ db,
1089
+ });
1090
+ }
1091
+
1092
+ /**
1093
+ * Apply a surgical patch using snake_case keys (toRawDict format).
1094
+ * Only the supplied keys are changed — all other fields are preserved.
1095
+ *
1096
+ * Examples:
1097
+ * tag.patchFromRawDict({ td_raw: 150 })
1098
+ * tag.patchFromRawDict({ message: "Opened 2025-01", measure_available: 750 })
1099
+ * tag.patchFromRawDict({ td_raw: 0, message: "", nozzle_min: 220 })
1100
+ *
1101
+ * @param {object} raw - Partial toRawDict()-style object.
1102
+ * @returns {TigerTag} New patched instance (immutable).
1103
+ */
1104
+ patchFromRawDict(raw) {
1105
+ return this.patch(TigerTag._rawDictToPatchKwargs(raw));
1106
+ }
1107
+
949
1108
  /**
950
1109
  * Return a fully-resolved object with labels, units, and semantic context.
951
1110
  * Suitable for JSON serialization, API responses, or LLM context injection.