@nextera.one/tps-standard 0.5.33 → 0.6.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.
Files changed (105) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +133 -56
  3. package/dist/date.d.ts +54 -0
  4. package/dist/date.js +174 -0
  5. package/dist/date.js.map +1 -0
  6. package/dist/driver-manager.d.ts +34 -0
  7. package/dist/driver-manager.js +53 -0
  8. package/dist/driver-manager.js.map +1 -0
  9. package/dist/drivers/chinese.d.ts +25 -0
  10. package/dist/drivers/chinese.js +485 -0
  11. package/dist/drivers/chinese.js.map +1 -0
  12. package/dist/drivers/gregorian.d.ts +3 -5
  13. package/dist/drivers/gregorian.js +26 -19
  14. package/dist/drivers/gregorian.js.map +1 -1
  15. package/dist/drivers/hijri.d.ts +1 -16
  16. package/dist/drivers/hijri.js +9 -102
  17. package/dist/drivers/hijri.js.map +1 -1
  18. package/dist/drivers/holocene.d.ts +6 -3
  19. package/dist/drivers/holocene.js +7 -20
  20. package/dist/drivers/holocene.js.map +1 -1
  21. package/dist/drivers/julian.d.ts +3 -10
  22. package/dist/drivers/julian.js +11 -71
  23. package/dist/drivers/julian.js.map +1 -1
  24. package/dist/drivers/persian.d.ts +1 -6
  25. package/dist/drivers/persian.js +17 -92
  26. package/dist/drivers/persian.js.map +1 -1
  27. package/dist/drivers/tps.d.ts +11 -28
  28. package/dist/drivers/tps.js +8 -58
  29. package/dist/drivers/tps.js.map +1 -1
  30. package/dist/drivers/unix.d.ts +5 -6
  31. package/dist/drivers/unix.js +10 -32
  32. package/dist/drivers/unix.js.map +1 -1
  33. package/dist/esm/date.js +170 -0
  34. package/dist/esm/date.js.map +1 -0
  35. package/dist/esm/driver-manager.js +49 -0
  36. package/dist/esm/driver-manager.js.map +1 -0
  37. package/dist/esm/drivers/chinese.js +481 -0
  38. package/dist/esm/drivers/chinese.js.map +1 -0
  39. package/dist/esm/drivers/gregorian.js +160 -0
  40. package/dist/esm/drivers/gregorian.js.map +1 -0
  41. package/dist/esm/drivers/hijri.js +184 -0
  42. package/dist/esm/drivers/hijri.js.map +1 -0
  43. package/dist/esm/drivers/holocene.js +115 -0
  44. package/dist/esm/drivers/holocene.js.map +1 -0
  45. package/dist/esm/drivers/julian.js +161 -0
  46. package/dist/esm/drivers/julian.js.map +1 -0
  47. package/dist/esm/drivers/persian.js +190 -0
  48. package/dist/esm/drivers/persian.js.map +1 -0
  49. package/dist/esm/drivers/tps.js +181 -0
  50. package/dist/esm/drivers/tps.js.map +1 -0
  51. package/dist/esm/drivers/unix.js +50 -0
  52. package/dist/esm/drivers/unix.js.map +1 -0
  53. package/dist/esm/index.js +873 -0
  54. package/dist/esm/index.js.map +1 -0
  55. package/dist/esm/types.js +28 -0
  56. package/dist/esm/types.js.map +1 -0
  57. package/dist/esm/uid.js +221 -0
  58. package/dist/esm/uid.js.map +1 -0
  59. package/dist/esm/utils/calendar.js +126 -0
  60. package/dist/esm/utils/calendar.js.map +1 -0
  61. package/dist/esm/utils/env.js +76 -0
  62. package/dist/esm/utils/env.js.map +1 -0
  63. package/dist/esm/utils/timezone.js +168 -0
  64. package/dist/esm/utils/timezone.js.map +1 -0
  65. package/dist/esm/utils/tps-string.js +160 -0
  66. package/dist/esm/utils/tps-string.js.map +1 -0
  67. package/dist/index.d.ts +84 -466
  68. package/dist/index.js +430 -1095
  69. package/dist/index.js.map +1 -1
  70. package/dist/types.d.ts +103 -0
  71. package/dist/types.js +31 -0
  72. package/dist/types.js.map +1 -0
  73. package/dist/uid.d.ts +48 -0
  74. package/dist/uid.js +225 -0
  75. package/dist/uid.js.map +1 -0
  76. package/dist/utils/calendar.d.ts +55 -0
  77. package/dist/utils/calendar.js +136 -0
  78. package/dist/utils/calendar.js.map +1 -0
  79. package/dist/utils/env.d.ts +12 -0
  80. package/dist/utils/env.js +79 -0
  81. package/dist/utils/env.js.map +1 -0
  82. package/dist/utils/timezone.d.ts +32 -0
  83. package/dist/utils/timezone.js +173 -0
  84. package/dist/utils/timezone.js.map +1 -0
  85. package/dist/utils/tps-string.d.ts +12 -0
  86. package/dist/utils/tps-string.js +164 -0
  87. package/dist/utils/tps-string.js.map +1 -0
  88. package/package.json +20 -5
  89. package/src/date.ts +243 -0
  90. package/src/driver-manager.ts +54 -0
  91. package/src/drivers/chinese.ts +542 -0
  92. package/src/drivers/gregorian.ts +29 -27
  93. package/src/drivers/hijri.ts +13 -113
  94. package/src/drivers/holocene.ts +11 -12
  95. package/src/drivers/julian.ts +18 -72
  96. package/src/drivers/persian.ts +25 -92
  97. package/src/drivers/tps.ts +16 -55
  98. package/src/drivers/unix.ts +12 -33
  99. package/src/index.ts +384 -1556
  100. package/src/types.ts +131 -0
  101. package/src/uid.ts +308 -0
  102. package/src/utils/calendar.ts +161 -0
  103. package/src/utils/env.ts +88 -0
  104. package/src/utils/timezone.ts +182 -0
  105. package/src/utils/tps-string.ts +166 -0
package/dist/index.js CHANGED
@@ -3,18 +3,40 @@
3
3
  * TPS: Temporal Positioning System
4
4
  * The Universal Protocol for Space-Time Coordinates.
5
5
  * @packageDocumentation
6
- * @version 0.5.2
6
+ * @version 0.6.0
7
7
  * @license Apache-2.0
8
8
  * @copyright 2026 TPS Standards Working Group
9
9
  *
10
+ * v0.5.35 Changes:
11
+ * - Added TPS.now(), TPS.diff(), TPS.add() convenience methods
12
+ * - Added Chinese Lunisolar (chin) calendar driver
13
+ * - Added DriverManager (driver registry separated from TPS class)
14
+ * - Added timezone utility (src/utils/timezone.ts) with IANA + offset support
15
+ * - TPS.toDate() now respects ;tz= extensions when present
16
+ * - ESM dual-mode exports + browser IIFE bundle
17
+ *
10
18
  * v0.5.0 Changes:
11
19
  * - Added Actor anchor (A:) for provenance tracking
12
20
  * - Added Signature (!) for cryptographic verification
13
21
  * - Added structural anchors (bldg, floor, room, zone)
14
22
  * - Added geospatial cell systems (S2, H3, Plus Code, what3words)
15
23
  */
24
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
25
+ if (k2 === undefined) k2 = k;
26
+ var desc = Object.getOwnPropertyDescriptor(m, k);
27
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
28
+ desc = { enumerable: true, get: function() { return m[k]; } };
29
+ }
30
+ Object.defineProperty(o, k2, desc);
31
+ }) : (function(o, m, k, k2) {
32
+ if (k2 === undefined) k2 = k;
33
+ o[k2] = m[k];
34
+ }));
35
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
36
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
37
+ };
16
38
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.TpsDate = exports.TPSUID7RB = exports.TPS = exports.TimeOrder = exports.DefaultCalendars = void 0;
39
+ exports.TPS = exports.getOffsetString = exports.localToUtc = exports.utcToLocal = exports.DriverManager = exports.Env = void 0;
18
40
  // built-in drivers are registered automatically; importing them here
19
41
  // ensures they are included when the library bundler/tree-shaker runs.
20
42
  const gregorian_1 = require("./drivers/gregorian");
@@ -24,35 +46,29 @@ const persian_1 = require("./drivers/persian");
24
46
  const hijri_1 = require("./drivers/hijri");
25
47
  const julian_1 = require("./drivers/julian");
26
48
  const holocene_1 = require("./drivers/holocene");
