@nextera.one/tps-standard 0.5.33 → 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.
Files changed (57) hide show
  1. package/dist/date.d.ts +54 -0
  2. package/dist/date.js +174 -0
  3. package/dist/date.js.map +1 -0
  4. package/dist/drivers/gregorian.d.ts +3 -5
  5. package/dist/drivers/gregorian.js +26 -19
  6. package/dist/drivers/gregorian.js.map +1 -1
  7. package/dist/drivers/hijri.d.ts +1 -16
  8. package/dist/drivers/hijri.js +9 -102
  9. package/dist/drivers/hijri.js.map +1 -1
  10. package/dist/drivers/holocene.d.ts +6 -3
  11. package/dist/drivers/holocene.js +7 -20
  12. package/dist/drivers/holocene.js.map +1 -1
  13. package/dist/drivers/julian.d.ts +3 -10
  14. package/dist/drivers/julian.js +11 -71
  15. package/dist/drivers/julian.js.map +1 -1
  16. package/dist/drivers/persian.d.ts +1 -6
  17. package/dist/drivers/persian.js +17 -92
  18. package/dist/drivers/persian.js.map +1 -1
  19. package/dist/drivers/tps.d.ts +11 -28
  20. package/dist/drivers/tps.js +8 -58
  21. package/dist/drivers/tps.js.map +1 -1
  22. package/dist/drivers/unix.d.ts +5 -6
  23. package/dist/drivers/unix.js +10 -32
  24. package/dist/drivers/unix.js.map +1 -1
  25. package/dist/index.d.ts +6 -477
  26. package/dist/index.js +33 -978
  27. package/dist/index.js.map +1 -1
  28. package/dist/types.d.ts +85 -0
  29. package/dist/types.js +30 -0
  30. package/dist/types.js.map +1 -0
  31. package/dist/uid.d.ts +48 -0
  32. package/dist/uid.js +225 -0
  33. package/dist/uid.js.map +1 -0
  34. package/dist/utils/calendar.d.ts +55 -0
  35. package/dist/utils/calendar.js +136 -0
  36. package/dist/utils/calendar.js.map +1 -0
  37. package/dist/utils/env.d.ts +12 -0
  38. package/dist/utils/env.js +79 -0
  39. package/dist/utils/env.js.map +1 -0
  40. package/dist/utils/tps-string.d.ts +12 -0
  41. package/dist/utils/tps-string.js +164 -0
  42. package/dist/utils/tps-string.js.map +1 -0
  43. package/package.json +1 -1
  44. package/src/date.ts +243 -0
  45. package/src/drivers/gregorian.ts +29 -27
  46. package/src/drivers/hijri.ts +13 -113
  47. package/src/drivers/holocene.ts +11 -12
  48. package/src/drivers/julian.ts +18 -72
  49. package/src/drivers/persian.ts +25 -92
  50. package/src/drivers/tps.ts +16 -55
  51. package/src/drivers/unix.ts +12 -33
  52. package/src/index.ts +18 -1446
  53. package/src/types.ts +107 -0
  54. package/src/uid.ts +308 -0
  55. package/src/utils/calendar.ts +161 -0
  56. package/src/utils/env.ts +88 -0
  57. package/src/utils/tps-string.ts +166 -0
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * TPS: Temporal Positioning System
3
3
  * The Universal Protocol for Space-Time Coordinates.
4
4
  * @packageDocumentation
5
- * @version 0.5.2
5
+ * @version 0.5.34
6
6
  * @license Apache-2.0
7
7
  * @copyright 2026 TPS Standards Working Group
8
8
  *
@@ -23,266 +23,18 @@ import { HijriDriver } from "./drivers/hijri";
23
23
  import { JulianDriver } from "./drivers/julian";
24
24
  import { HoloceneDriver } from "./drivers/holocene";
25
25
 
