@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/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.
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
export
|
|
30
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
-
}
|