@syncular/core 0.0.4-25 → 0.0.4-32
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/utils/compression.d.ts +14 -0
- package/dist/utils/compression.d.ts.map +1 -0
- package/dist/utils/compression.js +77 -0
- package/dist/utils/compression.js.map +1 -0
- package/dist/utils/crypto.d.ts +7 -0
- package/dist/utils/crypto.d.ts.map +1 -0
- package/dist/utils/crypto.js +25 -0
- package/dist/utils/crypto.js.map +1 -0
- package/dist/utils/id.d.ts +1 -0
- package/dist/utils/id.d.ts.map +1 -1
- package/dist/utils/id.js +16 -2
- package/dist/utils/id.js.map +1 -1
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -0
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/compression.test.ts +54 -0
- package/src/utils/compression.ts +91 -0
- package/src/utils/crypto.ts +33 -0
- package/src/utils/id.ts +19 -2
- package/src/utils/index.ts +2 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gzip-compress a byte array using CompressionStream when available,
|
|
3
|
+
* with node:zlib fallback for Node/Bun runtimes.
|
|
4
|
+
*/
|
|
5
|
+
export declare function gzipBytes(payload: Uint8Array): Promise<Uint8Array>;
|
|
6
|
+
/**
|
|
7
|
+
* Gzip-compress bytes and return a stream. When streaming compression is not
|
|
8
|
+
* available, falls back to eager compression and includes byteLength metadata.
|
|
9
|
+
*/
|
|
10
|
+
export declare function gzipBytesToStream(payload: Uint8Array): Promise<{
|
|
11
|
+
stream: ReadableStream<Uint8Array>;
|
|
12
|
+
byteLength?: number;
|
|
13
|
+
}>;
|
|
14
|
+
//# sourceMappingURL=compression.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compression.d.ts","sourceRoot":"","sources":["../../src/utils/compression.ts"],"names":[],"mappings":"AAwCA;;;GAGG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAqBxE;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC;IACpE,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC,CAgBD"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
function bytesToReadableStream(bytes) {
|
|
2
|
+
return new ReadableStream({
|
|
3
|
+
start(controller) {
|
|
4
|
+
controller.enqueue(bytes);
|
|
5
|
+
controller.close();
|
|
6
|
+
},
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
async function streamToBytes(stream) {
|
|
10
|
+
const reader = stream.getReader();
|
|
11
|
+
const chunks = [];
|
|
12
|
+
let total = 0;
|
|
13
|
+
try {
|
|
14
|
+
while (true) {
|
|
15
|
+
const { done, value } = await reader.read();
|
|
16
|
+
if (done)
|
|
17
|
+
break;
|
|
18
|
+
if (!value)
|
|
19
|
+
continue;
|
|
20
|
+
chunks.push(value);
|
|
21
|
+
total += value.length;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
reader.releaseLock();
|
|
26
|
+
}
|
|
27
|
+
if (chunks.length === 0)
|
|
28
|
+
return new Uint8Array();
|
|
29
|
+
if (chunks.length === 1)
|
|
30
|
+
return chunks[0] ?? new Uint8Array();
|
|
31
|
+
const merged = new Uint8Array(total);
|
|
32
|
+
let offset = 0;
|
|
33
|
+
for (const chunk of chunks) {
|
|
34
|
+
merged.set(chunk, offset);
|
|
35
|
+
offset += chunk.length;
|
|
36
|
+
}
|
|
37
|
+
return merged;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Gzip-compress a byte array using CompressionStream when available,
|
|
41
|
+
* with node:zlib fallback for Node/Bun runtimes.
|
|
42
|
+
*/
|
|
43
|
+
export async function gzipBytes(payload) {
|
|
44
|
+
if (typeof CompressionStream !== 'undefined') {
|
|
45
|
+
const stream = bytesToReadableStream(payload).pipeThrough(new CompressionStream('gzip'));
|
|
46
|
+
return streamToBytes(stream);
|
|
47
|
+
}
|
|
48
|
+
const nodeZlib = await import('node:zlib');
|
|
49
|
+
return await new Promise((resolve, reject) => {
|
|
50
|
+
nodeZlib.gzip(payload, (error, compressed) => {
|
|
51
|
+
if (error) {
|
|
52
|
+
reject(error);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
resolve(new Uint8Array(compressed));
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Gzip-compress bytes and return a stream. When streaming compression is not
|
|
61
|
+
* available, falls back to eager compression and includes byteLength metadata.
|
|
62
|
+
*/
|
|
63
|
+
export async function gzipBytesToStream(payload) {
|
|
64
|
+
if (typeof CompressionStream !== 'undefined') {
|
|
65
|
+
const source = bytesToReadableStream(payload);
|
|
66
|
+
const gzipStream = new CompressionStream('gzip');
|
|
67
|
+
return {
|
|
68
|
+
stream: source.pipeThrough(gzipStream),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const compressed = await gzipBytes(payload);
|
|
72
|
+
return {
|
|
73
|
+
stream: bytesToReadableStream(compressed),
|
|
74
|
+
byteLength: compressed.length,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=compression.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compression.js","sourceRoot":"","sources":["../../src/utils/compression.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../src/utils/crypto.ts"],"names":[],"mappings":"AAQA;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAmB3E"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const textEncoder = new TextEncoder();
|
|
2
|
+
function toHex(bytes) {
|
|
3
|
+
return Array.from(bytes)
|
|
4
|
+
.map((byte) => byte.toString(16).padStart(2, '0'))
|
|
5
|
+
.join('');
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Cross-runtime SHA-256 digest helper.
|
|
9
|
+
*
|
|
10
|
+
* Uses Web Crypto when available, with Node crypto fallback.
|
|
11
|
+
*/
|
|
12
|
+
export async function sha256Hex(input) {
|
|
13
|
+
const payload = typeof input === 'string' ? textEncoder.encode(input) : input;
|
|
14
|
+
if (typeof crypto !== 'undefined' && crypto.subtle) {
|
|
15
|
+
const digestBuffer = await crypto.subtle.digest('SHA-256', payload.slice().buffer);
|
|
16
|
+
return toHex(new Uint8Array(digestBuffer));
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const nodeCrypto = await import('node:crypto');
|
|
20
|
+
return nodeCrypto.createHash('sha256').update(payload).digest('hex');
|
|
21
|
+
}
|
|
22
|
+
catch { }
|
|
23
|
+
throw new Error('Failed to create SHA-256 hash, no crypto implementation available');
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=crypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../src/utils/crypto.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,GAAG,IAAI,WAAW,EAAE,CAAC;AAEtC,SAAS,KAAK,CAAC,KAAiB,EAAU;IACxC,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC;SACrB,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;SACjD,IAAI,CAAC,EAAE,CAAC,CAAC;AAAA,CACb;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,KAA0B,EAAmB;IAC3E,MAAM,OAAO,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IAE9E,IAAI,OAAO,MAAM,KAAK,WAAW,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QACnD,MAAM,YAAY,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAC7C,SAAS,EACT,OAAO,CAAC,KAAK,EAAE,CAAC,MAAM,CACvB,CAAC;QACF,OAAO,KAAK,CAAC,IAAI,UAAU,CAAC,YAAY,CAAC,CAAC,CAAC;IAC7C,CAAC;IAED,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,CAAC;QAC/C,OAAO,UAAU,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACvE,CAAC;IAAC,MAAM,CAAC,CAAA,CAAC;IAEV,MAAM,IAAI,KAAK,CACb,mEAAmE,CACpE,CAAC;AAAA,CACH"}
|
package/dist/utils/id.d.ts
CHANGED
package/dist/utils/id.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"id.d.ts","sourceRoot":"","sources":["../../src/utils/id.ts"],"names":[],"mappings":"AAAA,wBAAgB,QAAQ,IAAI,MAAM,
|
|
1
|
+
{"version":3,"file":"id.d.ts","sourceRoot":"","sources":["../../src/utils/id.ts"],"names":[],"mappings":"AAAA,wBAAgB,UAAU,IAAI,MAAM,CAmBnC;AAED,wBAAgB,QAAQ,IAAI,MAAM,CAEjC"}
|
package/dist/utils/id.js
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
|
-
export function
|
|
2
|
-
const cryptoObj = globalThis.crypto;
|
|
1
|
+
export function generateId() {
|
|
2
|
+
const cryptoObj = typeof crypto !== 'undefined' ? crypto : globalThis.crypto;
|
|
3
3
|
if (cryptoObj && typeof cryptoObj.randomUUID === 'function') {
|
|
4
4
|
return cryptoObj.randomUUID();
|
|
5
5
|
}
|
|
6
|
+
if (cryptoObj && typeof cryptoObj.getRandomValues === 'function') {
|
|
7
|
+
const bytes = new Uint8Array(16);
|
|
8
|
+
cryptoObj.getRandomValues(bytes);
|
|
9
|
+
// UUID v4 variant bits.
|
|
10
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
11
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
12
|
+
const hex = Array.from(bytes)
|
|
13
|
+
.map((byte) => byte.toString(16).padStart(2, '0'))
|
|
14
|
+
.join('');
|
|
15
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
16
|
+
}
|
|
6
17
|
return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
7
18
|
}
|
|
19
|
+
export function randomId() {
|
|
20
|
+
return generateId();
|
|
21
|
+
}
|
|
8
22
|
//# sourceMappingURL=id.js.map
|
package/dist/utils/id.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"id.js","sourceRoot":"","sources":["../../src/utils/id.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,
|
|
1
|
+
{"version":3,"file":"id.js","sourceRoot":"","sources":["../../src/utils/id.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,UAAU,GAAW;IACnC,MAAM,SAAS,GAAG,OAAO,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC;IAC7E,IAAI,SAAS,IAAI,OAAO,SAAS,CAAC,UAAU,KAAK,UAAU,EAAE,CAAC;QAC5D,OAAO,SAAS,CAAC,UAAU,EAAE,CAAC;IAChC,CAAC;IAED,IAAI,SAAS,IAAI,OAAO,SAAS,CAAC,eAAe,KAAK,UAAU,EAAE,CAAC;QACjE,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC;QACjC,SAAS,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;QACjC,wBAAwB;QACxB,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;QACrC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;QACrC,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC;aAC1B,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;aACjD,IAAI,CAAC,EAAE,CAAC,CAAC;QACZ,OAAO,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC;IACjH,CAAC;IAED,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;AAAA,CAC/D;AAED,MAAM,UAAU,QAAQ,GAAW;IACjC,OAAO,UAAU,EAAE,CAAC;AAAA,CACrB"}
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,MAAM,CAAC;AACrB,cAAc,UAAU,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,MAAM,CAAC;AACrB,cAAc,UAAU,CAAC"}
|
package/dist/utils/index.js
CHANGED
package/dist/utils/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,MAAM,CAAC;AACrB,cAAc,UAAU,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,MAAM,CAAC;AACrB,cAAc,UAAU,CAAC"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { gunzipSync } from 'node:zlib';
|
|
3
|
+
import { gzipBytes, gzipBytesToStream } from '../utils';
|
|
4
|
+
|
|
5
|
+
async function readStream(
|
|
6
|
+
stream: ReadableStream<Uint8Array>
|
|
7
|
+
): Promise<Uint8Array> {
|
|
8
|
+
const reader = stream.getReader();
|
|
9
|
+
const chunks: Uint8Array[] = [];
|
|
10
|
+
let total = 0;
|
|
11
|
+
|
|
12
|
+
while (true) {
|
|
13
|
+
const { done, value } = await reader.read();
|
|
14
|
+
if (done) break;
|
|
15
|
+
if (!value) continue;
|
|
16
|
+
chunks.push(value);
|
|
17
|
+
total += value.length;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (chunks.length === 0) return new Uint8Array();
|
|
21
|
+
if (chunks.length === 1) return chunks[0] ?? new Uint8Array();
|
|
22
|
+
|
|
23
|
+
const merged = new Uint8Array(total);
|
|
24
|
+
let offset = 0;
|
|
25
|
+
for (const chunk of chunks) {
|
|
26
|
+
merged.set(chunk, offset);
|
|
27
|
+
offset += chunk.length;
|
|
28
|
+
}
|
|
29
|
+
return merged;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('gzipBytes', () => {
|
|
33
|
+
it('compresses bytes that can be decompressed via gunzip', async () => {
|
|
34
|
+
const payload = new TextEncoder().encode(
|
|
35
|
+
'syncular compression test '.repeat(64)
|
|
36
|
+
);
|
|
37
|
+
const compressed = await gzipBytes(payload);
|
|
38
|
+
const decompressed = new Uint8Array(gunzipSync(compressed));
|
|
39
|
+
expect(decompressed).toEqual(payload);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('gzipBytesToStream', () => {
|
|
44
|
+
it('returns a gzip stream that round-trips', async () => {
|
|
45
|
+
const payload = new TextEncoder().encode('stream compression '.repeat(64));
|
|
46
|
+
const result = await gzipBytesToStream(payload);
|
|
47
|
+
const compressed = await readStream(result.stream);
|
|
48
|
+
const decompressed = new Uint8Array(gunzipSync(compressed));
|
|
49
|
+
expect(decompressed).toEqual(payload);
|
|
50
|
+
if (typeof result.byteLength === 'number') {
|
|
51
|
+
expect(result.byteLength).toBe(compressed.length);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
function bytesToReadableStream(bytes: Uint8Array): ReadableStream<Uint8Array> {
|
|
2
|
+
return new ReadableStream<Uint8Array>({
|
|
3
|
+
start(controller) {
|
|
4
|
+
controller.enqueue(bytes);
|
|
5
|
+
controller.close();
|
|
6
|
+
},
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function streamToBytes(
|
|
11
|
+
stream: ReadableStream<Uint8Array>
|
|
12
|
+
): Promise<Uint8Array> {
|
|
13
|
+
const reader = stream.getReader();
|
|
14
|
+
const chunks: Uint8Array[] = [];
|
|
15
|
+
let total = 0;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
while (true) {
|
|
19
|
+
const { done, value } = await reader.read();
|
|
20
|
+
if (done) break;
|
|
21
|
+
if (!value) continue;
|
|
22
|
+
chunks.push(value);
|
|
23
|
+
total += value.length;
|
|
24
|
+
}
|
|
25
|
+
} finally {
|
|
26
|
+
reader.releaseLock();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (chunks.length === 0) return new Uint8Array();
|
|
30
|
+
if (chunks.length === 1) return chunks[0] ?? new Uint8Array();
|
|
31
|
+
|
|
32
|
+
const merged = new Uint8Array(total);
|
|
33
|
+
let offset = 0;
|
|
34
|
+
for (const chunk of chunks) {
|
|
35
|
+
merged.set(chunk, offset);
|
|
36
|
+
offset += chunk.length;
|
|
37
|
+
}
|
|
38
|
+
return merged;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Gzip-compress a byte array using CompressionStream when available,
|
|
43
|
+
* with node:zlib fallback for Node/Bun runtimes.
|
|
44
|
+
*/
|
|
45
|
+
export async function gzipBytes(payload: Uint8Array): Promise<Uint8Array> {
|
|
46
|
+
if (typeof CompressionStream !== 'undefined') {
|
|
47
|
+
const stream = bytesToReadableStream(payload).pipeThrough(
|
|
48
|
+
new CompressionStream('gzip') as unknown as TransformStream<
|
|
49
|
+
Uint8Array,
|
|
50
|
+
Uint8Array
|
|
51
|
+
>
|
|
52
|
+
);
|
|
53
|
+
return streamToBytes(stream);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const nodeZlib = await import('node:zlib');
|
|
57
|
+
return await new Promise<Uint8Array>((resolve, reject) => {
|
|
58
|
+
nodeZlib.gzip(payload, (error, compressed) => {
|
|
59
|
+
if (error) {
|
|
60
|
+
reject(error);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
resolve(new Uint8Array(compressed));
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Gzip-compress bytes and return a stream. When streaming compression is not
|
|
70
|
+
* available, falls back to eager compression and includes byteLength metadata.
|
|
71
|
+
*/
|
|
72
|
+
export async function gzipBytesToStream(payload: Uint8Array): Promise<{
|
|
73
|
+
stream: ReadableStream<Uint8Array>;
|
|
74
|
+
byteLength?: number;
|
|
75
|
+
}> {
|
|
76
|
+
if (typeof CompressionStream !== 'undefined') {
|
|
77
|
+
const source = bytesToReadableStream(payload);
|
|
78
|
+
const gzipStream = new CompressionStream(
|
|
79
|
+
'gzip'
|
|
80
|
+
) as unknown as TransformStream<Uint8Array, Uint8Array>;
|
|
81
|
+
return {
|
|
82
|
+
stream: source.pipeThrough(gzipStream),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const compressed = await gzipBytes(payload);
|
|
87
|
+
return {
|
|
88
|
+
stream: bytesToReadableStream(compressed),
|
|
89
|
+
byteLength: compressed.length,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const textEncoder = new TextEncoder();
|
|
2
|
+
|
|
3
|
+
function toHex(bytes: Uint8Array): string {
|
|
4
|
+
return Array.from(bytes)
|
|
5
|
+
.map((byte) => byte.toString(16).padStart(2, '0'))
|
|
6
|
+
.join('');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Cross-runtime SHA-256 digest helper.
|
|
11
|
+
*
|
|
12
|
+
* Uses Web Crypto when available, with Node crypto fallback.
|
|
13
|
+
*/
|
|
14
|
+
export async function sha256Hex(input: string | Uint8Array): Promise<string> {
|
|
15
|
+
const payload = typeof input === 'string' ? textEncoder.encode(input) : input;
|
|
16
|
+
|
|
17
|
+
if (typeof crypto !== 'undefined' && crypto.subtle) {
|
|
18
|
+
const digestBuffer = await crypto.subtle.digest(
|
|
19
|
+
'SHA-256',
|
|
20
|
+
payload.slice().buffer
|
|
21
|
+
);
|
|
22
|
+
return toHex(new Uint8Array(digestBuffer));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const nodeCrypto = await import('node:crypto');
|
|
27
|
+
return nodeCrypto.createHash('sha256').update(payload).digest('hex');
|
|
28
|
+
} catch {}
|
|
29
|
+
|
|
30
|
+
throw new Error(
|
|
31
|
+
'Failed to create SHA-256 hash, no crypto implementation available'
|
|
32
|
+
);
|
|
33
|
+
}
|
package/src/utils/id.ts
CHANGED
|
@@ -1,7 +1,24 @@
|
|
|
1
|
-
export function
|
|
2
|
-
const cryptoObj = globalThis.crypto;
|
|
1
|
+
export function generateId(): string {
|
|
2
|
+
const cryptoObj = typeof crypto !== 'undefined' ? crypto : globalThis.crypto;
|
|
3
3
|
if (cryptoObj && typeof cryptoObj.randomUUID === 'function') {
|
|
4
4
|
return cryptoObj.randomUUID();
|
|
5
5
|
}
|
|
6
|
+
|
|
7
|
+
if (cryptoObj && typeof cryptoObj.getRandomValues === 'function') {
|
|
8
|
+
const bytes = new Uint8Array(16);
|
|
9
|
+
cryptoObj.getRandomValues(bytes);
|
|
10
|
+
// UUID v4 variant bits.
|
|
11
|
+
bytes[6] = (bytes[6]! & 0x0f) | 0x40;
|
|
12
|
+
bytes[8] = (bytes[8]! & 0x3f) | 0x80;
|
|
13
|
+
const hex = Array.from(bytes)
|
|
14
|
+
.map((byte) => byte.toString(16).padStart(2, '0'))
|
|
15
|
+
.join('');
|
|
16
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
6
19
|
return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
7
20
|
}
|
|
21
|
+
|
|
22
|
+
export function randomId(): string {
|
|
23
|
+
return generateId();
|
|
24
|
+
}
|
package/src/utils/index.ts
CHANGED