ts-server-lib 0.0.48

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 (46) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +8 -0
  3. package/db/TSJournal.d.ts +108 -0
  4. package/db/TSJournal.js +229 -0
  5. package/db/TSMongo.d.ts +103 -0
  6. package/db/TSMongo.js +516 -0
  7. package/db/TSRQW.d.ts +625 -0
  8. package/db/TSRQW.js +1204 -0
  9. package/db/TSRedis.d.ts +530 -0
  10. package/db/TSRedis.js +1368 -0
  11. package/db/TSRedisTB.d.ts +80 -0
  12. package/db/TSRedisTB.js +178 -0
  13. package/package.json +85 -0
  14. package/ussd/TSUssdMenu.d.ts +139 -0
  15. package/ussd/TSUssdMenu.js +368 -0
  16. package/ussd/TSUssdScreen.d.ts +58 -0
  17. package/ussd/TSUssdScreen.js +218 -0
  18. package/ussd/index.d.ts +3 -0
  19. package/ussd/index.js +19 -0
  20. package/ussd/providers/AfricasTalking.d.ts +3 -0
  21. package/ussd/providers/AfricasTalking.js +17 -0
  22. package/ussd/providers/AirtelDRC.d.ts +9 -0
  23. package/ussd/providers/AirtelDRC.js +31 -0
  24. package/ussd/providers/OrangeDRC.d.ts +5 -0
  25. package/ussd/providers/OrangeDRC.js +213 -0
  26. package/ussd/providers/VodacomDRC.d.ts +9 -0
  27. package/ussd/providers/VodacomDRC.js +48 -0
  28. package/ussd/providers/_.d.ts +55 -0
  29. package/ussd/providers/_.js +83 -0
  30. package/ussd/providers/index.d.ts +13 -0
  31. package/ussd/providers/index.js +56 -0
  32. package/utils/TSFifo.d.ts +109 -0
  33. package/utils/TSFifo.js +145 -0
  34. package/utils/TSFile.d.ts +36 -0
  35. package/utils/TSFile.js +244 -0
  36. package/utils/TSHash.d.ts +19 -0
  37. package/utils/TSHash.js +71 -0
  38. package/utils/TSRequest.d.ts +248 -0
  39. package/utils/TSRequest.js +689 -0
  40. package/utils/TSStub.d.ts +159 -0
  41. package/utils/TSStub.js +296 -0
  42. package/utils/abort.d.ts +18 -0
  43. package/utils/abort.js +97 -0
  44. package/utils/mime.json +11358 -0
  45. package/utils/object-keys.d.ts +39 -0
  46. package/utils/object-keys.js +52 -0
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ /**
3
+ * TSFifo — O(1) FIFO queue. The canonical replacement for `array.shift()` and `array.splice(0, n)`.
4
+ *
5
+ * ### Why this exists as a primitive
6
+ *
7
+ * `shift()` re-indexes every remaining element, so draining a queue of N with `shift()` is O(N²). The
8
+ * cost arrives at exactly the worst moment: queues are only deep when the system is already stressed, so
9
+ * the drain gets more expensive the more there is to drain. That is a positive feedback loop, not a
10
+ * constant factor — the same shape as the O(N) in-flight scan that was removed from the sports feed's
11
+ * pressure signal for the same reason.
12
+ *
13
+ * ### Why it lives HERE, in ts-server-lib
14
+ *
15
+ * It started in core-service, which was one layer too high: {@link TSRQWPool}'s own task backlog — the
16
+ * FIFTH instance of this defect — could not use it, because core-service depends on ts-server-lib and not
17
+ * the reverse. Copying it down would have produced two implementations of a module whose entire purpose is
18
+ * being the only one. So it sits at the bottom layer and core-service re-exports it, leaving
19
+ * `import { TSFifo } from 'core-service'` working for every service-side caller.
20
+ *
21
+ * It had already been diagnosed and hand-fixed once (`sports-service` `capacity-pump.ts`, head pointer plus
22
+ * periodic compaction, with the reasoning written out) while four other queues kept their own shape:
23
+ *
24
+ * - `feed/amqp/dispatch-admission.ts` — the admission gate's `hotQueue` / `moneyQueue`. Its own doc
25
+ * records `hotQueued` reaching **683** with 24.2 s waits; releasing 683 waiters by `shift()` is ~233 K
26
+ * element moves, on the money path, during a backlog.
27
+ * - `feed/amqp/worker.ts` — `pendingOutcomePublishes`, an uncapped array drained `splice(0, 32)` at a
28
+ * time, so each batch re-indexes everything still queued behind it.
29
+ * - `resolvers/outcomes.ts` — a 64-cap subscriber backlog. Cheap at that size, but it is the site that
30
+ * proves the point: its correct eviction is `'drop-oldest'`, the OPPOSITE of the admission queue's, and
31
+ * that decision was previously an unexplained `queue.shift()` in a callback.
32
+ * - `db/TSRQW.ts` `TSRQWPool.tasks` — the parse-pool backlog, right in the AMQP drain path. Inert while
33
+ * the pool keeps up (`parseQueued ~0` measured) and quadratic exactly when it stops keeping up, which
34
+ * is the moment a super-linear drain hurts most.
35
+ *
36
+ * Five queues, five shapes, one of them correct. That is what a per-site fix always converges to, so the
37
+ * shape is not the interesting part of this module — having exactly one of it is, and naming the overflow
38
+ * policy so the difference between those sites is a type rather than a comment.
39
+ *
40
+ * ALL FIVE now use this class; a sixth hand-rolled queue is the thing to reject in review.
41
+ *
42
+ * ### Design
43
+ *
44
+ * A head index makes `shift` O(1); the consumed prefix is reclaimed in one `splice` once it passes
45
+ * {@link TSFIFO_COMPACT_THRESHOLD}, which keeps the backing array from growing without bound while keeping
46
+ * amortised cost O(1). `maxSize` is optional and, when set, makes overflow an explicit, observable event
47
+ * rather than unbounded memory — a queue with no ceiling is a slower way to run out of RAM.
48
+ */
49
+ Object.defineProperty(exports, "__esModule", { value: true });
50
+ exports.TSFifo = exports.TSFIFO_COMPACT_THRESHOLD = void 0;
51
+ /**
52
+ * Consumed slots tolerated before the backing array is compacted.
53
+ *
54
+ * Compaction is one O(size) `splice` amortised over that many O(1) shifts, so a larger value trades
55
+ * transient memory for fewer memmoves. 1024 matches the value `capacity-pump` arrived at empirically.
56
+ */
57
+ exports.TSFIFO_COMPACT_THRESHOLD = 1024;
58
+ class TSFifo {
59
+ items = [];
60
+ head = 0;
61
+ peak = 0;
62
+ lost = 0;
63
+ compactThreshold;
64
+ maxSize;
65
+ overflow;
66
+ constructor(opts = {}) {
67
+ this.compactThreshold = opts.compactThreshold ?? exports.TSFIFO_COMPACT_THRESHOLD;
68
+ this.maxSize = opts.maxSize;
69
+ this.overflow = opts.overflow ?? 'reject-new';
70
+ }
71
+ /** Items currently queued. */
72
+ get size() {
73
+ return this.items.length - this.head;
74
+ }
75
+ /** Highest `size` observed since construction — the number a gauge should report. */
76
+ get peakSize() {
77
+ return this.peak;
78
+ }
79
+ /**
80
+ * Items lost to `maxSize` — rejected arrivals under `'reject-new'`, evicted heads under `'drop-oldest'`.
81
+ * Non-zero means the cap is doing something and should be visible: a silent drop is how data loss hides.
82
+ */
83
+ get dropped() {
84
+ return this.lost;
85
+ }
86
+ /**
87
+ * Append. Returns false only under `'reject-new'` overflow when the queue is full — nothing was
88
+ * enqueued. Under `'drop-oldest'` it always returns true, having evicted the head to make room.
89
+ */
90
+ push(item) {
91
+ if (this.maxSize !== undefined && this.size >= this.maxSize) {
92
+ if (this.overflow === 'reject-new') {
93
+ this.lost += 1;
94
+ return false;
95
+ }
96
+ // 'drop-oldest': evict the head, then fall through to append. Counted the same way — the caller
97
+ // needs to know it lost data regardless of which end went.
98
+ this.shift();
99
+ this.lost += 1;
100
+ }
101
+ this.items.push(item);
102
+ if (this.size > this.peak)
103
+ this.peak = this.size;
104
+ return true;
105
+ }
106
+ /** Remove and return the oldest item, or undefined when empty. O(1). */
107
+ shift() {
108
+ if (this.size === 0)
109
+ return undefined;
110
+ const item = this.items[this.head];
111
+ // Release the reference before advancing: without this the backing array pins every dequeued item
112
+ // until the next compaction, which for a 1024-slot prefix of large payloads is real memory.
113
+ this.items[this.head] = undefined;
114
+ this.head += 1;
115
+ this.compactIfNeeded();
116
+ return item;
117
+ }
118
+ /** Oldest item without removing it, or undefined when empty. */
119
+ peek() {
120
+ return this.size === 0 ? undefined : this.items[this.head];
121
+ }
122
+ /** Drain up to `max` items, oldest first. Amortised O(returned), never O(remaining). */
123
+ drain(max) {
124
+ const out = [];
125
+ for (let i = 0; i < max; i++) {
126
+ const item = this.shift();
127
+ if (item === undefined)
128
+ break;
129
+ out.push(item);
130
+ }
131
+ return out;
132
+ }
133
+ /** Discard everything. Use on shutdown/reset paths. */
134
+ clear() {
135
+ this.items = [];
136
+ this.head = 0;
137
+ }
138
+ compactIfNeeded() {
139
+ if (this.head < this.compactThreshold)
140
+ return;
141
+ this.items.splice(0, this.head);
142
+ this.head = 0;
143
+ }
144
+ }
145
+ exports.TSFifo = TSFifo;
@@ -0,0 +1,36 @@
1
+ type FileType = {
2
+ filename: string;
3
+ type: string;
4
+ ext?: string;
5
+ mime?: string;
6
+ charset?: string;
7
+ };
8
+ export declare class TSFile {
9
+ static list(path?: string): FileType[];
10
+ static info(filename: string): FileType;
11
+ private static readFileHead;
12
+ /** Prefer POSIX `file(1)` when present; otherwise extension + optional magic-byte sniff (Windows-friendly). */
13
+ static mime(filename: string): {
14
+ mime: string;
15
+ charset?: string;
16
+ ext?: string;
17
+ };
18
+ private static mimeFromFileCommand;
19
+ private static mimeFallback;
20
+ private static charsetForMime;
21
+ static extension(mime: string): string | undefined;
22
+ static mimeFromExtension(ext: string): string;
23
+ static mimeFromFilename(filename: string): string | undefined;
24
+ static isImageMime(mime: string | undefined | null): boolean;
25
+ static isVideoMime(mime: string | undefined | null): boolean;
26
+ static isImageFilename(filename: string): boolean;
27
+ static isVideoFilename(filename: string): boolean;
28
+ private static bytesMatch;
29
+ static sniffMimeFromMagicBytes(buffer: ArrayBuffer | Uint8Array): "image/gif" | "image/jpeg" | "image/png" | "video/mp4" | "video/webm" | undefined;
30
+ private static headerSuggestsBinaryVideo;
31
+ private static normalizeVideoExtMime;
32
+ static resolveMediaContentType(filename: string, headerContentType: string | undefined | null, buffer: ArrayBuffer | Uint8Array): string | undefined;
33
+ static readJson<T = unknown>(filename: string): Promise<T>;
34
+ static writeJson(filename: string, data: unknown, space?: number): Promise<void>;
35
+ }
36
+ export {};
@@ -0,0 +1,244 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.TSFile = void 0;
37
+ const child_process_1 = require("child_process");
38
+ const fs_1 = require("fs");
39
+ const promises_1 = require("fs/promises");
40
+ const path_1 = require("path");
41
+ const mimeDB = __importStar(require("./mime.json"));
42
+ const STAT_METHODS = ['isFile', 'isDirectory', 'isSymbolicLink', 'isFIFO', 'isSocket', 'isCharacterDevice', 'isBlockDevice'];
43
+ const OCTET_STREAM = 'application/octet-stream';
44
+ const MIME_MAP = (mimeDB.default ??
45
+ mimeDB);
46
+ const EXT_TO_MIME = new Map();
47
+ const MEDIA_MIME_OVERRIDES = {
48
+ mp4: 'video/mp4',
49
+ m4v: 'video/mp4',
50
+ mov: 'video/quicktime',
51
+ webm: 'video/webm',
52
+ mkv: 'video/x-matroska',
53
+ avi: 'video/x-msvideo',
54
+ jpg: 'image/jpeg'
55
+ };
56
+ for (const [mime, data] of Object.entries(MIME_MAP)) {
57
+ for (const ext of data.extensions ?? []) {
58
+ const key = ext.toLowerCase();
59
+ if (!EXT_TO_MIME.has(key)) {
60
+ EXT_TO_MIME.set(key, mime);
61
+ }
62
+ }
63
+ }
64
+ class TSFile {
65
+ static list(path = './') {
66
+ const l = (0, fs_1.readdirSync)(path);
67
+ return l.map(f => TSFile.info((0, path_1.join)(path, f)));
68
+ }
69
+ static info(filename) {
70
+ let type = 'Unknown';
71
+ const s = (0, fs_1.existsSync)(filename) ? (0, fs_1.lstatSync)(filename) : null;
72
+ for (const method of STAT_METHODS) {
73
+ const fn = s ? s[method] : null;
74
+ if (fn && typeof fn === 'function' && fn.call(s)) {
75
+ type = method.replace(/^is/, '');
76
+ break;
77
+ }
78
+ }
79
+ return { filename, type, ...TSFile.mime(filename) };
80
+ }
81
+ static readFileHead(filename, maxLen) {
82
+ let fd;
83
+ try {
84
+ fd = (0, fs_1.openSync)(filename, 'r');
85
+ }
86
+ catch {
87
+ return undefined;
88
+ }
89
+ try {
90
+ const buf = Buffer.allocUnsafe(maxLen);
91
+ const n = (0, fs_1.readSync)(fd, buf, 0, maxLen, 0);
92
+ return new Uint8Array(buf.buffer, buf.byteOffset, n);
93
+ }
94
+ finally {
95
+ (0, fs_1.closeSync)(fd);
96
+ }
97
+ }
98
+ /** Prefer POSIX `file(1)` when present; otherwise extension + optional magic-byte sniff (Windows-friendly). */
99
+ static mime(filename) {
100
+ const fromFile = TSFile.mimeFromFileCommand(filename);
101
+ if (fromFile)
102
+ return fromFile;
103
+ return TSFile.mimeFallback(filename);
104
+ }
105
+ static mimeFromFileCommand(filename) {
106
+ if (process.platform === 'win32')
107
+ return null;
108
+ try {
109
+ const result = (0, child_process_1.execFileSync)('file', ['--mime-type', '-i', filename], { encoding: 'utf-8', maxBuffer: 1024 * 1024 });
110
+ const parts = result.split(/\s+/).filter(Boolean);
111
+ parts.shift();
112
+ const [mime, charset] = parts.map(p => p.replace(/;$/, ''));
113
+ return { mime, charset, ext: TSFile.extension(mime) };
114
+ }
115
+ catch {
116
+ return null;
117
+ }
118
+ }
119
+ static mimeFallback(filename) {
120
+ const extFromPath = (0, path_1.extname)(filename).replace(/^\./, '').toLowerCase();
121
+ const fromExt = TSFile.mimeFromExtension(extFromPath);
122
+ if (!(0, fs_1.existsSync)(filename)) {
123
+ const mime = fromExt ?? OCTET_STREAM;
124
+ return {
125
+ mime,
126
+ charset: TSFile.charsetForMime(mime),
127
+ ext: TSFile.extension(mime)
128
+ };
129
+ }
130
+ const st = (0, fs_1.lstatSync)(filename);
131
+ if (st.isDirectory()) {
132
+ return { mime: 'inode/directory', charset: 'binary', ext: undefined };
133
+ }
134
+ if (!st.isFile()) {
135
+ return { mime: fromExt ?? OCTET_STREAM, charset: 'binary', ext: TSFile.extension(fromExt ?? OCTET_STREAM) };
136
+ }
137
+ const head = TSFile.readFileHead(filename, 512);
138
+ const sniff = head ? TSFile.sniffMimeFromMagicBytes(head) : undefined;
139
+ const mime = sniff ?? fromExt ?? OCTET_STREAM;
140
+ return {
141
+ mime,
142
+ charset: TSFile.charsetForMime(mime),
143
+ ext: TSFile.extension(mime)
144
+ };
145
+ }
146
+ static charsetForMime(mime) {
147
+ const m = mime.toLowerCase();
148
+ if (m.startsWith('text/'))
149
+ return 'utf-8';
150
+ if (m.endsWith('+json') || m.endsWith('+xml'))
151
+ return 'utf-8';
152
+ if (m === 'application/javascript' || m === 'application/json')
153
+ return 'utf-8';
154
+ return 'binary';
155
+ }
156
+ static extension(mime) {
157
+ const mimeData = MIME_MAP[mime];
158
+ const { extensions } = mimeData || {};
159
+ return extensions?.[0];
160
+ }
161
+ static mimeFromExtension(ext) {
162
+ const normalized = ext.replace(/^\./, '').toLowerCase();
163
+ return MEDIA_MIME_OVERRIDES[normalized] ?? EXT_TO_MIME.get(normalized);
164
+ }
165
+ static mimeFromFilename(filename) {
166
+ const ext = (0, path_1.extname)(filename).replace(/^\./, '');
167
+ if (!ext)
168
+ return undefined;
169
+ return TSFile.mimeFromExtension(ext);
170
+ }
171
+ static isImageMime(mime) {
172
+ return typeof mime === 'string' && mime.toLowerCase().startsWith('image/');
173
+ }
174
+ static isVideoMime(mime) {
175
+ return typeof mime === 'string' && mime.toLowerCase().startsWith('video/');
176
+ }
177
+ static isImageFilename(filename) {
178
+ return TSFile.isImageMime(TSFile.mimeFromFilename(filename));
179
+ }
180
+ static isVideoFilename(filename) {
181
+ return TSFile.isVideoMime(TSFile.mimeFromFilename(filename));
182
+ }
183
+ static bytesMatch(u, offset, seq) {
184
+ if (u.byteLength < offset + seq.length)
185
+ return false;
186
+ for (let i = 0; i < seq.length; i++) {
187
+ if (u[offset + i] !== seq[i])
188
+ return false;
189
+ }
190
+ return true;
191
+ }
192
+ static sniffMimeFromMagicBytes(buffer) {
193
+ const u = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
194
+ if (u.byteLength < 12)
195
+ return undefined;
196
+ if (TSFile.bytesMatch(u, 0, [0x1a, 0x45, 0xdf, 0xa3]))
197
+ return 'video/webm';
198
+ if (TSFile.bytesMatch(u, 4, [0x66, 0x74, 0x79, 0x70]))
199
+ return 'video/mp4';
200
+ if (TSFile.bytesMatch(u, 0, [0xff, 0xd8, 0xff]))
201
+ return 'image/jpeg';
202
+ if (TSFile.bytesMatch(u, 0, [0x89, 0x50, 0x4e, 0x47]))
203
+ return 'image/png';
204
+ if (TSFile.bytesMatch(u, 0, [0x47, 0x49, 0x46]))
205
+ return 'image/gif';
206
+ return undefined;
207
+ }
208
+ static headerSuggestsBinaryVideo(header) {
209
+ return TSFile.isImageMime(header) || header === OCTET_STREAM || !header;
210
+ }
211
+ static normalizeVideoExtMime(extMime) {
212
+ if (extMime === 'video/webm')
213
+ return 'video/webm';
214
+ if (extMime === 'video/quicktime')
215
+ return 'video/quicktime';
216
+ return 'video/mp4';
217
+ }
218
+ static resolveMediaContentType(filename, headerContentType, buffer) {
219
+ const header = (headerContentType?.split(';')[0] ?? '').trim().toLowerCase();
220
+ const extMime = TSFile.mimeFromFilename(filename)?.toLowerCase();
221
+ const sniff = TSFile.sniffMimeFromMagicBytes(buffer);
222
+ if (sniff && TSFile.isVideoMime(sniff) && (TSFile.isVideoMime(extMime) || TSFile.headerSuggestsBinaryVideo(header))) {
223
+ return sniff;
224
+ }
225
+ if (sniff && TSFile.isImageMime(sniff) && TSFile.isImageMime(extMime)) {
226
+ return sniff;
227
+ }
228
+ if (TSFile.isVideoMime(extMime) && TSFile.headerSuggestsBinaryVideo(header)) {
229
+ return TSFile.normalizeVideoExtMime(extMime);
230
+ }
231
+ if (TSFile.isImageMime(extMime) && TSFile.isVideoMime(header)) {
232
+ return extMime;
233
+ }
234
+ return header || extMime || OCTET_STREAM;
235
+ }
236
+ static async readJson(filename) {
237
+ const content = await (0, promises_1.readFile)(filename, 'utf-8');
238
+ return JSON.parse(content);
239
+ }
240
+ static async writeJson(filename, data, space = 2) {
241
+ await (0, promises_1.writeFile)(filename, `${JSON.stringify(data, null, space)}\n`, 'utf-8');
242
+ }
243
+ }
244
+ exports.TSFile = TSFile;
@@ -0,0 +1,19 @@
1
+ export type HmacEncoding = 'hex' | 'base64url' | 'base64';
2
+ export declare class TSHash {
3
+ static id: (length?: number) => string;
4
+ static md5: (text: string) => string;
5
+ static sha256: (text: string, encoding?: 'hex' | 'base64' | 'base64url') => string;
6
+ static hmacSha256: (data: string, secret: string | Buffer, encoding?: HmacEncoding) => string;
7
+ static verifyHmacSha256: (data: string, signature: string, secret: string | Buffer, encoding?: HmacEncoding) => boolean;
8
+ static encryptAesGcm: (plaintext: string, secret: string) => string;
9
+ static decryptAesGcm: (token: string, secret: string) => string | null;
10
+ static hashToken: (token: string) => Promise<string>;
11
+ /**
12
+ * Random bytes encoded as the requested format.
13
+ * - `'base64url'` (default) — opaque tokens, refresh-token-size secrets
14
+ * - `'hex'` — webhook signatures, short IDs, stub identifiers
15
+ * - `'base64'` — legacy base64 encoding
16
+ */
17
+ static randomId: (byteLength?: number, encoding?: 'base64url' | 'hex' | 'base64') => string;
18
+ static generateBackupCodes: (count?: number) => string[];
19
+ }
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TSHash = void 0;
4
+ const node_crypto_1 = require("node:crypto");
5
+ function idChar(e) {
6
+ const x = e & 63;
7
+ if (x < 36)
8
+ return x.toString(36);
9
+ if (x < 62)
10
+ return (x - 26).toString(36).toUpperCase();
11
+ return x > 62 ? '-' : '_';
12
+ }
13
+ const AES_ALGORITHM = 'aes-256-gcm';
14
+ const IV_LENGTH = 12;
15
+ const AUTH_TAG_LENGTH = 16;
16
+ class TSHash {
17
+ static id = (length = 9) => (0, node_crypto_1.getRandomValues)(new Uint8Array(length)).reduce((t, e) => t + idChar(e), '');
18
+ static md5 = (text) => (0, node_crypto_1.createHash)('md5').update(text).digest('hex');
19
+ static sha256 = (text, encoding = 'hex') => (0, node_crypto_1.createHash)('sha256').update(text).digest(encoding);
20
+ static hmacSha256 = (data, secret, encoding = 'hex') => (0, node_crypto_1.createHmac)('sha256', secret).update(data).digest(encoding);
21
+ static verifyHmacSha256 = (data, signature, secret, encoding = 'hex') => {
22
+ const expected = TSHash.hmacSha256(data, secret, encoding);
23
+ const sigBuf = Buffer.from(signature, encoding);
24
+ const expBuf = Buffer.from(expected, encoding);
25
+ if (sigBuf.length !== expBuf.length)
26
+ return false;
27
+ return (0, node_crypto_1.timingSafeEqual)(sigBuf, expBuf);
28
+ };
29
+ static encryptAesGcm = (plaintext, secret) => {
30
+ const key = (0, node_crypto_1.createHash)('sha256').update(secret).digest();
31
+ const iv = (0, node_crypto_1.randomBytes)(IV_LENGTH);
32
+ const cipher = (0, node_crypto_1.createCipheriv)(AES_ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
33
+ const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
34
+ const authTag = cipher.getAuthTag();
35
+ return Buffer.concat([iv, encrypted, authTag]).toString('base64url');
36
+ };
37
+ static decryptAesGcm = (token, secret) => {
38
+ try {
39
+ const buf = Buffer.from(token, 'base64url');
40
+ if (buf.length < IV_LENGTH + AUTH_TAG_LENGTH + 1)
41
+ return null;
42
+ const iv = buf.subarray(0, IV_LENGTH);
43
+ const authTag = buf.subarray(buf.length - AUTH_TAG_LENGTH);
44
+ const encrypted = buf.subarray(IV_LENGTH, buf.length - AUTH_TAG_LENGTH);
45
+ const key = (0, node_crypto_1.createHash)('sha256').update(secret).digest();
46
+ const decipher = (0, node_crypto_1.createDecipheriv)(AES_ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
47
+ decipher.setAuthTag(authTag);
48
+ return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('utf8');
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ };
54
+ static hashToken = (token) => Promise.resolve(TSHash.sha256(token, 'hex'));
55
+ /**
56
+ * Random bytes encoded as the requested format.
57
+ * - `'base64url'` (default) — opaque tokens, refresh-token-size secrets
58
+ * - `'hex'` — webhook signatures, short IDs, stub identifiers
59
+ * - `'base64'` — legacy base64 encoding
60
+ */
61
+ static randomId = (byteLength = 32, encoding = 'base64url') => (0, node_crypto_1.randomBytes)(byteLength).toString(encoding);
62
+ static generateBackupCodes = (count = 10) => {
63
+ const codes = [];
64
+ for (let i = 0; i < count; i++) {
65
+ const code = (0, node_crypto_1.randomBytes)(4).toString('hex').toUpperCase();
66
+ codes.push(`${code.slice(0, 4)}-${code.slice(4)}`);
67
+ }
68
+ return codes;
69
+ };
70
+ }
71
+ exports.TSHash = TSHash;