@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.
- 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/types.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
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
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Specifies the direction of the time-component hierarchy when serializing or
|
|
22
|
+
* deserializing a TPS string. The default is 'descending'.
|
|
23
|
+
*/
|
|
24
|
+
export enum TimeOrder {
|
|
25
|
+
DESC = "desc",
|
|
26
|
+
ASC = "asc",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface TPSComponents {
|
|
30
|
+
// --- TEMPORAL ---
|
|
31
|
+
calendar: string;
|
|
32
|
+
millennium: number;
|
|
33
|
+
century: number;
|
|
34
|
+
year: number;
|
|
35
|
+
month: number;
|
|
36
|
+
day: number;
|
|
37
|
+
hour: number;
|
|
38
|
+
minute: number;
|
|
39
|
+
second: number;
|
|
40
|
+
millisecond: number;
|
|
41
|
+
|
|
42
|
+
// --- OPTIONAL UNIX BACKUP ---
|
|
43
|
+
unixSeconds?: number;
|
|
44
|
+
|
|
45
|
+
// --- SPATIAL: GPS Coordinates ---
|
|
46
|
+
latitude?: number;
|
|
47
|
+
longitude?: number;
|
|
48
|
+
altitude?: number;
|
|
49
|
+
|
|
50
|
+
// --- SPATIAL: Geospatial Cells ---
|
|
51
|
+
s2Cell?: string;
|
|
52
|
+
h3Cell?: string;
|
|
53
|
+
plusCode?: string;
|
|
54
|
+
what3words?: string;
|
|
55
|
+
|
|
56
|
+
// --- SPATIAL: Structural Anchors ---
|
|
57
|
+
building?: string;
|
|
58
|
+
floor?: string;
|
|
59
|
+
room?: string;
|
|
60
|
+
zone?: string;
|
|
61
|
+
|
|
62
|
+
/** Raw pre-@ space anchor */
|
|
63
|
+
spaceAnchor?: string;
|
|
64
|
+
|
|
65
|
+
// --- SPATIAL: Privacy Markers ---
|
|
66
|
+
isUnknownLocation?: boolean;
|
|
67
|
+
isRedactedLocation?: boolean;
|
|
68
|
+
isHiddenLocation?: boolean;
|
|
69
|
+
|
|
70
|
+
// --- PROVENANCE ---
|
|
71
|
+
actor?: string;
|
|
72
|
+
signature?: string;
|
|
73
|
+
|
|
74
|
+
// --- CONTEXT ---
|
|
75
|
+
extensions?: Record<string, string>;
|
|
76
|
+
|
|
77
|
+
order?: TimeOrder;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Interface for Calendar Driver plugins.
|
|
82
|
+
*/
|
|
83
|
+
export interface CalendarDriver {
|
|
84
|
+
readonly code: string;
|
|
85
|
+
readonly name?: string;
|
|
86
|
+
getComponentsFromDate(date: Date): Partial<TPSComponents>;
|
|
87
|
+
getDateFromComponents(components: Partial<TPSComponents>): Date;
|
|
88
|
+
getFromDate(date: Date): string;
|
|
89
|
+
parseDate(input: string, format?: string): Partial<TPSComponents>;
|
|
90
|
+
format(components: Partial<TPSComponents>, format?: string): string;
|
|
91
|
+
validate(input: string | Partial<TPSComponents>): boolean;
|
|
92
|
+
getMetadata(): CalendarMetadata;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Metadata about a calendar system.
|
|
97
|
+
*/
|
|
98
|
+
export interface CalendarMetadata {
|
|
99
|
+
name: string;
|
|
100
|
+
monthNames?: string[];
|
|
101
|
+
monthNamesShort?: string[];
|
|
102
|
+
dayNames?: string[];
|
|
103
|
+
dayNamesShort?: string[];
|
|
104
|
+
isLunar?: boolean;
|
|
105
|
+
monthsPerYear?: number;
|
|
106
|
+
epochYear?: number;
|
|
107
|
+
}
|
package/src/uid.ts
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TPS-UID v1 — Temporal Positioning System Identifier (Binary Reversible)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { TPSComponents, DefaultCalendars, TimeOrder } from "./types";
|
|
6
|
+
import { Env } from "./utils/env";
|
|
7
|
+
import { TPS } from "./index";
|
|
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
|
+
}
|
package/src/utils/env.ts
ADDED
|
@@ -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
|
+
};
|