@nextera.one/tps-standard 0.5.33 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +133 -56
  3. package/dist/date.d.ts +54 -0
  4. package/dist/date.js +174 -0
  5. package/dist/date.js.map +1 -0
  6. package/dist/driver-manager.d.ts +34 -0
  7. package/dist/driver-manager.js +53 -0
  8. package/dist/driver-manager.js.map +1 -0
  9. package/dist/drivers/chinese.d.ts +25 -0
  10. package/dist/drivers/chinese.js +485 -0
  11. package/dist/drivers/chinese.js.map +1 -0
  12. package/dist/drivers/gregorian.d.ts +3 -5
  13. package/dist/drivers/gregorian.js +26 -19
  14. package/dist/drivers/gregorian.js.map +1 -1
  15. package/dist/drivers/hijri.d.ts +1 -16
  16. package/dist/drivers/hijri.js +9 -102
  17. package/dist/drivers/hijri.js.map +1 -1
  18. package/dist/drivers/holocene.d.ts +6 -3
  19. package/dist/drivers/holocene.js +7 -20
  20. package/dist/drivers/holocene.js.map +1 -1
  21. package/dist/drivers/julian.d.ts +3 -10
  22. package/dist/drivers/julian.js +11 -71
  23. package/dist/drivers/julian.js.map +1 -1
  24. package/dist/drivers/persian.d.ts +1 -6
  25. package/dist/drivers/persian.js +17 -92
  26. package/dist/drivers/persian.js.map +1 -1
  27. package/dist/drivers/tps.d.ts +11 -28
  28. package/dist/drivers/tps.js +8 -58
  29. package/dist/drivers/tps.js.map +1 -1
  30. package/dist/drivers/unix.d.ts +5 -6
  31. package/dist/drivers/unix.js +10 -32
  32. package/dist/drivers/unix.js.map +1 -1
  33. package/dist/esm/date.js +170 -0
  34. package/dist/esm/date.js.map +1 -0
  35. package/dist/esm/driver-manager.js +49 -0
  36. package/dist/esm/driver-manager.js.map +1 -0
  37. package/dist/esm/drivers/chinese.js +481 -0
  38. package/dist/esm/drivers/chinese.js.map +1 -0
  39. package/dist/esm/drivers/gregorian.js +160 -0
  40. package/dist/esm/drivers/gregorian.js.map +1 -0
  41. package/dist/esm/drivers/hijri.js +184 -0
  42. package/dist/esm/drivers/hijri.js.map +1 -0
  43. package/dist/esm/drivers/holocene.js +115 -0
  44. package/dist/esm/drivers/holocene.js.map +1 -0
  45. package/dist/esm/drivers/julian.js +161 -0
  46. package/dist/esm/drivers/julian.js.map +1 -0
  47. package/dist/esm/drivers/persian.js +190 -0
  48. package/dist/esm/drivers/persian.js.map +1 -0
  49. package/dist/esm/drivers/tps.js +181 -0
  50. package/dist/esm/drivers/tps.js.map +1 -0
  51. package/dist/esm/drivers/unix.js +50 -0
  52. package/dist/esm/drivers/unix.js.map +1 -0
  53. package/dist/esm/index.js +873 -0
  54. package/dist/esm/index.js.map +1 -0
  55. package/dist/esm/types.js +28 -0
  56. package/dist/esm/types.js.map +1 -0
  57. package/dist/esm/uid.js +221 -0
  58. package/dist/esm/uid.js.map +1 -0
  59. package/dist/esm/utils/calendar.js +126 -0
  60. package/dist/esm/utils/calendar.js.map +1 -0
  61. package/dist/esm/utils/env.js +76 -0
  62. package/dist/esm/utils/env.js.map +1 -0
  63. package/dist/esm/utils/timezone.js +168 -0
  64. package/dist/esm/utils/timezone.js.map +1 -0
  65. package/dist/esm/utils/tps-string.js +160 -0
  66. package/dist/esm/utils/tps-string.js.map +1 -0
  67. package/dist/index.d.ts +84 -466
  68. package/dist/index.js +430 -1095
  69. package/dist/index.js.map +1 -1
  70. package/dist/types.d.ts +103 -0
  71. package/dist/types.js +31 -0
  72. package/dist/types.js.map +1 -0
  73. package/dist/uid.d.ts +48 -0
  74. package/dist/uid.js +225 -0
  75. package/dist/uid.js.map +1 -0
  76. package/dist/utils/calendar.d.ts +55 -0
  77. package/dist/utils/calendar.js +136 -0
  78. package/dist/utils/calendar.js.map +1 -0
  79. package/dist/utils/env.d.ts +12 -0
  80. package/dist/utils/env.js +79 -0
  81. package/dist/utils/env.js.map +1 -0
  82. package/dist/utils/timezone.d.ts +32 -0
  83. package/dist/utils/timezone.js +173 -0
  84. package/dist/utils/timezone.js.map +1 -0
  85. package/dist/utils/tps-string.d.ts +12 -0
  86. package/dist/utils/tps-string.js +164 -0
  87. package/dist/utils/tps-string.js.map +1 -0
  88. package/package.json +20 -5
  89. package/src/date.ts +243 -0
  90. package/src/driver-manager.ts +54 -0
  91. package/src/drivers/chinese.ts +542 -0
  92. package/src/drivers/gregorian.ts +29 -27
  93. package/src/drivers/hijri.ts +13 -113
  94. package/src/drivers/holocene.ts +11 -12
  95. package/src/drivers/julian.ts +18 -72
  96. package/src/drivers/persian.ts +25 -92
  97. package/src/drivers/tps.ts +16 -55
  98. package/src/drivers/unix.ts +12 -33
  99. package/src/index.ts +384 -1556
  100. package/src/types.ts +131 -0
  101. package/src/uid.ts +308 -0
  102. package/src/utils/calendar.ts +161 -0
  103. package/src/utils/env.ts +88 -0
  104. package/src/utils/timezone.ts +182 -0
  105. package/src/utils/tps-string.ts +166 -0