27
- // Calendar codes are plain strings to allow arbitrary user-defined
28
- // calendars. The library still exports constants for the built-in values but
29
- // callers may also supply their own codes.
30
- exports.DefaultCalendars = {
31
- TPS: "tps",
32
- GREG: "greg",
33
- HIJ: "hij",
34
- PER: "per",
35
- JUL: "jul",
36
- HOLO: "holo",
37
- UNIX: "unix",
38
- };
39
- /**
40
- * Specifies the direction of the time-component hierarchy when serializing or
41
- * deserializing a TPS string. The default is `'descending'` (millennium → … →
42
- * second), but `'ascending'` produces the reverse order.
43
- */
44
- var TimeOrder;
45
- (function (TimeOrder) {
46
- TimeOrder["DESC"] = "desc";
47
- TimeOrder["ASC"] = "asc";
48
- })(TimeOrder || (exports.TimeOrder = TimeOrder = {}));
49
+ const chinese_1 = require("./drivers/chinese");
50
+ __exportStar(require("./types"), exports);
51
+ __exportStar(require("./uid"), exports);
52
+ __exportStar(require("./date"), exports);
53
+ var env_1 = require("./utils/env");
54
+ Object.defineProperty(exports, "Env", { enumerable: true, get: function () { return env_1.Env; } });
55
+ var driver_manager_1 = require("./driver-manager");
56
+ Object.defineProperty(exports, "DriverManager", { enumerable: true, get: function () { return driver_manager_1.DriverManager; } });
57
+ var timezone_1 = require("./utils/timezone");
58
+ Object.defineProperty(exports, "utcToLocal", { enumerable: true, get: function () { return timezone_1.utcToLocal; } });
59
+ Object.defineProperty(exports, "localToUtc", { enumerable: true, get: function () { return timezone_1.localToUtc; } });
60
+ Object.defineProperty(exports, "getOffsetString", { enumerable: true, get: function () { return timezone_1.getOffsetString; } });
61
+ const driver_manager_2 = require("./driver-manager");
62
+ const tps_string_1 = require("./utils/tps-string");
63
+ const timezone_2 = require("./utils/timezone");
64
+ const types_1 = require("./types");
49
65
  class TPS {
50
66
  /**
51
67
  * Registers a calendar driver plugin.
52
68
  * @param driver - The driver instance to register.
53
69
  */
54
70
  static registerDriver(driver) {
55
- this.drivers.set(driver.code, driver);
71
+ this.driverManager.register(driver);
56
72
  }
57
73
  /**
58
74
  * Gets a registered calendar driver.
@@ -60,7 +76,7 @@ class TPS {
60
76
  * @returns The driver or undefined.
61
77
  */
62
78
  static getDriver(code) {
63
- return this.drivers.get(code);
79
+ return this.driverManager.get(code);
64
80
  }
65
81
  // --- CORE METHODS ---
66
82
  /**
@@ -76,6 +92,24 @@ class TPS {
76
92
  let s = input.trim().replace(/\s+/g, "");
77
93
  if (!s)
78
94
  return s;
95
+ // ── 1.2 Compact scheme normalization (v0.6.0) ──────────────────────────
96
+ // TPS:... → tps://... (generic compact)
97
+ // NIP4:x → tps://net:ip4:x (IPv4 shorthand)
98
+ // NIP6:x → tps://net:ip6:x (IPv6 shorthand)
99
+ // NODE:x → tps://node:x (logical node shorthand)
100
+ if (/^TPS:/i.test(s) && !s.toLowerCase().startsWith("tps://")) {
101
+ // TPS:L:... or TPS:lat,lon... → tps://...
102
+ s = "tps://" + s.slice(4); // strip 'TPS:'
103
+ }
104
+ else if (/^NIP4:/i.test(s)) {
105
+ s = "tps://net:ip4:" + s.slice(5);
106
+ }
107
+ else if (/^NIP6:/i.test(s)) {
108
+ s = "tps://net:ip6:" + s.slice(5);
109
+ }
110
+ else if (/^NODE:/i.test(s)) {
111
+ s = "tps://node:" + s.slice(5);
112
+ }
79
113
  // ── 1.5 Convert legacy "/T:" separators to the new canonical "@T:".
80
114
  // The input may contain "/T:" from older versions; we normalise early so
81
115
  // subsequent logic can assume only the '@' form.
@@ -208,7 +242,7 @@ class TPS {
208
242
  timeStr = timeStr.split(/[!;?#]/)[0];
209
243
  }
210
244
  if (timeStr) {
211
- const parsed = this.parseTimeString(timeStr);
245
+ const parsed = (0, tps_string_1.parseTimeString)(timeStr);
212
246
  if (!parsed)
213
247
  return null;
214
248
  Object.assign(comp, parsed.components);
@@ -231,13 +265,34 @@ class TPS {
231
265
  signature = sigMatch.groups.sig;
232
266
  timeOnly = input.split(/[!;?#]/)[0];
233
267
  }
234
- const parsed = this.parseTimeString(timeOnly);
268
+ else {
269
+ // Strip extension/query/fragment suffix so parseTimeString sees only tokens
270
+ timeOnly = input.split(/[;?#]/)[0];
271
+ }
272
+ const parsed = (0, tps_string_1.parseTimeString)(timeOnly);
235
273
  if (!parsed)
236
274
  return null;
237
275
  const comp = parsed.components;
238
276
  if (signature)
239
277
  comp.signature = signature;
240
278
  comp.order = parsed.order;
279
+ // Route through the same group mapper used by REGEX_URI for consistency
280
+ // (handles extensions ;KEY:val and context #C:key=val)
281
+ const syntheticGroups = {
282
+ calendar: match.groups.calendar ?? "",
283
+ signature: match.groups.signature ?? "",
284
+ extensions: match.groups.extensions ?? "",
285
+ context: match.groups.context ?? "",
286
+ location: "", // no location in time-only string
287
+ actor: "",
288
+ };
289
+ const mappedComp = this._mapGroupsToComponents(syntheticGroups);
290
+ // Merge temporal components from parseTimeString with mapped metadata
291
+ Object.assign(comp, {
292
+ signature: mappedComp.signature || comp.signature,
293
+ extensions: mappedComp.extensions || comp.extensions,
294
+ context: mappedComp.context,
295
+ });
241
296
  return comp;
242
297
  }
243
298
  /**
@@ -246,64 +301,99 @@ class TPS {
246
301
  * @returns Full URI string (e.g. "tps://...").
247
302
  */
248
303
  static toURI(comp) {
249
- // 1. Build Space Part (L: anchor)
250
- let spacePart = "L:-"; // Default: unknown
251
- if (comp.spaceAnchor) {
252
- spacePart = comp.spaceAnchor;
253
- }
254
- else if (comp.isHiddenLocation) {
255
- spacePart = "L:~";
304
+ // ── 1. Location layers (v0.6.0) ──────────────────────────────────────────
305
+ // Build an ordered list of location layer strings, then join with ";"
306
+ const layers = [];
307
+ // Privacy shorthand takes priority
308
+ if (comp.isHiddenLocation) {
309
+ layers.push("L:~");
256
310
  }
257
311
  else if (comp.isRedactedLocation) {
258
- spacePart = "L:redacted";
312
+ layers.push("L:redacted");
259
313
  }
260
314
  else if (comp.isUnknownLocation) {
261
- spacePart = "L:-";
315
+ layers.push("L:-");
316
+ }
317
+ else if (comp.spaceAnchor) {
318
+ // Generic / legacy anchor (adm:, planet:, etc.)
319
+ layers.push(comp.spaceAnchor);
320
+ }
321
+ else if (comp.ipv4) {
322
+ layers.push(`net:ip4:${comp.ipv4}`);
323
+ }
324
+ else if (comp.ipv6) {
325
+ layers.push(`net:ip6:${comp.ipv6}`);
326
+ }
327
+ else if (comp.nodeName) {
328
+ layers.push(`node:${comp.nodeName}`);
262
329
  }
263
330
  else if (comp.s2Cell) {
264
- spacePart = `L:s2=${comp.s2Cell}`;
331
+ layers.push(`S2:${comp.s2Cell}`);
265
332
  }
266
333
  else if (comp.h3Cell) {
267
- spacePart = `L:h3=${comp.h3Cell}`;
268
- }
269
- else if (comp.plusCode) {
270
- spacePart = `L:plus=${comp.plusCode}`;
334
+ layers.push(`H3:${comp.h3Cell}`);
271
335
  }
272
336
  else if (comp.what3words) {
273
- spacePart = `L:w3w=${comp.what3words}`;
337
+ layers.push(`3W:${comp.what3words}`);
338
+ }
339
+ else if (comp.plusCode) {
340
+ layers.push(`plus:${comp.plusCode}`);
274
341
  }
275
342
  else if (comp.building) {
276
- spacePart = `L:bldg=${comp.building}`;
343
+ layers.push(`bldg:${comp.building}`);
277
344
  if (comp.floor)
278
- spacePart += `.floor=${comp.floor}`;
345
+ layers.push(`floor:${comp.floor}`);
279
346
  if (comp.room)
280
- spacePart += `.room=${comp.room}`;
347
+ layers.push(`room:${comp.room}`);
348
+ if (comp.door)
349
+ layers.push(`door:${comp.door}`);
281
350
  if (comp.zone)
282
- spacePart += `.zone=${comp.zone}`;
351
+ layers.push(`zone:${comp.zone}`);
283
352
  }
284
353
  else if (comp.latitude !== undefined && comp.longitude !== undefined) {
285
- spacePart = `L:${comp.latitude},${comp.longitude}`;
286
- if (comp.altitude !== undefined) {
287
- spacePart += `,${comp.altitude}m`;
288
- }
354
+ let gps = `L:${comp.latitude},${comp.longitude}`;
355
+ if (comp.altitude !== undefined)
356
+ gps += `,${comp.altitude}m`;
357
+ layers.push(gps);
289
358
  }
290
- // 2. Build Actor Part (A: anchor) - optional
291
- let actorPart = "";
292
- if (comp.actor) {
293
- actorPart = `/A:${comp.actor}`;
294
- }
295
- // 3. Build Time Part (handles order & signature)
296
- const timePart = this.buildTimePart(comp);
297
- // 5. Build Extensions
359
+ else {
360
+ layers.push("L:-"); // unknown fallback
361
+ }
362
+ // Place layer (P:) — appended after primary location
363
+ if (comp.placeCountryCode || comp.placeCountryName ||
364
+ comp.placeCityCode || comp.placeCityName) {
365
+ const pParts = [];
366
+ if (comp.placeCountryCode)
367
+ pParts.push(`cc=${comp.placeCountryCode}`);
368
+ if (comp.placeCountryName)
369
+ pParts.push(`cn=${comp.placeCountryName}`);
370
+ if (comp.placeCityCode)
371
+ pParts.push(`ci=${comp.placeCityCode}`);
372
+ if (comp.placeCityName)
373
+ pParts.push(`ct=${comp.placeCityName}`);
374
+ layers.push(`P:${pParts.join(",")}`);
375
+ }
376
+ const locationStr = layers.join(";");
377
+ // ── 2. Actor (/A:...) ─────────────────────────────────────────────────────
378
+ const actorPart = comp.actor ? `/A:${comp.actor}` : "";
379
+ // ── 3. Time (mandatory 9 tokens) ─────────────────────────────────────────
380
+ const timePart = (0, tps_string_1.buildTimePart)(comp);
381
+ // ── 4. Extensions (;KEY:val;...) ─────────────────────────────────────────
298
382
  let extPart = "";
299
383
  if (comp.extensions && Object.keys(comp.extensions).length > 0) {
300
- const extStrings = Object.entries(comp.extensions).map(([k, v]) => `${k}=${v}`);
301
- extPart = `;${extStrings.join(".")}`;
384
+ const extStrings = Object.entries(comp.extensions).map(([k, v]) => {
385
+ // Emit as KEY:val (preferred v0.6.0 style)
386
+ return `${k.toUpperCase()}:${v}`;
387
+ });
388
+ extPart = `;${extStrings.join(";")}`;
389
+ }
390
+ // ── 5. Context (#C:key=val;...) ──────────────────────────────────────────
391
+ let contextPart = "";
392
+ if (comp.context && Object.keys(comp.context).length > 0) {
393
+ const ctxStrings = Object.entries(comp.context).map(([k, v]) => `${k}=${v}`);
394
+ contextPart = `#C:${ctxStrings.join(";")}`;
302
395
  }
303
- // timePart already begins with 'T:'. The new canonical separator is '@'
304
- // instead of '/', so we interpolate it accordingly. Actor anchor (if
305
- // present) still uses a leading slash.
306
- return `tps://${spacePart}${actorPart}@${timePart}${extPart}`;
396
+ return `tps://${locationStr}${actorPart}@${timePart}${extPart}${contextPart}`;
307
397
  }
308
398
  /**
309
399
  * CONVERTER: Creates a TPS Time Object string from a JavaScript Date.
@@ -314,9 +404,9 @@ class TPS {
314
404
  * supported key is `order` which may be `'ascending'` or `'descending'`.
315
405
  * @returns Canonical string (e.g., "T:tps.m3.c1.y26...").
316
406
  */
317
- static fromDate(date = new Date(), calendar = exports.DefaultCalendars.TPS, opts) {
407
+ static fromDate(date = new Date(), calendar = types_1.DefaultCalendars.TPS, opts) {
318
408
  const normalizedCalendar = calendar.toLowerCase();
319
- const driver = this.drivers.get(normalizedCalendar);
409
+ const driver = this.driverManager.get(normalizedCalendar);
320
410
  if (driver) {
321
411
  // when caller requested an explicit order we can bypass the driver's
322
412
  // `fromDate` helper and instead generate components ourselves so that
@@ -326,21 +416,21 @@ class TPS {
326
416
  const comp = driver.getComponentsFromDate(date);
327
417
  comp.calendar = normalizedCalendar;
328
418
  comp.order = opts.order;
329
- return this.buildTimePart(comp);
419
+ return (0, tps_string_1.buildTimePart)(comp);
330
420
  }
331
421
  return driver.getFromDate(date);
332
422
  }
333
423
  // Fallback for old built-in calendars (shouldn't happen once drivers are
334
424
  // registered, but kept for backwards compatibility).
335
425
  const comp = { calendar: normalizedCalendar };
336
- if (normalizedCalendar === exports.DefaultCalendars.UNIX) {
426
+ if (normalizedCalendar === types_1.DefaultCalendars.UNIX) {
337
427
  const s = (date.getTime() / 1000).toFixed(3);
338
428
  comp.unixSeconds = parseFloat(s);
339
429
  if (opts?.order)
340
430
  comp.order = opts.order;
341
- return this.buildTimePart(comp);
431
+ return (0, tps_string_1.buildTimePart)(comp);
342
432
  }
343
- if (normalizedCalendar === exports.DefaultCalendars.GREG) {
433
+ if (normalizedCalendar === types_1.DefaultCalendars.GREG) {
344
434
  const fullYear = date.getUTCFullYear();
345
435
  comp.millennium = Math.floor(fullYear / 1000) + 1;
346
436
  comp.century = Math.floor((fullYear % 1000) / 100) + 1;
@@ -353,7 +443,7 @@ class TPS {
353
443
  comp.millisecond = date.getUTCMilliseconds();
354
444
  if (opts?.order)
355
445
  comp.order = opts.order;
356
- return this.buildTimePart(comp);
446
+ return (0, tps_string_1.buildTimePart)(comp);
357
447
  }
358
448
  throw new Error(`Calendar driver '${normalizedCalendar}' not implemented. Register a driver.`);
359
449
  }
@@ -382,13 +472,22 @@ class TPS {
382
472
  const parsed = this.parse(tpsString);
383
473
  if (!parsed)
384
474
  return null;
385
- const cal = parsed.calendar || exports.DefaultCalendars.TPS;
386
- const driver = this.drivers.get(cal);
475
+ const cal = parsed.calendar || types_1.DefaultCalendars.TPS;
476
+ const driver = this.driverManager.get(cal);
387
477
  if (!driver) {
388
478
  console.error(`Calendar driver '${cal}' not registered.`);
389
479
  return null;
390
480
  }
391
- return driver.getDateFromComponents(parsed);
481
+ const date = driver.getDateFromComponents(parsed);
482
+ // If the URI has a ;tz= extension, the calendar date was expressed in local
483
+ // time. Convert from local → UTC using the timezone utility.
484
+ const tz = parsed.extensions?.["tz"];
485
+ if (tz && date) {
486
+ const localMs = date.getTime();
487
+ const utcMs = (0, timezone_2.localToUtc)(localMs, tz);
488
+ return new Date(utcMs);
489
+ }
490
+ return date;
392
491
  }
393
492
  // --- DRIVER CONVENIENCE METHODS ---
394
493
  /**
@@ -410,7 +509,7 @@ class TPS {
410
509
  * ```
411
510
  */
412
511
  static parseCalendarDate(calendar, dateString, format) {
413
- const driver = this.drivers.get(calendar);
512
+ const driver = this.driverManager.get(calendar);
414
513
  if (!driver) {
415
514
  throw new Error(`Calendar driver '${calendar}' not found. Register a driver first.`);
416
515
  }
@@ -471,926 +570,319 @@ class TPS {
471
570
  * ```
472
571
  */
473
572
  static formatCalendarDate(calendar, components, format) {
474
- const driver = this.drivers.get(calendar);
573
+ const driver = this.driverManager.get(calendar);
475
574
  if (!driver) {
476
575
  throw new Error(`Calendar driver '${calendar}' not found.`);
477
576
  }
478
577
  // format is guaranteed by the interface, so we can call it directly.
479
578
  return driver.format(components, format);
480
579
  }
481
- // --- INTERNAL HELPERS ---
580
+ // --- CONVENIENCE METHODS ---
482
581
  /**
483
- * Generate the canonical `T:` time string for a set of components. The
484
- * `order` field (or `comp.order`) controls whether tokens are emitted in
485
- * ascending or descending hierarchy; if undefined the default
486
- * `'descending'` orientation is used.
582
+ * Returns a TPS time string for the current moment.
583
+ * Shorthand for `TPS.fromDate(new Date(), calendar, opts)`.
487
584
  *
488
- * Drivers may ignore this helper and produce their own time strings if they
489
- * implement custom ordering logic.
585
+ * @param calendar - Calendar code. Defaults to 'greg'.
586
+ * @param opts - Optional `order` (ASC/DESC) parameter.
587
+ * @returns TPS time string.
588
+ *
589
+ * @example
590
+ * ```ts
591
+ * TPS.now(); // "T:greg.m3.c1.y26.m3.d4.h06.m30.s00.m0"
592
+ * TPS.now('hij'); // "T:hij.y1447.m09.d05.h06.m30.s00"
593
+ * ```
490
594
  */
491
- static buildTimePart(comp) {
492
- const calendar = (comp.calendar || "").toLowerCase();
493
- if (!/^[a-z]{3,4}$/.test(calendar)) {
494
- throw new Error(`Invalid calendar code '${comp.calendar}'. Calendar code width must be 3–4 lowercase letters.`);
495
- }
496
- let time = `T:${calendar}`;
497
- if (calendar === exports.DefaultCalendars.UNIX) {
498
- if (comp.unixSeconds !== undefined) {
499
- time += `.s${comp.unixSeconds}`;
500
- }
501
- return time;
502
- }
503
- // sequence of [prefix, value, rank]
504
- // All four of millennium / month / minute / millisecond share the prefix 'm'.
505
- // Position within the ordered sequence disambiguates them during parsing.
506
- const tokens = [
507
- ["m", comp.millennium, 8], // m-token rank 8 → millennium
508
- ["c", comp.century, 7],
509
- ["y", comp.year, 6],
510
- ["m", comp.month, 5], // m-token rank 5 → month
511
- ["d", comp.day, 4],
512
- ["h", comp.hour, 3],
513
- ["m", comp.minute, 2], // m-token rank 2 → minute
514
- ["s", comp.second, 1],
515
- ["m", comp.millisecond, 0], // m-token rank 0 → millisecond
516
- ];
517
- const order = comp.order || TimeOrder.DESC;
518
- if (order === TimeOrder.ASC)
519
- tokens.reverse();
520
- for (const [pref, val] of tokens) {
521
- if (val !== undefined) {
522
- // seconds may be fractional
523
- time += `.${pref}${val}`;
524
- }
525
- }
526
- if (comp.signature) {
527
- time += `!${comp.signature}`;
528
- }
529
- return time;
595
+ static now(calendar = types_1.DefaultCalendars.GREG, opts) {
596
+ return this.fromDate(new Date(), calendar, opts);
530
597
  }
531
598
  /**
532
- * Parse the *time* portion of a TPS string (optionally beginning with
533
- * `T:`) into components and determine the component ordering. This helper
534
- * accepts tokens in **any** sequence and will return an `order` value of
535
- * `'ascending'` or `'descending'`.
599
+ * Returns the difference in milliseconds between two TPS strings.
600
+ * The result is `t2 - t1`; negative if t1 is after t2.
536
601
  *
537
- * The caller is responsible for stripping off a leading signature or other
538
- * trailer characters; this method will drop anything after `!`, `;`, `?` or
539
- * `#`.
602
+ * @param t1 - First TPS string (subtracted from t2).
603
+ * @param t2 - Second TPS string.
604
+ * @returns Milliseconds between the two moments, or NaN on parse failure.
540
605
  *
541
- * ### `m`-token disambiguation
542
- * All four of millennium (rank 8), month (rank 5), minute (rank 2) and
543
- * millisecond (rank 0) share the single-character prefix `m`. They are told
544
- * apart by their **position relative to the neighbouring tokens**. The
545
- * algorithm is:
606
+ * @example
607
+ * ```ts
608
+ * const ms = TPS.diff('T:greg.m3.c1.y26.m1.d1.h0.m0.s0.m0',
609
+ * 'T:greg.m3.c1.y26.m1.d2.h0.m0.s0.m0');
610
+ * // 86_400_000 (one day)
611
+ * ```
612
+ */
613
+ static diff(t1, t2) {
614
+ const d1 = this.toDate(t1);
615
+ const d2 = this.toDate(t2);
616
+ if (!d1 || !d2)
617
+ return NaN;
618
+ return d2.getTime() - d1.getTime();
619
+ }
620
+ /**
621
+ * Returns a new TPS string shifted by the given duration.
622
+ * The result is in the same calendar as the original string.
546
623
  *
547
- * 1. Pre-scan the non-`m` tokens (c, y, d, h, s) whose ranks are fixed to
548
- * determine whether the string is ascending or descending.
549
- * 2. While iterating, track `lastAssignedRank` the rank of the most
550
- * recently processed token (m or non-m).
551
- * 3. When an `m` token is encountered, derive its rank from `lastAssignedRank`
552
- * and the detected order:
553
- * - **DESC** null → 8 (mill) | rank > 5 → 5 (month) | rank > 2 → 2 (min) | else → 0 (ms)
554
- * - **ASC** null → 0 (ms) | rank < 2 → 2 (min) | rank < 5 → 5 (month) | else → 8 (mill)
624
+ * @param tpsStr - Source TPS string.
625
+ * @param duration - Object with optional `days`, `hours`, `minutes`, `seconds`, `milliseconds`.
626
+ * @returns Shifted TPS string, or null if the input is invalid.
555
627
  *
556
- * @param input - Time fragment (e.g. `"T:greg.m3.c1.y26"` or `"greg.m0.s25.m30"`)
628
+ * @example
629
+ * ```ts
630
+ * const t = 'T:greg.m3.c1.y26.m1.d9.h14.m30.s25.m0';
631
+ * TPS.add(t, { days: 7 }); // one week later
632
+ * TPS.add(t, { hours: -2 }); // two hours earlier
633
+ * ```
557
634
  */
558
- static parseTimeString(input) {
559
- let s = input.trim();
560
- // strip off anything after signature or extensions/query/fragment
561
- s = s.split(/[!;?#]/)[0];
562
- if (s.startsWith("T:"))
563
- s = s.slice(2);
564
- const parts = s.split(".");
565
- if (parts.length === 0)
635
+ static add(tpsStr, duration) {
636
+ const date = this.toDate(tpsStr);
637
+ if (!date)
566
638
  return null;
567
- const calendar = parts[0];
568
- const comp = { calendar };
569
- // Fixed-rank prefixes (unambiguous regardless of position)
570
- const fixedRankMap = {
571
- c: 7,
572
- y: 6,
573
- d: 4,
574
- h: 3,
575
- s: 1,
576
- };
577
- // ── Step 1: pre-scan non-m tokens to estimate order ─────────────────────
578
- // This is only needed to handle the first 'm' token when lastAssignedRank
579
- // is still null (nothing has been seen yet).
580
- let initialOrder = TimeOrder.DESC;
581
- if (calendar !== exports.DefaultCalendars.UNIX) {
582
- const nonMRanks = [];
583
- for (let i = 1; i < parts.length; i++) {
584
- const pr = parts[i]?.charAt(0);
585
- if (pr && pr in fixedRankMap)
586
- nonMRanks.push(fixedRankMap[pr]);
587
- }
588
- if (nonMRanks.length >= 2) {
589
- const isAsc = nonMRanks.every((v, i, a) => i === 0 || a[i - 1] <= v);
590
- if (isAsc)
591
- initialOrder = TimeOrder.ASC;
592
- }
593
- }
594
- // ── Step 2: resolve the semantic rank of an 'm' token ───────────────────
595
- const assignMRank = (lastRank, ord) => {
596
- if (ord === TimeOrder.DESC) {
597
- if (lastRank === null)
598
- return 8; // first token → millennium
599
- if (lastRank > 5)
600
- return 5; // after century / year → month
601
- if (lastRank > 2)
602
- return 2; // after day / hour → minute
603
- return 0; // after second → millisecond
604
- }
605
- else {
606
- if (lastRank === null)
607
- return 0; // first token → millisecond
608
- if (lastRank < 2)
609
- return 2; // after millisecond / second → minute
610
- if (lastRank < 5)
611
- return 5; // after minute / hour / day → month
612
- return 8; // after month / year / cent → millennium
613
- }
614
- };
615
- // ── Step 3: iterate and build components ────────────────────────────────
616
- const ranks = [];
617
- let lastAssignedRank = null;
618
- for (let i = 1; i < parts.length; i++) {
619
- const token = parts[i];
620
- if (!token)
621
- continue;
622
- const prefix = token.charAt(0);
623
- const value = token.slice(1);
624
- // UNIX calendar: single 's' token carries the full unix timestamp
625
- if (calendar === exports.DefaultCalendars.UNIX && prefix === "s") {
626
- comp.unixSeconds = parseFloat(value);
627
- ranks.push(9);
628
- continue;
629
- }
630
- if (prefix === "m") {
631
- const rank = assignMRank(lastAssignedRank, initialOrder);
632
- switch (rank) {
633
- case 8:
634
- comp.millennium = parseInt(value, 10);
635
- break;
636
- case 5:
637
- comp.month = parseInt(value, 10);
638
- break;
639
- case 2:
640
- comp.minute = parseInt(value, 10);
641
- break;
642
- case 0:
643
- comp.millisecond = parseInt(value, 10);
644
- break;
645
- }
646
- ranks.push(rank);
647
- lastAssignedRank = rank;
648
- }
649
- else {
650
- switch (prefix) {
651
- case "c":
652
- comp.century = parseInt(value, 10);
653
- ranks.push(7);
654
- lastAssignedRank = 7;
655
- break;
656
- case "y":
657
- comp.year = parseInt(value, 10);
658
- ranks.push(6);
659
- lastAssignedRank = 6;
660
- break;
661
- case "d":
662
- comp.day = parseInt(value, 10);
663
- ranks.push(4);
664
- lastAssignedRank = 4;
665
- break;
666
- case "h":
667
- comp.hour = parseInt(value, 10);
668
- ranks.push(3);
669
- lastAssignedRank = 3;
670
- break;
671
- case "s":
672
- comp.second = parseFloat(value);
673
- ranks.push(1);
674
- lastAssignedRank = 1;
675
- break;
676
- default:
677
- // unknown prefix – ignore
678
- break;
679
- }
680
- }
681
- }
682
- // ── Step 4: confirm order from the complete rank sequence ────────────────
683
- let order = TimeOrder.DESC;
684
- if (ranks.length > 1) {
685
- const isAsc = ranks.every((v, i, a) => i === 0 || a[i - 1] <= v);
686
- const isDesc = ranks.every((v, i, a) => i === 0 || a[i - 1] >= v);
687
- if (isAsc && !isDesc)
688
- order = TimeOrder.ASC;
689
- // mixed / single direction → defaults to DESC
690
- }
691
- return { components: comp, order };
639
+ const parsed = this.parse(tpsStr);
640
+ const calendar = parsed?.calendar ?? types_1.DefaultCalendars.GREG;
641
+ const order = parsed?.order;
642
+ const deltaMs = (duration.days ?? 0) * 86400000 +
643
+ (duration.hours ?? 0) * 3600000 +
644
+ (duration.minutes ?? 0) * 60000 +
645
+ (duration.seconds ?? 0) * 1000 +
646
+ (duration.milliseconds ?? 0);
647
+ const shifted = new Date(date.getTime() + deltaMs);
648
+ return this.fromDate(shifted, calendar, order ? { order } : undefined);
692
649
  }
650
+ // --- INTERNAL HELPERS ---
693
651
  static _mapGroupsToComponents(g) {
694
652
  const components = {};
695
653
  components.calendar = g.calendar;
696
- // Signature Mapping
654
+ // ── Signature ────────────────────────────────────────────────────────────
697
655
  if (g.signature) {
698
656
  components.signature = g.signature;
699
657
  }
700
- // Actor Mapping
658
+ // ── Actor (/A:...) ────────────────────────────────────────────────────────
701
659
  if (g.actor) {
702
- components.actor = g.actor;
660
+ components.actor = g.actor.trim();
703
661
  }
704
- // Space Mapping
705
- if (g.space) {
706
- // Privacy markers
707
- if (g.space === "unknown" || g.space === "-") {
708
- components.isUnknownLocation = true;
709
- }
710
- else if (g.space === "redacted") {
711
- components.isRedactedLocation = true;
712
- }
713
- else if (g.space === "hidden" || g.space === "~") {
714
- components.isHiddenLocation = true;
715
- }
716
- // Geospatial cells
717
- else if (g.s2) {
718
- components.s2Cell = g.s2;
719
- }
720
- else if (g.h3) {
721
- components.h3Cell = g.h3;
722
- }
723
- else if (g.plus) {
724
- components.plusCode = g.plus;
725
- }
726
- else if (g.w3w) {
727
- components.what3words = g.w3w;
728
- }
729
- // Structural anchors
730
- else if (g.bldg) {
731
- components.building = g.bldg;
732
- if (g.floor)
733
- components.floor = g.floor;
734
- if (g.room)
735
- components.room = g.room;
736
- if (g.zone)
737
- components.zone = g.zone;
738
- }
739
- // Generic pre-@ anchor (adm/node/net/planet/etc)
740
- else if (g.generic) {
741
- components.spaceAnchor = g.generic;
742
- }
743
- // GPS coordinates
744
- else {
745
- if (g.lat)
746
- components.latitude = parseFloat(g.lat);
747
- if (g.lon)
748
- components.longitude = parseFloat(g.lon);
749
- if (g.alt)
750
- components.altitude = parseFloat(g.alt);
751
- }
662
+ // ── Location layers (v0.6.0: multi-layer, ;-separated) ───────────────────
663
+ if (g.location) {
664
+ this._parseLocationLayers(g.location, components);
752
665
  }
753
- // Extensions Mapping
666
+ // ── Extensions (;KEY:val or ;key=val after T: tokens) ────────────────────
754
667
  if (g.extensions) {
755
668
  const extObj = {};
756
- const parts = g.extensions.split(".");
757
- parts.forEach((p) => {
758
- const eqIdx = p.indexOf("=");
759
- if (eqIdx > 0) {
760
- const key = p.substring(0, eqIdx);
761
- const val = p.substring(eqIdx + 1);
762
- if (key && val)
669
+ g.extensions.split(";").forEach((part) => {
670
+ part = part.trim();
671
+ if (!part)
672
+ return;
673
+ const colonIdx = part.indexOf(":");
674
+ const eqIdx = part.indexOf("=");
675
+ if (colonIdx > 0 && (eqIdx < 0 || colonIdx < eqIdx)) {
676
+ // KEY:val form (e.g. TZ:+03:00)
677
+ const key = part.substring(0, colonIdx).toLowerCase();
678
+ const val = part.substring(colonIdx + 1);
679
+ if (key && val !== undefined)
763
680
  extObj[key] = val;
764
681
  }
765
- else {
766
- // Legacy format: first char is key
767
- const key = p.charAt(0);
768
- const val = p.substring(1);
769
- if (key && val)
682
+ else if (eqIdx > 0) {
683
+ // key=val form (e.g. tz=+03:00)
684
+ const key = part.substring(0, eqIdx).toLowerCase();
685
+ const val = part.substring(eqIdx + 1);
686
+ if (key && val !== undefined)
770
687
  extObj[key] = val;
771
688
  }
772
689
  });
773
- components.extensions = extObj;
690
+ if (Object.keys(extObj).length > 0)
691
+ components.extensions = extObj;
692
+ }
693
+ // ── Context (#C:key=val;key=val) ─────────────────────────────────────────
694
+ if (g.context) {
695
+ const ctx = {};
696
+ g.context.split(";").forEach((part) => {
697
+ part = part.trim();
698
+ if (!part)
699
+ return;
700
+ const eqIdx = part.indexOf("=");
701
+ if (eqIdx > 0) {
702
+ ctx[part.substring(0, eqIdx)] = part.substring(eqIdx + 1);
703
+ }
704
+ });
705
+ if (Object.keys(ctx).length > 0)
706
+ components.context = ctx;
774
707
  }
775
708
  return components;
776
709
  }
777
- }
778
- exports.TPS = TPS;
779
- // --- PLUGIN REGISTRY ---
780
- TPS.drivers = new Map();
781
- // --- REGEX ---
782
- // Updated for v0.5.0: supports L: anchors, A: actor, ! signature, structural & geospatial anchors
783
- // Tokens may appear in any order; actual semantic parsing happens in
784
- // `parseTimeString()` so these patterns are intentionally permissive.
785
- // regex simply ensures prefix, space, calendar, and token characters;
786
- // token order is not enforced (parseTimeString handles semantics).
787
- TPS.REGEX_URI = new RegExp("^tps://" +
788
- // Location part (preserve named captures for space subfields)
789
- "(?:L:)?(?<space>" +
790
- "~|-|unknown|redacted|hidden|" +
791
- "s2=(?<s2>[a-fA-F0-9]+)|" +
792
- "h3=(?<h3>[a-fA-F0-9]+)|" +
793
- "plus=(?<plus>[A-Z0-9+]+)|" +
794
- "w3w=(?<w3w>[a-z]+\\.[a-z]+\\.[a-z]+)|" +
795
- "bldg=(?<bldg>[\\w-]+)(?:\\.floor=(?<floor>[\\w-]+))?(?:\\.room=(?<room>[\\w-]+))?(?:\\.zone=(?<zone>[\\w-]+))?|" +
796
- "(?<lat>-?\\d+(?:\\.\\d+)?),(?<lon>-?\\d+(?:\\.\\d+)?)(?:,(?<alt>-?\\d+(?:\\.\\d+)?)m?)?|" +
797
- "(?<generic>[^@/?#]+)" +
798
- ")" +
799
- "(?:/A:(?<actor>[^/@]+))?" +
800
- "@T:(?<calendar>[a-z]{3,4})" +
801
- "(?:\\.(?:m-?\\d+|c\\d+|y\\d+|d\\d{1,2}|h\\d{1,2}|s\\d+(?:\\.\\d+)?))*" +
802
- "(?:![^;?#]+)?" +
803
- "(?:;(?<extensions>[^?#]+))?" +
804
- "(?:\\?[^#]+)?" +
805
- "(?:#.+)?$");
806
- TPS.REGEX_TIME = new RegExp("^T:(?<calendar>[a-z]{3,4})" +
807
- "(?:\\.(?:m-?\\d+|c\\d+|y\\d+|d\\d{1,2}|h\\d{1,2}|s\\d+(?:\\.\\d+)?))*" +
808
- "(?:![^;?#]+)?$");
809
- // register built-in drivers and set default
810
- // (tps and gregorian provide canonical conversions before unix)
811
- TPS.registerDriver(new tps_1.TpsDriver());
812
- TPS.registerDriver(new gregorian_1.GregorianDriver());
813
- TPS.registerDriver(new unix_1.UnixDriver());
814
- TPS.registerDriver(new persian_1.PersianDriver());
815
- TPS.registerDriver(new hijri_1.HijriDriver());
816
- TPS.registerDriver(new julian_1.JulianDriver());
817
- TPS.registerDriver(new holocene_1.HoloceneDriver());
818
- /**
819
- * TPS-UID v1 — Temporal Positioning System Identifier (Binary Reversible)
820
- *
821
- * A time-first, reversible identifier that binds an event to a TPS coordinate.
822
- * Unlike UUIDs, TPS-UID identifies events in spacetime and allows exact
823
- * reconstruction of the original TPS string.
824
- *
825
- * Binary Schema (all integers big-endian):
826
- * ```
827
- * MAGIC 4 bytes "TPU7"
828
- * VER 1 byte 0x01
829
- * FLAGS 1 byte bit0 = compression flag
830
- * TIME 6 bytes epoch_ms (48-bit unsigned)
831
- * NONCE 4 bytes 32-bit random
832
- * LEN varint length of TPS payload
833
- * TPS bytes UTF-8 TPS string (raw or zlib-compressed)
834
- * ```
835
- *
836
- * @example
837
- * ```ts
838
- * const tps = 'tps://31.95,35.91@T:greg.m3.c1.y26.m01.d09';
839
- *
840
- * // Encode to binary
841
- * const bytes = TPSUID7RB.encodeBinary(tps);
842
- *
843
- * // Encode to base64url string
844
- * const id = TPSUID7RB.encodeBinaryB64(tps);
845
- * // → "tpsuid7rb_AFRQV..."
846
- *
847
- * // Decode back to original TPS
848
- * const decoded = TPSUID7RB.decodeBinaryB64(id);
849
- * console.log(decoded.tps); // exact original TPS
850
- * ```
851
- */
852
- class TPSUID7RB {
853
- // ---------------------------
854
- // Public API
855
- // ---------------------------
856
- /**
857
- * Encode TPS string to binary bytes (Uint8Array).
858
- * This is the canonical form for hashing, signing, and storage.
859
- *
860
- * @param tps - The TPS string to encode
861
- * @param opts - Encoding options (compress, epochMs override)
862
- * @returns Binary TPS-UID as Uint8Array
863
- */
864
- static encodeBinary(tps, opts = {}) {
865
- const compress = opts.compress ?? false;
866
- const epochMs = opts.epochMs ?? this.epochMsFromTPSString(tps);
867
- if (!Number.isInteger(epochMs) || epochMs < 0) {
868
- throw new Error("epochMs must be a non-negative integer");
869
- }
870
- if (epochMs > 0xffffffffffff) {
871
- throw new Error("epochMs exceeds 48-bit range");
872
- }
873
- const flags = compress ? 0x01 : 0x00;
874
- // Generate 32-bit nonce
875
- const nonceBuf = this.randomBytes(4);
876
- const nonce = ((nonceBuf[0] << 24) >>> 0) +
877
- ((nonceBuf[1] << 16) >>> 0) +
878
- ((nonceBuf[2] << 8) >>> 0) +
879
- nonceBuf[3];
880
- // Encode TPS to UTF-8
881
- const tpsUtf8 = new TextEncoder().encode(tps);
882
- // Optionally compress
883
- const payload = compress ? this.deflateRaw(tpsUtf8) : tpsUtf8;
884
- // Encode length as varint
885
- const lenVar = this.uvarintEncode(payload.length);
886
- // Construct binary structure
887
- const out = new Uint8Array(4 + 1 + 1 + 6 + 4 + lenVar.length + payload.length);
888
- let offset = 0;
889
- // MAGIC
890
- out.set(this.MAGIC, offset);
891
- offset += 4;
892
- // VER
893
- out[offset++] = this.VER;
894
- // FLAGS
895
- out[offset++] = flags;
896
- // TIME (48-bit big-endian)
897
- const timeBytes = this.writeU48(epochMs);
898
- out.set(timeBytes, offset);
899
- offset += 6;
900
- // NONCE (32-bit big-endian)
901
- out.set(nonceBuf, offset);
902
- offset += 4;
903
- // LEN (varint)
904
- out.set(lenVar, offset);
905
- offset += lenVar.length;
906
- // TPS payload
907
- out.set(payload, offset);
908
- return out;
909
- }
910
- /**
911
- * Decode binary bytes back to original TPS string.
912
- *
913
- * @param bytes - Binary TPS-UID
914
- * @returns Decoded result with original TPS string
915
- */
916
- static decodeBinary(bytes) {
917
- // Header min size: 4+1+1+6+4 + 1 (at least 1 byte varint) = 17
918
- if (bytes.length < 17) {
919
- throw new Error("TPSUID7RB: too short");
920
- }
921
- // MAGIC
922
- if (bytes[0] !== 0x54 ||
923
- bytes[1] !== 0x50 ||
924
- bytes[2] !== 0x55 ||
925
- bytes[3] !== 0x37) {
926
- throw new Error("TPSUID7RB: bad magic");
927
- }
928
- // VERSION
929
- const ver = bytes[4];
930
- if (ver !== this.VER) {
931
- throw new Error(`TPSUID7RB: unsupported version ${ver}`);
932
- }
933
- // FLAGS
934
- const flags = bytes[5];
935
- const compressed = (flags & 0x01) === 0x01;
936
- // TIME (48-bit big-endian)
937
- const epochMs = this.readU48(bytes, 6);
938
- // NONCE (32-bit big-endian)
939
- const nonce = ((bytes[12] << 24) >>> 0) +
940
- ((bytes[13] << 16) >>> 0) +
941
- ((bytes[14] << 8) >>> 0) +
942
- bytes[15];
943
- // LEN (varint at offset 16)
944
- let offset = 16;
945
- const { value: tpsLen, bytesRead } = this.uvarintDecode(bytes, offset);
946
- offset += bytesRead;
947
- if (offset + tpsLen > bytes.length) {
948
- throw new Error("TPSUID7RB: length overflow");
949
- }
950
- // TPS payload
951
- const payload = bytes.slice(offset, offset + tpsLen);
952
- const tpsUtf8 = compressed ? this.inflateRaw(payload) : payload;
953
- const tps = new TextDecoder().decode(tpsUtf8);
954
- return { version: "tpsuid7rb", epochMs, compressed, nonce, tps };
955
- }
956
710
  /**
957
- * Encode TPS to base64url string with prefix.
958
- * This is the transport/storage form.
711
+ * Parses a multi-layer location string (before @T:) into component fields.
712
+ * Layers are `;`-separated. Each layer is identified by its prefix token.
959
713
  *
960
- * @param tps - The TPS string to encode
961
- * @param opts - Encoding options
962
- * @returns Base64url encoded TPS-UID with prefix
714
+ * Supported layers:
715
+ * L:lat,lon[,altm] — GPS
716
+ * L:~|L:-|L:redacted — Privacy markers
717
+ * P:cc=JO,ci=AMM,... — Place (country/city codes and names)
718
+ * S2:token — S2 cell
719
+ * H3:token — H3 cell
720
+ * 3W:word.word.word — What3Words
721
+ * plus:token — Plus Code
722
+ * net:ip4:x.x.x.x — IPv4
723
+ * net:ip6:x::x — IPv6
724
+ * node:name — Logical node/host
725
+ * bldg:name — Building
726
+ * floor:x — Floor
727
+ * room:x — Room
728
+ * door:x — Door
729
+ * zone:x — Zone
963
730
  */
964
- static encodeBinaryB64(tps, opts) {
965
- const bytes = this.encodeBinary(tps, opts ?? {});
966
- return `${this.PREFIX}${this.base64UrlEncode(bytes)}`;
967
- }
968
- /**
969
- * Decode base64url string back to original TPS string.
970
- *
971
- * @param id - Base64url encoded TPS-UID with prefix
972
- * @returns Decoded result with original TPS string
973
- */
974
- static decodeBinaryB64(id) {
975
- const s = id.trim();
976
- if (!s.startsWith(this.PREFIX)) {
977
- throw new Error("TPSUID7RB: missing prefix");
978
- }
979
- const b64 = s.slice(this.PREFIX.length);
980
- const bytes = this.base64UrlDecode(b64);
981
- return this.decodeBinary(bytes);
982
- }
983
- /**
984
- * Validate base64url encoded TPS-UID format.
985
- * Note: This validates shape only; binary decode is authoritative.
986
- *
987
- * @param id - String to validate
988
- * @returns true if format is valid
989
- */
990
- static validateBinaryB64(id) {
991
- return this.REGEX.test(id.trim());
992
- }
993
- /**
994
- * Generate a TPS-UID from the current time and optional location.
995
- *
996
- * @param opts - Generation options
997
- * @returns Base64url encoded TPS-UID
998
- */
999
- static generate(opts) {
1000
- const now = new Date();
1001
- const time = TPS.fromDate(now, exports.DefaultCalendars.TPS, {
1002
- order: opts?.order,
1003
- });
1004
- let space = "unknown";
1005
- if (opts?.latitude !== undefined && opts?.longitude !== undefined) {
1006
- space = `${opts.latitude},${opts.longitude}`;
1007
- if (opts.altitude !== undefined) {
1008
- space += `,${opts.altitude}m`;
731
+ static _parseLocationLayers(location, components) {
732
+ const layers = location.trim().split(";");
733
+ for (const layer of layers) {
734
+ const l = layer.trim();
735
+ if (!l)
736
+ continue;
737
+ // Privacy shorthand
738
+ if (l === "L:~" || l === "L:hidden") {
739
+ components.isHiddenLocation = true;
740
+ continue;
1009
741
  }
1010
- }
1011
- const tps = `tps://${space}@${time}`;
1012
- return this.encodeBinaryB64(tps, {
1013
- compress: opts?.compress,
1014
- epochMs: now.getTime(),
1015
- });
1016
- }
1017
- // ---------------------------
1018
- // TPS String Helpers
1019
- // ---------------------------
1020
- /**
1021
- * Generate a TPS string from a Date and optional location.
1022
- */
1023
- /**
1024
- * Parse epoch milliseconds from a TPS string.
1025
- * Supports both URI format (tps://...) and time-only format (T:greg...)
1026
- */
1027
- static epochMsFromTPSString(tps) {
1028
- const date = TPS.toDate(tps);
1029
- if (date)
1030
- return date.getTime();
1031
- // If parse fails due to unsupported/extended extension payloads,
1032
- // strip extensions/query/fragment and retry. Epoch only depends on time.
1033
- const stripped = tps.replace(/;[^?#]*/, "").replace(/[?#].*$/, "");
1034
- const retryDate = TPS.toDate(stripped);
1035
- if (!retryDate)
1036
- throw new Error("TPS: unable to parse date for epoch");
1037
- return retryDate.getTime();
1038
- }
1039
- // ---------------------------
1040
- // Binary Helpers
1041
- // ---------------------------
1042
- /** Write 48-bit unsigned integer (big-endian) */
1043
- static writeU48(epochMs) {
1044
- const b = new Uint8Array(6);
1045
- // Use BigInt for proper 48-bit handling
1046
- const v = BigInt(epochMs);
1047
- b[0] = Number((v >> 40n) & 0xffn);
1048
- b[1] = Number((v >> 32n) & 0xffn);
1049
- b[2] = Number((v >> 24n) & 0xffn);
1050
- b[3] = Number((v >> 16n) & 0xffn);
1051
- b[4] = Number((v >> 8n) & 0xffn);
1052
- b[5] = Number(v & 0xffn);
1053
- return b;
1054
- }
1055
- /** Read 48-bit unsigned integer (big-endian) */
1056
- static readU48(bytes, offset) {
1057
- const v = (BigInt(bytes[offset]) << 40n) +
1058
- (BigInt(bytes[offset + 1]) << 32n) +
1059
- (BigInt(bytes[offset + 2]) << 24n) +
1060
- (BigInt(bytes[offset + 3]) << 16n) +
1061
- (BigInt(bytes[offset + 4]) << 8n) +
1062
- BigInt(bytes[offset + 5]);
1063
- const n = Number(v);
1064
- if (!Number.isSafeInteger(n)) {
1065
- throw new Error("TPSUID7RB: u48 not safe integer");
1066
- }
1067
- return n;
1068
- }
1069
- /** Encode unsigned integer as LEB128 varint */
1070
- static uvarintEncode(n) {
1071
- if (!Number.isInteger(n) || n < 0) {
1072
- throw new Error("uvarint must be non-negative int");
1073
- }
1074
- const out = [];
1075
- let x = n >>> 0;
1076
- while (x >= 0x80) {
1077
- out.push((x & 0x7f) | 0x80);
1078
- x >>>= 7;
1079
- }
1080
- out.push(x);
1081
- return new Uint8Array(out);
1082
- }
1083
- /** Decode LEB128 varint */
1084
- static uvarintDecode(bytes, offset) {
1085
- let x = 0;
1086
- let s = 0;
1087
- let i = 0;
1088
- while (true) {
1089
- if (offset + i >= bytes.length) {
1090
- throw new Error("uvarint overflow");
742
+ if (l === "L:-" || l === "L:unknown") {
743
+ components.isUnknownLocation = true;
744
+ continue;
1091
745
  }
1092
- const b = bytes[offset + i];
1093
- if (b < 0x80) {
1094
- if (i > 9 || (i === 9 && b > 1)) {
1095
- throw new Error("uvarint too large");
746
+ if (l === "L:redacted") {
747
+ components.isRedactedLocation = true;
748
+ continue;
749
+ }
750
+ // P: Place layer — P:cc=JO,ci=AMM,cn=Jordan,ct=Amman
751
+ if (l.startsWith("P:")) {
752
+ l.slice(2).split(",").forEach((pair) => {
753
+ const eq = pair.indexOf("=");
754
+ if (eq < 1)
755
+ return;
756
+ const k = pair.substring(0, eq).toLowerCase();
757
+ const v = pair.substring(eq + 1);
758
+ if (k === "cc")
759
+ components.placeCountryCode = v;
760
+ else if (k === "cn")
761
+ components.placeCountryName = v;
762
+ else if (k === "ci")
763
+ components.placeCityCode = v;
764
+ else if (k === "ct")
765
+ components.placeCityName = v;
766
+ });
767
+ continue;
768
+ }
769
+ // GPS coordinates (L:lat,lon[,alt])
770
+ if (l.startsWith("L:")) {
771
+ const coords = l.slice(2);
772
+ const m = coords.match(/^(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)(?:,(-?\d+(?:\.\d+)?)m?)?$/);
773
+ if (m) {
774
+ components.latitude = parseFloat(m[1]);
775
+ components.longitude = parseFloat(m[2]);
776
+ if (m[3])
777
+ components.altitude = parseFloat(m[3]);
1096
778
  }
1097
- x |= b << s;
1098
- return { value: x >>> 0, bytesRead: i + 1 };
779
+ continue;
1099
780
  }
1100
- x |= (b & 0x7f) << s;
1101
- s += 7;
1102
- i++;
1103
- if (i > 10) {
1104
- throw new Error("uvarint too long");
781
+ // Geospatial cells
782
+ if (/^S2:/i.test(l)) {
783
+ components.s2Cell = l.slice(3);
784
+ continue;
1105
785
  }
1106
- }
1107
- }
1108
- // ---------------------------
1109
- // Base64url Helpers
1110
- // ---------------------------
1111
- /** Encode bytes to base64url (no padding) */
1112
- static base64UrlEncode(bytes) {
1113
- // Node.js environment
1114
- if (typeof Buffer !== "undefined") {
1115
- return Buffer.from(bytes)
1116
- .toString("base64")
1117
- .replace(/\+/g, "-")
1118
- .replace(/\//g, "_")
1119
- .replace(/=+$/g, "");
1120
- }
1121
- // Browser environment
1122
- let binary = "";
1123
- for (let i = 0; i < bytes.length; i++) {
1124
- binary += String.fromCharCode(bytes[i]);
1125
- }
1126
- return btoa(binary)
1127
- .replace(/\+/g, "-")
1128
- .replace(/\//g, "_")
1129
- .replace(/=+$/g, "");
1130
- }
1131
- /** Decode base64url to bytes */
1132
- static base64UrlDecode(b64url) {
1133
- // Add padding
1134
- const padLen = (4 - (b64url.length % 4)) % 4;
1135
- const b64 = (b64url + "=".repeat(padLen))
1136
- .replace(/-/g, "+")
1137
- .replace(/_/g, "/");
1138
- // Node.js environment
1139
- if (typeof Buffer !== "undefined") {
1140
- return new Uint8Array(Buffer.from(b64, "base64"));
1141
- }
1142
- // Browser environment
1143
- const binary = atob(b64);
1144
- const bytes = new Uint8Array(binary.length);
1145
- for (let i = 0; i < binary.length; i++) {
1146
- bytes[i] = binary.charCodeAt(i);
1147
- }
1148
- return bytes;
1149
- }
1150
- // ---------------------------
1151
- // Compression Helpers
1152
- // ---------------------------
1153
- /** Compress using zlib deflate raw */
1154
- static deflateRaw(data) {
1155
- // Node.js environment
1156
- if (typeof require !== "undefined") {
1157
- try {
1158
- // eslint-disable-next-line @typescript-eslint/no-require-imports
1159
- const zlib = require("zlib");
1160
- return new Uint8Array(zlib.deflateRawSync(Buffer.from(data)));
786
+ if (/^H3:/i.test(l)) {
787
+ components.h3Cell = l.slice(3);
788
+ continue;
1161
789
  }
1162
- catch {
1163
- throw new Error("TPSUID7RB: compression not available");
790
+ if (/^3W:/i.test(l)) {
791
+ components.what3words = l.slice(3);
792
+ continue;
1164
793
  }
1165
- }
1166
- // Browser: would need pako or similar library
1167
- throw new Error("TPSUID7RB: compression not available in browser");
1168
- }
1169
- /** Decompress using zlib inflate raw */
1170
- static inflateRaw(data) {
1171
- // Node.js environment
1172
- if (typeof require !== "undefined") {
1173
- try {
1174
- // eslint-disable-next-line @typescript-eslint/no-require-imports
1175
- const zlib = require("zlib");
1176
- return new Uint8Array(zlib.inflateRawSync(Buffer.from(data)));
794
+ if (/^plus:/i.test(l)) {
795
+ components.plusCode = l.slice(5);
796
+ continue;
1177
797
  }
1178
- catch {
1179
- throw new Error("TPSUID7RB: decompression failed");
798
+ // Network
799
+ if (/^net:ip4:/i.test(l)) {
800
+ components.ipv4 = l.slice(8);
801
+ continue;
1180
802
  }
1181
- }
1182
- // Browser: would need pako or similar library
1183
- throw new Error("TPSUID7RB: decompression not available in browser");
1184
- }
1185
- // ---------------------------
1186
- // Cryptographic Sealing (Ed25519)
1187
- // ---------------------------
1188
- /**
1189
- * Seal (sign) a TPS string to create a cryptographically verifiable TPS-UID.
1190
- * This appends an Ed25519 signature to the binary form.
1191
- *
1192
- * @param tps - The TPS string to seal
1193
- * @param privateKey - Ed25519 private key (hex or buffer)
1194
- * @param opts - Encoding options
1195
- * @returns Sealed binary TPS-UID
1196
- */
1197
- static seal(tps, privateKey, opts) {
1198
- // 1. Create standard binary (unsealed first)
1199
- // We force the SEAL flag (bit 1) to be 0 initially for the "content to sign"
1200
- // But wait, we want the signature to cover the header too.
1201
- // Strategy: Construct the full binary with SEAL flag OFF, sign it, then set SEAL flag ON and append sig.
1202
- // Actually, the standard way is:
1203
- // Content = MAGIC + VER + FLAGS(with seal bit set) + TIME + NONCE + LEN + PAYLOAD
1204
- // Signature = Sign(Content)
1205
- // Final = Content + SEAL_TYPE + SIGNATURE
1206
- const compress = opts?.compress ?? false;
1207
- const epochMs = opts?.epochMs ?? this.epochMsFromTPSString(tps);
1208
- // Validate epoch
1209
- if (!Number.isInteger(epochMs) || epochMs < 0 || epochMs > 0xffffffffffff) {
1210
- throw new Error("epochMs must be a valid 48-bit non-negative integer");
1211
- }
1212
- // Flags: Bit 0 = compress, Bit 1 = sealed
1213
- const flags = (compress ? 0x01 : 0x00) | 0x02; // Set SEAL bit
1214
- // Generate Nonce
1215
- const nonceBuf = this.randomBytes(4);
1216
- // Encode Payload
1217
- const tpsUtf8 = new TextEncoder().encode(tps);
1218
- const payload = compress ? this.deflateRaw(tpsUtf8) : tpsUtf8;
1219
- const lenVar = this.uvarintEncode(payload.length);
1220
- // Construct Content (Header + Payload)
1221
- const contentLen = 4 + 1 + 1 + 6 + 4 + lenVar.length + payload.length;
1222
- const content = new Uint8Array(contentLen);
1223
- let offset = 0;
1224
- content.set(this.MAGIC, offset);
1225
- offset += 4;
1226
- content[offset++] = this.VER;
1227
- content[offset++] = flags;
1228
- content.set(this.writeU48(epochMs), offset);
1229
- offset += 6;
1230
- content.set(nonceBuf, offset);
1231
- offset += 4;
1232
- content.set(lenVar, offset);
1233
- offset += lenVar.length;
1234
- content.set(payload, offset);
1235
- // Sign the content
1236
- const signature = this.signEd25519(content, privateKey);
1237
- const sealType = 0x01; // Ed25519
1238
- // Final Output: Content + SealType (1) + Signature (64)
1239
- const final = new Uint8Array(contentLen + 1 + signature.length);
1240
- final.set(content, 0);
1241
- final.set([sealType], contentLen);
1242
- final.set(signature, contentLen + 1);
1243
- return final;
1244
- }
1245
- /**
1246
- * Verify a sealed TPS-UID and decode it.
1247
- * Throws if signature is invalid or not sealed.
1248
- *
1249
- * @param sealedBytes - The binary sealed TPS-UID
1250
- * @param publicKey - Ed25519 public key (hex or buffer) to verify against
1251
- * @returns Decoded result
1252
- */
1253
- static verifyAndDecode(sealedBytes, publicKey) {
1254
- if (sealedBytes.length < 18)
1255
- throw new Error("TPSUID7RB: too short");
1256
- // Check Magic
1257
- if (sealedBytes[0] !== 0x54 ||
1258
- sealedBytes[1] !== 0x50 ||
1259
- sealedBytes[2] !== 0x55 ||
1260
- sealedBytes[3] !== 0x37) {
1261
- throw new Error("TPSUID7RB: bad magic");
1262
- }
1263
- // Check Flags for Sealed Bit (bit 1)
1264
- const flags = sealedBytes[5];
1265
- if ((flags & 0x02) === 0) {
1266
- throw new Error("TPSUID7RB: not a sealed UID");
1267
- }
1268
- // 1. Parse the structure to find where content ends
1269
- // We need to parse LEN and Payload to find the split point
1270
- let offset = 16; // Start of LEN
1271
- // Decode LEN
1272
- const { value: tpsLen, bytesRead } = this.uvarintDecode(sealedBytes, offset);
1273
- offset += bytesRead;
1274
- const payloadEnd = offset + tpsLen;
1275
- if (payloadEnd > sealedBytes.length) {
1276
- throw new Error("TPSUID7RB: length overflow (truncated)");
1277
- }
1278
- // The Content to verify matches exactly [0 ... payloadEnd]
1279
- const content = sealedBytes.slice(0, payloadEnd);
1280
- // After content: SealType (1 byte) + Signature
1281
- if (sealedBytes.length <= payloadEnd + 1) {
1282
- throw new Error("TPSUID7RB: missing signature data");
1283
- }
1284
- const sealType = sealedBytes[payloadEnd];
1285
- if (sealType !== 0x01) {
1286
- throw new Error(`TPSUID7RB: unsupported seal type 0x${sealType.toString(16)}`);
1287
- }
1288
- const signature = sealedBytes.slice(payloadEnd + 1);
1289
- if (signature.length !== 64) {
1290
- throw new Error(`TPSUID7RB: invalid Ed25519 signature length ${signature.length}`);
1291
- }
1292
- // Verify
1293
- const isValid = this.verifyEd25519(content, signature, publicKey);
1294
- if (!isValid) {
1295
- throw new Error("TPSUID7RB: signature verification failed");
1296
- }
1297
- // Decode (reuse standard logic, but ignoring the extra bytes at end is fine?)
1298
- // Actually standard logic doesn't expect trailing bytes unless we tell it to.
1299
- // But since we verified, we can just slice the content and decode that as a strict binary
1300
- // EXCEPT standard decodeBinary checks strict length.
1301
- // So we manually decode the components here to be safe and efficient.
1302
- return this.decodeBinary(content); // Reuse strict decoder on the content part
1303
- }
1304
- // --- Crypto Implementation (Ed25519) ---
1305
- static signEd25519(data, privateKey) {
1306
- if (typeof require !== "undefined") {
1307
- try {
1308
- // eslint-disable-next-line @typescript-eslint/no-require-imports
1309
- const crypto = require("crypto");
1310
- let key;
1311
- if (typeof privateKey === "string") {
1312
- if (privateKey.includes("PRIVATE KEY")) {
1313
- // PEM format — use directly
1314
- key = privateKey;
1315
- }
1316
- else {
1317
- // Hex-encoded DER/PKCS8
1318
- key = crypto.createPrivateKey({
1319
- key: Buffer.from(privateKey, "hex"),
1320
- format: "der",
1321
- type: "pkcs8",
1322
- });
1323
- }
1324
- }
1325
- else if (typeof privateKey === "object" &&
1326
- privateKey !== null &&
1327
- "asymmetricKeyType" in privateKey) {
1328
- // Node.js KeyObject (e.g. from crypto.generateKeyPairSync)
1329
- key = privateKey;
1330
- }
1331
- else {
1332
- // Buffer or Uint8Array — assume DER/PKCS8 encoded
1333
- key = crypto.createPrivateKey({
1334
- key: Buffer.from(privateKey),
1335
- format: "der",
1336
- type: "pkcs8",
1337
- });
1338
- }
1339
- return new Uint8Array(crypto.sign(null, data, key));
803
+ if (/^net:ip6:/i.test(l)) {
804
+ components.ipv6 = l.slice(8);
805
+ continue;
1340
806
  }
1341
- catch (e) {
1342
- throw new Error("TPSUID7RB: signing failed (check key format)");
807
+ if (/^node:/i.test(l)) {
808
+ components.nodeName = l.slice(5);
809
+ continue;
1343
810
  }
1344
- }
1345
- throw new Error("TPSUID7RB: signing not available in browser");
1346
- }
1347
- static verifyEd25519(data, signature, publicKey) {
1348
- if (typeof require !== "undefined") {
1349
- try {
1350
- // eslint-disable-next-line @typescript-eslint/no-require-imports
1351
- const crypto = require("crypto");
1352
- return crypto.verify(null, data, publicKey, signature);
811
+ // Structural
812
+ if (/^bldg:/i.test(l)) {
813
+ components.building = l.slice(5);
814
+ continue;
1353
815
  }
1354
- catch {
1355
- return false;
816
+ if (/^floor:/i.test(l)) {
817
+ components.floor = l.slice(6);
818
+ continue;
1356
819
  }
1357
- }
1358
- throw new Error("TPSUID7RB: verification not available in browser");
1359
- }
1360
- // ---------------------------
1361
- // Random Bytes
1362
- // ---------------------------
1363
- /** Generate cryptographically secure random bytes */
1364
- static randomBytes(length) {
1365
- // Node.js environment
1366
- if (typeof require !== "undefined") {
1367
- try {
1368
- // eslint-disable-next-line @typescript-eslint/no-require-imports
1369
- const crypto = require("crypto");
1370
- return new Uint8Array(crypto.randomBytes(length));
820
+ if (/^room:/i.test(l)) {
821
+ components.room = l.slice(5);
822
+ continue;
1371
823
  }
1372
- catch {
1373
- // Fallback to crypto.getRandomValues
824
+ if (/^door:/i.test(l)) {
825
+ components.door = l.slice(5);
826
+ continue;
827
+ }
828
+ if (/^zone:/i.test(l)) {
829
+ components.zone = l.slice(5);
830
+ continue;
831
+ }
832
+ // Fallback: generic space anchor (adm:, planet:, legacy strings)
833
+ if (l) {
834
+ components.spaceAnchor = components.spaceAnchor
835
+ ? components.spaceAnchor + ";" + l
836
+ : l;
1374
837
  }
1375
838
  }
1376
- // Browser or fallback
1377
- if (typeof crypto !== "undefined" && crypto.getRandomValues) {
1378
- const bytes = new Uint8Array(length);
1379
- crypto.getRandomValues(bytes);
1380
- return bytes;
1381
- }
1382
- throw new Error("TPSUID7RB: no crypto available");
1383
839
  }
1384
840
  }
1385
- exports.TPSUID7RB = TPSUID7RB;
1386
- /** Magic bytes: "TPU7" */
1387
- TPSUID7RB.MAGIC = new Uint8Array([0x54, 0x50, 0x55, 0x37]);
1388
- /** Version 1 */
1389
- TPSUID7RB.VER = 0x01;
1390
- /** String prefix for base64url encoded form */
1391
- TPSUID7RB.PREFIX = "tpsuid7rb_";
1392
- /** Regex for validating base64url encoded form */
1393
- TPSUID7RB.REGEX = /^tpsuid7rb_[A-Za-z0-9_-]+$/;
841
+ exports.TPS = TPS;
842
+ // --- PLUGIN REGISTRY ---
843
+ /** Shared DriverManager instance use TPS.driverManager for direct access. */
844
+ TPS.driverManager = new driver_manager_2.DriverManager();
845
+ // --- REGEX (v0.6.0) ---
846
+ // The URI and time regexes are intentionally permissive in the location &
847
+ // extension sections — detailed semantic parsing happens in
848
+ // _mapGroupsToComponents() and the layer parsers below.
849
+ //
850
+ // Structure:
851
+ // tps://[location]/A:[actor]@T:[cal].[tokens];[ext];...#C:[ctx];...
852
+ //
853
+ // The `;` separator is used consistently:
854
+ // - between location layers (before @T:)
855
+ // - between extensions (after T: tokens, before #)
856
+ // - between context key=val pairs (after #C:)
857
+ TPS.REGEX_URI = new RegExp("^tps://" +
858
+ // Location: everything up to optional /A: actor and then @T:
859
+ "(?<location>[^@]+?)" +
860
+ // Optional actor overlay
861
+ "(?:/A:(?<actor>[^@]+))?" +
862
+ // Time section
863
+ "@T:(?<calendar>[a-z]{3,4})" +
864
+ "(?<tokens>(?:\\.[a-z]-?[\\d.]+)*)" +
865
+ // Optional signature
866
+ "(?:!(?<signature>[^;#]+))?" +
867
+ // Optional extensions (;KEY:val;key=val;...)
868
+ "(?:;(?<extensions>[^#]+))?" +
869
+ // Optional context fragment (#C:key=val;...)
870
+ "(?:#C:(?<context>.+))?$");
871
+ TPS.REGEX_TIME = new RegExp("^T:(?<calendar>[a-z]{3,4})" +
872
+ "(?<tokens>(?:\\.[a-z]-?[\\d.]+)*)" +
873
+ "(?:!(?<signature>[^;#]+))?" +
874
+ "(?:;(?<extensions>[^#]+))?" +
875
+ "(?:#C:(?<context>.+))?$");
876
+ // register built-in drivers and set default
877
+ // (tps and gregorian provide canonical conversions before unix)
878
+ TPS.registerDriver(new tps_1.TpsDriver());
879
+ TPS.registerDriver(new gregorian_1.GregorianDriver());
880
+ TPS.registerDriver(new unix_1.UnixDriver());
881
+ TPS.registerDriver(new persian_1.PersianDriver());
882
+ TPS.registerDriver(new hijri_1.HijriDriver());
883
+ TPS.registerDriver(new julian_1.JulianDriver());
884
+ TPS.registerDriver(new holocene_1.HoloceneDriver());
885
+ TPS.registerDriver(new chinese_1.ChineseDriver());
1394
886
  /**
1395
887
  * `TpsDate` is a Date-like wrapper with native TPS conversion helpers.
1396
888
  *
@@ -1401,161 +893,4 @@ TPSUID7RB.REGEX = /^tpsuid7rb_[A-Za-z0-9_-]+$/;
1401
893
  * - `new TpsDate(tpsString)`
1402
894
  * - `new TpsDate(year, monthIndex, day?, hour?, minute?, second?, ms?)`
1403
895
  */
1404
- class TpsDate {
1405
- getTpsComponents() {
1406
- const parsed = TPS.parse(this.toTPS(exports.DefaultCalendars.TPS));
1407
- if (!parsed) {
1408
- throw new Error("TpsDate: failed to derive TPS components");
1409
- }
1410
- return parsed;
1411
- }
1412
- getTpsFullYear() {
1413
- const comp = this.getTpsComponents();
1414
- return (comp.millennium - 1) * 1000 + (comp.century - 1) * 100 + comp.year;
1415
- }
1416
- constructor(...args) {
1417
- if (args.length === 0) {
1418
- this.internal = new Date();
1419
- return;
1420
- }
1421
- if (args.length === 1) {
1422
- const value = args[0];
1423
- if (value instanceof TpsDate) {
1424
- this.internal = new Date(value.getTime());
1425
- return;
1426
- }
1427
- if (value instanceof Date) {
1428
- this.internal = new Date(value.getTime());
1429
- return;
1430
- }
1431
- if (typeof value === "string" && TpsDate.looksLikeTPS(value)) {
1432
- const parsed = TPS.toDate(value);
1433
- if (!parsed) {
1434
- throw new RangeError(`Invalid TPS date string: ${value}`);
1435
- }
1436
- this.internal = parsed;
1437
- return;
1438
- }
1439
- this.internal = new Date(value);
1440
- return;
1441
- }
1442
- const [year, monthIndex, day, hours, minutes, seconds, ms] = args;
1443
- this.internal = new Date(year, monthIndex, day ?? 1, hours ?? 0, minutes ?? 0, seconds ?? 0, ms ?? 0);
1444
- }
1445
- static looksLikeTPS(input) {
1446
- const s = input.trim();
1447
- return s.startsWith("tps://") || s.startsWith("T:") || s.startsWith("t:");
1448
- }
1449
- static now() {
1450
- return Date.now();
1451
- }
1452
- static parse(input) {
1453
- if (this.looksLikeTPS(input)) {
1454
- const d = TPS.toDate(input);
1455
- return d ? d.getTime() : Number.NaN;
1456
- }
1457
- return Date.parse(input);
1458
- }
1459
- static UTC(year, monthIndex, day, hours, minutes, seconds, ms) {
1460
- return Date.UTC(year, monthIndex, day ?? 1, hours ?? 0, minutes ?? 0, seconds ?? 0, ms ?? 0);
1461
- }
1462
- static fromTPS(tps) {
1463
- return new TpsDate(tps);
1464
- }
1465
- toGregorianDate() {
1466
- return new Date(this.internal.getTime());
1467
- }
1468
- toDate() {
1469
- return this.toGregorianDate();
1470
- }
1471
- toTPS(calendar = exports.DefaultCalendars.TPS, opts) {
1472
- return TPS.fromDate(this.internal, calendar, opts);
1473
- }
1474
- toTPSURI(calendar = exports.DefaultCalendars.TPS, opts) {
1475
- const time = this.toTPS(calendar, { order: opts?.order });
1476
- const comp = TPS.parse(time);
1477
- if (opts?.latitude !== undefined && opts?.longitude !== undefined) {
1478
- comp.latitude = opts.latitude;
1479
- comp.longitude = opts.longitude;
1480
- if (opts.altitude !== undefined)
1481
- comp.altitude = opts.altitude;
1482
- }
1483
- else if (opts?.isHiddenLocation) {
1484
- comp.isHiddenLocation = true;
1485
- }
1486
- else if (opts?.isRedactedLocation) {
1487
- comp.isRedactedLocation = true;
1488
- }
1489
- else {
1490
- comp.isUnknownLocation = true;
1491
- }
1492
- return TPS.toURI(comp);
1493
- }
1494
- getTime() {
1495
- return this.internal.getTime();
1496
- }
1497
- valueOf() {
1498
- return this.internal.valueOf();
1499
- }
1500
- toString() {
1501
- return this.toTPS(exports.DefaultCalendars.TPS);
1502
- }
1503
- toISOString() {
1504
- return this.internal.toISOString();
1505
- }
1506
- toUTCString() {
1507
- return this.internal.toUTCString();
1508
- }
1509
- toJSON() {
1510
- return this.internal.toJSON();
1511
- }
1512
- getFullYear() {
1513
- return this.getTpsFullYear();
1514
- }
1515
- getUTCFullYear() {
1516
- return this.getTpsFullYear();
1517
- }
1518
- getMonth() {
1519
- return this.getTpsComponents().month - 1;
1520
- }
1521
- getUTCMonth() {
1522
- return this.getTpsComponents().month - 1;
1523
- }
1524
- getDate() {
1525
- return this.getTpsComponents().day;
1526
- }
1527
- getUTCDate() {
1528
- return this.getTpsComponents().day;
1529
- }
1530
- getHours() {
1531
- return this.getTpsComponents().hour;
1532
- }
1533
- getUTCHours() {
1534
- return this.getTpsComponents().hour;
1535
- }
1536
- getMinutes() {
1537
- return this.getTpsComponents().minute;
1538
- }
1539
- getUTCMinutes() {
1540
- return this.getTpsComponents().minute;
1541
- }
1542
- getSeconds() {
1543
- return this.getTpsComponents().second;
1544
- }
1545
- getUTCSeconds() {
1546
- return this.getTpsComponents().second;
1547
- }
1548
- getMilliseconds() {
1549
- return this.getTpsComponents().millisecond;
1550
- }
1551
- getUTCMilliseconds() {
1552
- return this.getTpsComponents().millisecond;
1553
- }
1554
- [Symbol.toPrimitive](hint) {
1555
- if (hint === "number")
1556
- return this.valueOf();
1557
- return this.toString();
1558
- }
1559
- }
1560
- exports.TpsDate = TpsDate;
1561
896
  //# sourceMappingURL=index.js.map