@nextera.one/tps-standard 0.5.3 → 0.5.34
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/dist/date.d.ts +54 -0
- package/dist/date.js +174 -0
- package/dist/date.js.map +1 -0
- package/dist/drivers/gregorian.d.ts +3 -5
- package/dist/drivers/gregorian.js +26 -19
- package/dist/drivers/gregorian.js.map +1 -1
- package/dist/drivers/hijri.d.ts +1 -16
- package/dist/drivers/hijri.js +9 -102
- package/dist/drivers/hijri.js.map +1 -1
- package/dist/drivers/holocene.d.ts +6 -3
- package/dist/drivers/holocene.js +7 -20
- package/dist/drivers/holocene.js.map +1 -1
- package/dist/drivers/julian.d.ts +3 -10
- package/dist/drivers/julian.js +11 -71
- package/dist/drivers/julian.js.map +1 -1
- package/dist/drivers/persian.d.ts +1 -6
- package/dist/drivers/persian.js +17 -92
- package/dist/drivers/persian.js.map +1 -1
- package/dist/drivers/tps.d.ts +11 -28
- package/dist/drivers/tps.js +8 -58
- package/dist/drivers/tps.js.map +1 -1
- package/dist/drivers/unix.d.ts +5 -6
- package/dist/drivers/unix.js +10 -32
- package/dist/drivers/unix.js.map +1 -1
- package/dist/index.d.ts +6 -477
- package/dist/index.js +33 -978
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +85 -0
- package/dist/types.js +30 -0
- package/dist/types.js.map +1 -0
- package/dist/uid.d.ts +48 -0
- package/dist/uid.js +225 -0
- package/dist/uid.js.map +1 -0
- package/dist/utils/calendar.d.ts +55 -0
- package/dist/utils/calendar.js +136 -0
- package/dist/utils/calendar.js.map +1 -0
- package/dist/utils/env.d.ts +12 -0
- package/dist/utils/env.js +79 -0
- package/dist/utils/env.js.map +1 -0
- package/dist/utils/tps-string.d.ts +12 -0
- package/dist/utils/tps-string.js +164 -0
- package/dist/utils/tps-string.js.map +1 -0
- package/package.json +1 -1
- package/src/date.ts +243 -0
- package/src/drivers/gregorian.ts +29 -27
- package/src/drivers/hijri.ts +13 -113
- package/src/drivers/holocene.ts +11 -12
- package/src/drivers/julian.ts +18 -72
- package/src/drivers/persian.ts +25 -92
- package/src/drivers/tps.ts +16 -55
- package/src/drivers/unix.ts +12 -33
- package/src/index.ts +18 -1446
- package/src/types.ts +107 -0
- package/src/uid.ts +308 -0
- package/src/utils/calendar.ts +161 -0
- package/src/utils/env.ts +88 -0
- package/src/utils/tps-string.ts +166 -0
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* TPS: Temporal Positioning System
|
|
4
4
|
* The Universal Protocol for Space-Time Coordinates.
|
|
5
5
|
* @packageDocumentation
|
|
6
|
-
* @version 0.5.
|
|
6
|
+
* @version 0.5.34
|
|
7
7
|
* @license Apache-2.0
|
|
8
8
|
* @copyright 2026 TPS Standards Working Group
|
|
9
9
|
*
|
|
@@ -13,8 +13,22 @@
|
|
|
13
13
|
* - Added structural anchors (bldg, floor, room, zone)
|
|
14
14
|
* - Added geospatial cell systems (S2, H3, Plus Code, what3words)
|
|
15
15
|
*/
|
|
16
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
19
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
20
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
21
|
+
}
|
|
22
|
+
Object.defineProperty(o, k2, desc);
|
|
23
|
+
}) : (function(o, m, k, k2) {
|
|
24
|
+
if (k2 === undefined) k2 = k;
|
|
25
|
+
o[k2] = m[k];
|
|
26
|
+
}));
|
|
27
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
28
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
29
|
+
};
|
|
16
30
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.
|
|
31
|
+
exports.TPS = exports.Env = void 0;
|
|
18
32
|
// built-in drivers are registered automatically; importing them here
|
|
19
33
|
// ensures they are included when the library bundler/tree-shaker runs.
|
|
20
34
|
const gregorian_1 = require("./drivers/gregorian");
|
|
@@ -24,28 +38,13 @@ const persian_1 = require("./drivers/persian");
|
|
|
24
38
|
const hijri_1 = require("./drivers/hijri");
|
|
25
39
|
const julian_1 = require("./drivers/julian");
|
|
26
40
|
const holocene_1 = require("./drivers/holocene");
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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 = {}));
|
|
41
|
+
__exportStar(require("./types"), exports);
|
|
42
|
+
__exportStar(require("./uid"), exports);
|
|
43
|
+
__exportStar(require("./date"), exports);
|
|
44
|
+
var env_1 = require("./utils/env");
|
|
45
|
+
Object.defineProperty(exports, "Env", { enumerable: true, get: function () { return env_1.Env; } });
|
|
46
|
+
const tps_string_1 = require("./utils/tps-string");
|
|
47
|
+
const types_1 = require("./types");
|
|
49
48
|
class TPS {
|
|
50
49
|
/**
|
|
51
50
|
* Registers a calendar driver plugin.
|
|
@@ -208,7 +207,7 @@ class TPS {
|
|
|
208
207
|
timeStr = timeStr.split(/[!;?#]/)[0];
|
|
209
208
|
}
|
|
210
209
|
if (timeStr) {
|
|
211
|
-
const parsed =
|
|
210
|
+
const parsed = (0, tps_string_1.parseTimeString)(timeStr);
|
|
212
211
|
if (!parsed)
|
|
213
212
|
return null;
|
|
214
213
|
Object.assign(comp, parsed.components);
|
|
@@ -231,7 +230,7 @@ class TPS {
|
|
|
231
230
|
signature = sigMatch.groups.sig;
|
|
232
231
|
timeOnly = input.split(/[!;?#]/)[0];
|
|
233
232
|
}
|
|
234
|
-
const parsed =
|
|
233
|
+
const parsed = (0, tps_string_1.parseTimeString)(timeOnly);
|
|
235
234
|
if (!parsed)
|
|
236
235
|
return null;
|
|
237
236
|
const comp = parsed.components;
|
|
@@ -293,7 +292,7 @@ class TPS {
|
|
|
293
292
|
actorPart = `/A:${comp.actor}`;
|
|
294
293
|
}
|
|
295
294
|
// 3. Build Time Part (handles order & signature)
|
|
296
|
-
const timePart =
|
|
295
|
+
const timePart = (0, tps_string_1.buildTimePart)(comp);
|
|
297
296
|
// 5. Build Extensions
|
|
298
297
|
let extPart = "";
|
|
299
298
|
if (comp.extensions && Object.keys(comp.extensions).length > 0) {
|
|
@@ -314,7 +313,7 @@ class TPS {
|
|
|
314
313
|
* supported key is `order` which may be `'ascending'` or `'descending'`.
|
|
315
314
|
* @returns Canonical string (e.g., "T:tps.m3.c1.y26...").
|
|
316
315
|
*/
|
|
317
|
-
static fromDate(date = new Date(), calendar =
|
|
316
|
+
static fromDate(date = new Date(), calendar = types_1.DefaultCalendars.TPS, opts) {
|
|
318
317
|
const normalizedCalendar = calendar.toLowerCase();
|
|
319
318
|
const driver = this.drivers.get(normalizedCalendar);
|
|
320
319
|
if (driver) {
|
|
@@ -326,21 +325,21 @@ class TPS {
|
|
|
326
325
|
const comp = driver.getComponentsFromDate(date);
|
|
327
326
|
comp.calendar = normalizedCalendar;
|
|
328
327
|
comp.order = opts.order;
|
|
329
|
-
return
|
|
328
|
+
return (0, tps_string_1.buildTimePart)(comp);
|
|
330
329
|
}
|
|
331
330
|
return driver.getFromDate(date);
|
|
332
331
|
}
|
|
333
332
|
// Fallback for old built-in calendars (shouldn't happen once drivers are
|
|
334
333
|
// registered, but kept for backwards compatibility).
|
|
335
334
|
const comp = { calendar: normalizedCalendar };
|
|
336
|
-
if (normalizedCalendar ===
|
|
335
|
+
if (normalizedCalendar === types_1.DefaultCalendars.UNIX) {
|
|
337
336
|
const s = (date.getTime() / 1000).toFixed(3);
|
|
338
337
|
comp.unixSeconds = parseFloat(s);
|
|
339
338
|
if (opts?.order)
|
|
340
339
|
comp.order = opts.order;
|
|
341
|
-
return
|
|
340
|
+
return (0, tps_string_1.buildTimePart)(comp);
|
|
342
341
|
}
|
|
343
|
-
if (normalizedCalendar ===
|
|
342
|
+
if (normalizedCalendar === types_1.DefaultCalendars.GREG) {
|
|
344
343
|
const fullYear = date.getUTCFullYear();
|
|
345
344
|
comp.millennium = Math.floor(fullYear / 1000) + 1;
|
|
346
345
|
comp.century = Math.floor((fullYear % 1000) / 100) + 1;
|
|
@@ -353,7 +352,7 @@ class TPS {
|
|
|
353
352
|
comp.millisecond = date.getUTCMilliseconds();
|
|
354
353
|
if (opts?.order)
|
|
355
354
|
comp.order = opts.order;
|
|
356
|
-
return
|
|
355
|
+
return (0, tps_string_1.buildTimePart)(comp);
|
|
357
356
|
}
|
|
358
357
|
throw new Error(`Calendar driver '${normalizedCalendar}' not implemented. Register a driver.`);
|
|
359
358
|
}
|
|
@@ -382,7 +381,7 @@ class TPS {
|
|
|
382
381
|
const parsed = this.parse(tpsString);
|
|
383
382
|
if (!parsed)
|
|
384
383
|
return null;
|
|
385
|
-
const cal = parsed.calendar ||
|
|
384
|
+
const cal = parsed.calendar || types_1.DefaultCalendars.TPS;
|
|
386
385
|
const driver = this.drivers.get(cal);
|
|
387
386
|
if (!driver) {
|
|
388
387
|
console.error(`Calendar driver '${cal}' not registered.`);
|
|
@@ -479,217 +478,6 @@ class TPS {
|
|
|
479
478
|
return driver.format(components, format);
|
|
480
479
|
}
|
|
481
480
|
// --- INTERNAL HELPERS ---
|
|
482
|
-
/**
|
|
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.
|
|
487
|
-
*
|
|
488
|
-
* Drivers may ignore this helper and produce their own time strings if they
|
|
489
|
-
* implement custom ordering logic.
|
|
490
|
-
*/
|
|
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;
|
|
530
|
-
}
|
|
531
|
-
/**
|
|
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'`.
|
|
536
|
-
*
|
|
537
|
-
* The caller is responsible for stripping off a leading signature or other
|
|
538
|
-
* trailer characters; this method will drop anything after `!`, `;`, `?` or
|
|
539
|
-
* `#`.
|
|
540
|
-
*
|
|
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:
|
|
546
|
-
*
|
|
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)
|
|
555
|
-
*
|
|
556
|
-
* @param input - Time fragment (e.g. `"T:greg.m3.c1.y26"` or `"greg.m0.s25.m30"`)
|
|
557
|
-
*/
|
|
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)
|
|
566
|
-
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 };
|
|
692
|
-
}
|
|
693
481
|
static _mapGroupsToComponents(g) {
|
|
694
482
|
const components = {};
|
|
695
483
|
components.calendar = g.calendar;
|
|
@@ -815,582 +603,6 @@ TPS.registerDriver(new persian_1.PersianDriver());
|
|
|
815
603
|
TPS.registerDriver(new hijri_1.HijriDriver());
|
|
816
604
|
TPS.registerDriver(new julian_1.JulianDriver());
|
|
817
605
|
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
|
-
/**
|
|
957
|
-
* Encode TPS to base64url string with prefix.
|
|
958
|
-
* This is the transport/storage form.
|
|
959
|
-
*
|
|
960
|
-
* @param tps - The TPS string to encode
|
|
961
|
-
* @param opts - Encoding options
|
|
962
|
-
* @returns Base64url encoded TPS-UID with prefix
|
|
963
|
-
*/
|
|
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`;
|
|
1009
|
-
}
|
|
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");
|
|
1091
|
-
}
|
|
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");
|
|
1096
|
-
}
|
|
1097
|
-
x |= b << s;
|
|
1098
|
-
return { value: x >>> 0, bytesRead: i + 1 };
|
|
1099
|
-
}
|
|
1100
|
-
x |= (b & 0x7f) << s;
|
|
1101
|
-
s += 7;
|
|
1102
|
-
i++;
|
|
1103
|
-
if (i > 10) {
|
|
1104
|
-
throw new Error("uvarint too long");
|
|
1105
|
-
}
|
|
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)));
|
|
1161
|
-
}
|
|
1162
|
-
catch {
|
|
1163
|
-
throw new Error("TPSUID7RB: compression not available");
|
|
1164
|
-
}
|
|
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)));
|
|
1177
|
-
}
|
|
1178
|
-
catch {
|
|
1179
|
-
throw new Error("TPSUID7RB: decompression failed");
|
|
1180
|
-
}
|
|
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));
|
|
1340
|
-
}
|
|
1341
|
-
catch (e) {
|
|
1342
|
-
throw new Error("TPSUID7RB: signing failed (check key format)");
|
|
1343
|
-
}
|
|
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);
|
|
1353
|
-
}
|
|
1354
|
-
catch {
|
|
1355
|
-
return false;
|
|
1356
|
-
}
|
|
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));
|
|
1371
|
-
}
|
|
1372
|
-
catch {
|
|
1373
|
-
// Fallback to crypto.getRandomValues
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
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
|
-
}
|
|
1384
|
-
}
|
|
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_-]+$/;
|
|
1394
606
|
/**
|
|
1395
607
|
* `TpsDate` is a Date-like wrapper with native TPS conversion helpers.
|
|
1396
608
|
*
|
|
@@ -1401,161 +613,4 @@ TPSUID7RB.REGEX = /^tpsuid7rb_[A-Za-z0-9_-]+$/;
|
|
|
1401
613
|
* - `new TpsDate(tpsString)`
|
|
1402
614
|
* - `new TpsDate(year, monthIndex, day?, hour?, minute?, second?, ms?)`
|
|
1403
615
|
*/
|
|
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
616
|
//# sourceMappingURL=index.js.map
|