package/src/types.ts ADDED
@@ -0,0 +1,131 @@
1
+ /**
2
+ * TPS: Temporal Positioning System
3
+ * Shared types and interfaces.
4
+ */
5
+
6
+ /**
7
+ * Calendar codes are plain strings to allow arbitrary user-defined
8
+ * calendars. The library still exports constants for the built-in values.
9
+ */
10
+ export const DefaultCalendars = {
11
+ TPS: "tps",
12
+ GREG: "greg",
13
+ HIJ: "hij",
14
+ PER: "per",
15
+ JUL: "jul",
16
+ HOLO: "holo",
17
+ UNIX: "unix",
18
+ CHIN: "chin",
19
+ } as const;
20
+
21
+ /**
22
+ * Specifies the direction of the time-component hierarchy when serializing or
23
+ * deserializing a TPS string. The default is 'descending'.
24
+ */
25
+ export enum TimeOrder {
26
+ DESC = "desc",
27
+ ASC = "asc",
28
+ }
29
+
30
+ export interface TPSComponents {
31
+ // --- TEMPORAL ---
32
+ calendar: string;
33
+ millennium: number;
34
+ century: number;
35
+ year: number;
36
+ month: number;
37
+ day: number;
38
+ hour: number;
39
+ minute: number;
40
+ second: number;
41
+ millisecond: number;
42
+
43
+ // --- OPTIONAL UNIX BACKUP ---
44
+ unixSeconds?: number;
45
+
46
+ // --- SPATIAL: GPS Coordinates ---
47
+ latitude?: number;
48
+ longitude?: number;
49
+ altitude?: number;
50
+
51
+ // --- SPATIAL: Geospatial Cells ---
52
+ s2Cell?: string;
53
+ h3Cell?: string;
54
+ plusCode?: string;
55
+ what3words?: string;
56
+
57
+ // --- SPATIAL: Place layer (P:cc=JO,ci=AMM,...) ---
58
+ /** ISO 3166-1 alpha-2 country code, e.g. "JO" → P:cc=JO */
59
+ placeCountryCode?: string;
60
+ /** Full country name, e.g. "Jordan" → P:cn=Jordan */
61
+ placeCountryName?: string;
62
+ /** City IATA/ISO code, e.g. "AMM" → P:ci=AMM */
63
+ placeCityCode?: string;
64
+ /** Full city name, e.g. "Amman" → P:ct=Amman */
65
+ placeCityName?: string;
66
+
67
+ // --- SPATIAL: Network / Digital Location ---
68
+ /** IPv4 address (net:ip4:x or NIP4:x) */
69
+ ipv4?: string;
70
+ /** IPv6 address (net:ip6:x or NIP6:x) */
71
+ ipv6?: string;
72
+ /** Logical node / host name (node:api-1 or NODE:api-1) */
73
+ nodeName?: string;
74
+
75
+ // --- SPATIAL: Structural Anchors ---
76
+ building?: string;
77
+ floor?: string;
78
+ room?: string;
79
+ door?: string;
80
+ zone?: string;
81
+
82
+ /** Raw pre-@ space anchor (generic/legacy/planet/adm) */
83
+ spaceAnchor?: string;
84
+
85
+ // --- SPATIAL: Privacy Markers ---
86
+ isUnknownLocation?: boolean;
87
+ isRedactedLocation?: boolean;
88
+ isHiddenLocation?: boolean;
89
+
90
+ // --- PROVENANCE ---
91
+ actor?: string;
92
+ signature?: string;
93
+
94
+ // --- EXTENSIONS (;KEY:val or ;key=val after T: tokens, before #) ---
95
+ extensions?: Record<string, string>;
96
+
97
+ // --- CONTEXT (#C:key=val;key=val in fragment) ---
98
+ /** Structured context key-value pairs from the #C: fragment block */
99
+ context?: Record<string, string>;
100
+
101
+ order?: TimeOrder;
102
+ }
103
+
104
+ /**
105
+ * Interface for Calendar Driver plugins.
106
+ */
107
+ export interface CalendarDriver {
108
+ readonly code: string;
109
+ readonly name?: string;
110
+ getComponentsFromDate(date: Date): Partial<TPSComponents>;
111
+ getDateFromComponents(components: Partial<TPSComponents>): Date;
112
+ getFromDate(date: Date): string;
113
+ parseDate(input: string, format?: string): Partial<TPSComponents>;
114
+ format(components: Partial<TPSComponents>, format?: string): string;
115
+ validate(input: string | Partial<TPSComponents>): boolean;
116
+ getMetadata(): CalendarMetadata;
117
+ }
118
+
119
+ /**
120
+ * Metadata about a calendar system.
121
+ */
122
+ export interface CalendarMetadata {
123
+ name: string;
124
+ monthNames?: string[];
125
+ monthNamesShort?: string[];
126
+ dayNames?: string[];
127
+ dayNamesShort?: string[];
128
+ isLunar?: boolean;
129
+ monthsPerYear?: number;
130
+ epochYear?: number;
131
+ }
package/src/uid.ts ADDED
@@ -0,0 +1,308 @@
1
+ /**
2
+ * TPS-UID v1 — Temporal Positioning System Identifier (Binary Reversible)
3
+ */
4
+
5
+ import { TPS } from "./index";
6
+ import { DefaultCalendars, TimeOrder } from "./types";
7
+ import { Env } from "./utils/env";
8
+
9
+ /**
10
+ * Decoded result from TPSUID7RB binary format.
11
+ */
12
+ export type TPSUID7RBDecodeResult = {
13
+ version: "tpsuid7rb";
14
+ epochMs: number;
15
+ compressed: boolean;
16
+ nonce: number;
17
+ tps: string;
18
+ };
19
+
20
+ /**
21
+ * Encoding options for TPSUID7RB.
22
+ */
23
+ export type TPSUID7RBEncodeOptions = {
24
+ compress?: boolean;
25
+ epochMs?: number;
26
+ };
27
+
28
+ export class TPSUID7RB {
29
+ private static readonly MAGIC = new Uint8Array([0x54, 0x50, 0x55, 0x37]);
30
+ private static readonly VER = 0x01;
31
+ private static readonly PREFIX = "tpsuid7rb_";
32
+ public static readonly REGEX = /^tpsuid7rb_[A-Za-z0-9_-]+$/;
33
+
34
+ static encodeBinary(
35
+ tps: string,
36
+ opts: TPSUID7RBEncodeOptions = {},
37
+ ): Uint8Array {
38
+ const compress = opts.compress ?? false;
39
+ const epochMs = opts.epochMs ?? this.epochMsFromTPSString(tps);
40
+
41
+ if (!Number.isInteger(epochMs) || epochMs < 0 || epochMs > 0xffffffffffff) {
42
+ throw new Error(
43
+ "TPSUID7RB: Invalid epochMs (must be 48-bit non-negative integer)",
44
+ );
45
+ }
46
+
47
+ const flags = compress ? 0x01 : 0x00;
48
+ const nonceBuf = Env.randomBytes(4);
49
+
50
+ const tpsUtf8 = new TextEncoder().encode(tps);
51
+ const payload = compress ? Env.deflate(tpsUtf8) : tpsUtf8;
52
+ const lenVar = this.uvarintEncode(payload.length);
53
+
54
+ const out = new Uint8Array(
55
+ 4 + 1 + 1 + 6 + 4 + lenVar.length + payload.length,
56
+ );
57
+ let offset = 0;
58
+
59
+ out.set(this.MAGIC, offset);
60
+ offset += 4;
61
+ out[offset++] = this.VER;
62
+ out[offset++] = flags;
63
+ out.set(this.writeU48(epochMs), offset);
64
+ offset += 6;
65
+ out.set(nonceBuf, offset);
66
+ offset += 4;
67
+ out.set(lenVar, offset);
68
+ offset += lenVar.length;
69
+ out.set(payload, offset);
70
+
71
+ return out;
72
+ }
73
+
74
+ static decodeBinary(bytes: Uint8Array): TPSUID7RBDecodeResult {
75
+ if (bytes.length < 17) throw new Error("TPSUID7RB: too short");
76
+ if (
77
+ bytes[0] !== 0x54 ||
78
+ bytes[1] !== 0x50 ||
79
+ bytes[2] !== 0x55 ||
80
+ bytes[3] !== 0x37
81
+ ) {
82
+ throw new Error("TPSUID7RB: bad magic");
83
+ }
84
+
85
+ const ver = bytes[4];
86
+ if (ver !== this.VER)
87
+ throw new Error(`TPSUID7RB: unsupported version ${ver}`);
88
+
89
+ const flags = bytes[5];
90
+ const compressed = (flags & 0x01) === 0x01;
91
+ const epochMs = this.readU48(bytes, 6);
92
+ const nonce =
93
+ ((bytes[12] << 24) >>> 0) +
94
+ ((bytes[13] << 16) >>> 0) +
95
+ ((bytes[14] << 8) >>> 0) +
96
+ bytes[15];
97
+
98
+ let offset = 16;
99
+ const { value: tpsLen, bytesRead } = this.uvarintDecode(bytes, offset);
100
+ offset += bytesRead;
101
+
102
+ if (offset + tpsLen > bytes.length)
103
+ throw new Error("TPSUID7RB: length overflow");
104
+
105
+ const payload = bytes.slice(offset, offset + tpsLen);
106
+ const tpsUtf8 = compressed ? Env.inflate(payload) : payload;
107
+ const tps = new TextDecoder().decode(tpsUtf8);
108
+
109
+ return { version: "tpsuid7rb", epochMs, compressed, nonce, tps };
110
+ }
111
+
112
+ static encodeBinaryB64(tps: string, opts?: TPSUID7RBEncodeOptions): string {
113
+ const bytes = this.encodeBinary(tps, opts ?? {});
114
+ return `${this.PREFIX}${this.base64UrlEncode(bytes)}`;
115
+ }
116
+
117
+ static decodeBinaryB64(id: string): TPSUID7RBDecodeResult {
118
+ const s = id.trim();
119
+ if (!s.startsWith(this.PREFIX))
120
+ throw new Error("TPSUID7RB: missing prefix");
121
+ return this.decodeBinary(this.base64UrlDecode(s.slice(this.PREFIX.length)));
122
+ }
123
+
124
+ static validateBinaryB64(id: string): boolean {
125
+ return this.REGEX.test(id.trim());
126
+ }
127
+
128
+ static generate(opts?: {
129
+ latitude?: number;
130
+ longitude?: number;
131
+ altitude?: number;
132
+ compress?: boolean;
133
+ order?: TimeOrder;
134
+ }): string {
135
+ const now = new Date();
136
+ const time = TPS.fromDate(now, DefaultCalendars.TPS, {
137
+ order: opts?.order,
138
+ });
139
+ let space = "unknown";
140
+
141
+ if (opts?.latitude !== undefined && opts?.longitude !== undefined) {
142
+ space = `${opts.latitude},${opts.longitude}`;
143
+ if (opts.altitude !== undefined) space += `,${opts.altitude}m`;
144
+ }
145
+
146
+ const tps = `tps://${space}@${time}`;
147
+ return this.encodeBinaryB64(tps, {
148
+ compress: opts?.compress,
149
+ epochMs: now.getTime(),
150
+ });
151
+ }
152
+
153
+ static seal(
154
+ tps: string,
155
+ privateKey: any,
156
+ opts?: TPSUID7RBEncodeOptions,
157
+ ): Uint8Array {
158
+ const compress = opts?.compress ?? false;
159
+ const epochMs = opts?.epochMs ?? this.epochMsFromTPSString(tps);
160
+
161
+ if (!Number.isInteger(epochMs) || epochMs < 0 || epochMs > 0xffffffffffff) {
162
+ throw new Error("TPSUID7RB: Invalid epochMs");
163
+ }
164
+
165
+ const flags = (compress ? 0x01 : 0x00) | 0x02; // Set SEAL bit
166
+ const nonceBuf = Env.randomBytes(4);
167
+
168
+ const tpsUtf8 = new TextEncoder().encode(tps);
169
+ const payload = compress ? Env.deflate(tpsUtf8) : tpsUtf8;
170
+ const lenVar = this.uvarintEncode(payload.length);
171
+
172
+ const contentLen = 4 + 1 + 1 + 6 + 4 + lenVar.length + payload.length;
173
+ const content = new Uint8Array(contentLen);
174
+ let offset = 0;
175
+
176
+ content.set(this.MAGIC, offset);
177
+ offset += 4;
178
+ content[offset++] = this.VER;
179
+ content[offset++] = flags;
180
+ content.set(this.writeU48(epochMs), offset);
181
+ offset += 6;
182
+ content.set(nonceBuf, offset);
183
+ offset += 4;
184
+ content.set(lenVar, offset);
185
+ offset += lenVar.length;
186
+ content.set(payload, offset);
187
+
188
+ const signature = Env.signEd25519(content, privateKey);
189
+ const final = new Uint8Array(contentLen + 1 + signature.length);
190
+ final.set(content, 0);
191
+ final.set([0x01], contentLen); // Ed25519 type
192
+ final.set(signature, contentLen + 1);
193
+
194
+ return final;
195
+ }
196
+
197
+ static verifyAndDecode(
198
+ sealedBytes: Uint8Array,
199
+ publicKey: any,
200
+ ): TPSUID7RBDecodeResult {
201
+ if (sealedBytes.length < 18) throw new Error("TPSUID7RB: too short");
202
+ if (sealedBytes[5] & 0x02 ? false : true)
203
+ throw new Error("TPSUID7RB: not sealed");
204
+
205
+ let offset = 16;
206
+ const { value: tpsLen, bytesRead } = this.uvarintDecode(
207
+ sealedBytes,
208
+ offset,
209
+ );
210
+ const payloadEnd = offset + bytesRead + tpsLen;
211
+
212
+ const content = sealedBytes.slice(0, payloadEnd);
213
+ const signature = sealedBytes.slice(payloadEnd + 1);
214
+
215
+ if (!Env.verifyEd25519(content, signature, publicKey)) {
216
+ throw new Error("TPSUID7RB: verification failed");
217
+ }
218
+
219
+ return this.decodeBinary(content);
220
+ }
221
+
222
+ public static epochMsFromTPSString(tps: string): number {
223
+ const date = TPS.toDate(tps);
224
+ if (date) return date.getTime();
225
+ const stripped = tps.replace(/;[^?#]*/, "").replace(/[?#].*$/, "");
226
+ const retryDate = TPS.toDate(stripped);
227
+ if (!retryDate) throw new Error("TPS: unable to parse date for epoch");
228
+ return retryDate.getTime();
229
+ }
230
+
231
+ private static writeU48(epochMs: number): Uint8Array {
232
+ const b = new Uint8Array(6);
233
+ const v = BigInt(epochMs);
234
+ b[0] = Number((v >> 40n) & 0xffn);
235
+ b[1] = Number((v >> 32n) & 0xffn);
236
+ b[2] = Number((v >> 24n) & 0xffn);
237
+ b[3] = Number((v >> 16n) & 0xffn);
238
+ b[4] = Number((v >> 8n) & 0xffn);
239
+ b[5] = Number(v & 0xffn);
240
+ return b;
241
+ }
242
+
243
+ private static readU48(bytes: Uint8Array, offset: number): number {
244
+ const v =
245
+ (BigInt(bytes[offset]) << 40n) +
246
+ (BigInt(bytes[offset + 1]) << 32n) +
247
+ (BigInt(bytes[offset + 2]) << 24n) +
248
+ (BigInt(bytes[offset + 3]) << 16n) +
249
+ (BigInt(bytes[offset + 4]) << 8n) +
250
+ BigInt(bytes[offset + 5]);
251
+ return Number(v);
252
+ }
253
+
254
+ private static uvarintEncode(n: number): Uint8Array {
255
+ const out: number[] = [];
256
+ let x = n >>> 0;
257
+ while (x >= 0x80) {
258
+ out.push((x & 0x7f) | 0x80);
259
+ x >>>= 7;
260
+ }
261
+ out.push(x);
262
+ return new Uint8Array(out);
263
+ }
264
+
265
+ private static uvarintDecode(
266
+ bytes: Uint8Array,
267
+ offset: number,
268
+ ): { value: number; bytesRead: number } {
269
+ let x = 0,
270
+ s = 0,
271
+ i = 0;
272
+ while (true) {
273
+ const b = bytes[offset + i];
274
+ if (b < 0x80) {
275
+ x |= b << s;
276
+ return { value: x >>> 0, bytesRead: i + 1 };
277
+ }
278
+ x |= (b & 0x7f) << s;
279
+ s += 7;
280
+ i++;
281
+ }
282
+ }
283
+
284
+ private static base64UrlEncode(bytes: Uint8Array): string {
285
+ if (typeof Buffer !== "undefined") {
286
+ return Buffer.from(bytes)
287
+ .toString("base64")
288
+ .replace(/\+/g, "-")
289
+ .replace(/\//g, "_")
290
+ .replace(/=+$/g, "");
291
+ }
292
+ return btoa(String.fromCharCode(...bytes))
293
+ .replace(/\+/g, "-")
294
+ .replace(/\//g, "_")
295
+ .replace(/=+$/g, "");
296
+ }
297
+
298
+ private static base64UrlDecode(b64url: string): Uint8Array {
299
+ const padLen = (4 - (b64url.length % 4)) % 4;
300
+ const b64 = (b64url + "=".repeat(padLen))
301
+ .replace(/-/g, "+")
302
+ .replace(/_/g, "/");
303
+ if (typeof Buffer !== "undefined")
304
+ return new Uint8Array(Buffer.from(b64, "base64"));
305
+ const binary = atob(b64);
306
+ return new Uint8Array(Array.from(binary).map((c) => c.charCodeAt(0)));
307
+ }
308
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Shared Calendar Math Utilities
3
+ */
4
+
5
+ /**
6
+ * Gregorian -> Julian Day Number
7
+ */
8
+ export function gregorianToJdn(gy: number, gm: number, gd: number): number {
9
+ const a = Math.floor((14 - gm) / 12);
10
+ const y = gy + 4800 - a;
11
+ const m = gm + 12 * a - 3;
12
+ return (
13
+ gd +
14
+ Math.floor((153 * m + 2) / 5) +
15
+ 365 * y +
16
+ Math.floor(y / 4) -
17
+ Math.floor(y / 100) +
18
+ Math.floor(y / 400) -
19
+ 32045
20
+ );
21
+ }
22
+
23
+ /**
24
+ * Julian Day Number -> Gregorian
25
+ */
26
+ export function jdnToGregorian(jdn: number): {
27
+ gy: number;
28
+ gm: number;
29
+ gd: number;
30
+ } {
31
+ const a = jdn + 32044;
32
+ const b = Math.floor((4 * a + 3) / 146097);
33
+ const c = a - Math.floor((146097 * b) / 4);
34
+ const d = Math.floor((4 * c + 3) / 1461);
35
+ const e = c - Math.floor((1461 * d) / 4);
36
+ const m = Math.floor((5 * e + 2) / 153);
37
+ const gd = e - Math.floor((153 * m + 2) / 5) + 1;
38
+ const gm = m + 3 - 12 * Math.floor(m / 10);
39
+ const gy = 100 * b + d - 4800 + Math.floor(m / 10);
40
+ return { gy, gm, gd };
41
+ }
42
+
43
+ /**
44
+ * Julian -> Julian Day Number
45
+ */
46
+ export function julianToJdn(jy: number, jm: number, jd: number): number {
47
+ const a = Math.floor((14 - jm) / 12);
48
+ const y = jy + 4800 - a;
49
+ const m = jm + 12 * a - 3;
50
+ return (
51
+ jd + Math.floor((153 * m + 2) / 5) + 365 * y + Math.floor(y / 4) - 32083
52
+ );
53
+ }
54
+
55
+ /**
56
+ * Julian Day Number -> Julian
57
+ */
58
+ export function jdnToJulian(jdn: number): {
59
+ jy: number;
60
+ jm: number;
61
+ jd: number;
62
+ } {
63
+ const c = jdn + 32082;
64
+ const d = Math.floor((4 * c + 3) / 1461);
65
+ const e = c - Math.floor((1461 * d) / 4);
66
+ const m = Math.floor((5 * e + 2) / 153);
67
+ const jd = e - Math.floor((153 * m + 2) / 5) + 1;
68
+ const jm = m + 3 - 12 * Math.floor(m / 10);
69
+ const jy = d - 4800 + Math.floor(m / 10);
70
+ return { jy, jm, jd };
71
+ }
72
+
73
+ /**
74
+ * Persian -> Julian Day Number
75
+ */
76
+ export function persianToJdn(jy: number, jm: number, jd: number): number {
77
+ const EPOCH = 1948320;
78
+ const epbase = jy - (jy >= 0 ? 474 : 473);
79
+ const epyear = 474 + (epbase % 2820);
80
+ return (
81
+ jd +
82
+ (jm <= 7 ? (jm - 1) * 31 : (jm - 1) * 30 + 6) +
83
+ Math.floor((epyear * 682 - 110) / 2816) +
84
+ (epyear - 1) * 365 +
85
+ Math.floor(epbase / 2820) * 1029983 +
86
+ EPOCH -
87
+ 1
88
+ );
89
+ }
90
+
91
+ /**
92
+ * Julian Day Number -> Persian
93
+ */
94
+ export function jdnToPersian(jdn: number): {
95
+ jy: number;
96
+ jm: number;
97
+ jd: number;
98
+ } {
99
+ const depoch = jdn - persianToJdn(475, 1, 1);
100
+ const cycle = Math.floor(depoch / 1029983);
101
+ const cyear = depoch % 1029983;
102
+ let ycycle: number;
103
+ if (cyear === 1029982) {
104
+ ycycle = 2820;
105
+ } else {
106
+ const aux1 = Math.floor(cyear / 366);
107
+ const aux2 = cyear % 366;
108
+ ycycle =
109
+ Math.floor((2134 * aux1 + 2816 * aux2 + 2815) / 1028522) + aux1 + 1;
110
+ }
111
+ const jy = ycycle + 2820 * cycle + 474;
112
+ const yday = jdn - persianToJdn(jy, 1, 1) + 1;
113
+ const jm = yday <= 186 ? Math.ceil(yday / 31) : Math.ceil((yday - 6) / 30);
114
+ const jd = jdn - persianToJdn(jy, jm, 1) + 1;
115
+ return { jy: jy <= 0 ? jy - 1 : jy, jm, jd };
116
+ }
117
+
118
+ /**
119
+ * Convert Gregorian to Hijri (Tabular Islamic Calendar).
120
+ */
121
+ export function gregorianToHijri(
122
+ gy: number,
123
+ gm: number,
124
+ gd: number,
125
+ ): { hy: number; hm: number; hd: number } {
126
+ const jdn = gregorianToJdn(gy, gm, gd);
127
+ const L = jdn - 1948440 + 10632;
128
+ const N = Math.floor((L - 1) / 10631);
129
+ const L2 = L - 10631 * N + 354;
130
+ const J =
131
+ Math.floor((10985 - L2) / 5316) * Math.floor((50 * L2) / 17719) +
132
+ Math.floor(L2 / 5670) * Math.floor((43 * L2) / 15238);
133
+ const L3 =
134
+ L2 -
135
+ Math.floor((30 - J) / 15) * Math.floor((17719 * J) / 50) -
136
+ Math.floor(J / 16) * Math.floor((15238 * J) / 43) +
137
+ 29;
138
+ const hm = Math.floor((24 * L3) / 709);
139
+ const hd = L3 - Math.floor((709 * hm) / 24);
140
+ const hy = 30 * N + J - 30;
141
+ return { hy, hm, hd };
142
+ }
143
+
144
+ /**
145
+ * Convert Hijri to Gregorian.
146
+ */
147
+ export function hijriToGregorian(
148
+ hy: number,
149
+ hm: number,
150
+ hd: number,
151
+ ): { gy: number; gm: number; gd: number } {
152
+ const jdn =
153
+ Math.floor((11 * hy + 3) / 30) +
154
+ 354 * hy +
155
+ 30 * hm -
156
+ Math.floor((hm - 1) / 2) +
157
+ hd +
158
+ 1948440 -
159
+ 385;
160
+ return jdnToGregorian(jdn);
161
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Environment & Compatibility Layer
3
+ * Abstracts Node.js vs Browser differences.
4
+ */
5
+
6
+ /* eslint-disable @typescript-eslint/no-require-imports */
7
+
8
+ const isNode = typeof require !== "undefined";
9
+
10
+ /**
11
+ * Node.js crypto module (conditional)
12
+ */
13
+ const crypto = isNode ? require("crypto") : null;
14
+
15
+ /**
16
+ * Node.js zlib module (conditional)
17
+ */
18
+ const zlib = isNode ? require("zlib") : null;
19
+
20
+ export const Env = {
21
+ isNode,
22
+
23
+ randomBytes(length: number): Uint8Array {
24
+ if (isNode && crypto) {
25
+ return new Uint8Array(crypto.randomBytes(length));
26
+ }
27
+ if (typeof window !== "undefined" && window.crypto) {
28
+ return window.crypto.getRandomValues(new Uint8Array(length));
29
+ }
30
+ throw new Error("TPS: randomBytes not available in this environment");
31
+ },
32
+
33
+ deflate(data: Uint8Array): Uint8Array {
34
+ if (isNode && zlib) {
35
+ return new Uint8Array(zlib.deflateRawSync(data));
36
+ }
37
+ throw new Error("TPS: deflate not available in this environment");
38
+ },
39
+
40
+ inflate(data: Uint8Array): Uint8Array {
41
+ if (isNode && zlib) {
42
+ return new Uint8Array(zlib.inflateRawSync(data));
43
+ }
44
+ throw new Error("TPS: inflate not available in this environment");
45
+ },
46
+
47
+ signEd25519(data: Uint8Array, privateKey: any): Uint8Array {
48
+ if (isNode && crypto) {
49
+ let key: any;
50
+ if (typeof privateKey === "string") {
51
+ if (privateKey.includes("PRIVATE KEY")) {
52
+ key = privateKey;
53
+ } else {
54
+ key = crypto.createPrivateKey({
55
+ key: Buffer.from(privateKey, "hex"),
56
+ format: "der",
57
+ type: "pkcs8",
58
+ });
59
+ }
60
+ } else if (
61
+ typeof privateKey === "object" &&
62
+ privateKey !== null &&
63
+ "asymmetricKeyType" in privateKey
64
+ ) {
65
+ key = privateKey;
66
+ } else {
67
+ key = crypto.createPrivateKey({
68
+ key: Buffer.from(privateKey),
69
+ format: "der",
70
+ type: "pkcs8",
71
+ });
72
+ }
73
+ return new Uint8Array(crypto.sign(null, data, key));
74
+ }
75
+ throw new Error("TPS: signEd25519 not available in this environment");
76
+ },
77
+
78
+ verifyEd25519(
79
+ data: Uint8Array,
80
+ signature: Uint8Array,
81
+ publicKey: any,
82
+ ): boolean {
83
+ if (isNode && crypto) {
84
+ return crypto.verify(null, data, publicKey, signature);
85
+ }
86
+ throw new Error("TPS: verifyEd25519 not available in this environment");
87
+ },
88
+ };