26
- // Calendar codes are plain strings to allow arbitrary user-defined
27
- // calendars. The library still exports constants for the built-in values but
28
- // callers may also supply their own codes.
29
- export const DefaultCalendars = {
30
- TPS: "tps",
31
- GREG: "greg",
32
- HIJ: "hij",
33
- PER: "per",
34
- JUL: "jul",
35
- HOLO: "holo",
36
- UNIX: "unix",
37
- } as const;
26
+ export * from "./types";
27
+ export * from "./uid";
28
+ export * from "./date";
29
+ export { Env } from "./utils/env";
30
+ import { buildTimePart, parseTimeString } from "./utils/tps-string";
38
31
 
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
- export enum TimeOrder {
45
- DESC = "desc",
46
- ASC = "asc",
47
- }
48
-
49
- export interface TPSComponents {
50
- // --- TEMPORAL ---
51
- calendar: string;
52
- // --- REQUIRED TEMPORAL FIELDS ---
53
- // All of the traditional Gregorian components are now mandatory. This
54
- // reflects the fact that a valid TPS time object must contain a complete
55
- // timestamp when using the canonical calendar formats.
56
- millennium: number;
57
- century: number;
58
- year: number;
59
- month: number;
60
- day: number;
61
- hour: number;
62
- minute: number;
63
- second: number;
64
- /** Sub-second precision (0–999). Encoded as the last `m` token. */
65
- millisecond: number;
66
- // --- OPTIONAL UNIX BACKUP ---
67
- // `unixSeconds` remains optional to support the Unix driver and other
68
- // cases where a simple epoch value is preferred.
69
- unixSeconds?: number;
70
-
71
- // --- SPATIAL: GPS Coordinates ---
72
- latitude?: number;
73
- longitude?: number;
74
- altitude?: number;
75
-
76
- // --- SPATIAL: Geospatial Cells ---
77
- /** Google S2 cell ID (hierarchical, prefix-searchable) */
78
- s2Cell?: string;
79
- /** Uber H3 cell ID (hexagonal grid) */
80
- h3Cell?: string;
81
- /** Open Location Code / Plus Code */
82
- plusCode?: string;
83
- /** what3words address (e.g. "filled.count.soap") */
84
- what3words?: string;
85
-
86
- // --- SPATIAL: Structural Anchors ---
87
- /** Physical building identifier */
88
- building?: string;
89
- /** Vertical division (level) */
90
- floor?: string;
91
- /** Enclosed space identifier */
92
- room?: string;
93
- /** Logical area within building */
94
- zone?: string;
95
-
96
- /** Raw pre-@ space anchor (e.g. adm:city:SA:riyadh, node:api-1, net:ip4:203.0.113.10) */
97
- spaceAnchor?: string;
98
-
99
- // --- SPATIAL: Privacy Markers ---
100
- /** Technical missing data (e.g. server log without GPS) */
101
- isUnknownLocation?: boolean;
102
- /** Removed for legal/security reasons (e.g. GDPR) */
103
- isRedactedLocation?: boolean;
104
- /** Masked by user preference (e.g. "Don't show my location") */
105
- isHiddenLocation?: boolean;
106
-
107
- // --- PROVENANCE ---
108
- /** Actor anchor - identifies observer/witness (e.g. "did:web:sensor.example.com", "node:gateway-01") */
109
- actor?: string;
110
- /** Verification hash appended to time (e.g. "sha256:8f3e2a...") */
111
- signature?: string;
112
-
113
- // --- CONTEXT ---
114
- extensions?: Record<string, string>;
115
-
116
- order?: TimeOrder;
117
- }
118
-
119
- // --- PLUGIN ARCHITECTURE ---
120
-
121
- /**
122
- * Interface for Calendar Driver plugins.
123
- * Implementations provide conversion logic to/from Gregorian and support for
124
- * external calendar libraries.
125
- *
126
- * @example Using a driver to parse a Hijri date string
127
- * ```ts
128
- * const driver = TPS.getDriver('hij');
129
- * if (driver?.parseDate) {
130
- * const components = driver.parseDate('1447-07-21');
131
- * const gregDate = driver.toGregorian(components);
132
- * const tpsString = TPS.fromDate(gregDate, 'hij');
133
- * }
134
- * ```
135
- *
136
- * @example Wrapping an external library (moment-hijri)
137
- * ```ts
138
- * import moment from 'moment-hijri';
139
- *
140
- * class HijriDriver implements CalendarDriver {
141
- * readonly code = 'hij';
142
- *
143
- * parseDate(input: string, format?: string): Partial<TPSComponents> {
144
- * const m = moment(input, format || 'iYYYY-iMM-iDD');
145
- * return {
146
- * calendar: 'hij',
147
- * year: m.iYear(),
148
- * month: m.iMonth() + 1,
149
- * day: m.iDate()
150
- * };
151
- * }
152
- *
153
- * fromGregorian(date: Date): Partial<TPSComponents> {
154
- * const m = moment(date);
155
- * return {
156
- * calendar: 'hij',
157
- * year: m.iYear(),
158
- * month: m.iMonth() + 1,
159
- * day: m.iDate(),
160
- * hour: m.hour(),
161
- * minute: m.minute(),
162
- * second: m.second()
163
- * };
164
- * }
165
- *
166
- * // ... other methods
167
- * }
168
- * ```
169
- */
170
- export interface CalendarDriver {
171
- /** The calendar code this driver handles (e.g., 'hij', 'jul'). */
172
- readonly code: string;
173
-
174
- /**
175
- * Human-readable name for this calendar (optional).
176
- * @example "Hijri (Islamic)"
177
- */
178
- readonly name?: string;
179
-
180
- /**
181
- * Converts a Date to this calendar's components.
182
- * @param date - The Gregorian Date object.
183
- * @returns Partial TPS components for year, month, day, etc.
184
- */
185
- getComponentsFromDate(date: Date): Partial<TPSComponents>;
186
-
187
- /**
188
- * Converts this calendar's components to a Date.
189
- * @param components - Partial TPS components (year, month, day, etc.).
190
- * @returns A JavaScript Date object.
191
- */
192
- getDateFromComponents(components: Partial<TPSComponents>): Date;
193
-
194
- /**
195
- * Generates a TPS time string for this calendar from a Date.
196
- * @param date - The Gregorian Date object.
197
- * @returns A TPS time string (e.g., "T:hij.y1447.m07.d21...").
198
- */
199
- getFromDate(date: Date): string;
200
-
201
- // --- NEW ENHANCED METHODS (Optional) ---
202
-
203
- /**
204
- * Parse a calendar-specific date string into TPS components.
205
- * This allows drivers to handle native date formats from external libraries.
206
- *
207
- * @param input - Date string in calendar-native format (e.g., '1447-07-21' for Hijri)
208
- * @param format - Optional format string (driver-specific, e.g., 'iYYYY-iMM-iDD')
209
- * @returns Partial TPS components
210
- *
211
- * @example
212
- * ```ts
213
- * // Hijri driver
214
- * driver.parseDate('1447-07-21'); // → { year: 1447, month: 7, day: 21, calendar: 'hij' }
215
- *
216
- * // With time
217
- * driver.parseDate('1447-07-21 14:30:00'); // → { year: 1447, month: 7, day: 21, hour: 14, ... }
218
- * ```
219
- */
220
- parseDate(input: string, format?: string): Partial<TPSComponents>;
221
-
222
- /**
223
- * Format TPS components to a calendar-specific date string.
224
- * Inverse of parseDate().
225
- *
226
- * @param components - TPS components to format
227
- * @param format - Optional format string (driver-specific)
228
- * @returns Formatted date string in calendar-native format
229
- *
230
- * @example
231
- * ```ts
232
- * driver.format({ year: 1447, month: 7, day: 21 }); // → '1447-07-21'
233
- * driver.format({ year: 1447, month: 7, day: 21 }, 'short'); // → '21/7/1447'
234
- * ```
235
- */
236
- format(components: Partial<TPSComponents>, format?: string): string;
237
-
238
- /**
239
- * Validate a calendar-specific date string or components.
240
- *
241
- * @param input - Date string or components to validate
242
- * @returns true if valid for this calendar
243
- *
244
- * @example
245
- * ```ts
246
- * driver.validate('1447-13-01'); // → false (month 13 invalid)
247
- * driver.validate({ year: 1447, month: 7, day: 31 }); // → false (Rajab has 30 days)
248
- * ```
249
- */
250
- validate(input: string | Partial<TPSComponents>): boolean;
251
-
252
- /**
253
- * Get calendar metadata (month names, day names, etc.).
254
- * Useful for UI rendering.
255
- *
256
- * @example
257
- * ```ts
258
- * driver.getMetadata().monthNames
259
- * // → ['Muharram', 'Safar', 'Rabi I', ...]
260
- * ```
261
- */
262
- getMetadata(): CalendarMetadata;
263
- }
264
-
265
- /**
266
- * Metadata about a calendar system.
267
- */
268
- export interface CalendarMetadata {
269
- /** Human-readable calendar name */
270
- name: string;
271
- /** Month names in order (1-12 or 1-13) */
272
- monthNames?: string[];
273
- /** Short month names */
274
- monthNamesShort?: string[];
275
- /** Day of week names (Sunday=0 or locale-specific) */
276
- dayNames?: string[];
277
- /** Short day names */
278
- dayNamesShort?: string[];
279
- /** Whether this calendar is lunar-based */
280
- isLunar?: boolean;
281
- /** Number of months per year */
282
- monthsPerYear?: number;
283
- /** Epoch year (for reference) */
284
- epochYear?: number;
285
- }
32
+ import {
33
+ CalendarDriver,
34
+ TPSComponents,
35
+ TimeOrder,
36
+ DefaultCalendars,
37
+ } from "./types";
286
38
 
