diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 084864b..c150386 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: gleam-version: '1.13.0' - uses: actions/setup-node@v4 with: - node-version: '22' + node-version: lts/* - name: check version run: | @@ -31,10 +31,6 @@ jobs: - run: gleam deps download - run: gleam format --check - - run: gleam run -m histogram_builder -t erlang - - run: gleam test -t erlang - - run: gleam run -m histogram_builder -t javascript - - run: gleam test -t javascript - run: gleam publish -y env: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7d93cd0..f26d80f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,10 +18,32 @@ jobs: gleam-version: '1.13.0' - uses: actions/setup-node@v4 with: - node-version: '22' + node-version: lts/* - run: gleam deps download - run: gleam format --check - run: gleam run -m histogram_builder -t erlang - run: gleam test -t erlang - run: gleam run -m histogram_builder -t javascript - run: gleam test -t javascript + + playwright-test: + timeout-minutes: 60 + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./web_test + steps: + - uses: actions/checkout@v4 + - uses: erlef/setup-beam@v1 + with: + otp-version: '28' + rebar3-version: '3' + gleam-version: '1.13.0' + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - run: gleam deps download + - run: npm ci + - run: gleam run -m lustre/dev build + - run: npx playwright install --with-deps + - run: npx playwright test diff --git a/README.md b/README.md index 33c7c9c..f0f45a5 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,14 @@ This is a port of [paralleldrive/cuid2@v3.0.0](https://github.com/paralleldrive/cuid2/tree/v3.0.0) in Gleam that works on all target. For more detailed information about Cuid2, please refer to the [original documentation](https://github.com/paralleldrive/cuid2/blob/v3.0.0/README.md). -On `javascript` target this package uses [paulmillr/noble-hashes@2.0.1](https://github.com/paulmillr/noble-hashes/tree/2.0.1) for browser compatibility. +### Notes on JavaScript target + +This package uses either [noble-hashes](https://github.com/paulmillr/noble-hashes) or [node:crypto](https://nodejs.org/api/crypto.html#cryptohashalgorithm-data-options) for hashing. + +On browser `noble-hashes` is **required**. +You can install it by using `npm install @noble/hashes`. + +On server this package will try to use `noble-hashes` first, then fallback to `node:crypto` if `noble-hashes` is not installed. ## Cuid2 @@ -41,6 +48,12 @@ Add to your Gleam project: gleam add glecuid ``` +Install [noble-hashes](https://github.com/paulmillr/noble-hashes) if your project needs to run on browser (see [note](#notes-on-javascript-target)): + +``` +npm install @noble/hashes +``` + Generate some ids: ```gleam diff --git a/gleam.toml b/gleam.toml index c1ced87..3030bd4 100644 --- a/gleam.toml +++ b/gleam.toml @@ -1,5 +1,5 @@ name = "glecuid" -version = "1.0.4" +version = "2.0.0" licences = ["MIT"] description = "A Gleam implementation of CUID2, the secure, collision-resistant ids optimized for horizontal scaling and performance." diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..0b1fd66 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,44 @@ +{ + "name": "glecuid", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@noble/hashes": "^2.0.1" + }, + "devDependencies": { + "@types/node": "^24.10.1" + } + }, + "node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..873d318 --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "type": "module", + "dependencies": { + "@noble/hashes": "^2.0.1" + }, + "devDependencies": { + "@types/node": "^24.10.1" + } +} diff --git a/src/@noble/hashes/LICENSE b/src/@noble/hashes/LICENSE deleted file mode 100644 index 5b91e4c..0000000 --- a/src/@noble/hashes/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2022 Paul Miller (https://paulmillr.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the “Software”), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/src/@noble/hashes/_u64.ts b/src/@noble/hashes/_u64.ts deleted file mode 100644 index 703c7da..0000000 --- a/src/@noble/hashes/_u64.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Internal helpers for u64. BigUint64Array is too slow as per 2025, so we implement it using Uint32Array. - * @todo re-check https://issues.chromium.org/issues/42212588 - * @module - */ -const U32_MASK64 = /* @__PURE__ */ BigInt(2 ** 32 - 1); -const _32n = /* @__PURE__ */ BigInt(32); - -function fromBig( - n: bigint, - le = false -): { - h: number; - l: number; -} { - if (le) return { h: Number(n & U32_MASK64), l: Number((n >> _32n) & U32_MASK64) }; - return { h: Number((n >> _32n) & U32_MASK64) | 0, l: Number(n & U32_MASK64) | 0 }; -} - -function split(lst: bigint[], le = false): Uint32Array[] { - const len = lst.length; - let Ah = new Uint32Array(len); - let Al = new Uint32Array(len); - for (let i = 0; i < len; i++) { - const { h, l } = fromBig(lst[i], le); - [Ah[i], Al[i]] = [h, l]; - } - return [Ah, Al]; -} - -const toBig = (h: number, l: number): bigint => (BigInt(h >>> 0) << _32n) | BigInt(l >>> 0); -// for Shift in [0, 32) -const shrSH = (h: number, _l: number, s: number): number => h >>> s; -const shrSL = (h: number, l: number, s: number): number => (h << (32 - s)) | (l >>> s); -// Right rotate for Shift in [1, 32) -const rotrSH = (h: number, l: number, s: number): number => (h >>> s) | (l << (32 - s)); -const rotrSL = (h: number, l: number, s: number): number => (h << (32 - s)) | (l >>> s); -// Right rotate for Shift in (32, 64), NOTE: 32 is special case. -const rotrBH = (h: number, l: number, s: number): number => (h << (64 - s)) | (l >>> (s - 32)); -const rotrBL = (h: number, l: number, s: number): number => (h >>> (s - 32)) | (l << (64 - s)); -// Right rotate for shift===32 (just swaps l&h) -const rotr32H = (_h: number, l: number): number => l; -const rotr32L = (h: number, _l: number): number => h; -// Left rotate for Shift in [1, 32) -const rotlSH = (h: number, l: number, s: number): number => (h << s) | (l >>> (32 - s)); -const rotlSL = (h: number, l: number, s: number): number => (l << s) | (h >>> (32 - s)); -// Left rotate for Shift in (32, 64), NOTE: 32 is special case. -const rotlBH = (h: number, l: number, s: number): number => (l << (s - 32)) | (h >>> (64 - s)); -const rotlBL = (h: number, l: number, s: number): number => (h << (s - 32)) | (l >>> (64 - s)); - -// JS uses 32-bit signed integers for bitwise operations which means we cannot -// simple take carry out of low bit sum by shift, we need to use division. -function add( - Ah: number, - Al: number, - Bh: number, - Bl: number -): { - h: number; - l: number; -} { - const l = (Al >>> 0) + (Bl >>> 0); - return { h: (Ah + Bh + ((l / 2 ** 32) | 0)) | 0, l: l | 0 }; -} -// Addition with more than 2 elements -const add3L = (Al: number, Bl: number, Cl: number): number => (Al >>> 0) + (Bl >>> 0) + (Cl >>> 0); -const add3H = (low: number, Ah: number, Bh: number, Ch: number): number => - (Ah + Bh + Ch + ((low / 2 ** 32) | 0)) | 0; -const add4L = (Al: number, Bl: number, Cl: number, Dl: number): number => - (Al >>> 0) + (Bl >>> 0) + (Cl >>> 0) + (Dl >>> 0); -const add4H = (low: number, Ah: number, Bh: number, Ch: number, Dh: number): number => - (Ah + Bh + Ch + Dh + ((low / 2 ** 32) | 0)) | 0; -const add5L = (Al: number, Bl: number, Cl: number, Dl: number, El: number): number => - (Al >>> 0) + (Bl >>> 0) + (Cl >>> 0) + (Dl >>> 0) + (El >>> 0); -const add5H = (low: number, Ah: number, Bh: number, Ch: number, Dh: number, Eh: number): number => - (Ah + Bh + Ch + Dh + Eh + ((low / 2 ** 32) | 0)) | 0; - -// prettier-ignore -export { - add, add3H, add3L, add4H, add4L, add5H, add5L, fromBig, rotlBH, rotlBL, rotlSH, rotlSL, rotr32H, rotr32L, rotrBH, rotrBL, rotrSH, rotrSL, shrSH, shrSL, split, toBig -}; -// prettier-ignore -const u64: { fromBig: typeof fromBig; split: typeof split; toBig: (h: number, l: number) => bigint; shrSH: (h: number, _l: number, s: number) => number; shrSL: (h: number, l: number, s: number) => number; rotrSH: (h: number, l: number, s: number) => number; rotrSL: (h: number, l: number, s: number) => number; rotrBH: (h: number, l: number, s: number) => number; rotrBL: (h: number, l: number, s: number) => number; rotr32H: (_h: number, l: number) => number; rotr32L: (h: number, _l: number) => number; rotlSH: (h: number, l: number, s: number) => number; rotlSL: (h: number, l: number, s: number) => number; rotlBH: (h: number, l: number, s: number) => number; rotlBL: (h: number, l: number, s: number) => number; add: typeof add; add3L: (Al: number, Bl: number, Cl: number) => number; add3H: (low: number, Ah: number, Bh: number, Ch: number) => number; add4L: (Al: number, Bl: number, Cl: number, Dl: number) => number; add4H: (low: number, Ah: number, Bh: number, Ch: number, Dh: number) => number; add5H: (low: number, Ah: number, Bh: number, Ch: number, Dh: number, Eh: number) => number; add5L: (Al: number, Bl: number, Cl: number, Dl: number, El: number) => number; } = { - fromBig, split, toBig, - shrSH, shrSL, - rotrSH, rotrSL, rotrBH, rotrBL, - rotr32H, rotr32L, - rotlSH, rotlSL, rotlBH, rotlBL, - add, add3L, add3H, add4L, add4H, add5H, add5L, -}; -export default u64; diff --git a/src/@noble/hashes/sha3.ts b/src/@noble/hashes/sha3.ts deleted file mode 100644 index cf257fc..0000000 --- a/src/@noble/hashes/sha3.ts +++ /dev/null @@ -1,295 +0,0 @@ -/** - * SHA3 (keccak) hash function, based on a new "Sponge function" design. - * Different from older hashes, the internal state is bigger than output size. - * - * Check out [FIPS-202](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.202.pdf), - * [Website](https://keccak.team/keccak.html), - * [the differences between SHA-3 and Keccak](https://crypto.stackexchange.com/questions/15727/what-are-the-key-differences-between-the-draft-sha-3-standard-and-the-keccak-sub). - * - * Check out `sha3-addons` module for cSHAKE, k12, and others. - * @module - */ -import { rotlBH, rotlBL, rotlSH, rotlSL, split } from './_u64.ts'; -// prettier-ignore -import { - abytes, aexists, anumber, aoutput, - clean, createHasher, - oidNist, - swap32IfBE, - u32, - type CHash, type CHashXOF, - type Hash, - type HashInfo, - type HashXOF -} from './utils.ts'; - -// No __PURE__ annotations in sha3 header: -// EVERYTHING is in fact used on every export. -// Various per round constants calculations -const _0n = BigInt(0); -const _1n = BigInt(1); -const _2n = BigInt(2); -const _7n = BigInt(7); -const _256n = BigInt(256); -const _0x71n = BigInt(0x71); -const SHA3_PI: number[] = []; -const SHA3_ROTL: number[] = []; -const _SHA3_IOTA: bigint[] = []; // no pure annotation: var is always used -for (let round = 0, R = _1n, x = 1, y = 0; round < 24; round++) { - // Pi - [x, y] = [y, (2 * x + 3 * y) % 5]; - SHA3_PI.push(2 * (5 * y + x)); - // Rotational - SHA3_ROTL.push((((round + 1) * (round + 2)) / 2) % 64); - // Iota - let t = _0n; - for (let j = 0; j < 7; j++) { - R = ((R << _1n) ^ ((R >> _7n) * _0x71n)) % _256n; - if (R & _2n) t ^= _1n << ((_1n << BigInt(j)) - _1n); - } - _SHA3_IOTA.push(t); -} -const IOTAS = split(_SHA3_IOTA, true); -const SHA3_IOTA_H = IOTAS[0]; -const SHA3_IOTA_L = IOTAS[1]; - -// Left rotation (without 0, 32, 64) -const rotlH = (h: number, l: number, s: number) => (s > 32 ? rotlBH(h, l, s) : rotlSH(h, l, s)); -const rotlL = (h: number, l: number, s: number) => (s > 32 ? rotlBL(h, l, s) : rotlSL(h, l, s)); - -/** `keccakf1600` internal function, additionally allows to adjust round count. */ -export function keccakP(s: Uint32Array, rounds: number = 24): void { - const B = new Uint32Array(5 * 2); - // NOTE: all indices are x2 since we store state as u32 instead of u64 (bigints to slow in js) - for (let round = 24 - rounds; round < 24; round++) { - // Theta θ - for (let x = 0; x < 10; x++) B[x] = s[x] ^ s[x + 10] ^ s[x + 20] ^ s[x + 30] ^ s[x + 40]; - for (let x = 0; x < 10; x += 2) { - const idx1 = (x + 8) % 10; - const idx0 = (x + 2) % 10; - const B0 = B[idx0]; - const B1 = B[idx0 + 1]; - const Th = rotlH(B0, B1, 1) ^ B[idx1]; - const Tl = rotlL(B0, B1, 1) ^ B[idx1 + 1]; - for (let y = 0; y < 50; y += 10) { - s[x + y] ^= Th; - s[x + y + 1] ^= Tl; - } - } - // Rho (ρ) and Pi (π) - let curH = s[2]; - let curL = s[3]; - for (let t = 0; t < 24; t++) { - const shift = SHA3_ROTL[t]; - const Th = rotlH(curH, curL, shift); - const Tl = rotlL(curH, curL, shift); - const PI = SHA3_PI[t]; - curH = s[PI]; - curL = s[PI + 1]; - s[PI] = Th; - s[PI + 1] = Tl; - } - // Chi (χ) - for (let y = 0; y < 50; y += 10) { - for (let x = 0; x < 10; x++) B[x] = s[y + x]; - for (let x = 0; x < 10; x++) s[y + x] ^= ~B[(x + 2) % 10] & B[(x + 4) % 10]; - } - // Iota (ι) - s[0] ^= SHA3_IOTA_H[round]; - s[1] ^= SHA3_IOTA_L[round]; - } - clean(B); -} - -/** Keccak sponge function. */ -export class Keccak implements Hash, HashXOF { - protected state: Uint8Array; - protected pos = 0; - protected posOut = 0; - protected finished = false; - protected state32: Uint32Array; - protected destroyed = false; - - public blockLen: number; - public suffix: number; - public outputLen: number; - protected enableXOF = false; - protected rounds: number; - - // NOTE: we accept arguments in bytes instead of bits here. - constructor( - blockLen: number, - suffix: number, - outputLen: number, - enableXOF = false, - rounds: number = 24 - ) { - this.blockLen = blockLen; - this.suffix = suffix; - this.outputLen = outputLen; - this.enableXOF = enableXOF; - this.rounds = rounds; - // Can be passed from user as dkLen - anumber(outputLen, 'outputLen'); - // 1600 = 5x5 matrix of 64bit. 1600 bits === 200 bytes - // 0 < blockLen < 200 - if (!(0 < blockLen && blockLen < 200)) - throw new Error('only keccak-f1600 function is supported'); - this.state = new Uint8Array(200); - this.state32 = u32(this.state); - } - clone(): Keccak { - return this._cloneInto(); - } - protected keccak(): void { - swap32IfBE(this.state32); - keccakP(this.state32, this.rounds); - swap32IfBE(this.state32); - this.posOut = 0; - this.pos = 0; - } - update(data: Uint8Array): this { - aexists(this); - abytes(data); - const { blockLen, state } = this; - const len = data.length; - for (let pos = 0; pos < len; ) { - const take = Math.min(blockLen - this.pos, len - pos); - for (let i = 0; i < take; i++) state[this.pos++] ^= data[pos++]; - if (this.pos === blockLen) this.keccak(); - } - return this; - } - protected finish(): void { - if (this.finished) return; - this.finished = true; - const { state, suffix, pos, blockLen } = this; - // Do the padding - state[pos] ^= suffix; - if ((suffix & 0x80) !== 0 && pos === blockLen - 1) this.keccak(); - state[blockLen - 1] ^= 0x80; - this.keccak(); - } - protected writeInto(out: Uint8Array): Uint8Array { - aexists(this, false); - abytes(out); - this.finish(); - const bufferOut = this.state; - const { blockLen } = this; - for (let pos = 0, len = out.length; pos < len; ) { - if (this.posOut >= blockLen) this.keccak(); - const take = Math.min(blockLen - this.posOut, len - pos); - out.set(bufferOut.subarray(this.posOut, this.posOut + take), pos); - this.posOut += take; - pos += take; - } - return out; - } - xofInto(out: Uint8Array): Uint8Array { - // Sha3/Keccak usage with XOF is probably mistake, only SHAKE instances can do XOF - if (!this.enableXOF) throw new Error('XOF is not possible for this instance'); - return this.writeInto(out); - } - xof(bytes: number): Uint8Array { - anumber(bytes); - return this.xofInto(new Uint8Array(bytes)); - } - digestInto(out: Uint8Array): Uint8Array { - aoutput(out, this); - if (this.finished) throw new Error('digest() was already called'); - this.writeInto(out); - this.destroy(); - return out; - } - digest(): Uint8Array { - return this.digestInto(new Uint8Array(this.outputLen)); - } - destroy(): void { - this.destroyed = true; - clean(this.state); - } - _cloneInto(to?: Keccak): Keccak { - const { blockLen, suffix, outputLen, rounds, enableXOF } = this; - to ||= new Keccak(blockLen, suffix, outputLen, enableXOF, rounds); - to.state32.set(this.state32); - to.pos = this.pos; - to.posOut = this.posOut; - to.finished = this.finished; - to.rounds = rounds; - // Suffix can change in cSHAKE - to.suffix = suffix; - to.outputLen = outputLen; - to.enableXOF = enableXOF; - to.destroyed = this.destroyed; - return to; - } -} - -const genKeccak = (suffix: number, blockLen: number, outputLen: number, info: HashInfo = {}) => - createHasher(() => new Keccak(blockLen, suffix, outputLen), info); - -/** SHA3-224 hash function. */ -export const sha3_224: CHash = /* @__PURE__ */ genKeccak( - 0x06, - 144, - 28, - /* @__PURE__ */ oidNist(0x07) -); -/** SHA3-256 hash function. Different from keccak-256. */ -export const sha3_256: CHash = /* @__PURE__ */ genKeccak( - 0x06, - 136, - 32, - /* @__PURE__ */ oidNist(0x08) -); -/** SHA3-384 hash function. */ -export const sha3_384: CHash = /* @__PURE__ */ genKeccak( - 0x06, - 104, - 48, - /* @__PURE__ */ oidNist(0x09) -); -/** SHA3-512 hash function. */ -export const sha3_512: CHash = /* @__PURE__ */ genKeccak( - 0x06, - 72, - 64, - /* @__PURE__ */ oidNist(0x0a) -); - -/** keccak-224 hash function. */ -export const keccak_224: CHash = /* @__PURE__ */ genKeccak(0x01, 144, 28); -/** keccak-256 hash function. Different from SHA3-256. */ -export const keccak_256: CHash = /* @__PURE__ */ genKeccak(0x01, 136, 32); -/** keccak-384 hash function. */ -export const keccak_384: CHash = /* @__PURE__ */ genKeccak(0x01, 104, 48); -/** keccak-512 hash function. */ -export const keccak_512: CHash = /* @__PURE__ */ genKeccak(0x01, 72, 64); - -/** Options for SHAKE XOF. */ -export type ShakeOpts = { dkLen?: number }; - -const genShake = (suffix: number, blockLen: number, outputLen: number, info: HashInfo = {}) => - createHasher( - (opts: ShakeOpts = {}) => - new Keccak(blockLen, suffix, opts.dkLen === undefined ? outputLen : opts.dkLen, true), - info - ); - -/** SHAKE128 XOF with 128-bit security. */ -export const shake128: CHashXOF = - /* @__PURE__ */ - genShake(0x1f, 168, 16, /* @__PURE__ */ oidNist(0x0b)); -/** SHAKE256 XOF with 256-bit security. */ -export const shake256: CHashXOF = - /* @__PURE__ */ - genShake(0x1f, 136, 32, /* @__PURE__ */ oidNist(0x0c)); - -/** SHAKE128 XOF with 256-bit output (NIST version). */ -export const shake128_32: CHashXOF = - /* @__PURE__ */ - genShake(0x1f, 168, 32, /* @__PURE__ */ oidNist(0x0b)); -/** SHAKE256 XOF with 512-bit output (NIST version). */ -export const shake256_64: CHashXOF = - /* @__PURE__ */ - genShake(0x1f, 136, 64, /* @__PURE__ */ oidNist(0x0c)); diff --git a/src/@noble/hashes/utils.ts b/src/@noble/hashes/utils.ts deleted file mode 100644 index 8b3b1a9..0000000 --- a/src/@noble/hashes/utils.ts +++ /dev/null @@ -1,338 +0,0 @@ -/** - * Utilities for hex, bytes, CSPRNG. - * @module - */ -/*! noble-hashes - MIT License (c) 2022 Paul Miller (paulmillr.com) */ -/** Checks if something is Uint8Array. Be careful: nodejs Buffer will return true. */ -export function isBytes(a: unknown): a is Uint8Array { - return a instanceof Uint8Array || (ArrayBuffer.isView(a) && a.constructor.name === 'Uint8Array'); -} - -/** Asserts something is positive integer. */ -export function anumber(n: number, title: string = ''): void { - if (!Number.isSafeInteger(n) || n < 0) { - const prefix = title && `"${title}" `; - throw new Error(`${prefix}expected integer >= 0, got ${n}`); - } -} - -/** Asserts something is Uint8Array. */ -export function abytes(value: Uint8Array, length?: number, title: string = ''): Uint8Array { - const bytes = isBytes(value); - const len = value?.length; - const needsLen = length !== undefined; - if (!bytes || (needsLen && len !== length)) { - const prefix = title && `"${title}" `; - const ofLen = needsLen ? ` of length ${length}` : ''; - const got = bytes ? `length=${len}` : `type=${typeof value}`; - throw new Error(prefix + 'expected Uint8Array' + ofLen + ', got ' + got); - } - return value; -} - -/** Asserts something is hash */ -export function ahash(h: CHash): void { - if (typeof h !== 'function' || typeof h.create !== 'function') - throw new Error('Hash must wrapped by utils.createHasher'); - anumber(h.outputLen); - anumber(h.blockLen); -} - -/** Asserts a hash instance has not been destroyed / finished */ -export function aexists(instance: any, checkFinished = true): void { - if (instance.destroyed) throw new Error('Hash instance has been destroyed'); - if (checkFinished && instance.finished) throw new Error('Hash#digest() has already been called'); -} - -/** Asserts output is properly-sized byte array */ -export function aoutput(out: any, instance: any): void { - abytes(out, undefined, 'digestInto() output'); - const min = instance.outputLen; - if (out.length < min) { - throw new Error('"digestInto() output" expected to be of length >=' + min); - } -} - -/** Generic type encompassing 8/16/32-byte arrays - but not 64-byte. */ -// prettier-ignore -export type TypedArray = Int8Array | Uint8ClampedArray | Uint8Array | - Uint16Array | Int16Array | Uint32Array | Int32Array; - -/** Cast u8 / u16 / u32 to u8. */ -export function u8(arr: TypedArray): Uint8Array { - return new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength); -} - -/** Cast u8 / u16 / u32 to u32. */ -export function u32(arr: TypedArray): Uint32Array { - return new Uint32Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 4)); -} - -/** Zeroize a byte array. Warning: JS provides no guarantees. */ -export function clean(...arrays: TypedArray[]): void { - for (let i = 0; i < arrays.length; i++) { - arrays[i].fill(0); - } -} - -/** Create DataView of an array for easy byte-level manipulation. */ -export function createView(arr: TypedArray): DataView { - return new DataView(arr.buffer, arr.byteOffset, arr.byteLength); -} - -/** The rotate right (circular right shift) operation for uint32 */ -export function rotr(word: number, shift: number): number { - return (word << (32 - shift)) | (word >>> shift); -} - -/** The rotate left (circular left shift) operation for uint32 */ -export function rotl(word: number, shift: number): number { - return (word << shift) | ((word >>> (32 - shift)) >>> 0); -} - -/** Is current platform little-endian? Most are. Big-Endian platform: IBM */ -export const isLE: boolean = /* @__PURE__ */ (() => - new Uint8Array(new Uint32Array([0x11223344]).buffer)[0] === 0x44)(); - -/** The byte swap operation for uint32 */ -export function byteSwap(word: number): number { - return ( - ((word << 24) & 0xff000000) | - ((word << 8) & 0xff0000) | - ((word >>> 8) & 0xff00) | - ((word >>> 24) & 0xff) - ); -} -/** Conditionally byte swap if on a big-endian platform */ -export const swap8IfBE: (n: number) => number = isLE - ? (n: number) => n - : (n: number) => byteSwap(n); - -/** In place byte swap for Uint32Array */ -export function byteSwap32(arr: Uint32Array): Uint32Array { - for (let i = 0; i < arr.length; i++) { - arr[i] = byteSwap(arr[i]); - } - return arr; -} - -export const swap32IfBE: (u: Uint32Array) => Uint32Array = isLE - ? (u: Uint32Array) => u - : byteSwap32; - -// Built-in hex conversion https://caniuse.com/mdn-javascript_builtins_uint8array_fromhex -const hasHexBuiltin: boolean = /* @__PURE__ */ (() => - // @ts-ignore - typeof Uint8Array.from([]).toHex === 'function' && typeof Uint8Array.fromHex === 'function')(); - -// Array where index 0xf0 (240) is mapped to string 'f0' -const hexes = /* @__PURE__ */ Array.from({ length: 256 }, (_, i) => - i.toString(16).padStart(2, '0') -); - -/** - * Convert byte array to hex string. Uses built-in function, when available. - * @example bytesToHex(Uint8Array.from([0xca, 0xfe, 0x01, 0x23])) // 'cafe0123' - */ -export function bytesToHex(bytes: Uint8Array): string { - abytes(bytes); - // @ts-ignore - if (hasHexBuiltin) return bytes.toHex(); - // pre-caching improves the speed 6x - let hex = ''; - for (let i = 0; i < bytes.length; i++) { - hex += hexes[bytes[i]]; - } - return hex; -} - -// We use optimized technique to convert hex string to byte array -const asciis = { _0: 48, _9: 57, A: 65, F: 70, a: 97, f: 102 } as const; -function asciiToBase16(ch: number): number | undefined { - if (ch >= asciis._0 && ch <= asciis._9) return ch - asciis._0; // '2' => 50-48 - if (ch >= asciis.A && ch <= asciis.F) return ch - (asciis.A - 10); // 'B' => 66-(65-10) - if (ch >= asciis.a && ch <= asciis.f) return ch - (asciis.a - 10); // 'b' => 98-(97-10) - return; -} - -/** - * Convert hex string to byte array. Uses built-in function, when available. - * @example hexToBytes('cafe0123') // Uint8Array.from([0xca, 0xfe, 0x01, 0x23]) - */ -export function hexToBytes(hex: string): Uint8Array { - if (typeof hex !== 'string') throw new Error('hex string expected, got ' + typeof hex); - // @ts-ignore - if (hasHexBuiltin) return Uint8Array.fromHex(hex); - const hl = hex.length; - const al = hl / 2; - if (hl % 2) throw new Error('hex string expected, got unpadded hex of length ' + hl); - const array = new Uint8Array(al); - for (let ai = 0, hi = 0; ai < al; ai++, hi += 2) { - const n1 = asciiToBase16(hex.charCodeAt(hi)); - const n2 = asciiToBase16(hex.charCodeAt(hi + 1)); - if (n1 === undefined || n2 === undefined) { - const char = hex[hi] + hex[hi + 1]; - throw new Error('hex string expected, got non-hex character "' + char + '" at index ' + hi); - } - array[ai] = n1 * 16 + n2; // multiply first octet, e.g. 'a3' => 10*16+3 => 160 + 3 => 163 - } - return array; -} - -/** - * There is no setImmediate in browser and setTimeout is slow. - * Call of async fn will return Promise, which will be fullfiled only on - * next scheduler queue processing step and this is exactly what we need. - */ -export const nextTick = async (): Promise => {}; - -/** Returns control to thread each 'tick' ms to avoid blocking. */ -export async function asyncLoop( - iters: number, - tick: number, - cb: (i: number) => void -): Promise { - let ts = Date.now(); - for (let i = 0; i < iters; i++) { - cb(i); - // Date.now() is not monotonic, so in case if clock goes backwards we return return control too - const diff = Date.now() - ts; - if (diff >= 0 && diff < tick) continue; - await nextTick(); - ts += diff; - } -} - -// Global symbols, but ts doesn't see them: https://github.com/microsoft/TypeScript/issues/31535 -declare const TextEncoder: any; - -/** - * Converts string to bytes using UTF8 encoding. - * Built-in doesn't validate input to be string: we do the check. - * @example utf8ToBytes('abc') // Uint8Array.from([97, 98, 99]) - */ -export function utf8ToBytes(str: string): Uint8Array { - if (typeof str !== 'string') throw new Error('string expected'); - return new Uint8Array(new TextEncoder().encode(str)); // https://bugzil.la/1681809 -} - -/** KDFs can accept string or Uint8Array for user convenience. */ -export type KDFInput = string | Uint8Array; - -/** - * Helper for KDFs: consumes uint8array or string. - * When string is passed, does utf8 decoding, using TextDecoder. - */ -export function kdfInputToBytes(data: KDFInput, errorTitle = ''): Uint8Array { - if (typeof data === 'string') return utf8ToBytes(data); - return abytes(data, undefined, errorTitle); -} - -/** Copies several Uint8Arrays into one. */ -export function concatBytes(...arrays: Uint8Array[]): Uint8Array { - let sum = 0; - for (let i = 0; i < arrays.length; i++) { - const a = arrays[i]; - abytes(a); - sum += a.length; - } - const res = new Uint8Array(sum); - for (let i = 0, pad = 0; i < arrays.length; i++) { - const a = arrays[i]; - res.set(a, pad); - pad += a.length; - } - return res; -} - -type EmptyObj = {}; -/** Merges default options and passed options. */ -export function checkOpts( - defaults: T1, - opts?: T2 -): T1 & T2 { - if (opts !== undefined && {}.toString.call(opts) !== '[object Object]') - throw new Error('options must be object or undefined'); - const merged = Object.assign(defaults, opts); - return merged as T1 & T2; -} - -/** Common interface for all hashes. */ -export interface Hash { - blockLen: number; // Bytes per block - outputLen: number; // Bytes in output - update(buf: Uint8Array): this; - digestInto(buf: Uint8Array): void; - digest(): Uint8Array; - destroy(): void; - _cloneInto(to?: T): T; - clone(): T; -} - -/** PseudoRandom (number) Generator */ -export interface PRG { - addEntropy(seed: Uint8Array): void; - randomBytes(length: number): Uint8Array; - clean(): void; -} - -/** - * XOF: streaming API to read digest in chunks. - * Same as 'squeeze' in keccak/k12 and 'seek' in blake3, but more generic name. - * When hash used in XOF mode it is up to user to call '.destroy' afterwards, since we cannot - * destroy state, next call can require more bytes. - */ -export type HashXOF> = Hash & { - xof(bytes: number): Uint8Array; // Read 'bytes' bytes from digest stream - xofInto(buf: Uint8Array): Uint8Array; // read buf.length bytes from digest stream into buf -}; - -/** Hash constructor */ -export type HasherCons = Opts extends undefined ? () => T : (opts?: Opts) => T; -/** Optional hash params. */ -export type HashInfo = { - oid?: Uint8Array; // DER encoded OID in bytes -}; -/** Hash function */ -export type CHash = Hash, Opts = undefined> = { - outputLen: number; - blockLen: number; -} & HashInfo & - (Opts extends undefined - ? { - (msg: Uint8Array): Uint8Array; - create(): T; - } - : { - (msg: Uint8Array, opts?: Opts): Uint8Array; - create(opts?: Opts): T; - }); -/** XOF with output */ -export type CHashXOF = HashXOF, Opts = undefined> = CHash; - -/** Creates function with outputLen, blockLen, create properties from a class constructor. */ -export function createHasher, Opts = undefined>( - hashCons: HasherCons, - info: HashInfo = {} -): CHash { - const hashC: any = (msg: Uint8Array, opts?: Opts) => hashCons(opts).update(msg).digest(); - const tmp = hashCons(undefined); - hashC.outputLen = tmp.outputLen; - hashC.blockLen = tmp.blockLen; - hashC.create = (opts?: Opts) => hashCons(opts); - Object.assign(hashC, info); - return Object.freeze(hashC); -} - -/** Cryptographically secure PRNG. Uses internal OS-level `crypto.getRandomValues`. */ -export function randomBytes(bytesLength = 32): Uint8Array { - const cr = typeof globalThis === 'object' ? (globalThis as any).crypto : null; - if (typeof cr?.getRandomValues !== 'function') - throw new Error('crypto.getRandomValues must be defined'); - return cr.getRandomValues(new Uint8Array(bytesLength)); -} - -/** Creates OID opts for NIST hashes, with prefix 06 09 60 86 48 01 65 03 04 02. */ -export const oidNist = (suffix: number): Required => ({ - oid: Uint8Array.from([0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, suffix]), -}); diff --git a/src/crypto_ffi.ts b/src/crypto_ffi.ts new file mode 100644 index 0000000..bad9a74 --- /dev/null +++ b/src/crypto_ffi.ts @@ -0,0 +1,25 @@ +// @ts-ignore +import { BitArray } from './gleam.mjs'; + +let hasher: ((data: Uint8Array) => Uint8Array) | null = null; + +try { + const crypto = await import('@noble/hashes/sha3.js'); + hasher = (data) => crypto.sha3_512(data); +} catch { + const crypto = await import('node:crypto'); + hasher = (data) => crypto.hash('SHA3-512', data, 'buffer'); +} + +/** + * Hashes the input using SHA3-512 algorithm. + * @param input - Input string. + * @returns Hash output as Gleam's `BitArray`. + */ +export function hash(input: string): BitArray { + const encoder = new TextEncoder(); + const data = encoder.encode(input); + if (!hasher) throw new Error('Hasher is not available.'); + const hash = hasher(data); + return new BitArray(hash); +} diff --git a/src/glecuid/cuid2.gleam b/src/glecuid/cuid2.gleam index 112e40e..b125b4e 100644 --- a/src/glecuid/cuid2.gleam +++ b/src/glecuid/cuid2.gleam @@ -1,41 +1,11 @@ -import gleam/float -import gleam/int -import gleam/option.{type Option, None, Some} -import gleam/pair -import gleam/regexp -import gleam/string -import gleam/time/timestamp -import glecuid/internals/util - -// ---- Types --------------------------------------------- - -/// Type to represent CUIDv2 generator configuration. -/// -pub opaque type Generator { - Generator( - counter: Option(fn() -> Int), - fingerprint: Option(String), - length: Int, - randomizer: fn() -> Float, - ) -} +import glecuid/internals/core.{type Generator} +import glecuid/internals/crypto // ---- Constants ----------------------------------------- /// Constant representing CUIDv2 default length. /// -pub const default_length = 24 - -/// Constant representing a default `Generator`. -/// -const default_generator = Generator( - counter: None, - fingerprint: None, - length: default_length, - randomizer: float.random, -) - -const initial_count_max = 476_782_367 +pub const default_length = core.default_length // ---- Main functions ------------------------------------ @@ -49,7 +19,7 @@ const initial_count_max = 476_782_367 /// ``` /// pub fn create_id() -> String { - generate(default_generator) + core.generate(core.default_generator, crypto.hash) } /// Generates a CUIDv2 using custom generator. @@ -67,51 +37,13 @@ pub fn create_id() -> String { /// ``` /// pub fn generate(g: Generator) -> String { - let Generator(counter, fingerprint, length, randomizer) = g - - let first_letter = util.random_letter(randomizer) - - let time = { - timestamp.system_time() - |> timestamp.to_unix_seconds_and_nanoseconds() - |> pair.first() - |> int.to_base36() - } - - let count = { - case counter { - Some(x) -> x() - None -> - util.random_int(randomizer, initial_count_max) - |> util.bump_or_initialize_counter() - } - |> int.to_base36() - } - - let fingerprint = case fingerprint { - Some(x) -> x - None -> util.create_fingerprint(randomizer) - } - - let salt = util.create_entropy(randomizer, length) - - first_letter - <> { time <> salt <> count <> fingerprint } - |> util.hash() - |> string.slice(1, length - 1) - |> string.lowercase() + core.generate(g, crypto.hash) } /// Determines whether or not the string is a valid CUIDv2. /// pub fn is_cuid(input: String) -> Bool { - let length = input |> string.length() - let assert Ok(regex) = regexp.from_string("^[a-z][0-9a-z]+$") - - case regexp.check(regex, input) { - True if 2 <= length && length <= util.big_length -> True - _ -> False - } + core.is_cuid(input) } // ---- Configs functions --------------------------------- @@ -119,7 +51,7 @@ pub fn is_cuid(input: String) -> Bool { /// Creates a `Generator` with default values to be customized. /// pub fn new() -> Generator { - default_generator + core.default_generator } /// Sets a custom counter to be used by the `Generator`. @@ -128,7 +60,7 @@ pub fn new() -> Generator { /// every time it is called. /// pub fn with_counter(g: Generator, counter: fn() -> Int) -> Generator { - Generator(..g, counter: counter |> Some()) + core.with_counter(g, counter) } /// Sets a random counter to be used by the `Generator`. @@ -136,7 +68,7 @@ pub fn with_counter(g: Generator, counter: fn() -> Int) -> Generator { /// This counter returns a random `Int` instead of an incrementing value. /// pub fn with_random_counter(g: Generator) -> Generator { - Generator(..g, counter: Some(fn() { int.random(util.big_length) })) + core.with_random_counter(g) } /// Sets a custom fingerprint to be used by the `Generator`. @@ -145,13 +77,13 @@ pub fn with_random_counter(g: Generator) -> Generator { /// when generating ids in a distributed system. /// pub fn with_fingerprint(g: Generator, fingerprint: String) -> Generator { - Generator(..g, fingerprint: fingerprint |> Some()) + core.with_fingerprint(g, fingerprint) } /// Sets the length of the ids generated by the `Generator`. /// pub fn with_length(g: Generator, length: Int) -> Generator { - Generator(..g, length:) + core.with_length(g, length) } /// Sets a custom randomizer to be used by the `Generator`. @@ -160,5 +92,5 @@ pub fn with_length(g: Generator, length: Int) -> Generator { /// between zero (inclusive) and one (exclusive). /// pub fn with_randomizer(g: Generator, randomizer: fn() -> Float) -> Generator { - Generator(..g, randomizer:) + core.with_randomizer(g, randomizer) } diff --git a/src/glecuid/internals/core.gleam b/src/glecuid/internals/core.gleam new file mode 100644 index 0000000..cf9d36f --- /dev/null +++ b/src/glecuid/internals/core.gleam @@ -0,0 +1,151 @@ +import gleam/float +import gleam/int +import gleam/option.{type Option, None, Some} +import gleam/pair +import gleam/regexp +import gleam/string +import gleam/time/timestamp +import glecuid/internals/util + +// ---- Types --------------------------------------------- + +/// Type to represent CUIDv2 generator configuration. +/// +pub opaque type Generator { + Generator( + counter: Option(fn() -> Int), + fingerprint: Option(String), + length: Int, + randomizer: fn() -> Float, + ) +} + +// ---- Constants ----------------------------------------- + +/// Constant representing CUIDv2 default length. +/// +pub const default_length = 24 + +/// Constant representing a default `Generator`. +/// +pub const default_generator = Generator( + counter: None, + fingerprint: None, + length: default_length, + randomizer: float.random, +) + +const initial_count_max = 476_782_367 + +// ---- Main functions ------------------------------------ + +/// Generates a CUIDv2 using custom generator. +/// +/// ## Examples +/// +/// ```gleam +/// cuid2.new() +/// |> cuid2.with_length(10) +/// |> cuid2.with_fingerprint("my_machine") +/// |> cuid2.with_counter(fn() { int.random(100) }) +/// |> cuid2.with_randomizer(fn() { 0.5 }) +/// |> cuid2.generate() +/// // -> "av77nekw5e" +/// ``` +/// +pub fn generate(g: Generator, hasher: fn(String) -> String) -> String { + let Generator(counter, fingerprint, length, randomizer) = g + + let first_letter = util.random_letter(randomizer) + + let time = { + timestamp.system_time() + |> timestamp.to_unix_seconds_and_nanoseconds() + |> pair.first() + |> int.to_base36() + } + + let count = { + case counter { + Some(x) -> x() + None -> + util.random_int(randomizer, initial_count_max) + |> util.bump_or_initialize_counter() + } + |> int.to_base36() + } + + let fingerprint = case fingerprint { + Some(x) -> x + None -> util.create_fingerprint(randomizer, hasher) + } + + let salt = util.create_entropy(randomizer, length) + + first_letter + <> { time <> salt <> count <> fingerprint } + |> hasher() + |> string.slice(1, length - 1) + |> string.lowercase() +} + +/// Determines whether or not the string is a valid CUIDv2. +/// +pub fn is_cuid(input: String) -> Bool { + let length = input |> string.length() + let assert Ok(regex) = regexp.from_string("^[a-z][0-9a-z]+$") + + case regexp.check(regex, input) { + True if 2 <= length && length <= util.big_length -> True + _ -> False + } +} + +// ---- Configs functions --------------------------------- + +/// Creates a `Generator` with default values to be customized. +/// +pub fn new() -> Generator { + default_generator +} + +/// Sets a custom counter to be used by the `Generator`. +/// +/// Counter is a function that returns an incrementing `Int` +/// every time it is called. +/// +pub fn with_counter(g: Generator, counter: fn() -> Int) -> Generator { + Generator(..g, counter: counter |> Some()) +} + +/// Sets a random counter to be used by the `Generator`. +/// +/// This counter returns a random `Int` instead of an incrementing value. +/// +pub fn with_random_counter(g: Generator) -> Generator { + Generator(..g, counter: Some(fn() { int.random(util.big_length) })) +} + +/// Sets a custom fingerprint to be used by the `Generator`. +/// +/// Fingerprint is a unique `String` to help prevent collision +/// when generating ids in a distributed system. +/// +pub fn with_fingerprint(g: Generator, fingerprint: String) -> Generator { + Generator(..g, fingerprint: fingerprint |> Some()) +} + +/// Sets the length of the ids generated by the `Generator`. +/// +pub fn with_length(g: Generator, length: Int) -> Generator { + Generator(..g, length:) +} + +/// Sets a custom randomizer to be used by the `Generator`. +/// +/// Randomizer is a function that returns a random `Float` +/// between zero (inclusive) and one (exclusive). +/// +pub fn with_randomizer(g: Generator, randomizer: fn() -> Float) -> Generator { + Generator(..g, randomizer:) +} diff --git a/src/glecuid/internals/crypto.gleam b/src/glecuid/internals/crypto.gleam new file mode 100644 index 0000000..f4f935d --- /dev/null +++ b/src/glecuid/internals/crypto.gleam @@ -0,0 +1,17 @@ +import gleam/string +import glecuid/internals/util + +/// Hashes the input. +/// +pub fn hash(input: String) -> String { + // https://github.com/paralleldrive/cuid2/blob/v3.0.0/src/index.js#L32 + // Drop the first character because it will bias the histogram + // to the left. + do_hash(input) + |> util.bit_array_to_base36() + |> string.drop_start(1) +} + +@external(erlang, "glecuid_ffi", "hash") +@external(javascript, "../../crypto_ffi.ts", "hash") +fn do_hash(input: String) -> BitArray diff --git a/src/glecuid/internals/util.gleam b/src/glecuid/internals/util.gleam index 469ac46..1dcd611 100644 --- a/src/glecuid/internals/util.gleam +++ b/src/glecuid/internals/util.gleam @@ -1,4 +1,3 @@ -import gleam/bit_array import gleam/bool import gleam/erlang/process import gleam/float @@ -80,10 +79,13 @@ fn create_entropy_loop( /// Generates a fingerprint of the host environtment. /// -pub fn create_fingerprint(randomizer: fn() -> Float) -> String { +pub fn create_fingerprint( + randomizer: fn() -> Float, + hasher: fn(String) -> String, +) -> String { get_global_object() <> create_entropy(randomizer, big_length) - |> hash() + |> hasher() |> string.slice(0, big_length) } @@ -97,29 +99,6 @@ pub fn get_global_object() -> String { process.self() |> string.inspect() } -/// Hashes the input. -/// -pub fn hash(input: String) -> String { - // https://github.com/paralleldrive/cuid2/blob/v3.0.0/src/index.js#L32 - // Drop the first character because it will bias the histogram - // to the left. - do_hash(input) - |> bit_array_to_base36() - |> string.drop_start(1) -} - -@target(erlang) -fn do_hash(input: String) -> BitArray { - bit_array.from_string(input) |> do_hash_() -} - -@external(erlang, "glecuid_ffi", "hash") -fn do_hash_(input: BitArray) -> BitArray - -@target(javascript) -@external(javascript, "../../glecuid_ffi.ts", "hash") -fn do_hash(input: String) -> BitArray - /// Generates a random int between zero and the given maximum /// using the provided randomizer. /// The lower number is inclusive, the upper number is exclusive. diff --git a/src/glecuid_ffi.ts b/src/glecuid_ffi.ts index 0b81291..14f3aa6 100644 --- a/src/glecuid_ffi.ts +++ b/src/glecuid_ffi.ts @@ -1,6 +1,3 @@ -import { BitArray } from './gleam.mjs'; -import { sha3_512 } from './@noble/hashes/sha3.ts'; - /** * The global counter. */ @@ -59,15 +56,3 @@ export function get_global_object(): string { : {}; return Object.keys(global_object).toString(); } - -/** - * Hashes the input using SHA3-512 algorithm. - * @param input - Input string. - * @returns Hash output as Gleam's `BitArray`. - */ -export function hash(input: string): BitArray { - const encoder = new TextEncoder(); - const data = encoder.encode(input); - const hash = sha3_512(data); - return new BitArray(hash); -} diff --git a/test/util_test.gleam b/test/util_test.gleam index 1300937..dc37809 100644 --- a/test/util_test.gleam +++ b/test/util_test.gleam @@ -1,5 +1,6 @@ import gleam/float import gleam/string +import glecuid/internals/crypto import glecuid/internals/util pub fn bit_array_to_base36_test() { @@ -33,7 +34,8 @@ pub fn create_entropy_test() { } pub fn create_fingerprint_test() { - assert util.create_fingerprint(float.random) |> string.length() >= 32 + assert util.create_fingerprint(float.random, crypto.hash) |> string.length() + >= 32 } pub fn get_global_object_test() { diff --git a/web_test/.gitignore b/web_test/.gitignore new file mode 100644 index 0000000..33ab407 --- /dev/null +++ b/web_test/.gitignore @@ -0,0 +1,17 @@ +*.beam +*.ez +/build +erl_crash.dump +/test/artifacts +/node_modules + +# Lustre Dev Tools +/.lustre +/dist + +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/.auth/ \ No newline at end of file diff --git a/web_test/gleam.toml b/web_test/gleam.toml new file mode 100644 index 0000000..01845b9 --- /dev/null +++ b/web_test/gleam.toml @@ -0,0 +1,11 @@ +name = "glecuid_web_test" +version = "1.0.0" + +[dependencies] +gleam_stdlib = ">= 0.44.0 and < 2.0.0" +glecuid = { path = ".." } +lustre = ">= 5.4.0 and < 6.0.0" + +[dev-dependencies] +gleeunit = ">= 1.0.0 and < 2.0.0" +lustre_dev_tools = ">= 2.3.1 and < 3.0.0" diff --git a/web_test/manifest.toml b/web_test/manifest.toml new file mode 100644 index 0000000..56564fe --- /dev/null +++ b/web_test/manifest.toml @@ -0,0 +1,51 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, + { name = "booklet", version = "1.1.0", build_tools = ["gleam"], requirements = [], otp_app = "booklet", source = "hex", outer_checksum = "08E0FDB78DC4D8A5D3C80295B021505C7D2A2E7B6C6D5EAB7286C36F4A53C851" }, + { name = "directories", version = "1.2.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib", "platform", "simplifile"], otp_app = "directories", source = "hex", outer_checksum = "D13090CFCDF6759B87217E8DDD73A75903A700148A82C1D33799F333E249BF9E" }, + { name = "envoy", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "850DA9D29D2E5987735872A2B5C81035146D7FE19EFC486129E44440D03FD832" }, + { name = "exception", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "329D269D5C2A314F7364BD2711372B6F2C58FA6F39981572E5CA68624D291F8C" }, + { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, + { name = "gleam_community_ansi", version = "1.4.3", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_regexp", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "8A62AE9CC6EA65BEA630D95016D6C07E4F9973565FA3D0DE68DC4200D8E0DD27" }, + { name = "gleam_community_colour", version = "2.0.2", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "E34DD2C896AC3792151EDA939DA435FF3B69922F33415ED3C4406C932FBE9634" }, + { name = "gleam_crypto", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "50774BAFFF1144E7872814C566C5D653D83A3EBF23ACC3156B757A1B6819086E" }, + { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" }, + { name = "gleam_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" }, + { name = "gleam_httpc", version = "5.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "C545172618D07811494E97AAA4A0FB34DA6F6D0061FDC8041C2F8E3BE2B2E48F" }, + { name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" }, + { name = "gleam_otp", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "BA6A294E295E428EC1562DC1C11EA7530DCB981E8359134BEABC8493B7B2258E" }, + { name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" }, + { name = "gleam_stdlib", version = "0.67.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "6CE3E4189A8B8EC2F73AB61A2FBDE49F159D6C9C61C49E3B3082E439F260D3D0" }, + { name = "gleam_time", version = "1.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "0DF3834D20193F0A38D0EB21F0A78D48F2EC276C285969131B86DF8D4EF9E762" }, + { name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" }, + { name = "glecuid", version = "1.0.4", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_regexp", "gleam_stdlib", "gleam_time"], otp_app = "glecuid", source = "hex", outer_checksum = "9A237FF6550BCB3BDDD9E1C9A8BC2178252B7C6C505407D093CCF0247586E2AB" }, + { name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" }, + { name = "glint", version = "1.2.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "2214C7CEFDE457CEE62140C3D4899B964E05236DA74E4243DFADF4AF29C382BB" }, + { name = "glisten", version = "8.0.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "534BB27C71FB9E506345A767C0D76B17A9E9199934340C975DC003C710E3692D" }, + { name = "gramps", version = "6.0.0", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "8B7195978FBFD30B43DF791A8A272041B81E45D245314D7A41FC57237AA882A0" }, + { name = "group_registry", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "group_registry", source = "hex", outer_checksum = "BC798A53D6F2406DB94E27CB45C57052CB56B32ACF7CC16EA20F6BAEC7E36B90" }, + { name = "houdini", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "houdini", source = "hex", outer_checksum = "5DB1053F1AF828049C2B206D4403C18970ABEF5C18671CA3C2D2ED0DD64F6385" }, + { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, + { name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" }, + { name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" }, + { name = "lustre", version = "5.4.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib", "houdini"], otp_app = "lustre", source = "hex", outer_checksum = "40E097BABCE65FB7C460C073078611F7F5802EB07E1A9BFB5C229F71B60F8E50" }, + { name = "lustre_dev_tools", version = "2.3.1", build_tools = ["gleam"], requirements = ["argv", "booklet", "filepath", "gleam_community_ansi", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_httpc", "gleam_json", "gleam_otp", "gleam_regexp", "gleam_stdlib", "glint", "group_registry", "justin", "lustre", "mist", "polly", "simplifile", "tom", "wisp"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "2C8C646FF45087C31C2DE8088C2F6DB26E8CEE52B3A883F47F6B2C4F5A16C9C6" }, + { name = "marceau", version = "1.3.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "2D1C27504BEF45005F5DFB18591F8610FB4BFA91744878210BDC464412EC44E9" }, + { name = "mist", version = "5.0.3", build_tools = ["gleam"], requirements = ["exception", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "gleam_yielder", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "7C4BE717A81305323C47C8A591E6B9BA4AC7F56354BF70B4D3DF08CC01192668" }, + { name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" }, + { name = "polly", version = "2.1.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib", "simplifile"], otp_app = "polly", source = "hex", outer_checksum = "1BA4D0ACE9BCF52AEA6AD9DE020FD8220CCA399A379E50A1775FC5C1204FCF56" }, + { name = "simplifile", version = "2.3.1", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "957E0E5B75927659F1D2A1B7B75D7B9BA96FAA8D0C53EA71C4AD9CD0C6B848F6" }, + { name = "snag", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "274F41D6C3ECF99F7686FDCE54183333E41D2C1CA5A3A673F9A8B2C7A4401077" }, + { name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" }, + { name = "tom", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_time"], otp_app = "tom", source = "hex", outer_checksum = "74D0C5A3761F7A7D06994755D4D5AD854122EF8E9F9F76A3E7547606D8C77091" }, + { name = "wisp", version = "2.1.0", build_tools = ["gleam"], requirements = ["directories", "exception", "filepath", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "houdini", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "362BDDD11BF48EB38CDE51A73BC7D1B89581B395CA998E3F23F11EC026151C54" }, +] + +[requirements] +gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } +glecuid = { version = ">= 1.0.4 and < 2.0.0" } +gleeunit = { version = ">= 1.0.0 and < 2.0.0" } +lustre = { version = ">= 5.4.0 and < 6.0.0" } +lustre_dev_tools = { version = ">= 2.3.1 and < 3.0.0" } diff --git a/web_test/package-lock.json b/web_test/package-lock.json new file mode 100644 index 0000000..cdae153 --- /dev/null +++ b/web_test/package-lock.json @@ -0,0 +1,1068 @@ +{ + "name": "web_test", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "web_test", + "license": "ISC", + "devDependencies": { + "@noble/hashes": "^2.0.1", + "@playwright/test": "^1.57.0", + "@types/express": "^5.0.6", + "@types/node": "^24.10.1", + "express": "^5.2.1" + } + }, + "node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@playwright/test": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", + "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", + "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/body-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/web_test/package.json b/web_test/package.json new file mode 100644 index 0000000..d0e4637 --- /dev/null +++ b/web_test/package.json @@ -0,0 +1,16 @@ +{ + "name": "web_test", + "scripts": { + "start": "node ./src/server.mjs" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@noble/hashes": "^2.0.1", + "@playwright/test": "^1.57.0", + "@types/express": "^5.0.6", + "@types/node": "^24.10.1", + "express": "^5.2.1" + } +} diff --git a/web_test/playwright.config.ts b/web_test/playwright.config.ts new file mode 100644 index 0000000..2a8e4d5 --- /dev/null +++ b/web_test/playwright.config.ts @@ -0,0 +1,34 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './test', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + webServer: { + command: 'npm run start', + reuseExistingServer: !process.env.CI, + }, + use: { + baseURL: 'http://localhost:3000', + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + ], +}); diff --git a/web_test/src/glecuid_web_test.gleam b/web_test/src/glecuid_web_test.gleam new file mode 100644 index 0000000..0dc12fc --- /dev/null +++ b/web_test/src/glecuid_web_test.gleam @@ -0,0 +1,17 @@ +import glecuid/cuid2 +import lustre +import lustre/effect +import lustre/element/html + +pub fn main() { + let app = + lustre.application( + fn(_) { #("", effect.from(fn(cb) { cb(cuid2.create_id()) })) }, + fn(_, new_id: String) { #(new_id, effect.none()) }, + fn(id) { html.text(id) }, + ) + + let assert Ok(_) = lustre.start(app, "#app", Nil) + + Nil +} diff --git a/web_test/src/server.mjs b/web_test/src/server.mjs new file mode 100644 index 0000000..339fcca --- /dev/null +++ b/web_test/src/server.mjs @@ -0,0 +1,5 @@ +import express from 'express'; + +const app = express(); +app.use(express.static('dist')); +app.listen(3000); diff --git a/web_test/test/glecuid.spec.ts b/web_test/test/glecuid.spec.ts new file mode 100644 index 0000000..67239d9 --- /dev/null +++ b/web_test/test/glecuid.spec.ts @@ -0,0 +1,8 @@ +import { test, expect } from '@playwright/test'; + +test('has generated cuid2', async ({ page }) => { + await page.goto('/'); + const element = page.locator('#app'); + const text = await element.innerText(); + expect(text).toHaveLength(24); +});