287
39
  export class TPS {
288
40
  // --- PLUGIN REGISTRY ---
@@ -504,7 +256,7 @@ export class TPS {
504
256
  timeStr = timeStr.split(/[!;?#]/)[0];
505
257
  }
506
258
  if (timeStr) {
507
- const parsed = this.parseTimeString(timeStr);
259
+ const parsed = parseTimeString(timeStr);
508
260
  if (!parsed) return null;
509
261
  Object.assign(comp, parsed.components);
510
262
  comp.order = parsed.order;
@@ -525,7 +277,7 @@ export class TPS {
525
277
  signature = sigMatch.groups.sig;
526
278
  timeOnly = input.split(/[!;?#]/)[0];
527
279
  }
528
- const parsed = this.parseTimeString(timeOnly);
280
+ const parsed = parseTimeString(timeOnly);
529
281
  if (!parsed) return null;
530
282
  const comp = parsed.components as TPSComponents;
531
283
  if (signature) comp.signature = signature;
@@ -577,7 +329,7 @@ export class TPS {
577
329
  }
578
330
 
579
331
  // 3. Build Time Part (handles order & signature)
580
- const timePart = this.buildTimePart(comp);
332
+ const timePart = buildTimePart(comp);
581
333
 
582
334
  // 5. Build Extensions
583
335
  let extPart = "";
@@ -619,7 +371,7 @@ export class TPS {
619
371
  const comp = driver.getComponentsFromDate(date) as TPSComponents;
620
372
  comp.calendar = normalizedCalendar;
621
373
  comp.order = opts.order;
622
- return this.buildTimePart(comp);
374
+ return buildTimePart(comp);
623
375
  }
624
376
  return driver.getFromDate(date);
625
377
  }
@@ -632,7 +384,7 @@ export class TPS {
632
384
  const s = (date.getTime() / 1000).toFixed(3);
633
385
  comp.unixSeconds = parseFloat(s);
634
386
  if (opts?.order) comp.order = opts.order;
635
- return this.buildTimePart(comp);
387
+ return buildTimePart(comp);
636
388
  }
637
389
 
638
390
  if (normalizedCalendar === DefaultCalendars.GREG) {
@@ -647,7 +399,7 @@ export class TPS {
647
399
  comp.second = date.getUTCSeconds();
648
400
  comp.millisecond = date.getUTCMilliseconds();
649
401
  if (opts?.order) comp.order = opts.order;
650
- return this.buildTimePart(comp);
402
+ return buildTimePart(comp);
651
403
  }
652
404
 
653
405
  throw new Error(
@@ -809,223 +561,6 @@ export class TPS {
809
561
 
810
562
  // --- INTERNAL HELPERS ---
811
563
 
812
- /**
813
- * Generate the canonical `T:` time string for a set of components. The
814
- * `order` field (or `comp.order`) controls whether tokens are emitted in
815
- * ascending or descending hierarchy; if undefined the default
816
- * `'descending'` orientation is used.
817
- *
818
- * Drivers may ignore this helper and produce their own time strings if they
819
- * implement custom ordering logic.
820
- */
821
- public static buildTimePart(comp: TPSComponents): string {
822
- const calendar = (comp.calendar || "").toLowerCase();
823
- if (!/^[a-z]{3,4}$/.test(calendar)) {
824
- throw new Error(
825
- `Invalid calendar code '${comp.calendar}'. Calendar code width must be 3–4 lowercase letters.`,
826
- );
827
- }
828
-
829
- let time = `T:${calendar}`;
830
- if (calendar === DefaultCalendars.UNIX) {
831
- if (comp.unixSeconds !== undefined) {
832
- time += `.s${comp.unixSeconds}`;
833
- }
834
- return time;
835
- }
836
-
837
- // sequence of [prefix, value, rank]
838
- // All four of millennium / month / minute / millisecond share the prefix 'm'.
839
- // Position within the ordered sequence disambiguates them during parsing.
840
- const tokens: Array<[string, number | undefined, number]> = [
841
- ["m", comp.millennium, 8], // m-token rank 8 → millennium
842
- ["c", comp.century, 7],
843
- ["y", comp.year, 6],
844
- ["m", comp.month, 5], // m-token rank 5 → month
845
- ["d", comp.day, 4],
846
- ["h", comp.hour, 3],
847
- ["m", comp.minute, 2], // m-token rank 2 → minute
848
- ["s", comp.second, 1],
849
- ["m", comp.millisecond, 0], // m-token rank 0 → millisecond
850
- ];
851
-
852
- const order: TimeOrder = comp.order || TimeOrder.DESC;
853
- if (order === TimeOrder.ASC) tokens.reverse();
854
-
855
- for (const [pref, val] of tokens) {
856
- if (val !== undefined) {
857
- // seconds may be fractional
858
- time += `.${pref}${val}`;
859
- }
860
- }
861
-
862
- if (comp.signature) {
863
- time += `!${comp.signature}`;
864
- }
865
-
866
- return time;
867
- }
868
-
869
- /**
870
- * Parse the *time* portion of a TPS string (optionally beginning with
871
- * `T:`) into components and determine the component ordering. This helper
872
- * accepts tokens in **any** sequence and will return an `order` value of
873
- * `'ascending'` or `'descending'`.
874
- *
875
- * The caller is responsible for stripping off a leading signature or other
876
- * trailer characters; this method will drop anything after `!`, `;`, `?` or
877
- * `#`.
878
- *
879
- * ### `m`-token disambiguation
880
- * All four of millennium (rank 8), month (rank 5), minute (rank 2) and
881
- * millisecond (rank 0) share the single-character prefix `m`. They are told
882
- * apart by their **position relative to the neighbouring tokens**. The
883
- * algorithm is:
884
- *
885
- * 1. Pre-scan the non-`m` tokens (c, y, d, h, s) whose ranks are fixed to
886
- * determine whether the string is ascending or descending.
887
- * 2. While iterating, track `lastAssignedRank` – the rank of the most
888
- * recently processed token (m or non-m).
889
- * 3. When an `m` token is encountered, derive its rank from `lastAssignedRank`
890
- * and the detected order:
891
- * - **DESC** null → 8 (mill) | rank > 5 → 5 (month) | rank > 2 → 2 (min) | else → 0 (ms)
892
- * - **ASC** null → 0 (ms) | rank < 2 → 2 (min) | rank < 5 → 5 (month) | else → 8 (mill)
893
- *
894
- * @param input - Time fragment (e.g. `"T:greg.m3.c1.y26"` or `"greg.m0.s25.m30"`)
895
- */
896
- static parseTimeString(
897
- input: string,
898
- ): { components: Partial<TPSComponents>; order: TimeOrder } | null {
899
- let s = input.trim();
900
- // strip off anything after signature or extensions/query/fragment
901
- s = s.split(/[!;?#]/)[0];
902
- if (s.startsWith("T:")) s = s.slice(2);
903
- const parts = s.split(".");
904
- if (parts.length === 0) return null;
905
- const calendar = parts[0];
906
- const comp: Partial<TPSComponents> = { calendar };
907
-
908
- // Fixed-rank prefixes (unambiguous regardless of position)
909
- const fixedRankMap: Record<string, number> = {
910
- c: 7,
911
- y: 6,
912
- d: 4,
913
- h: 3,
914
- s: 1,
915
- };
916
-
917
- // ── Step 1: pre-scan non-m tokens to estimate order ─────────────────────
918
- // This is only needed to handle the first 'm' token when lastAssignedRank
919
- // is still null (nothing has been seen yet).
920
- let initialOrder: TimeOrder = TimeOrder.DESC;
921
- if (calendar !== DefaultCalendars.UNIX) {
922
- const nonMRanks: number[] = [];
923
- for (let i = 1; i < parts.length; i++) {
924
- const pr = parts[i]?.charAt(0);
925
- if (pr && pr in fixedRankMap) nonMRanks.push(fixedRankMap[pr]);
926
- }
927
- if (nonMRanks.length >= 2) {
928
- const isAsc = nonMRanks.every((v, i, a) => i === 0 || a[i - 1] <= v);
929
- if (isAsc) initialOrder = TimeOrder.ASC;
930
- }
931
- }
932
-
933
- // ── Step 2: resolve the semantic rank of an 'm' token ───────────────────
934
- const assignMRank = (lastRank: number | null, ord: TimeOrder): number => {
935
- if (ord === TimeOrder.DESC) {
936
- if (lastRank === null) return 8; // first token → millennium
937
- if (lastRank > 5) return 5; // after century / year → month
938
- if (lastRank > 2) return 2; // after day / hour → minute
939
- return 0; // after second → millisecond
940
- } else {
941
- if (lastRank === null) return 0; // first token → millisecond
942
- if (lastRank < 2) return 2; // after millisecond / second → minute
943
- if (lastRank < 5) return 5; // after minute / hour / day → month
944
- return 8; // after month / year / cent → millennium
945
- }
946
- };
947
-
948
- // ── Step 3: iterate and build components ────────────────────────────────
949
- const ranks: number[] = [];
950
- let lastAssignedRank: number | null = null;
951
-
952
- for (let i = 1; i < parts.length; i++) {
953
- const token = parts[i];
954
- if (!token) continue;
955
- const prefix = token.charAt(0);
956
- const value = token.slice(1);
957
-
958
- // UNIX calendar: single 's' token carries the full unix timestamp
959
- if (calendar === DefaultCalendars.UNIX && prefix === "s") {
960
- comp.unixSeconds = parseFloat(value);
961
- ranks.push(9);
962
- continue;
963
- }
964
-
965
- if (prefix === "m") {
966
- const rank = assignMRank(lastAssignedRank, initialOrder);
967
- switch (rank) {
968
- case 8:
969
- comp.millennium = parseInt(value, 10);
970
- break;
971
- case 5:
972
- comp.month = parseInt(value, 10);
973
- break;
974
- case 2:
975
- comp.minute = parseInt(value, 10);
976
- break;
977
- case 0:
978
- comp.millisecond = parseInt(value, 10);
979
- break;
980
- }
981
- ranks.push(rank);
982
- lastAssignedRank = rank;
983
- } else {
984
- switch (prefix) {
985
- case "c":
986
- comp.century = parseInt(value, 10);
987
- ranks.push(7);
988
- lastAssignedRank = 7;
989
- break;
990
- case "y":
991
- comp.year = parseInt(value, 10);
992
- ranks.push(6);
993
- lastAssignedRank = 6;
994
- break;
995
- case "d":
996
- comp.day = parseInt(value, 10);
997
- ranks.push(4);
998
- lastAssignedRank = 4;
999
- break;
1000
- case "h":
1001
- comp.hour = parseInt(value, 10);
1002
- ranks.push(3);
1003
- lastAssignedRank = 3;
1004
- break;
1005
- case "s":
1006
- comp.second = parseFloat(value);
1007
- ranks.push(1);
1008
- lastAssignedRank = 1;
1009
- break;
1010
- default:
1011
- // unknown prefix – ignore
1012
- break;
1013
- }
1014
- }
1015
- }
1016
-
1017
- // ── Step 4: confirm order from the complete rank sequence ────────────────
1018
- let order: TimeOrder = TimeOrder.DESC;
1019
- if (ranks.length > 1) {
1020
- const isAsc = ranks.every((v, i, a) => i === 0 || a[i - 1] <= v);
1021
- const isDesc = ranks.every((v, i, a) => i === 0 || a[i - 1] >= v);
1022
- if (isAsc && !isDesc) order = TimeOrder.ASC;
1023
- // mixed / single direction → defaults to DESC
1024
- }
1025
-
1026
- return { components: comp, order };
1027
- }
1028
-
1029
564
  private static _mapGroupsToComponents(
1030
565
  g: Record<string, string>,
1031
566
  ): TPSComponents {
@@ -1115,725 +650,6 @@ TPS.registerDriver(new HijriDriver());
1115
650
  TPS.registerDriver(new JulianDriver());
1116
651
  TPS.registerDriver(new HoloceneDriver());
1117
652
 
1118
- // --- TPS-UID v1 Types ---
1119
-
1120
- /**
1121
- * Decoded result from TPSUID7RB binary format.
1122
- */
1123
- export type TPSUID7RBDecodeResult = {
1124
- /** Version identifier */
1125
- version: "tpsuid7rb";
1126
- /** Epoch milliseconds (UTC) */
1127
- epochMs: number;
1128
- /** Whether the TPS payload was compressed */
1129
- compressed: boolean;
1130
- /** 32-bit nonce for collision prevention */
1131
- nonce: number;
1132
- /** The original TPS string (exact reconstruction) */
1133
- tps: string;
1134
- };
1135
-
1136
- /**
1137
- * Encoding options for TPSUID7RB.
1138
- */
1139
- export type TPSUID7RBEncodeOptions = {
1140
- /** Enable zlib compression of TPS payload */
1141
- compress?: boolean;
1142
- /** Override epoch milliseconds (default: parsed from TPS) */
1143
- epochMs?: number;
1144
- };
1145
-
1146
- /**
1147
- * TPS-UID v1 — Temporal Positioning System Identifier (Binary Reversible)
1148
- *
1149
- * A time-first, reversible identifier that binds an event to a TPS coordinate.
1150
- * Unlike UUIDs, TPS-UID identifies events in spacetime and allows exact
1151
- * reconstruction of the original TPS string.
1152
- *
1153
- * Binary Schema (all integers big-endian):
1154
- * ```
1155
- * MAGIC 4 bytes "TPU7"
1156
- * VER 1 byte 0x01
1157
- * FLAGS 1 byte bit0 = compression flag
1158
- * TIME 6 bytes epoch_ms (48-bit unsigned)
1159
- * NONCE 4 bytes 32-bit random
1160
- * LEN varint length of TPS payload
1161
- * TPS bytes UTF-8 TPS string (raw or zlib-compressed)
1162
- * ```
1163
- *
1164
- * @example
1165
- * ```ts
1166
- * const tps = 'tps://31.95,35.91@T:greg.m3.c1.y26.m01.d09';
1167
- *
1168
- * // Encode to binary
1169
- * const bytes = TPSUID7RB.encodeBinary(tps);
1170
- *
1171
- * // Encode to base64url string
1172
- * const id = TPSUID7RB.encodeBinaryB64(tps);
1173
- * // → "tpsuid7rb_AFRQV..."
1174
- *
1175
- * // Decode back to original TPS
1176
- * const decoded = TPSUID7RB.decodeBinaryB64(id);
1177
- * console.log(decoded.tps); // exact original TPS
1178
- * ```
1179
- */
1180
- export class TPSUID7RB {
1181
- /** Magic bytes: "TPU7" */
1182
- private static readonly MAGIC = new Uint8Array([0x54, 0x50, 0x55, 0x37]);
1183
- /** Version 1 */
1184
- private static readonly VER = 0x01;
1185
- /** String prefix for base64url encoded form */
1186
- private static readonly PREFIX = "tpsuid7rb_";
1187
- /** Regex for validating base64url encoded form */
1188
- public static readonly REGEX = /^tpsuid7rb_[A-Za-z0-9_-]+$/;
1189
-
1190
- // ---------------------------
1191
- // Public API
1192
- // ---------------------------
1193
-
1194
- /**
1195
- * Encode TPS string to binary bytes (Uint8Array).
1196
- * This is the canonical form for hashing, signing, and storage.
1197
- *
1198
- * @param tps - The TPS string to encode
1199
- * @param opts - Encoding options (compress, epochMs override)
1200
- * @returns Binary TPS-UID as Uint8Array
1201
- */
1202
- static encodeBinary(
1203
- tps: string,
1204
- opts: TPSUID7RBEncodeOptions = {},
1205
- ): Uint8Array {
1206
- const compress = opts.compress ?? false;
1207
- const epochMs = opts.epochMs ?? this.epochMsFromTPSString(tps);
1208
-
1209
- if (!Number.isInteger(epochMs) || epochMs < 0) {
1210
- throw new Error("epochMs must be a non-negative integer");
1211
- }
1212
- if (epochMs > 0xffffffffffff) {
1213
- throw new Error("epochMs exceeds 48-bit range");
1214
- }
1215
-
1216
- const flags = compress ? 0x01 : 0x00;
1217
-
1218
- // Generate 32-bit nonce
1219
- const nonceBuf = this.randomBytes(4);
1220
- const nonce =
1221
- ((nonceBuf[0] << 24) >>> 0) +
1222
- ((nonceBuf[1] << 16) >>> 0) +
1223
- ((nonceBuf[2] << 8) >>> 0) +
1224
- nonceBuf[3];
1225
-
1226
- // Encode TPS to UTF-8
1227
- const tpsUtf8 = new TextEncoder().encode(tps);
1228
-
1229
- // Optionally compress
1230
- const payload = compress ? this.deflateRaw(tpsUtf8) : tpsUtf8;
1231
-
1232
- // Encode length as varint
1233
- const lenVar = this.uvarintEncode(payload.length);
1234
-
1235
- // Construct binary structure
1236
- const out = new Uint8Array(
1237
- 4 + 1 + 1 + 6 + 4 + lenVar.length + payload.length,
1238
- );
1239
- let offset = 0;
1240
-
1241
- // MAGIC
1242
- out.set(this.MAGIC, offset);
1243
- offset += 4;
1244
-
1245
- // VER
1246
- out[offset++] = this.VER;
1247
-
1248
- // FLAGS
1249
- out[offset++] = flags;
1250
-
1251
- // TIME (48-bit big-endian)
1252
- const timeBytes = this.writeU48(epochMs);
1253
- out.set(timeBytes, offset);
1254
- offset += 6;
1255
-
1256
- // NONCE (32-bit big-endian)
1257
- out.set(nonceBuf, offset);
1258
- offset += 4;
1259
-
1260
- // LEN (varint)
1261
- out.set(lenVar, offset);
1262
- offset += lenVar.length;
1263
-
1264
- // TPS payload
1265
- out.set(payload, offset);
1266
-
1267
- return out;
1268
- }
1269
-
1270
- /**
1271
- * Decode binary bytes back to original TPS string.
1272
- *
1273
- * @param bytes - Binary TPS-UID
1274
- * @returns Decoded result with original TPS string
1275
- */
1276
- static decodeBinary(bytes: Uint8Array): TPSUID7RBDecodeResult {
1277
- // Header min size: 4+1+1+6+4 + 1 (at least 1 byte varint) = 17
1278
- if (bytes.length < 17) {
1279
- throw new Error("TPSUID7RB: too short");
1280
- }
1281
-
1282
- // MAGIC
1283
- if (
1284
- bytes[0] !== 0x54 ||
1285
- bytes[1] !== 0x50 ||
1286
- bytes[2] !== 0x55 ||
1287
- bytes[3] !== 0x37
1288
- ) {
1289
- throw new Error("TPSUID7RB: bad magic");
1290
- }
1291
-
1292
- // VERSION
1293
- const ver = bytes[4];
1294
- if (ver !== this.VER) {
1295
- throw new Error(`TPSUID7RB: unsupported version ${ver}`);
1296
- }
1297
-
1298
- // FLAGS
1299
- const flags = bytes[5];
1300
- const compressed = (flags & 0x01) === 0x01;
1301
-
1302
- // TIME (48-bit big-endian)
1303
- const epochMs = this.readU48(bytes, 6);
1304
-
1305
- // NONCE (32-bit big-endian)
1306
- const nonce =
1307
- ((bytes[12] << 24) >>> 0) +
1308
- ((bytes[13] << 16) >>> 0) +
1309
- ((bytes[14] << 8) >>> 0) +
1310
- bytes[15];
1311
-
1312
- // LEN (varint at offset 16)
1313
- let offset = 16;
1314
- const { value: tpsLen, bytesRead } = this.uvarintDecode(bytes, offset);
1315
- offset += bytesRead;
1316
-
1317
- if (offset + tpsLen > bytes.length) {
1318
- throw new Error("TPSUID7RB: length overflow");
1319
- }
1320
-
1321
- // TPS payload
1322
- const payload = bytes.slice(offset, offset + tpsLen);
1323
- const tpsUtf8 = compressed ? this.inflateRaw(payload) : payload;
1324
- const tps = new TextDecoder().decode(tpsUtf8);
1325
-
1326
- return { version: "tpsuid7rb", epochMs, compressed, nonce, tps };
1327
- }
1328
-
1329
- /**
1330
- * Encode TPS to base64url string with prefix.
1331
- * This is the transport/storage form.
1332
- *
1333
- * @param tps - The TPS string to encode
1334
- * @param opts - Encoding options
1335
- * @returns Base64url encoded TPS-UID with prefix
1336
- */
1337
- static encodeBinaryB64(tps: string, opts?: TPSUID7RBEncodeOptions): string {
1338
- const bytes = this.encodeBinary(tps, opts ?? {});
1339
- return `${this.PREFIX}${this.base64UrlEncode(bytes)}`;
1340
- }
1341
-
1342
- /**
1343
- * Decode base64url string back to original TPS string.
1344
- *
1345
- * @param id - Base64url encoded TPS-UID with prefix
1346
- * @returns Decoded result with original TPS string
1347
- */
1348
- static decodeBinaryB64(id: string): TPSUID7RBDecodeResult {
1349
- const s = id.trim();
1350
- if (!s.startsWith(this.PREFIX)) {
1351
- throw new Error("TPSUID7RB: missing prefix");
1352
- }
1353
- const b64 = s.slice(this.PREFIX.length);
1354
- const bytes = this.base64UrlDecode(b64);
1355
- return this.decodeBinary(bytes);
1356
- }
1357
-
1358
- /**
1359
- * Validate base64url encoded TPS-UID format.
1360
- * Note: This validates shape only; binary decode is authoritative.
1361
- *
1362
- * @param id - String to validate
1363
- * @returns true if format is valid
1364
- */
1365
- static validateBinaryB64(id: string): boolean {
1366
- return this.REGEX.test(id.trim());
1367
- }
1368
-
1369
- /**
1370
- * Generate a TPS-UID from the current time and optional location.
1371
- *
1372
- * @param opts - Generation options
1373
- * @returns Base64url encoded TPS-UID
1374
- */
1375
- static generate(opts?: {
1376
- latitude?: number;
1377
- longitude?: number;
1378
- altitude?: number;
1379
- compress?: boolean;
1380
- order?: TimeOrder;
1381
- }): string {
1382
- const now = new Date();
1383
- const time = TPS.fromDate(now, DefaultCalendars.TPS, {
1384
- order: opts?.order,
1385
- });
1386
- let space = "unknown";
1387
-
1388
- if (opts?.latitude !== undefined && opts?.longitude !== undefined) {
1389
- space = `${opts.latitude},${opts.longitude}`;
1390
- if (opts.altitude !== undefined) {
1391
- space += `,${opts.altitude}m`;
1392
- }
1393
- }
1394
-
1395
- const tps = `tps://${space}@${time}`;
1396
-
1397
- return this.encodeBinaryB64(tps, {
1398
- compress: opts?.compress,
1399
- epochMs: now.getTime(),
1400
- });
1401
- }
1402
-
1403
- // ---------------------------
1404
- // TPS String Helpers
1405
- // ---------------------------
1406
-
1407
- /**
1408
- * Generate a TPS string from a Date and optional location.
1409
- */
1410
-
1411
- /**
1412
- * Parse epoch milliseconds from a TPS string.
1413
- * Supports both URI format (tps://...) and time-only format (T:greg...)
1414
- */
1415
- static epochMsFromTPSString(tps: string): number {
1416
- const date = TPS.toDate(tps);
1417
- if (date) return date.getTime();
1418
-
1419
- // If parse fails due to unsupported/extended extension payloads,
1420
- // strip extensions/query/fragment and retry. Epoch only depends on time.
1421
- const stripped = tps.replace(/;[^?#]*/, "").replace(/[?#].*$/, "");
1422
- const retryDate = TPS.toDate(stripped);
1423
- if (!retryDate) throw new Error("TPS: unable to parse date for epoch");
1424
- return retryDate.getTime();
1425
- }
1426
-
1427
- // ---------------------------
1428
- // Binary Helpers
1429
- // ---------------------------
1430
-
1431
- /** Write 48-bit unsigned integer (big-endian) */
1432
- private static writeU48(epochMs: number): Uint8Array {
1433
- const b = new Uint8Array(6);
1434
- // Use BigInt for proper 48-bit handling
1435
- const v = BigInt(epochMs);
1436
- b[0] = Number((v >> 40n) & 0xffn);
1437
- b[1] = Number((v >> 32n) & 0xffn);
1438
- b[2] = Number((v >> 24n) & 0xffn);
1439
- b[3] = Number((v >> 16n) & 0xffn);
1440
- b[4] = Number((v >> 8n) & 0xffn);
1441
- b[5] = Number(v & 0xffn);
1442
- return b;
1443
- }
1444
-
1445
- /** Read 48-bit unsigned integer (big-endian) */
1446
- private static readU48(bytes: Uint8Array, offset: number): number {
1447
- const v =
1448
- (BigInt(bytes[offset]) << 40n) +
1449
- (BigInt(bytes[offset + 1]) << 32n) +
1450
- (BigInt(bytes[offset + 2]) << 24n) +
1451
- (BigInt(bytes[offset + 3]) << 16n) +
1452
- (BigInt(bytes[offset + 4]) << 8n) +
1453
- BigInt(bytes[offset + 5]);
1454
-
1455
- const n = Number(v);
1456
- if (!Number.isSafeInteger(n)) {
1457
- throw new Error("TPSUID7RB: u48 not safe integer");
1458
- }
1459
- return n;
1460
- }
1461
-
1462
- /** Encode unsigned integer as LEB128 varint */
1463
- private static uvarintEncode(n: number): Uint8Array {
1464
- if (!Number.isInteger(n) || n < 0) {
1465
- throw new Error("uvarint must be non-negative int");
1466
- }
1467
- const out: number[] = [];
1468
- let x = n >>> 0;
1469
- while (x >= 0x80) {
1470
- out.push((x & 0x7f) | 0x80);
1471
- x >>>= 7;
1472
- }
1473
- out.push(x);
1474
- return new Uint8Array(out);
1475
- }
1476
-
1477
- /** Decode LEB128 varint */
1478
- private static uvarintDecode(
1479
- bytes: Uint8Array,
1480
- offset: number,
1481
- ): { value: number; bytesRead: number } {
1482
- let x = 0;
1483
- let s = 0;
1484
- let i = 0;
1485
- while (true) {
1486
- if (offset + i >= bytes.length) {
1487
- throw new Error("uvarint overflow");
1488
- }
1489
- const b = bytes[offset + i];
1490
- if (b < 0x80) {
1491
- if (i > 9 || (i === 9 && b > 1)) {
1492
- throw new Error("uvarint too large");
1493
- }
1494
- x |= b << s;
1495
- return { value: x >>> 0, bytesRead: i + 1 };
1496
- }
1497
- x |= (b & 0x7f) << s;
1498
- s += 7;
1499
- i++;
1500
- if (i > 10) {
1501
- throw new Error("uvarint too long");
1502
- }
1503
- }
1504
- }
1505
-
1506
- // ---------------------------
1507
- // Base64url Helpers
1508
- // ---------------------------
1509
-
1510
- /** Encode bytes to base64url (no padding) */
1511
- private static base64UrlEncode(bytes: Uint8Array): string {
1512
- // Node.js environment
1513
- if (typeof Buffer !== "undefined") {
1514
- return Buffer.from(bytes)
1515
- .toString("base64")
1516
- .replace(/\+/g, "-")
1517
- .replace(/\//g, "_")
1518
- .replace(/=+$/g, "");
1519
- }
1520
- // Browser environment
1521
- let binary = "";
1522
- for (let i = 0; i < bytes.length; i++) {
1523
- binary += String.fromCharCode(bytes[i]);
1524
- }
1525
- return btoa(binary)
1526
- .replace(/\+/g, "-")
1527
- .replace(/\//g, "_")
1528
- .replace(/=+$/g, "");
1529
- }
1530
-
1531
- /** Decode base64url to bytes */
1532
- private static base64UrlDecode(b64url: string): Uint8Array {
1533
- // Add padding
1534
- const padLen = (4 - (b64url.length % 4)) % 4;
1535
- const b64 = (b64url + "=".repeat(padLen))
1536
- .replace(/-/g, "+")
1537
- .replace(/_/g, "/");
1538
-
1539
- // Node.js environment
1540
- if (typeof Buffer !== "undefined") {
1541
- return new Uint8Array(Buffer.from(b64, "base64"));
1542
- }
1543
- // Browser environment
1544
- const binary = atob(b64);
1545
- const bytes = new Uint8Array(binary.length);
1546
- for (let i = 0; i < binary.length; i++) {
1547
- bytes[i] = binary.charCodeAt(i);
1548
- }
1549
- return bytes;
1550
- }
1551
-
1552
- // ---------------------------
1553
- // Compression Helpers
1554
- // ---------------------------
1555
-
1556
- /** Compress using zlib deflate raw */
1557
- private static deflateRaw(data: Uint8Array): Uint8Array {
1558
- // Node.js environment
1559
- if (typeof require !== "undefined") {
1560
- try {
1561
- // eslint-disable-next-line @typescript-eslint/no-require-imports
1562
- const zlib = require("zlib");
1563
- return new Uint8Array(zlib.deflateRawSync(Buffer.from(data)));
1564
- } catch {
1565
- throw new Error("TPSUID7RB: compression not available");
1566
- }
1567
- }
1568
- // Browser: would need pako or similar library
1569
- throw new Error("TPSUID7RB: compression not available in browser");
1570
- }
1571
-
1572
- /** Decompress using zlib inflate raw */
1573
- private static inflateRaw(data: Uint8Array): Uint8Array {
1574
- // Node.js environment
1575
- if (typeof require !== "undefined") {
1576
- try {
1577
- // eslint-disable-next-line @typescript-eslint/no-require-imports
1578
- const zlib = require("zlib");
1579
- return new Uint8Array(zlib.inflateRawSync(Buffer.from(data)));
1580
- } catch {
1581
- throw new Error("TPSUID7RB: decompression failed");
1582
- }
1583
- }
1584
- // Browser: would need pako or similar library
1585
- throw new Error("TPSUID7RB: decompression not available in browser");
1586
- }
1587
-
1588
- // ---------------------------
1589
- // Cryptographic Sealing (Ed25519)
1590
- // ---------------------------
1591
-
1592
- /**
1593
- * Seal (sign) a TPS string to create a cryptographically verifiable TPS-UID.
1594
- * This appends an Ed25519 signature to the binary form.
1595
- *
1596
- * @param tps - The TPS string to seal
1597
- * @param privateKey - Ed25519 private key (hex or buffer)
1598
- * @param opts - Encoding options
1599
- * @returns Sealed binary TPS-UID
1600
- */
1601
- static seal(
1602
- tps: string,
1603
- privateKey: string | Buffer | Uint8Array,
1604
- opts?: TPSUID7RBEncodeOptions,
1605
- ): Uint8Array {
1606
- // 1. Create standard binary (unsealed first)
1607
- // We force the SEAL flag (bit 1) to be 0 initially for the "content to sign"
1608
- // But wait, we want the signature to cover the header too.
1609
- // Strategy: Construct the full binary with SEAL flag OFF, sign it, then set SEAL flag ON and append sig.
1610
- // Actually, the standard way is:
1611
- // Content = MAGIC + VER + FLAGS(with seal bit set) + TIME + NONCE + LEN + PAYLOAD
1612
- // Signature = Sign(Content)
1613
- // Final = Content + SEAL_TYPE + SIGNATURE
1614
-
1615
- const compress = opts?.compress ?? false;
1616
- const epochMs = opts?.epochMs ?? this.epochMsFromTPSString(tps);
1617
-
1618
- // Validate epoch
1619
- if (!Number.isInteger(epochMs) || epochMs < 0 || epochMs > 0xffffffffffff) {
1620
- throw new Error("epochMs must be a valid 48-bit non-negative integer");
1621
- }
1622
-
1623
- // Flags: Bit 0 = compress, Bit 1 = sealed
1624
- const flags = (compress ? 0x01 : 0x00) | 0x02; // Set SEAL bit
1625
-
1626
- // Generate Nonce
1627
- const nonceBuf = this.randomBytes(4);
1628
-
1629
- // Encode Payload
1630
- const tpsUtf8 = new TextEncoder().encode(tps);
1631
- const payload = compress ? this.deflateRaw(tpsUtf8) : tpsUtf8;
1632
- const lenVar = this.uvarintEncode(payload.length);
1633
-
1634
- // Construct Content (Header + Payload)
1635
- const contentLen = 4 + 1 + 1 + 6 + 4 + lenVar.length + payload.length;
1636
- const content = new Uint8Array(contentLen);
1637
- let offset = 0;
1638
-
1639
- content.set(this.MAGIC, offset);
1640
- offset += 4;
1641
- content[offset++] = this.VER;
1642
- content[offset++] = flags;
1643
- content.set(this.writeU48(epochMs), offset);
1644
- offset += 6;
1645
- content.set(nonceBuf, offset);
1646
- offset += 4;
1647
- content.set(lenVar, offset);
1648
- offset += lenVar.length;
1649
- content.set(payload, offset);
1650
-
1651
- // Sign the content
1652
- const signature = this.signEd25519(content, privateKey);
1653
- const sealType = 0x01; // Ed25519
1654
-
1655
- // Final Output: Content + SealType (1) + Signature (64)
1656
- const final = new Uint8Array(contentLen + 1 + signature.length);
1657
- final.set(content, 0);
1658
- final.set([sealType], contentLen);
1659
- final.set(signature, contentLen + 1);
1660
-
1661
- return final;
1662
- }
1663
-
1664
- /**
1665
- * Verify a sealed TPS-UID and decode it.
1666
- * Throws if signature is invalid or not sealed.
1667
- *
1668
- * @param sealedBytes - The binary sealed TPS-UID
1669
- * @param publicKey - Ed25519 public key (hex or buffer) to verify against
1670
- * @returns Decoded result
1671
- */
1672
- static verifyAndDecode(
1673
- sealedBytes: Uint8Array,
1674
- publicKey: string | Buffer | Uint8Array,
1675
- ): TPSUID7RBDecodeResult {
1676
- if (sealedBytes.length < 18) throw new Error("TPSUID7RB: too short");
1677
-
1678
- // Check Magic
1679
- if (
1680
- sealedBytes[0] !== 0x54 ||
1681
- sealedBytes[1] !== 0x50 ||
1682
- sealedBytes[2] !== 0x55 ||
1683
- sealedBytes[3] !== 0x37
1684
- ) {
1685
- throw new Error("TPSUID7RB: bad magic");
1686
- }
1687
-
1688
- // Check Flags for Sealed Bit (bit 1)
1689
- const flags = sealedBytes[5];
1690
- if ((flags & 0x02) === 0) {
1691
- throw new Error("TPSUID7RB: not a sealed UID");
1692
- }
1693
-
1694
- // 1. Parse the structure to find where content ends
1695
- // We need to parse LEN and Payload to find the split point
1696
- let offset = 16; // Start of LEN
1697
- // Decode LEN
1698
- const { value: tpsLen, bytesRead } = this.uvarintDecode(
1699
- sealedBytes,
1700
- offset,
1701
- );
1702
- offset += bytesRead;
1703
- const payloadEnd = offset + tpsLen;
1704
-
1705
- if (payloadEnd > sealedBytes.length) {
1706
- throw new Error("TPSUID7RB: length overflow (truncated)");
1707
- }
1708
-
1709
- // The Content to verify matches exactly [0 ... payloadEnd]
1710
- const content = sealedBytes.slice(0, payloadEnd);
1711
-
1712
- // After content: SealType (1 byte) + Signature
1713
- if (sealedBytes.length <= payloadEnd + 1) {
1714
- throw new Error("TPSUID7RB: missing signature data");
1715
- }
1716
-
1717
- const sealType = sealedBytes[payloadEnd];
1718
- if (sealType !== 0x01) {
1719
- throw new Error(
1720
- `TPSUID7RB: unsupported seal type 0x${sealType.toString(16)}`,
1721
- );
1722
- }
1723
-
1724
- const signature = sealedBytes.slice(payloadEnd + 1);
1725
- if (signature.length !== 64) {
1726
- throw new Error(
1727
- `TPSUID7RB: invalid Ed25519 signature length ${signature.length}`,
1728
- );
1729
- }
1730
-
1731
- // Verify
1732
- const isValid = this.verifyEd25519(content, signature, publicKey);
1733
- if (!isValid) {
1734
- throw new Error("TPSUID7RB: signature verification failed");
1735
- }
1736
-
1737
- // Decode (reuse standard logic, but ignoring the extra bytes at end is fine?)
1738
- // Actually standard logic doesn't expect trailing bytes unless we tell it to.
1739
- // But since we verified, we can just slice the content and decode that as a strict binary
1740
- // EXCEPT standard decodeBinary checks strict length.
1741
- // So we manually decode the components here to be safe and efficient.
1742
-
1743
- return this.decodeBinary(content); // Reuse strict decoder on the content part
1744
- }
1745
-
1746
- // --- Crypto Implementation (Ed25519) ---
1747
-
1748
- private static signEd25519(
1749
- data: Uint8Array,
1750
- privateKey: string | Buffer | Uint8Array,
1751
- ): Uint8Array {
1752
- if (typeof require !== "undefined") {
1753
- try {
1754
- // eslint-disable-next-line @typescript-eslint/no-require-imports
1755
- const crypto = require("crypto");
1756
-
1757
- let key: any;
1758
- if (typeof privateKey === "string") {
1759
- if (privateKey.includes("PRIVATE KEY")) {
1760
- // PEM format — use directly
1761
- key = privateKey;
1762
- } else {
1763
- // Hex-encoded DER/PKCS8
1764
- key = crypto.createPrivateKey({
1765
- key: Buffer.from(privateKey, "hex"),
1766
- format: "der",
1767
- type: "pkcs8",
1768
- });
1769
- }
1770
- } else if (
1771
- typeof privateKey === "object" &&
1772
- privateKey !== null &&
1773
- "asymmetricKeyType" in privateKey
1774
- ) {
1775
- // Node.js KeyObject (e.g. from crypto.generateKeyPairSync)
1776
- key = privateKey;
1777
- } else {
1778
- // Buffer or Uint8Array — assume DER/PKCS8 encoded
1779
- key = crypto.createPrivateKey({
1780
- key: Buffer.from(privateKey as Uint8Array),
1781
- format: "der",
1782
- type: "pkcs8",
1783
- });
1784
- }
1785
-
1786
- return new Uint8Array(crypto.sign(null, data, key));
1787
- } catch (e) {
1788
- throw new Error("TPSUID7RB: signing failed (check key format)");
1789
- }
1790
- }
1791
- throw new Error("TPSUID7RB: signing not available in browser");
1792
- }
1793
-
1794
- private static verifyEd25519(
1795
- data: Uint8Array,
1796
- signature: Uint8Array,
1797
- publicKey: string | Buffer | Uint8Array,
1798
- ): boolean {
1799
- if (typeof require !== "undefined") {
1800
- try {
1801
- // eslint-disable-next-line @typescript-eslint/no-require-imports
1802
- const crypto = require("crypto");
1803
- return crypto.verify(null, data, publicKey, signature);
1804
- } catch {
1805
- return false;
1806
- }
1807
- }
1808
- throw new Error("TPSUID7RB: verification not available in browser");
1809
- }
1810
-
1811
- // ---------------------------
1812
- // Random Bytes
1813
- // ---------------------------
1814
-
1815
- /** Generate cryptographically secure random bytes */
1816
- private static randomBytes(length: number): Uint8Array {
1817
- // Node.js environment
1818
- if (typeof require !== "undefined") {
1819
- try {
1820
- // eslint-disable-next-line @typescript-eslint/no-require-imports
1821
- const crypto = require("crypto");
1822
- return new Uint8Array(crypto.randomBytes(length));
1823
- } catch {
1824
- // Fallback to crypto.getRandomValues
1825
- }
1826
- }
1827
- // Browser or fallback
1828
- if (typeof crypto !== "undefined" && crypto.getRandomValues) {
1829
- const bytes = new Uint8Array(length);
1830
- crypto.getRandomValues(bytes);
1831
- return bytes;
1832
- }
1833
- throw new Error("TPSUID7RB: no crypto available");
1834
- }
1835
- }
1836
-
1837
653
  /**
1838
654
  * `TpsDate` is a Date-like wrapper with native TPS conversion helpers.
1839
655
  *
@@ -1844,247 +660,3 @@ export class TPSUID7RB {
1844
660
  * - `new TpsDate(tpsString)`
1845
661
  * - `new TpsDate(year, monthIndex, day?, hour?, minute?, second?, ms?)`
1846
662
  */
1847
- export class TpsDate {
1848
- private readonly internal: Date;
1849
-
1850
- private getTpsComponents(): TPSComponents {
1851
- const parsed = TPS.parse(this.toTPS(DefaultCalendars.TPS));
1852
- if (!parsed) {
1853
- throw new Error("TpsDate: failed to derive TPS components");
1854
- }
1855
- return parsed;
1856
- }
1857
-
1858
- private getTpsFullYear(): number {
1859
- const comp = this.getTpsComponents();
1860
- return (comp.millennium - 1) * 1000 + (comp.century - 1) * 100 + comp.year;
1861
- }
1862
-
1863
- constructor();
1864
- constructor(value: string | number | Date | TpsDate);
1865
- constructor(
1866
- year: number,
1867
- monthIndex: number,
1868
- day?: number,
1869
- hours?: number,
1870
- minutes?: number,
1871
- seconds?: number,
1872
- ms?: number,
1873
- );
1874
- constructor(
1875
- ...args:
1876
- | []
1877
- | [string | number | Date | TpsDate]
1878
- | [number, number, number?, number?, number?, number?, number?]
1879
- ) {
1880
- if (args.length === 0) {
1881
- this.internal = new Date();
1882
- return;
1883
- }
1884
-
1885
- if (args.length === 1) {
1886
- const value = args[0];
1887
- if (value instanceof TpsDate) {
1888
- this.internal = new Date(value.getTime());
1889
- return;
1890
- }
1891
- if (value instanceof Date) {
1892
- this.internal = new Date(value.getTime());
1893
- return;
1894
- }
1895
- if (typeof value === "string" && TpsDate.looksLikeTPS(value)) {
1896
- const parsed = TPS.toDate(value);
1897
- if (!parsed) {
1898
- throw new RangeError(`Invalid TPS date string: ${value}`);
1899
- }
1900
- this.internal = parsed;
1901
- return;
1902
- }
1903
-
1904
- this.internal = new Date(value);
1905
- return;
1906
- }
1907
-
1908
- const [year, monthIndex, day, hours, minutes, seconds, ms] = args;
1909
- this.internal = new Date(
1910
- year,
1911
- monthIndex,
1912
- day ?? 1,
1913
- hours ?? 0,
1914
- minutes ?? 0,
1915
- seconds ?? 0,
1916
- ms ?? 0,
1917
- );
1918
- }
1919
-
1920
- private static looksLikeTPS(input: string): boolean {
1921
- const s = input.trim();
1922
- return s.startsWith("tps://") || s.startsWith("T:") || s.startsWith("t:");
1923
- }
1924
-
1925
- static now(): number {
1926
- return Date.now();
1927
- }
1928
-
1929
- static parse(input: string): number {
1930
- if (this.looksLikeTPS(input)) {
1931
- const d = TPS.toDate(input);
1932
- return d ? d.getTime() : Number.NaN;
1933
- }
1934
- return Date.parse(input);
1935
- }
1936
-
1937
- static UTC(
1938
- year: number,
1939
- monthIndex: number,
1940
- day?: number,
1941
- hours?: number,
1942
- minutes?: number,
1943
- seconds?: number,
1944
- ms?: number,
1945
- ): number {
1946
- return Date.UTC(
1947
- year,
1948
- monthIndex,
1949
- day ?? 1,
1950
- hours ?? 0,
1951
- minutes ?? 0,
1952
- seconds ?? 0,
1953
- ms ?? 0,
1954
- );
1955
- }
1956
-
1957
- static fromTPS(tps: string): TpsDate {
1958
- return new TpsDate(tps);
1959
- }
1960
-
1961
- toGregorianDate(): Date {
1962
- return new Date(this.internal.getTime());
1963
- }
1964
-
1965
- toDate(): Date {
1966
- return this.toGregorianDate();
1967
- }
1968
-
1969
- toTPS(
1970
- calendar: string = DefaultCalendars.TPS,
1971
- opts?: { order?: TimeOrder },
1972
- ): string {
1973
- return TPS.fromDate(this.internal, calendar, opts);
1974
- }
1975
-
1976
- toTPSURI(
1977
- calendar: string = DefaultCalendars.TPS,
1978
- opts?: {
1979
- order?: TimeOrder;
1980
- latitude?: number;
1981
- longitude?: number;
1982
- altitude?: number;
1983
- isUnknownLocation?: boolean;
1984
- isHiddenLocation?: boolean;
1985
- isRedactedLocation?: boolean;
1986
- },
1987
- ): string {
1988
- const time = this.toTPS(calendar, { order: opts?.order });
1989
- const comp = TPS.parse(time) as TPSComponents;
1990
-
1991
- if (opts?.latitude !== undefined && opts?.longitude !== undefined) {
1992
- comp.latitude = opts.latitude;
1993
- comp.longitude = opts.longitude;
1994
- if (opts.altitude !== undefined) comp.altitude = opts.altitude;
1995
- } else if (opts?.isHiddenLocation) {
1996
- comp.isHiddenLocation = true;
1997
- } else if (opts?.isRedactedLocation) {
1998
- comp.isRedactedLocation = true;
1999
- } else {
2000
- comp.isUnknownLocation = true;
2001
- }
2002
-
2003
- return TPS.toURI(comp);
2004
- }
2005
-
2006
- getTime(): number {
2007
- return this.internal.getTime();
2008
- }
2009
-
2010
- valueOf(): number {
2011
- return this.internal.valueOf();
2012
- }
2013
-
2014
- toString(): string {
2015
- return this.toTPS(DefaultCalendars.TPS);
2016
- }
2017
-
2018
- toISOString(): string {
2019
- return this.internal.toISOString();
2020
- }
2021
-
2022
- toUTCString(): string {
2023
- return this.internal.toUTCString();
2024
- }
2025
-
2026
- toJSON(): string | null {
2027
- return this.internal.toJSON();
2028
- }
2029
-
2030
- getFullYear(): number {
2031
- return this.getTpsFullYear();
2032
- }
2033
-
2034
- getUTCFullYear(): number {
2035
- return this.getTpsFullYear();
2036
- }
2037
-
2038
- getMonth(): number {
2039
- return this.getTpsComponents().month - 1;
2040
- }
2041
-
2042
- getUTCMonth(): number {
2043
- return this.getTpsComponents().month - 1;
2044
- }
2045
-
2046
- getDate(): number {
2047
- return this.getTpsComponents().day;
2048
- }
2049
-
2050
- getUTCDate(): number {
2051
- return this.getTpsComponents().day;
2052
- }
2053
-
2054
- getHours(): number {
2055
- return this.getTpsComponents().hour;
2056
- }
2057
-
2058
- getUTCHours(): number {
2059
- return this.getTpsComponents().hour;
2060
- }
2061
-
2062
- getMinutes(): number {
2063
- return this.getTpsComponents().minute;
2064
- }
2065
-
2066
- getUTCMinutes(): number {
2067
- return this.getTpsComponents().minute;
2068
- }
2069
-
2070
- getSeconds(): number {
2071
- return this.getTpsComponents().second;
2072
- }
2073
-
2074
- getUTCSeconds(): number {
2075
- return this.getTpsComponents().second;
2076
- }
2077
-
2078
- getMilliseconds(): number {
2079
- return this.getTpsComponents().millisecond;
2080
- }
2081
-
2082
- getUTCMilliseconds(): number {
2083
- return this.getTpsComponents().millisecond;
2084
- }
2085
-
2086
- [Symbol.toPrimitive](hint: string): string | number {
2087
- if (hint === "number") return this.valueOf();
2088
- return this.toString();
2089
- }
2090
- }