From 2b00ef6e1419aed28882d50edcb077642fd24cba Mon Sep 17 00:00:00 2001 From: Nordin Date: Mon, 22 Jun 2026 11:12:30 +0800 Subject: [PATCH 01/13] add CLAUDE.md for project documentation and usage guidelines --- CLAUDE.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..b7d63d7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,49 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +`acl` is a Node.js Access Control List library inspired by Zend_ACL. It models authorization as **users → roles → resources → permissions**, with role hierarchies (parents). It ships three storage backends (Redis, MongoDB, in-memory) and an Express middleware for protecting routes. + +## Commands + +```bash +npm test # full suite against all 4 backends +npx mocha test/runner.js --reporter spec # same thing directly +npx mocha test/runner.js --grep "isAllowed" # run a single test/describe block by name +npm run cover # istanbul coverage +``` + +**Tests require live Redis and MongoDB** on localhost (Redis `127.0.0.1:6379`, MongoDB `mongodb://localhost:27017/acltest`). Without them the Redis/MongoDB suites fail; the Memory suite still runs. [test/runner.js](test/runner.js) registers four `describe` blocks (MongoDB default, MongoDB useSingle, Redis, Memory) and runs the *same* shared test bodies from [test/tests.js](test/tests.js) and [test/backendtests.js](test/backendtests.js) against each backend's `this.backend`. So a test written once is automatically exercised on every backend. + +## Architecture + +- **[index.js](index.js)** — entry point. Exports the `Acl` class and lazily exposes `redisBackend`, `memoryBackend`, `mongodbBackend` via getters. +- **[lib/acl.js](lib/acl.js)** — all ACL logic (the public API: `allow`, `isAllowed`, `addUserRoles`, `whatResources`, `middleware`, etc.). This is the only file with business logic; the backends are dumb storage. +- **[lib/backend.js](lib/backend.js)** — the **backend interface contract** (documentation/reference, not instantiated). Any new backend must implement these methods. +- **[lib/{redis,mongodb,memory}-backend.js](lib/)** — the three storage implementations. +- **[lib/contract.js](lib/contract.js)** — a runtime design-by-contract argument validator. + +### Backend abstraction (the key concept) + +`acl.js` never talks to a database directly — it talks to a backend through a small key/value-of-sets interface defined in [lib/backend.js](lib/backend.js): `get`, `union`, `unions`, `add`, `del`, `remove`, `clean`, plus a **transaction** model via `begin()` / `end(transaction, cb)`. + +Writes are batched: callers do `var t = backend.begin()`, queue mutations with `add`/`del`/`remove` (which push onto `t`), then `backend.end(t, cb)` commits them. In the memory backend a transaction is literally an array of closures executed on `end`; Redis/MongoDB map this onto their native multi/batch primitives. When adding or modifying behavior, preserve this batch-then-commit pattern rather than writing eagerly. + +Data is organized into **buckets** (namespaces): `meta`, `parents`, `permissions`, `resources`, `roles`, `users`. The header comment in [lib/acl.js](lib/acl.js) documents the exact Redis key layout (e.g. `acl_allows_{resourceName}_{roleName}`). + +### Async model + +Internally the library is promise-based (bluebird). The constructor **promisifies** the callback-style backend methods (`backend.getAsync`, `unionAsync`, etc.) so `acl.js` can use promises throughout. Public API methods accept an **optional trailing callback** but also return a promise — support both when adding/editing methods (see how existing methods end with `.nodeify(callback)`). + +### contract.js + +Every backend method and many ACL methods begin with a `contract(arguments).params(...).end()` call that validates argument types at runtime (e.g. `'string|number'`, `'array'`). It is a no-op unless `contract.debug === true`, which [lib/acl.js](lib/acl.js) sets globally. Keep these contract guards in sync when changing a method's signature. + +## Conventions + +- Plain ES5, `"use strict"`, callback + promise dual API. No build step, no transpilation, no linter configured. +- `'*'` is the wildcard meaning "all permissions". +- User ids, role names, and resource names are **case-sensitive**. +- When adding a feature, add its test once to [test/tests.js](test/tests.js) so it runs across all backends; only put storage-specific tests in [test/backendtests.js](test/backendtests.js). From 28cca3e6be2a7b7d8d55a04462b81270582ef4ab Mon Sep 17 00:00:00 2001 From: Nordin Date: Mon, 22 Jun 2026 11:12:41 +0800 Subject: [PATCH 02/13] add modernization plan for TypeScript rewrite of node_acl --- MODERNIZATION.md | 105 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 MODERNIZATION.md diff --git a/MODERNIZATION.md b/MODERNIZATION.md new file mode 100644 index 0000000..c238c96 --- /dev/null +++ b/MODERNIZATION.md @@ -0,0 +1,105 @@ +# Modernization Plan + +Plan to rewrite `node_acl` as a modern, TypeScript, promise-native ACL library published under a new name (e.g. `acl-next` or `@you/acl`). + +The original architecture is sound — keep the **users → roles → resources → permissions** model, the **backend abstraction**, the **transaction `begin`/`end`** pattern, and the **bucket** namespacing. Everything else is replaceable. + +--- + +## Target stack + +| Concern | Now | Target | Notes | +| --- | --- | --- | --- | +| Language | ES5, `var`, prototypes | **TypeScript** (strict), classes, ESM | Ship dual ESM + CJS | +| Promises | `bluebird` | **native** Promises / `async`/`await` | Delete bluebird | +| Utilities | `lodash@4`, `async@2` | **native** (`Set`, `Array.flat`, spread) | Delete both | +| Arg validation | `contract.js` (runtime) | **TS types** (compile-time) | Delete contract.js entirely | +| API style | dual callback + promise | **promise-only** | Breaking → justifies new major/name | +| Redis driver | `redis@2` | `redis@4+` (promise-native) | API changed significantly | +| Mongo driver | `mongodb@2` | `mongodb@6+` (promise-native) | API changed significantly | +| Tests | mocha + chai, needs local DBs | **vitest** + **testcontainers** | Spins up Redis/Mongo in Docker per run | +| Lint/format | none | **Biome** (or ESLint + Prettier) | Biome = one fast tool | +| CI | Travis (Node 0.10–stable) | **GitHub Actions**, Node 18/20/22 LTS | Matrix | +| Build | none | **tsup** (esbuild) | Emits ESM, CJS, and `.d.ts` | +| Docs | hand-written README | README + **typedoc** | Generated API reference | +| Node support | `>= 0.10` | `>= 18` | | + +--- + +## Phase 0 — Foundation (no logic changes) + +Goal: a buildable, lintable, empty TS project skeleton. + +1. New repo / package name; update `package.json` (`name`, `type: "module"`, `exports` map, `engines`, `files`). +2. Add `LICENSE` keeping the original MIT notice (`Copyright (c) 2011-2013 Manuel Astudillo`) **plus** your own line. Note "fork of optimalbits/node_acl" in README. +3. `tsconfig.json` with `strict: true`, `noUncheckedIndexedAccess`, `target: ES2022`, `moduleResolution: bundler`. +4. tsup config → dual ESM/CJS + declarations. +5. Biome (or ESLint+Prettier) config. +6. GitHub Actions: lint + typecheck + test matrix on Node 18/20/22. +7. `src/` layout: `src/index.ts`, `src/acl.ts`, `src/types.ts`, `src/backends/{memory,redis,mongodb}.ts`. + +**Risk:** low. **Output:** green CI on an empty shell. + +## Phase 1 — Test harness first (safety net) + +Goal: port the existing test suite *before* touching logic, so the rewrite is verified against known-good behavior. + +1. Port [test/tests.js](test/tests.js) + [test/backendtests.js](test/backendtests.js) to vitest, keeping the "run the same suite against every backend" structure from [test/runner.js](test/runner.js). +2. Replace the "requires local Redis/Mongo" assumption with **testcontainers** — tests start ephemeral DB containers, so `npm test` works on any machine with Docker and in CI without service config. +3. Keep the Memory backend suite able to run without Docker (fast inner loop). + +**Risk:** medium (testcontainers + Docker in CI). **Output:** the full behavioral spec, runnable. + +## Phase 2 — Types & backend interface + +1. Define `src/types.ts`: `UserId = string | number`, `Role`, `Resource`, `Permission`, and the `Backend` interface (typed version of [lib/backend.js](lib/backend.js)): `get`, `union`, `unions`, `add`, `del`, `remove`, `clean`, `begin`, `end`. +2. Decide the transaction type. Recommended: keep it opaque (`type Transaction = unknown` per backend, or a generic `Backend`), preserving the queue-then-commit model. +3. Make backend methods **return Promises** instead of taking callbacks — this is what lets `acl.ts` drop bluebird's `promisify`. + +## Phase 3 — Port backends (memory → redis → mongo) + +1. **Memory** first (no external dep, pure logic). Replace lodash with `Set`/`Array.flat`. This validates the test harness end-to-end. +2. **Redis** on `redis@4`: rewrite using native promise API and `multi()` for the transaction commit. Map `begin`→start a command queue, `end`→`exec()`. +3. **MongoDB** on `mongodb@6`: rewrite with the modern collection API; preserve the `useSingle` option (one collection vs per-resource). + +**Risk:** medium-high — redis/mongo driver APIs changed a lot between v2 and current. The ported test suite is your guardrail. + +## Phase 4 — Port core (`acl.ts`) + +1. Translate [lib/acl.js](lib/acl.js) public API to a `class Acl`, **promise-only** (drop the `nodeify`/callback dual path). +2. Replace bluebird chains with `async`/`await`. +3. Replace all lodash/async calls with native equivalents. +4. **Delete `contract.js`** — runtime `params(...)` checks become TS parameter types. (Optionally add light runtime guards only at the public boundary if you want JS-consumer safety.) +5. Keep `'*'` wildcard semantics and case-sensitivity behavior intact (tests enforce this). + +## Phase 5 — Express middleware + +1. Type `acl.middleware(numPathComponents?, userId?, permissions?)`. +2. Consider decoupling from Express's exact types (accept a minimal `{ url, method, session }`-shaped request) so it works with Express 5, Fastify adapters, etc. — or ship `@types/express` as a peer/optional dep. + +## Phase 6 — Packaging, docs, release + +1. Verify the `exports` map resolves for both `import` and `require`; test with `arethetypeswrong` and `publint`. +2. Rewrite README for the new name + promise-only API; add a **migration guide** from `node_acl` (callbacks→promises, dropped bluebird-specific behavior). +3. Generate API docs with typedoc. +4. `npm publish` as `1.0.0` (it's a clean break). Add a CHANGELOG and semantic-release if you want automation. + +--- + +## Other suggestions / nice-to-haves + +- **Make the package zero-dependency at runtime.** After dropping bluebird/lodash/async, the core needs no deps; the redis/mongo drivers become `peerDependencies` (the consumer brings their own client) so you don't pin their versions. This is the single biggest modernization win. +- **`exactOptionalPropertyTypes` + generics on `Backend`** for full type safety across backends. +- **Add a `deny`/explicit-denial feature** — listed as "Future work" in the original README and never built. A clean differentiator for the fork. +- **Batch/typed query helpers** and an `isAllowedAny` / `isAllowedAll` distinction for clearer multi-permission semantics. +- **Benchmarks** (e.g. `tinybench`) so dependency changes don't regress performance. +- **`provenance` on publish** (npm `--provenance` via GitHub Actions) for supply-chain trust. +- **Drop Docker requirement for unit tests** by keeping memory-backend tests as the fast path; gate container tests behind a separate `test:integration` script. + +--- + +## Suggested order of attack + +Phase 0 → 1 → 3a (memory) → 4 → 5 → 3b/3c (redis/mongo) → 2 is woven through 2–4 → 6. + +Rationale: get a buildable shell + ported tests + the memory backend working with the new core **first** (no Docker, no driver upgrades) to prove the whole design, then tackle the higher-risk redis/mongo driver upgrades against a passing suite. From ec40b530fa320caf69824aed6ddab2ada5821dad Mon Sep 17 00:00:00 2001 From: Nordin Date: Mon, 22 Jun 2026 11:15:41 +0800 Subject: [PATCH 03/13] docs: capture pre-modernization test baseline (394 passing) Records the legacy ES5 suite result across all four backends (Mongo default/useSingle, Redis, Memory) as the behavior-preserving acceptance criterion for the TypeScript rewrite. Co-Authored-By: Claude Opus 4.8 (1M context) --- TEST-BASELINE.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 TEST-BASELINE.md diff --git a/TEST-BASELINE.md b/TEST-BASELINE.md new file mode 100644 index 0000000..55d8632 --- /dev/null +++ b/TEST-BASELINE.md @@ -0,0 +1,39 @@ +# Test Baseline (pre-modernization) + +Captured on the legacy ES5 codebase **before** the TypeScript rewrite, to verify the rewrite preserves behavior. Run on branch `modernize-typescript` at its first commit. + +## Result + +``` +394 passing (676ms) +6 pending +0 failing +``` + +The suite ([test/runner.js](test/runner.js)) runs the **same** shared specs ([test/tests.js](test/tests.js), [test/backendtests.js](test/backendtests.js)) against four backend configurations: + +| Suite | Backend | +| --- | --- | +| MongoDB - Default | per-resource collections | +| MongoDB - useSingle | single collection | +| Redis | redis client | +| Memory | in-memory | + +The 6 pending specs are `it.skip`/unimplemented placeholders in the legacy suite (not failures). + +## Environment used to capture this + +| Item | Value | +| --- | --- | +| Node | v20.19.0 | +| npm | 10.8.2 | +| Redis | local service on `127.0.0.1:6379` | +| MongoDB | Docker `mongo:4.4` on `localhost:27017` | +| Command | `npm test` (`mocha test/runner.js --reporter spec`) | + +> Note: the legacy suite requires Redis and MongoDB reachable on localhost. Mongo was provided here via: +> `docker run -d --name acl-mongo-baseline -p 27017:27017 mongo:4.4` + +## Acceptance criterion for the rewrite + +The modernized suite (vitest + testcontainers) must reproduce **the same passing specs across all four backend configurations** before the rewrite is considered behavior-preserving. The 6 pending specs may be implemented or dropped, but no previously-passing spec may regress. From fa90f1146b3e6adc3b5b248240de53dd5f01da7f Mon Sep 17 00:00:00 2001 From: Nordin Date: Mon, 22 Jun 2026 11:19:17 +0800 Subject: [PATCH 04/13] =?UTF-8?q?build:=20Phase=200=20=E2=80=94=20TypeScri?= =?UTF-8?q?pt=20toolchain=20scaffold?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Set up the modern toolchain alongside the legacy ES5 sources (lib/, test/*.js remain until ported and removed in a later phase): - package.json: renamed to acl-next, ESM+CJS exports map, zero runtime deps, redis/mongodb as optional peerDependencies, Node >=18 - tsconfig: strict + noUncheckedIndexedAccess + exactOptionalPropertyTypes - tsup: dual ESM/CJS build with .d.ts - biome: lint + format (scoped to new src/test, legacy ignored) - vitest: test runner (timeouts sized for testcontainers) - GitHub Actions CI: lint/typecheck/build/test on Node 18/20/22 Verified: tsc --noEmit, build, biome check, and vitest all green. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 26 + .gitignore | 4 + biome.json | 30 + package-lock.json | 4957 ++++++++++++++++++++++++++++++++++++++ package.json | 83 +- src/index.ts | 13 + test/smoke.test.ts | 8 + tsconfig.json | 23 + tsup.config.ts | 12 + vitest.config.ts | 10 + 10 files changed, 5144 insertions(+), 22 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 biome.json create mode 100644 package-lock.json create mode 100644 src/index.ts create mode 100644 test/smoke.test.ts create mode 100644 tsconfig.json create mode 100644 tsup.config.ts create mode 100644 vitest.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f95910b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,26 @@ +name: CI + +on: + push: + branches: [master, modernize-typescript] + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node: [18, 20, 22] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + cache: npm + - run: npm ci + - run: npm run lint + - run: npm run typecheck + - run: npm run build + # testcontainers needs Docker, available on ubuntu-latest runners. + - run: npm test diff --git a/.gitignore b/.gitignore index e053845..5f22c7f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,9 @@ node_modules/* +dist/ +coverage/ +*.tsbuildinfo .DS_Store *~ .idea/ +.vscode/ .tags* diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..bedaecb --- /dev/null +++ b/biome.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignore": ["dist", "node_modules", "*.json", "index.js", "lib/**", "test/*.js"] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double", + "semicolons": "always", + "trailingCommas": "all" + } + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2af3782 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4957 @@ +{ + "name": "acl-next", + "version": "1.0.0-alpha.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "acl-next", + "version": "1.0.0-alpha.0", + "license": "MIT", + "devDependencies": { + "@biomejs/biome": "^1.9.4", + "@types/node": "^20.14.0", + "mongodb": "^6.8.0", + "redis": "^4.7.0", + "testcontainers": "^10.13.0", + "tsup": "^8.2.0", + "typescript": "^5.5.0", + "vitest": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "mongodb": ">=5", + "redis": ">=4" + }, + "peerDependenciesMeta": { + "mongodb": { + "optional": true + }, + "redis": { + "optional": true + } + } + }, + "node_modules/@balena/dockerignore": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", + "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@biomejs/biome": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz", + "integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==", + "dev": true, + "hasInstallScript": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "1.9.4", + "@biomejs/cli-darwin-x64": "1.9.4", + "@biomejs/cli-linux-arm64": "1.9.4", + "@biomejs/cli-linux-arm64-musl": "1.9.4", + "@biomejs/cli-linux-x64": "1.9.4", + "@biomejs/cli-linux-x64-musl": "1.9.4", + "@biomejs/cli-win32-arm64": "1.9.4", + "@biomejs/cli-win32-x64": "1.9.4" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz", + "integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz", + "integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz", + "integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz", + "integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz", + "integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz", + "integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz", + "integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz", + "integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", + "integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", + "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.11.tgz", + "integrity": "sha512-o9rAHc0IpIjuPSxRutWpE1F62x7n+4mVS4rCNHkzhIUMQcc18bb6xEq5wd2NdN0WjepIyXIppRshYI2kQDOZVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/client": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", + "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/graph": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", + "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz", + "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz", + "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz", + "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/docker-modem": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", + "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/ssh2": "*" + } + }, + "node_modules/@types/dockerode": { + "version": "3.3.47", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.47.tgz", + "integrity": "sha512-ShM1mz7rCjdssXt7Xz0u1/R2BJC7piWa3SJpUBiVjCf2A3XNn4cP6pUVaD8bLanpPVVn4IKzJuw3dOvkJ8IbYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/docker-modem": "*", + "@types/node": "*", + "@types/ssh2": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/ssh2": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.5.tgz", + "integrity": "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18" + } + }, + "node_modules/@types/ssh2-streams": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/@types/ssh2-streams/-/ssh2-streams-0.1.13.tgz", + "integrity": "sha512-faHyY3brO9oLEA0QlcO8N2wT7R0+1sHWZvQ+y3rMLwdY1ZyS1z0W3t65j9PqT4HmQ6ALzNe7RZlNuCNE0wBSWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ssh2/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/ssh2/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-lock": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", + "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bare-events": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", + "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.2.tgz", + "integrity": "sha512-aTvMFUWkBmjzKtEQMDGGDNF8bkfpD5N1b/FCwt7A3wrU4t1o/e/85Wzkluh6JlODCjqVESYCkQCdTXqZ9G7VFg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.1.tgz", + "integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.1.tgz", + "integrity": "sha512-ghj2DSK/2e99a1anTVPCV4m4YIYtrbXhfM7V3D7XZLOTsybnYyaJloymGqssQc8l/or0UoDyRtNQkmkEF/ysgQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", + "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.5.tgz", + "integrity": "sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/bson": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/buildcheck": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz", + "integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==", + "dev": true, + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/byline": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", + "integrity": "sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "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/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/docker-compose": { + "version": "0.24.8", + "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-0.24.8.tgz", + "integrity": "sha512-plizRs/Vf15H+GCVxq2EUvyPK7ei9b/cVesHvjnX4xaXjM9spHe2Ytq0BitndFgvTJ3E3NljPNUEl7BAN43iZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "yaml": "^2.2.2" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/docker-modem": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.7.tgz", + "integrity": "sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.1", + "readable-stream": "^3.5.0", + "split-ca": "^1.0.1", + "ssh2": "^1.15.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/docker-modem/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/dockerode": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.12.tgz", + "integrity": "sha512-/bCZd6KlGcjZO8Buqmi/vXuqEGVEZ0PNjx/biBNqJD3MhK9DmdiAuKxqfNhflgDESDIiBz3qF+0e55+CpnrUcw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@balena/dockerignore": "^1.0.2", + "@grpc/grpc-js": "^1.11.1", + "@grpc/proto-loader": "^0.7.13", + "docker-modem": "^5.0.7", + "protobufjs": "^7.3.2", + "tar-fs": "^2.1.4", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/dockerode/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/dockerode/node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/dockerode/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-port": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.2.0.tgz", + "integrity": "sha512-afP4W205ONCuMoPBqcR6PSXnzX35KTcJygfJfcp+QY+uwm3p20p1YczWXhlICIzGMCxYBQcySEcOgsJcrkyobg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "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/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "dev": true, + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/mongodb": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.21.0.tgz", + "integrity": "sha512-URyb/VXMjJ4da46OeSXg+puO39XH9DeQpWCslifrRn9JWugy0D+DvvBvkm2WxmHe61O/H19JM66p1z7RHVkZ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.3.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "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/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nan": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.27.0.tgz", + "integrity": "sha512-hC+0LidcL3XE4rp1C4H54KujgXKzbfyTngZTwBByQxsOxCEKZT0MPQ4hOKUH2jU1OYstqdDH4onyHPDzcV0XdQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/properties-reader": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/properties-reader/-/properties-reader-2.3.0.tgz", + "integrity": "sha512-z597WicA7nDZxK12kZqHr2TcvwNU1GCfA5UwfDY/HDp3hXPoPlb5rlEx9bwGTiJnc0OqbBTkU975jDToth8Gxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^1.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/properties?sponsor=1" + } + }, + "node_modules/protobufjs": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz", + "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==", + "dev": true, + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/redis": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.7.1.tgz", + "integrity": "sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "./packages/*" + ], + "dependencies": { + "@redis/bloom": "1.2.0", + "@redis/client": "1.6.1", + "@redis/graph": "1.1.1", + "@redis/json": "1.0.7", + "@redis/search": "1.2.0", + "@redis/time-series": "1.1.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "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/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/split-ca": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", + "integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ssh-remote-port-forward": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/ssh-remote-port-forward/-/ssh-remote-port-forward-1.0.4.tgz", + "integrity": "sha512-x0LV1eVDwjf1gmG7TTnfqIzf+3VPRz7vrNIjX6oYLbeCrf/PeVY6hkT68Mg+q02qXxQhrLjB0jfgvhevoCRmLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ssh2": "^0.5.48", + "ssh2": "^1.4.0" + } + }, + "node_modules/ssh-remote-port-forward/node_modules/@types/ssh2": { + "version": "0.5.52", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.52.tgz", + "integrity": "sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/ssh2-streams": "*" + } + }, + "node_modules/ssh2": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", + "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.23.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/streamx": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", + "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/tar-fs": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", + "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/testcontainers": { + "version": "10.28.0", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.28.0.tgz", + "integrity": "sha512-1fKrRRCsgAQNkarjHCMKzBKXSJFmzNTiTbhb5E/j5hflRXChEtHvkefjaHlgkNUjfw92/Dq8LTgwQn6RDBFbMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@balena/dockerignore": "^1.0.2", + "@types/dockerode": "^3.3.35", + "archiver": "^7.0.1", + "async-lock": "^1.4.1", + "byline": "^5.0.0", + "debug": "^4.3.5", + "docker-compose": "^0.24.8", + "dockerode": "^4.0.5", + "get-port": "^7.1.0", + "proper-lockfile": "^4.1.2", + "properties-reader": "^2.3.0", + "ssh-remote-port-forward": "^1.0.4", + "tar-fs": "^3.0.7", + "tmp": "^0.2.3", + "undici": "^5.29.0" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=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" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + } + } +} diff --git a/package.json b/package.json index 496e6c6..3f19aaf 100644 --- a/package.json +++ b/package.json @@ -1,32 +1,71 @@ { - "name": "acl", - "version": "0.4.11", - "description": "An Access Control List module, based on Redis with Express middleware support", + "name": "acl-next", + "version": "1.0.0-alpha.0", + "description": "Modern TypeScript Access Control List (RBAC) with Redis, MongoDB and in-memory backends. Fork of optimalbits/node_acl.", "keywords": [ - "middleware", "acl", - "web" + "rbac", + "authorization", + "access-control", + "permissions", + "roles", + "middleware", + "express", + "redis", + "mongodb", + "typescript" + ], + "license": "MIT", + "contributors": [ + "Manuel Astudillo (original node_acl author)" + ], + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "files": [ + "dist" ], - "repository": "git://github.com/optimalbits/node_acl.git", - "author": "Manuel Astudillo ", - "homepage": "https://github.com/optimalbits/node_acl", + "sideEffects": false, "engines": { - "node": ">= 0.10" + "node": ">=18" }, - "main": "./index.js", - "dependencies": { - "async": "^2.1.4", - "bluebird": "^3.0.2", - "lodash": "^4.17.3", - "mongodb": "^2.0.47", - "redis": "^2.2.5" + "scripts": { + "build": "tsup", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:unit": "vitest run --dir test/unit", + "test:watch": "vitest", + "lint": "biome check .", + "format": "biome format --write ." }, - "devDependencies": { - "mocha": "^3.2.0", - "chai": "^3.4.0" + "peerDependencies": { + "mongodb": ">=5", + "redis": ">=4" }, - "scripts": { - "test": "mocha test/runner.js --reporter spec", - "cover": "istanbul cover -- _mocha test/runner.js --reporter spec" + "peerDependenciesMeta": { + "mongodb": { + "optional": true + }, + "redis": { + "optional": true + } + }, + "devDependencies": { + "@biomejs/biome": "^1.9.4", + "@types/node": "^20.14.0", + "mongodb": "^6.8.0", + "redis": "^4.7.0", + "testcontainers": "^10.13.0", + "tsup": "^8.2.0", + "typescript": "^5.5.0", + "vitest": "^2.0.0" } } diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..9f6cea6 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,13 @@ +/** + * acl-next — modern TypeScript ACL / RBAC. + * + * Fork of optimalbits/node_acl (MIT, (c) 2011-2013 Manuel Astudillo). + * + * Public surface is filled in across the modernization phases: + * - Phase 2: types & Backend interface + * - Phase 3: backends (memory, redis, mongodb) + * - Phase 4: the Acl class + * - Phase 5: express middleware + */ + +export const VERSION = "1.0.0-alpha.0"; diff --git a/test/smoke.test.ts b/test/smoke.test.ts new file mode 100644 index 0000000..efc3d2e --- /dev/null +++ b/test/smoke.test.ts @@ -0,0 +1,8 @@ +import { describe, expect, it } from "vitest"; +import { VERSION } from "../src/index.js"; + +describe("scaffold smoke test", () => { + it("exposes a version string", () => { + expect(VERSION).toMatch(/^\d+\.\d+\.\d+/); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7f244c2 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022"], + "types": ["node"], + "declaration": true, + "outDir": "dist", + "rootDir": ".", + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noImplicitOverride": true, + "noFallthroughCasesInSwitch": true, + "verbatimModuleSyntax": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src", "test"], + "exclude": ["node_modules", "dist", "lib"] +} diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..906adf4 --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["esm", "cjs"], + dts: true, + clean: true, + sourcemap: true, + treeshake: true, + // redis / mongodb are optional peer deps — never bundle them. + external: ["redis", "mongodb"], +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..ade1920 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["test/**/*.test.ts"], + // Integration tests spin up Docker containers via testcontainers and need headroom. + testTimeout: 60_000, + hookTimeout: 120_000, + }, +}); From fb0922430ce3553042da4384a2fb9c707b6d5f96 Mon Sep 17 00:00:00 2001 From: Nordin Date: Mon, 22 Jun 2026 11:20:14 +0800 Subject: [PATCH 05/13] =?UTF-8?q?feat:=20Phase=202=20=E2=80=94=20typed=20d?= =?UTF-8?q?omain=20model=20and=20Backend=20interface?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add src/types.ts: promise-native, generic Backend interface (typed successor to lib/backend.js) plus core domain types (UserId, Role, Resource, Permission, AclOptions, AllowRule, Logger). Re-exported from the package entry point. No runtime behavior yet. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/index.ts | 15 ++++++++ src/types.ts | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 src/types.ts diff --git a/src/index.ts b/src/index.ts index 9f6cea6..3d7a32d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,3 +11,18 @@ */ export const VERSION = "1.0.0-alpha.0"; + +export type { + AclOptions, + AllowRule, + Backend, + Buckets, + Key, + Logger, + OneOrMany, + Permission, + Resource, + Role, + StoredValue, + UserId, +} from "./types.js"; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..8c99f9f --- /dev/null +++ b/src/types.ts @@ -0,0 +1,100 @@ +/** + * Core domain types and the storage Backend interface. + * + * This is the typed, promise-native successor to the legacy lib/backend.js + * contract. Backends are namespaced (bucketed) key -> set-of-values stores + * with batched, committed writes. + */ + +/** A user identifier. Users are created implicitly by assigning them roles. */ +export type UserId = string | number; + +/** A role name. `"*"` is reserved to mean "all permissions". */ +export type Role = string; + +/** A resource name (e.g. a URL or logical resource). */ +export type Resource = string; + +/** A permission/action name (e.g. `"get"`, `"edit"`). */ +export type Permission = string; + +/** A key within a bucket. */ +export type Key = string | number; + +/** A value stored inside a bucket's set. */ +export type StoredValue = string | number; + +/** One or many of `T` — mirrors the original "accepts string or array" API. */ +export type OneOrMany = T | T[]; + +/** Optional logger; when provided, the Acl instance emits debug output. */ +export interface Logger { + debug(...args: unknown[]): void; +} + +/** Names of the internal storage buckets. Overridable via {@link AclOptions}. */ +export interface Buckets { + meta: string; + parents: string; + permissions: string; + resources: string; + roles: string; + users: string; +} + +/** Options accepted by the Acl constructor. */ +export interface AclOptions { + buckets?: Partial; +} + +/** A single entry of the array form of `allow(...)`. */ +export interface AllowRule { + roles: OneOrMany; + allows: Array<{ + resources: OneOrMany; + permissions: OneOrMany; + }>; +} + +/** + * Storage backend interface. + * + * Writes are not applied immediately: callers obtain a transaction with + * {@link Backend.begin}, queue mutations with {@link Backend.add}, + * {@link Backend.del} and {@link Backend.remove}, then commit them with + * {@link Backend.end}. Backends that support it commit atomically. + * + * @typeParam T - the backend-specific transaction type produced by `begin`. + */ +export interface Backend { + /** Start a transaction (a queue of pending mutations). */ + begin(): T; + + /** Commit a transaction. */ + end(transaction: T): Promise; + + /** Remove all stored data. */ + clean(): Promise; + + /** Get the set of values stored at `bucket`/`key` (empty array if none). */ + get(bucket: string, key: Key): Promise; + + /** Union of the sets stored at the given `keys` within one `bucket`. */ + union(bucket: string, keys: Key[]): Promise; + + /** + * Per-bucket union of `keys` across multiple `buckets`, keyed by bucket. + * Optional — Acl falls back to repeated {@link Backend.union} calls when a + * backend does not implement it. + */ + unions?(buckets: string[], keys: Key[]): Promise>; + + /** Queue: add `values` to the set at `bucket`/`key`. */ + add(transaction: T, bucket: string, key: Key, values: OneOrMany): void; + + /** Queue: delete the given `keys` from `bucket`. */ + del(transaction: T, bucket: string, keys: OneOrMany): void; + + /** Queue: remove `values` from the set at `bucket`/`key`. */ + remove(transaction: T, bucket: string, key: Key, values: OneOrMany): void; +} From a010f71a913349331b6121ce7b9c8ff8f7503cde Mon Sep 17 00:00:00 2001 From: Nordin Date: Mon, 22 Jun 2026 11:23:57 +0800 Subject: [PATCH 06/13] =?UTF-8?q?feat:=20Phase=203a=20=E2=80=94=20in-memor?= =?UTF-8?q?y=20backend=20(promise-native,=20zero-dep)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port lib/memory-backend.js to TypeScript: - implements Backend, async/await throughout - native Map/Set instead of lodash - IDs coerced to strings (per decision); preserves legacy regexp bucket-name fallback in union() - batched begin/queue/end transaction model preserved Adds 9 unit tests covering add/get/union/unions/del/remove/clean and transaction deferral. tsc, biome, vitest all green. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/backends/memory.ts | 146 +++++++++++++++++++++++++++++++++++++++ src/index.ts | 3 + test/unit/memory.test.ts | 80 +++++++++++++++++++++ 3 files changed, 229 insertions(+) create mode 100644 src/backends/memory.ts create mode 100644 test/unit/memory.test.ts diff --git a/src/backends/memory.ts b/src/backends/memory.ts new file mode 100644 index 0000000..811e99e --- /dev/null +++ b/src/backends/memory.ts @@ -0,0 +1,146 @@ +import type { Backend, Key, OneOrMany, StoredValue } from "../types.js"; + +/** A queued mutation. The memory transaction is simply a list of these. */ +type Mutation = () => void; + +/** In-memory transaction: an ordered list of pending mutations. */ +export type MemoryTransaction = Mutation[]; + +const toArray = (value: OneOrMany): T[] => (Array.isArray(value) ? value : [value]); + +/** Keys and values are normalized to strings (see migration notes). */ +const toStr = (value: Key | StoredValue): string => `${value}`; + +/** + * In-memory storage backend. No external dependencies — ideal for tests and + * single-process apps. Data lives in a `Map>`. + */ +export class MemoryBackend implements Backend { + private buckets = new Map>(); + + begin(): MemoryTransaction { + return []; + } + + async end(transaction: MemoryTransaction): Promise { + for (const mutation of transaction) { + mutation(); + } + } + + async clean(): Promise { + this.buckets.clear(); + } + + async get(bucket: string, key: Key): Promise { + const values = this.buckets.get(bucket)?.get(toStr(key)); + return values ? [...values] : []; + } + + async unions(buckets: string[], keys: Key[]): Promise> { + const keyStrs = keys.map(toStr); + const result: Record = {}; + + for (const bucket of buckets) { + const store = this.buckets.get(bucket); + if (!store) { + result[bucket] = []; + continue; + } + const union = new Set(); + for (const key of keyStrs) { + for (const value of store.get(key) ?? []) { + union.add(value); + } + } + result[bucket] = [...union]; + } + + return result; + } + + async union(bucket: string, keys: Key[]): Promise { + let store = this.buckets.get(bucket); + + // Legacy behavior: if no exact bucket matches, treat existing bucket names + // as regular expressions and use the first that matches `bucket`. + if (!store) { + for (const name of this.buckets.keys()) { + if (new RegExp(`^${name}$`).test(bucket)) { + store = this.buckets.get(name); + break; + } + } + } + + if (!store) { + return []; + } + + const union = new Set(); + for (const key of keys) { + for (const value of store.get(toStr(key)) ?? []) { + union.add(value); + } + } + return [...union]; + } + + add( + transaction: MemoryTransaction, + bucket: string, + key: Key, + values: OneOrMany, + ): void { + const keyStr = toStr(key); + const valueStrs = toArray(values).map(toStr); + + transaction.push(() => { + let store = this.buckets.get(bucket); + if (!store) { + store = new Map(); + this.buckets.set(bucket, store); + } + const existing = store.get(keyStr); + // New values first, then existing — matches the legacy union order. + store.set( + keyStr, + existing ? [...new Set([...valueStrs, ...existing])] : [...new Set(valueStrs)], + ); + }); + } + + del(transaction: MemoryTransaction, bucket: string, keys: OneOrMany): void { + const keyStrs = toArray(keys).map(toStr); + + transaction.push(() => { + const store = this.buckets.get(bucket); + if (!store) { + return; + } + for (const key of keyStrs) { + store.delete(key); + } + }); + } + + remove( + transaction: MemoryTransaction, + bucket: string, + key: Key, + values: OneOrMany, + ): void { + const keyStr = toStr(key); + const toRemove = new Set(toArray(values).map(toStr)); + + transaction.push(() => { + const existing = this.buckets.get(bucket)?.get(keyStr); + if (existing) { + this.buckets.get(bucket)?.set( + keyStr, + existing.filter((value) => !toRemove.has(value)), + ); + } + }); + } +} diff --git a/src/index.ts b/src/index.ts index 3d7a32d..0564a37 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,9 @@ export const VERSION = "1.0.0-alpha.0"; +export { MemoryBackend } from "./backends/memory.js"; +export type { MemoryTransaction } from "./backends/memory.js"; + export type { AclOptions, AllowRule, diff --git a/test/unit/memory.test.ts b/test/unit/memory.test.ts new file mode 100644 index 0000000..0fd42b6 --- /dev/null +++ b/test/unit/memory.test.ts @@ -0,0 +1,80 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { MemoryBackend } from "../../src/backends/memory.js"; + +describe("MemoryBackend", () => { + let backend: MemoryBackend; + + beforeEach(() => { + backend = new MemoryBackend(); + }); + + /** Helper: run a single queued mutation through a transaction. */ + const commit = async (queue: (t: ReturnType) => void) => { + const t = backend.begin(); + queue(t); + await backend.end(t); + }; + + it("returns an empty array for missing bucket/key", async () => { + expect(await backend.get("nope", "missing")).toEqual([]); + }); + + it("does not apply mutations until the transaction is committed", async () => { + const t = backend.begin(); + backend.add(t, "users", "u1", "admin"); + expect(await backend.get("users", "u1")).toEqual([]); // not committed yet + await backend.end(t); + expect(await backend.get("users", "u1")).toEqual(["admin"]); + }); + + it("adds values as a set (deduplicated, union with existing)", async () => { + await commit((t) => backend.add(t, "users", "u1", ["a", "b"])); + await commit((t) => backend.add(t, "users", "u1", ["b", "c"])); + expect((await backend.get("users", "u1")).sort()).toEqual(["a", "b", "c"]); + }); + + it("coerces numeric keys and values to strings", async () => { + await commit((t) => backend.add(t, "roles", "guest", 42)); + expect(await backend.get("roles", "guest")).toEqual(["42"]); + await commit((t) => backend.add(t, "users", 7, "guest")); + expect(await backend.get("users", 7)).toEqual(["guest"]); + expect(await backend.get("users", "7")).toEqual(["guest"]); + }); + + it("removes values from a key", async () => { + await commit((t) => backend.add(t, "users", "u1", ["a", "b", "c"])); + await commit((t) => backend.remove(t, "users", "u1", ["b"])); + expect((await backend.get("users", "u1")).sort()).toEqual(["a", "c"]); + }); + + it("deletes keys", async () => { + await commit((t) => backend.add(t, "users", "u1", "a")); + await commit((t) => backend.del(t, "users", "u1")); + expect(await backend.get("users", "u1")).toEqual([]); + }); + + it("unions keys within a bucket", async () => { + await commit((t) => { + backend.add(t, "perm", "r1", ["view", "edit"]); + backend.add(t, "perm", "r2", ["edit", "delete"]); + }); + expect((await backend.union("perm", ["r1", "r2"])).sort()).toEqual(["delete", "edit", "view"]); + }); + + it("unions keys across multiple buckets", async () => { + await commit((t) => { + backend.add(t, "b1", "k", ["x"]); + backend.add(t, "b2", "k", ["y"]); + }); + const result = await backend.unions(["b1", "b2", "missing"], ["k"]); + expect(result.b1).toEqual(["x"]); + expect(result.b2).toEqual(["y"]); + expect(result.missing).toEqual([]); + }); + + it("clean() wipes all data", async () => { + await commit((t) => backend.add(t, "users", "u1", "a")); + await backend.clean(); + expect(await backend.get("users", "u1")).toEqual([]); + }); +}); From 43746628e551ddba840a71c8f33fd490958fe2a1 Mon Sep 17 00:00:00 2001 From: Nordin Date: Mon, 22 Jun 2026 11:29:54 +0800 Subject: [PATCH 07/13] =?UTF-8?q?feat:=20Phase=204=20+=20Phase=201=20?= =?UTF-8?q?=E2=80=94=20core=20Acl=20class=20and=20ported=20behavioral=20su?= =?UTF-8?q?ite?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port lib/acl.js to a TypeScript Acl class: - promise-only (legacy callback dual-API dropped), async/await throughout - no bluebird / lodash / contract.js (runtime arg-checking replaced by TS types); native Set-based union helpers - typed overloads for allow() and whatResources() - generic over the backend transaction type: Acl Port the full legacy shared suite (test/tests.js + backendtests.js) to a backend-agnostic vitest module (test/shared/acl-suite.ts) and run it against the memory backend (test/memory-acl.test.ts). Result: 96 ported behavioral specs + 9 memory unit + 1 smoke = 106 green. tsc, biome all clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/acl.ts | 452 ++++++++++++++++++++++++++++++++++ src/index.ts | 2 + test/memory-acl.test.ts | 4 + test/shared/acl-suite.ts | 513 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 971 insertions(+) create mode 100644 src/acl.ts create mode 100644 test/memory-acl.test.ts create mode 100644 test/shared/acl-suite.ts diff --git a/src/acl.ts b/src/acl.ts new file mode 100644 index 0000000..25b0383 --- /dev/null +++ b/src/acl.ts @@ -0,0 +1,452 @@ +import type { + AclOptions, + AllowRule, + Backend, + Buckets, + Logger, + OneOrMany, + Permission, + Resource, + Role, + UserId, +} from "./types.js"; + +const DEFAULT_BUCKETS: Buckets = { + meta: "meta", + parents: "parents", + permissions: "permissions", + resources: "resources", + roles: "roles", + users: "users", +}; + +const toArray = (value: OneOrMany): T[] => (Array.isArray(value) ? value : [value]); + +/** Set-union of two arrays, preserving first-seen order. */ +const union = (a: readonly T[], b: readonly T[]): T[] => [...new Set([...a, ...b])]; + +/** The per-resource permissions bucket name (e.g. `allows_blogs`). */ +const allowsBucket = (resource: Resource): string => `allows_${resource}`; + +const keyFromAllowsBucket = (bucket: string): string => bucket.replace(/^allows_/, ""); + +/** + * Access Control List. Models authorization as users -> roles -> resources -> + * permissions, with role hierarchies (parents). + * + * Promise-native: every method returns a Promise (the legacy callback API is + * intentionally dropped). Storage is delegated to a {@link Backend}. + * + * @typeParam T - the backend transaction type. + */ +export class Acl { + readonly backend: Backend; + readonly logger: Logger | undefined; + readonly buckets: Buckets; + + constructor(backend: Backend, logger?: Logger, options?: AclOptions) { + this.backend = backend; + this.logger = logger; + this.buckets = { ...DEFAULT_BUCKETS, ...options?.buckets }; + } + + /** Adds roles to a given user id. */ + async addUserRoles(userId: UserId, roles: OneOrMany): Promise { + const transaction = this.backend.begin(); + this.backend.add(transaction, this.buckets.meta, "users", userId); + this.backend.add(transaction, this.buckets.users, userId, roles); + for (const role of toArray(roles)) { + this.backend.add(transaction, this.buckets.roles, role, userId); + } + await this.backend.end(transaction); + } + + /** Removes roles from a given user id. */ + async removeUserRoles(userId: UserId, roles: OneOrMany): Promise { + const transaction = this.backend.begin(); + this.backend.remove(transaction, this.buckets.users, userId, roles); + for (const role of toArray(roles)) { + this.backend.remove(transaction, this.buckets.roles, role, userId); + } + await this.backend.end(transaction); + } + + /** Returns all the roles assigned to a given user id. */ + userRoles(userId: UserId): Promise { + return this.backend.get(this.buckets.users, userId); + } + + /** Returns all users that have the given role. */ + roleUsers(roleName: Role): Promise { + return this.backend.get(this.buckets.roles, roleName); + } + + /** Returns whether the user has the given role. */ + async hasRole(userId: UserId, role: Role): Promise { + const roles = await this.userRoles(userId); + return roles.includes(role); + } + + /** Adds one or more parent roles to a role. */ + async addRoleParents(role: Role, parents: OneOrMany): Promise { + const transaction = this.backend.begin(); + this.backend.add(transaction, this.buckets.meta, "roles", role); + this.backend.add(transaction, this.buckets.parents, role, parents); + await this.backend.end(transaction); + } + + /** Removes parent role(s) from a role. Omit `parents` to remove all of them. */ + async removeRoleParents(role: Role, parents?: OneOrMany): Promise { + const transaction = this.backend.begin(); + if (parents !== undefined) { + this.backend.remove(transaction, this.buckets.parents, role, parents); + } else { + this.backend.del(transaction, this.buckets.parents, role); + } + await this.backend.end(transaction); + } + + /** Removes a role from the system, including all its permissions. */ + async removeRole(role: Role): Promise { + // Note: this is not fully transactional. + const resources = await this.backend.get(this.buckets.resources, role); + const transaction = this.backend.begin(); + + for (const resource of resources) { + this.backend.del(transaction, allowsBucket(resource), role); + } + + this.backend.del(transaction, this.buckets.resources, role); + this.backend.del(transaction, this.buckets.parents, role); + this.backend.del(transaction, this.buckets.roles, role); + this.backend.remove(transaction, this.buckets.meta, "roles", role); + + // The `users` bucket keeps the removed role: we don't know which users + // have it assigned. + await this.backend.end(transaction); + } + + /** Removes a resource from the system. */ + async removeResource(resource: Resource): Promise { + const roles = await this.backend.get(this.buckets.meta, "roles"); + const transaction = this.backend.begin(); + this.backend.del(transaction, allowsBucket(resource), roles); + for (const role of roles) { + this.backend.remove(transaction, this.buckets.resources, role, resource); + } + await this.backend.end(transaction); + } + + /** Adds permissions to roles over resources (compact array form). */ + allow(rules: AllowRule[]): Promise; + /** Adds the given permissions to the given roles over the given resources. */ + allow( + roles: OneOrMany, + resources: OneOrMany, + permissions: OneOrMany, + ): Promise; + async allow( + roles: AllowRule[] | OneOrMany, + resources?: OneOrMany, + permissions?: OneOrMany, + ): Promise { + if (resources === undefined) { + return this.allowEx(roles as AllowRule[]); + } + + const rolesArr = toArray(roles as OneOrMany); + const resourcesArr = toArray(resources); + const transaction = this.backend.begin(); + + this.backend.add(transaction, this.buckets.meta, "roles", rolesArr); + + for (const resource of resourcesArr) { + for (const role of rolesArr) { + this.backend.add( + transaction, + allowsBucket(resource), + role, + permissions as OneOrMany, + ); + } + } + for (const role of rolesArr) { + this.backend.add(transaction, this.buckets.resources, role, resourcesArr); + } + + await this.backend.end(transaction); + } + + /** Removes permissions from a role over resources. Omit `permissions` to remove all. */ + removeAllow( + role: Role, + resources: OneOrMany, + permissions?: OneOrMany, + ): Promise { + return this.removePermissions( + role, + toArray(resources), + permissions !== undefined ? toArray(permissions) : null, + ); + } + + /** + * Removes permissions from a role over the given resources. When + * `permissions` is null the resource is fully revoked for the role. + * + * Note: loses atomicity when pruning emptied role/resource links. + */ + async removePermissions( + role: Role, + resources: Resource[], + permissions: Permission[] | null, + ): Promise { + const transaction = this.backend.begin(); + for (const resource of resources) { + const bucket = allowsBucket(resource); + if (permissions) { + this.backend.remove(transaction, bucket, role, permissions); + } else { + this.backend.del(transaction, bucket, role); + this.backend.remove(transaction, this.buckets.resources, role, resource); + } + } + await this.backend.end(transaction); + + // Remove the resource from the role when no rights remain. Not atomic. + const cleanup = this.backend.begin(); + await Promise.all( + resources.map(async (resource) => { + const remaining = await this.backend.get(allowsBucket(resource), role); + if (remaining.length === 0) { + this.backend.remove(cleanup, this.buckets.resources, role, resource); + } + }), + ); + await this.backend.end(cleanup); + } + + /** + * Returns, per resource, the permissions a user has. Uses the backend's + * `unions` optimization when available. + */ + async allowedPermissions( + userId: UserId, + resources: OneOrMany, + ): Promise> { + if (!userId) { + return {}; + } + if (this.backend.unions) { + return this.optimizedAllowedPermissions(userId, resources); + } + + const resourcesArr = toArray(resources); + const roles = await this.userRoles(userId); + const result: Record = {}; + await Promise.all( + resourcesArr.map(async (resource) => { + result[resource] = await this.resourcePermissions(roles, resource); + }), + ); + return result; + } + + /** `allowedPermissions` variant using the backend `unions` bulk query. */ + async optimizedAllowedPermissions( + userId: UserId, + resources: OneOrMany, + ): Promise> { + if (!userId) { + return {}; + } + const resourcesArr = toArray(resources); + const roles = await this.allUserRoles(userId); + const buckets = resourcesArr.map(allowsBucket); + + const response = + roles.length === 0 + ? Object.fromEntries(buckets.map((bucket) => [bucket, [] as Permission[]])) + : // biome-ignore lint/style/noNonNullAssertion: guarded by the `this.backend.unions` caller + await this.backend.unions!(buckets, roles); + + const result: Record = {}; + for (const bucket of Object.keys(response)) { + result[keyFromAllowsBucket(bucket)] = response[bucket] ?? []; + } + return result; + } + + /** Checks if a user is allowed all of the given permissions on a resource. */ + async isAllowed( + userId: UserId, + resource: Resource, + permissions: OneOrMany, + ): Promise { + const roles = await this.backend.get(this.buckets.users, userId); + if (roles.length) { + return this.areAnyRolesAllowed(roles, resource, permissions); + } + return false; + } + + /** Returns true if any of the roles has all of the given permissions. */ + areAnyRolesAllowed( + roles: OneOrMany, + resource: Resource, + permissions: OneOrMany, + ): Promise { + const rolesArr = toArray(roles); + const permsArr = toArray(permissions); + if (rolesArr.length === 0) { + return Promise.resolve(false); + } + return this.checkPermissions(rolesArr, resource, permsArr); + } + + /** Returns a map of resource -> permissions the roles have. */ + whatResources(roles: OneOrMany): Promise>; + /** Returns the resources the roles have all of the given permissions over. */ + whatResources(roles: OneOrMany, permissions: OneOrMany): Promise; + whatResources( + roles: OneOrMany, + permissions?: OneOrMany, + ): Promise | Resource[]> { + const rolesArr = toArray(roles); + const perms = permissions === undefined ? undefined : toArray(permissions); + return this.permittedResources(rolesArr, perms); + } + + /** Backing implementation for {@link whatResources}. */ + async permittedResources( + roles: OneOrMany, + permissions?: Permission[], + ): Promise | Resource[]> { + const rolesArr = toArray(roles); + const resources = await this.rolesResources(rolesArr); + + if (permissions === undefined) { + const result: Record = {}; + await Promise.all( + resources.map(async (resource) => { + result[resource] = await this.resourcePermissions(rolesArr, resource); + }), + ); + return result; + } + + const result: Resource[] = []; + await Promise.all( + resources.map(async (resource) => { + const p = await this.resourcePermissions(rolesArr, resource); + if (permissions.some((perm) => p.includes(perm))) { + result.push(resource); + } + }), + ); + return result; + } + + // --------------------------------------------------------------------------- + // Private helpers + // --------------------------------------------------------------------------- + + /** Compact array form of {@link allow}. */ + private async allowEx(rules: OneOrMany): Promise { + const demuxed: Array<{ + roles: OneOrMany; + resources: OneOrMany; + permissions: OneOrMany; + }> = []; + + for (const rule of toArray(rules)) { + for (const a of rule.allows) { + demuxed.push({ roles: rule.roles, resources: a.resources, permissions: a.permissions }); + } + } + + // Sequential to mirror the legacy bluebird.reduce. + for (const d of demuxed) { + await this.allow(d.roles, d.resources, d.permissions); + } + } + + /** Direct parents of the given roles. */ + private rolesParents(roles: Role[]): Promise { + return this.backend.union(this.buckets.parents, roles); + } + + /** All roles in the hierarchy, including the given roles. */ + private async allRoles(roleNames: Role[]): Promise { + const parents = await this.rolesParents(roleNames); + if (parents.length > 0) { + const parentRoles = await this.allRoles(parents); + return union(roleNames, parentRoles); + } + return roleNames; + } + + /** All roles in the hierarchy of the given user. */ + private async allUserRoles(userId: UserId): Promise { + const roles = await this.userRoles(userId); + if (roles && roles.length > 0) { + return this.allRoles(roles); + } + return []; + } + + /** All resources reachable by the given roles (through the hierarchy). */ + private async rolesResources(roles: OneOrMany): Promise { + const allRoles = await this.allRoles(toArray(roles)); + const result: Resource[] = []; + await Promise.all( + allRoles.map(async (role) => { + const resources = await this.backend.get(this.buckets.resources, role); + result.push(...resources); + }), + ); + return result; + } + + /** Permissions the given roles (and their parents) have over a resource. */ + private async resourcePermissions(roles: Role[], resource: Resource): Promise { + if (roles.length === 0) { + return []; + } + const resourcePermissions = await this.backend.union(allowsBucket(resource), roles); + const parents = await this.rolesParents(roles); + if (parents?.length) { + const morePermissions = await this.resourcePermissions(parents, resource); + return union(resourcePermissions, morePermissions); + } + return resourcePermissions; + } + + /** + * Whether the roles (and their parents) satisfy all permissions on a resource. + * + * NOTE: does not handle circular role hierarchies. + */ + private async checkPermissions( + roles: Role[], + resource: Resource, + permissions: Permission[], + ): Promise { + const resourcePermissions = await this.backend.union(allowsBucket(resource), roles); + + if (resourcePermissions.includes("*")) { + return true; + } + + const remaining = permissions.filter((p) => !resourcePermissions.includes(p)); + if (remaining.length === 0) { + return true; + } + + const parents = await this.backend.union(this.buckets.parents, roles); + if (parents?.length) { + return this.checkPermissions(parents, resource, remaining); + } + return false; + } +} diff --git a/src/index.ts b/src/index.ts index 0564a37..f9da4ba 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,8 @@ export const VERSION = "1.0.0-alpha.0"; +export { Acl } from "./acl.js"; +export { Acl as default } from "./acl.js"; export { MemoryBackend } from "./backends/memory.js"; export type { MemoryTransaction } from "./backends/memory.js"; diff --git a/test/memory-acl.test.ts b/test/memory-acl.test.ts new file mode 100644 index 0000000..79aadb8 --- /dev/null +++ b/test/memory-acl.test.ts @@ -0,0 +1,4 @@ +import { MemoryBackend } from "../src/backends/memory.js"; +import { runAclSuite } from "./shared/acl-suite.js"; + +runAclSuite("Memory backend", () => new MemoryBackend()); diff --git a/test/shared/acl-suite.ts b/test/shared/acl-suite.ts new file mode 100644 index 0000000..5ac4660 --- /dev/null +++ b/test/shared/acl-suite.ts @@ -0,0 +1,513 @@ +import { beforeAll, describe, expect, it } from "vitest"; +import { Acl } from "../../src/acl.js"; +import type { Backend } from "../../src/types.js"; + +/** + * The shared behavioral suite, ported faithfully from the legacy + * test/tests.js + test/backendtests.js. It is stateful and ordered: each block + * builds on the previous one, exactly as the original suite did. + * + * Run it once per backend by passing a factory that yields a *clean* backend. + */ +export function runAclSuite(name: string, makeBackend: () => Promise | Backend): void { + describe(name, () => { + let backend: Backend; + let acl: Acl; + + beforeAll(async () => { + backend = await makeBackend(); + await backend.clean(); + acl = new Acl(backend); + }); + + // --- constructor ------------------------------------------------------- + describe("constructor", () => { + it("uses default bucket names", () => { + const a = new Acl(backend); + expect(a.buckets.meta).toBe("meta"); + expect(a.buckets.parents).toBe("parents"); + expect(a.buckets.permissions).toBe("permissions"); + expect(a.buckets.resources).toBe("resources"); + expect(a.buckets.roles).toBe("roles"); + expect(a.buckets.users).toBe("users"); + }); + + it("uses given bucket names", () => { + const a = new Acl(backend, undefined, { + buckets: { + meta: "Meta", + parents: "Parents", + permissions: "Permissions", + resources: "Resources", + roles: "Roles", + users: "Users", + }, + }); + expect(a.buckets.meta).toBe("Meta"); + expect(a.buckets.parents).toBe("Parents"); + expect(a.buckets.permissions).toBe("Permissions"); + expect(a.buckets.resources).toBe("Resources"); + expect(a.buckets.roles).toBe("Roles"); + expect(a.buckets.users).toBe("Users"); + }); + }); + + // --- allow / addUserRoles --------------------------------------------- + describe("allow", () => { + it("guest to view blogs", () => acl.allow("guest", "blogs", "view")); + it("guest to view forums", () => acl.allow("guest", "forums", "view")); + it("member to view/edit/delete blogs", () => + acl.allow("member", "blogs", ["edit", "view", "delete"])); + }); + + describe("Add user roles", () => { + it("joed => guest, jsmith => member, harry => admin, test@test.com => member", async () => { + await acl.addUserRoles("joed", "guest"); + await acl.addUserRoles("jsmith", "member"); + await acl.addUserRoles("harry", "admin"); + await acl.addUserRoles("test@test.com", "member"); + }); + + it("0 => guest, 1 => member, 2 => admin", async () => { + await acl.addUserRoles(0, "guest"); + await acl.addUserRoles(1, "member"); + await acl.addUserRoles(2, "admin"); + }); + }); + + describe("read User Roles", () => { + it("userRoles / hasRole", async () => { + await acl.addUserRoles("harry", "admin"); + expect(await acl.userRoles("harry")).toEqual(["admin"]); + expect(await acl.hasRole("harry", "admin")).toBe(true); + expect(await acl.hasRole("harry", "no role")).toBe(false); + }); + }); + + describe("read Role Users", () => { + it("roleUsers", async () => { + await acl.addUserRoles("harry", "admin"); + const users = await acl.roleUsers("admin"); + expect(users).toContain("harry"); + expect(users).not.toContain("invalid User"); + }); + }); + + describe("allow more", () => { + it("admin view/add/edit/delete users", () => + acl.allow("admin", "users", ["add", "edit", "view", "delete"])); + it("foo view/edit blogs", () => acl.allow("foo", "blogs", ["edit", "view"])); + it("bar to view/delete blogs", () => acl.allow("bar", "blogs", ["view", "delete"])); + }); + + describe("add role parents", () => { + it("add them", () => acl.addRoleParents("baz", ["foo", "bar"])); + }); + + describe("add user roles (baz)", () => { + it("string userId", () => acl.addUserRoles("james", "baz")); + it("numeric userId", () => acl.addUserRoles(3, "baz")); + }); + + describe("allow admin to do anything", () => { + it("add them", () => acl.allow("admin", ["blogs", "forums"], "*")); + }); + + describe("Arguments in one array", () => { + it("give role fumanchu an array of resources and permissions", () => + acl.allow([ + { + roles: "fumanchu", + allows: [ + { resources: "blogs", permissions: "get" }, + { resources: ["forums", "news"], permissions: ["get", "put", "delete"] }, + { + resources: ["/path/file/file1.txt", "/path/file/file2.txt"], + permissions: ["get", "put", "delete"], + }, + ], + }, + ])); + }); + + describe("Add fumanchu role to suzanne", () => { + it("string userId", () => acl.addUserRoles("suzanne", "fumanchu")); + it("numeric userId", () => acl.addUserRoles(4, "fumanchu")); + }); + + // --- allowance queries ------------------------------------------------- + describe("isAllowed", () => { + const allowed = (u: string | number, r: string, p: string | string[]) => + acl.isAllowed(u, r, p); + + it("joed view blogs", async () => expect(await allowed("joed", "blogs", "view")).toBe(true)); + it("userId=0 view blogs", async () => expect(await allowed(0, "blogs", "view")).toBe(true)); + it("joed view forums", async () => + expect(await allowed("joed", "forums", "view")).toBe(true)); + it("userId=0 view forums", async () => expect(await allowed(0, "forums", "view")).toBe(true)); + it("joed edit forums (no)", async () => + expect(await allowed("joed", "forums", "edit")).toBe(false)); + it("userId=0 edit forums (no)", async () => + expect(await allowed(0, "forums", "edit")).toBe(false)); + it("jsmith edit forums (no)", async () => + expect(await allowed("jsmith", "forums", "edit")).toBe(false)); + it("jsmith edit blogs", async () => + expect(await allowed("jsmith", "blogs", "edit")).toBe(true)); + it("test@test.com edit forums (no)", async () => + expect(await allowed("test@test.com", "forums", "edit")).toBe(false)); + it("test@test.com edit blogs", async () => + expect(await allowed("test@test.com", "blogs", "edit")).toBe(true)); + it("userId=1 edit blogs", async () => expect(await allowed(1, "blogs", "edit")).toBe(true)); + it("jsmith edit/view/clone blogs (no)", async () => + expect(await allowed("jsmith", "blogs", ["edit", "view", "clone"])).toBe(false)); + it("test@test.com edit/view/clone blogs (no)", async () => + expect(await allowed("test@test.com", "blogs", ["edit", "view", "clone"])).toBe(false)); + it("userId=1 edit/view/clone blogs (no)", async () => + expect(await allowed(1, "blogs", ["edit", "view", "clone"])).toBe(false)); + it("jsmith edit/clone blogs (no)", async () => + expect(await allowed("jsmith", "blogs", ["edit", "clone"])).toBe(false)); + it("james add blogs (no)", async () => + expect(await allowed("james", "blogs", "add")).toBe(false)); + it("userId=3 add blogs (no)", async () => + expect(await allowed(3, "blogs", "add")).toBe(false)); + it("suzanne add blogs (no)", async () => + expect(await allowed("suzanne", "blogs", "add")).toBe(false)); + it("userId=4 add blogs (no)", async () => + expect(await allowed(4, "blogs", "add")).toBe(false)); + it("suzanne get blogs", async () => + expect(await allowed("suzanne", "blogs", "get")).toBe(true)); + it("userId=4 get blogs", async () => expect(await allowed(4, "blogs", "get")).toBe(true)); + it("suzanne put/delete news", async () => + expect(await allowed("suzanne", "news", ["put", "delete"])).toBe(true)); + it("userId=4 put/delete news", async () => + expect(await allowed(4, "news", ["put", "delete"])).toBe(true)); + it("suzanne put/delete forums", async () => + expect(await allowed("suzanne", "forums", ["put", "delete"])).toBe(true)); + it("userId=4 put/delete forums", async () => + expect(await allowed(4, "forums", ["put", "delete"])).toBe(true)); + it("nobody view blogs (no)", async () => + expect(await allowed("nobody", "blogs", "view")).toBe(false)); + it("nobody view nothing (no)", async () => + expect(await allowed("nobody", "nothing", "view")).toBe(false)); + }); + + describe("allowedPermissions", () => { + it("james over blogs and forums", async () => { + const p = await acl.allowedPermissions("james", ["blogs", "forums"]); + expect(p).toHaveProperty("blogs"); + expect(p).toHaveProperty("forums"); + expect(p.blogs).toContain("edit"); + expect(p.blogs).toContain("delete"); + expect(p.blogs).toContain("view"); + expect(p.forums).toHaveLength(0); + }); + it("userId=3 over blogs and forums", async () => { + const p = await acl.allowedPermissions(3, ["blogs", "forums"]); + expect(p.blogs).toContain("edit"); + expect(p.blogs).toContain("delete"); + expect(p.blogs).toContain("view"); + expect(p.forums).toHaveLength(0); + }); + it("nonsense user over blogs and forums", async () => { + const p = await acl.allowedPermissions("nonsense", ["blogs", "forums"]); + expect(p.forums).toHaveLength(0); + expect(p.blogs).toHaveLength(0); + }); + }); + + // --- whatResources ----------------------------------------------------- + describe("whatResources queries", () => { + it('"bar" some rights', async () => { + const r = (await acl.whatResources("bar")) as Record; + expect(r.blogs).toContain("view"); + expect(r.blogs).toContain("delete"); + }); + it('"bar" view rights', async () => { + const r = (await acl.whatResources("bar", "view")) as string[]; + expect(r).toContain("blogs"); + }); + it('"fumanchu" some rights', async () => { + const r = (await acl.whatResources("fumanchu")) as Record; + expect(r.blogs).toContain("get"); + expect(r.forums).toContain("delete"); + expect(r.forums).toContain("get"); + expect(r.forums).toContain("put"); + expect(r.news).toContain("delete"); + expect(r.news).toContain("get"); + expect(r.news).toContain("put"); + expect(r["/path/file/file1.txt"]).toContain("delete"); + expect(r["/path/file/file2.txt"]).toContain("put"); + }); + it('"baz" some rights', async () => { + const r = (await acl.whatResources("baz")) as Record; + expect(r.blogs).toContain("view"); + expect(r.blogs).toContain("delete"); + expect(r.blogs).toContain("edit"); + }); + }); + + // --- removeAllow ------------------------------------------------------- + describe("removeAllow", () => { + it("remove get from blogs/forums for fumanchu", () => + acl.removeAllow("fumanchu", ["blogs", "forums"], "get")); + it("remove delete from news for fumanchu", () => + acl.removeAllow("fumanchu", "news", "delete")); + it("remove view from blogs for bar", () => acl.removeAllow("bar", "blogs", "view")); + }); + + describe("See if permissions were removed", () => { + it("fumanchu rights after removal", async () => { + const r = (await acl.whatResources("fumanchu")) as Record; + expect("blogs" in r).toBe(false); + expect(r).toHaveProperty("news"); + expect(r.news).toContain("get"); + expect(r.news).toContain("put"); + expect(r.news).not.toContain("delete"); + expect(r).toHaveProperty("forums"); + expect(r.forums).toContain("delete"); + expect(r.forums).toContain("put"); + }); + }); + + // --- removeRole -------------------------------------------------------- + describe("removeRole", () => { + it("remove fumanchu", () => acl.removeRole("fumanchu")); + it("remove member", () => acl.removeRole("member")); + it("remove foo", () => acl.removeRole("foo")); + }); + + describe("Was role removed?", () => { + it("fumanchu has no resources", async () => { + const r = (await acl.whatResources("fumanchu")) as Record; + expect(Object.keys(r)).toHaveLength(0); + }); + it("member has no resources", async () => { + const r = (await acl.whatResources("member")) as Record; + expect(Object.keys(r)).toHaveLength(0); + }); + it("jsmith over blogs and forums (empty)", async () => { + const p = await acl.allowedPermissions("jsmith", ["blogs", "forums"]); + expect(p.blogs).toHaveLength(0); + expect(p.forums).toHaveLength(0); + }); + it("test@test.com over blogs and forums (empty)", async () => { + const p = await acl.allowedPermissions("test@test.com", ["blogs", "forums"]); + expect(p.blogs).toHaveLength(0); + expect(p.forums).toHaveLength(0); + }); + it("james over blogs still has delete", async () => { + const p = await acl.allowedPermissions("james", "blogs"); + expect(p).toHaveProperty("blogs"); + expect(p.blogs).toContain("delete"); + }); + }); + + // --- RoleParentRemoval (self-contained) -------------------------------- + describe("RoleParentRemoval", () => { + beforeAll(async () => { + await acl.allow("parent1", "x", "read1"); + await acl.allow("parent2", "x", "read2"); + await acl.allow("parent3", "x", "read3"); + await acl.allow("parent4", "x", "read4"); + await acl.allow("parent5", "x", "read5"); + await acl.addRoleParents("child", ["parent1", "parent2", "parent3", "parent4", "parent5"]); + }); + + const childResources = async () => + (await acl.whatResources("child")) as Record; + + it("environment check", async () => { + const r = await childResources(); + expect(r.x).toHaveLength(5); + expect(r.x).toEqual(expect.arrayContaining(["read1", "read2", "read3", "read4", "read5"])); + }); + + it("returns a promise removing a specific parent role", () => + acl.removeRoleParents("child", "parentX")); + it("returns a promise removing multiple specific parent roles", () => + acl.removeRoleParents("child", ["parentX", "parentY"])); + + it('remove non-existent "parentX" keeps all 5', async () => { + await acl.removeRoleParents("child", "parentX"); + const r = await childResources(); + expect(r.x).toHaveLength(5); + }); + + it('remove "parent1" leaves 4', async () => { + await acl.removeRoleParents("child", "parent1"); + const r = await childResources(); + expect(r.x).toHaveLength(4); + expect(r.x).toEqual(expect.arrayContaining(["read2", "read3", "read4", "read5"])); + }); + + it('remove "parent2" & "parent3" leaves 2', async () => { + await acl.removeRoleParents("child", ["parent2", "parent3"]); + const r = await childResources(); + expect(r.x).toHaveLength(2); + expect(r.x).toEqual(expect.arrayContaining(["read4", "read5"])); + }); + + it("remove all parent roles", async () => { + await acl.removeRoleParents("child"); + const r = await childResources(); + expect(r).not.toHaveProperty("x"); + }); + + it("remove all parent roles again (idempotent)", async () => { + await acl.removeRoleParents("child"); + const r = await childResources(); + expect(r).not.toHaveProperty("x"); + }); + + it("remove specific parent when none remain", async () => { + await acl.removeRoleParents("child", "parent1"); + const r = await childResources(); + expect(r).not.toHaveProperty("x"); + }); + + it("remove all parent roles resolves", () => acl.removeRoleParents("child")); + }); + + // --- removeResource ---------------------------------------------------- + describe("removeResource", () => { + it("remove blogs", () => acl.removeResource("blogs")); + it("remove users", () => acl.removeResource("users")); + }); + + describe("allowedPermissions after resource removal", () => { + it("james over blogs (empty)", async () => { + const p = await acl.allowedPermissions("james", "blogs"); + expect(p).toHaveProperty("blogs"); + expect(p.blogs).toHaveLength(0); + }); + it("userId=4 over blogs (empty)", async () => { + const p = await acl.allowedPermissions(4, "blogs"); + expect(p).toHaveProperty("blogs"); + expect(p.blogs).toHaveLength(0); + }); + }); + + describe("whatResources after resource removal", () => { + it('"baz" has nothing', async () => { + const r = (await acl.whatResources("baz")) as Record; + expect(typeof r).toBe("object"); + expect(Object.keys(r)).toHaveLength(0); + }); + it('"admin" lost users and blogs', async () => { + const r = (await acl.whatResources("admin")) as Record; + expect("users" in r).toBe(false); + expect("blogs" in r).toBe(false); + }); + }); + + // --- removeUserRoles --------------------------------------------------- + describe("Remove user roles", () => { + it("guest from joed", () => acl.removeUserRoles("joed", "guest")); + it("guest from userId=0", () => acl.removeUserRoles(0, "guest")); + it("admin from harry", () => acl.removeUserRoles("harry", "admin")); + it("admin from userId=2", () => acl.removeUserRoles(2, "admin")); + }); + + describe("Were roles removed?", () => { + it("harry over forums and blogs (empty)", async () => { + const p = await acl.allowedPermissions("harry", ["forums", "blogs"]); + expect(typeof p).toBe("object"); + expect(p.forums).toHaveLength(0); + }); + }); + + // --- Github issue #55 -------------------------------------------------- + describe("Github issue #55: removeAllow removing all permissions", () => { + it("removeAllow removes only the named permission", async () => { + await acl.addUserRoles("jannette", "member"); + await acl.allow("member", "blogs", ["view", "update"]); + expect(await acl.isAllowed("jannette", "blogs", "view")).toBe(true); + await acl.removeAllow("member", "blogs", "update"); + expect(await acl.isAllowed("jannette", "blogs", "view")).toBe(true); + expect(await acl.isAllowed("jannette", "blogs", "update")).toBe(false); + await acl.removeAllow("member", "blogs", "view"); + expect(await acl.isAllowed("jannette", "blogs", "view")).toBe(false); + }); + }); + + // --- Github issue #32 -------------------------------------------------- + describe("Github issue #32: removeRole removes the entire allows document", () => { + it("add roles/resources/permissions", () => + acl.allow( + ["role1", "role2", "role3"], + ["res1", "res2", "res3"], + ["perm1", "perm2", "perm3"], + )); + + it("add user roles and parent roles", async () => { + await acl.addUserRoles("user1", "role1"); + await acl.addRoleParents("role1", "parentRole1"); + }); + + it("add user roles and parent roles (numeric)", async () => { + await acl.addUserRoles(1, "role1"); + await acl.addRoleParents("role1", "parentRole1"); + }); + + it("roles have permissions as assigned", async () => { + const r1 = (await acl.whatResources("role1")) as Record; + expect([...(r1.res1 ?? [])].sort()).toEqual(["perm1", "perm2", "perm3"]); + const r2 = (await acl.whatResources("role2")) as Record; + expect([...(r2.res1 ?? [])].sort()).toEqual(["perm1", "perm2", "perm3"]); + }); + + it('remove "role1"', () => acl.removeRole("role1")); + + it('"role1" empty, "role2" intact', async () => { + await acl.removeRole("role1"); + const r1 = (await acl.whatResources("role1")) as Record; + expect(Object.keys(r1)).toHaveLength(0); + const r2 = (await acl.whatResources("role2")) as Record; + expect([...(r2.res1 ?? [])].sort()).toEqual(["perm1", "perm2", "perm3"]); + }); + }); + + // --- backend unions conformance ---------------------------------------- + describe("backend unions", () => { + const testData: Record = { + key1: ["1", "2", "3"], + key2: ["3", "2", "4"], + key3: ["3", "4", "5"], + }; + const dataBuckets = ["bucket1", "bucket2"]; + + beforeAll(async () => { + if (!backend.unions) return; + await backend.clean(); + const t = backend.begin(); + for (const key of Object.keys(testData)) { + for (const bucket of dataBuckets) { + backend.add(t, bucket, key, testData[key] as string[]); + } + } + await backend.end(t); + }); + + it("responds with an appropriate map", async () => { + if (!backend.unions) return; + const result = await backend.unions(dataBuckets, Object.keys(testData)); + expect(result.bucket1?.sort()).toEqual(["1", "2", "3", "4", "5"]); + expect(result.bucket2?.sort()).toEqual(["1", "2", "3", "4", "5"]); + }); + + it("gets only the specified keys", async () => { + if (!backend.unions) return; + const result = await backend.unions(dataBuckets, ["key1"]); + expect(result.bucket1?.sort()).toEqual(["1", "2", "3"]); + expect(result.bucket2?.sort()).toEqual(["1", "2", "3"]); + }); + + it("gets only the specified buckets", async () => { + if (!backend.unions) return; + const result = await backend.unions(["bucket1"], ["key1"]); + expect(Object.keys(result)).toEqual(["bucket1"]); + expect(result.bucket1?.sort()).toEqual(["1", "2", "3"]); + }); + }); + }); +} From 3d8b6cd957abc78438d995414f9b01a55521cfd3 Mon Sep 17 00:00:00 2001 From: Nordin Date: Mon, 22 Jun 2026 11:32:49 +0800 Subject: [PATCH 08/13] =?UTF-8?q?feat:=20Phase=203b=20=E2=80=94=20Redis=20?= =?UTF-8?q?backend=20(redis=20v4,=20testcontainers)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port lib/redis-backend.js to TypeScript on the node-redis v4 promise API: - structural RedisClientLike/RedisMultiLike types keep the module decoupled from the driver (redis is an optional peer dep) - multi() as the transaction; sAdd/sRem/del queued, exec() on end - same prefix_bucket@key scheme as legacy; IDs coerced to strings Runs the full shared behavioral suite against a real Redis 7 container via testcontainers: 96/96 green. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/backends/redis.ts | 94 ++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 2 + test/redis-acl.test.ts | 22 ++++++++++ 3 files changed, 118 insertions(+) create mode 100644 src/backends/redis.ts create mode 100644 test/redis-acl.test.ts diff --git a/src/backends/redis.ts b/src/backends/redis.ts new file mode 100644 index 0000000..8239cc5 --- /dev/null +++ b/src/backends/redis.ts @@ -0,0 +1,94 @@ +import type { Backend, Key, OneOrMany, StoredValue } from "../types.js"; + +/** + * Minimal structural type for a node-redis v4+ client. Declared here (rather + * than importing from `redis`) so this module stays decoupled from the driver: + * any compatible client works, and consumers without `redis` installed can + * still type-check the rest of the package. + */ +export interface RedisClientLike { + multi(): RedisMultiLike; + sMembers(key: string): Promise; + sUnion(keys: string[]): Promise; + keys(pattern: string): Promise; + del(keys: string | string[]): Promise; +} + +export interface RedisMultiLike { + sAdd(key: string, members: string | string[]): RedisMultiLike; + sRem(key: string, members: string | string[]): RedisMultiLike; + del(keys: string | string[]): RedisMultiLike; + exec(): Promise; +} + +const toArray = (value: OneOrMany): T[] => (Array.isArray(value) ? value : [value]); +const toStr = (value: Key | StoredValue): string => `${value}`; + +/** Redis storage backend (node-redis v4+). Sets map directly to Redis sets. */ +export class RedisBackend implements Backend { + private readonly redis: RedisClientLike; + private readonly prefix: string; + + constructor(redis: RedisClientLike, prefix = "acl") { + this.redis = redis; + this.prefix = prefix; + } + + begin(): RedisMultiLike { + return this.redis.multi(); + } + + async end(transaction: RedisMultiLike): Promise { + await transaction.exec(); + } + + async clean(): Promise { + const keys = await this.redis.keys(`${this.prefix}*`); + if (keys.length) { + await this.redis.del(keys); + } + } + + get(bucket: string, key: Key): Promise { + return this.redis.sMembers(this.bucketKey(bucket, key)); + } + + async unions(buckets: string[], keys: Key[]): Promise> { + const result: Record = {}; + await Promise.all( + buckets.map(async (bucket) => { + result[bucket] = await this.redis.sUnion(this.bucketKeys(bucket, keys)); + }), + ); + return result; + } + + union(bucket: string, keys: Key[]): Promise { + return this.redis.sUnion(this.bucketKeys(bucket, keys)); + } + + add(transaction: RedisMultiLike, bucket: string, key: Key, values: OneOrMany): void { + transaction.sAdd(this.bucketKey(bucket, key), toArray(values).map(toStr)); + } + + del(transaction: RedisMultiLike, bucket: string, keys: OneOrMany): void { + transaction.del(toArray(keys).map((key) => this.bucketKey(bucket, key))); + } + + remove( + transaction: RedisMultiLike, + bucket: string, + key: Key, + values: OneOrMany, + ): void { + transaction.sRem(this.bucketKey(bucket, key), toArray(values).map(toStr)); + } + + private bucketKey(bucket: string, key: Key): string { + return `${this.prefix}_${bucket}@${key}`; + } + + private bucketKeys(bucket: string, keys: Key[]): string[] { + return keys.map((key) => this.bucketKey(bucket, key)); + } +} diff --git a/src/index.ts b/src/index.ts index f9da4ba..e09cfe2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,8 @@ export { Acl } from "./acl.js"; export { Acl as default } from "./acl.js"; export { MemoryBackend } from "./backends/memory.js"; export type { MemoryTransaction } from "./backends/memory.js"; +export { RedisBackend } from "./backends/redis.js"; +export type { RedisClientLike, RedisMultiLike } from "./backends/redis.js"; export type { AclOptions, diff --git a/test/redis-acl.test.ts b/test/redis-acl.test.ts new file mode 100644 index 0000000..ee6890f --- /dev/null +++ b/test/redis-acl.test.ts @@ -0,0 +1,22 @@ +import { type RedisClientType, createClient } from "redis"; +import { GenericContainer, type StartedTestContainer } from "testcontainers"; +import { afterAll } from "vitest"; +import { RedisBackend } from "../src/backends/redis.js"; +import { runAclSuite } from "./shared/acl-suite.js"; + +let container: StartedTestContainer | undefined; +let client: RedisClientType | undefined; + +runAclSuite("Redis backend", async () => { + container = await new GenericContainer("redis:7-alpine").withExposedPorts(6379).start(); + client = createClient({ + socket: { host: container.getHost(), port: container.getMappedPort(6379) }, + }); + await client.connect(); + return new RedisBackend(client); +}); + +afterAll(async () => { + await client?.quit(); + await container?.stop(); +}); From e05f5608e10ce37d2a12385ed9525cb5f54e9582 Mon Sep 17 00:00:00 2001 From: Nordin Date: Mon, 22 Jun 2026 11:39:27 +0800 Subject: [PATCH 09/13] =?UTF-8?q?feat:=20Phase=203c=20=E2=80=94=20MongoDB?= =?UTF-8?q?=20backend=20(mongodb=20v6,=20testcontainers)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port lib/mongodb-backend.js to TypeScript on the mongodb v4+ promise API: - doc-per-key set model with field-name encode/decode (dots -> %2E) - structural MongoDbLike/MongoCollectionLike types keep the driver an optional peer dep - options object (prefix/useSingle/useRawCollectionNames) replaces the legacy positional+quirky-boolean constructor; sane useRawCollectionNames - series transaction; updateOne/, , deleteMany, createIndex Runs the full shared suite against a real Mongo 6 container in BOTH default and useSingle modes: 192/192 green. Bumped vitest hookTimeout to tolerate cold image pulls. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/backends/mongodb.ts | 187 +++++++++++++++++++++++++++++++++++++++ src/index.ts | 7 ++ test/mongodb-acl.test.ts | 37 ++++++++ vitest.config.ts | 3 +- 4 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 src/backends/mongodb.ts create mode 100644 test/mongodb-acl.test.ts diff --git a/src/backends/mongodb.ts b/src/backends/mongodb.ts new file mode 100644 index 0000000..50e15b3 --- /dev/null +++ b/src/backends/mongodb.ts @@ -0,0 +1,187 @@ +import type { Backend, Key, OneOrMany, StoredValue } from "../types.js"; + +/** + * Minimal structural types for the mongodb v4+ driver, declared here so this + * module stays decoupled from the driver (mongodb is an optional peer dep). + */ +export interface MongoCollectionLike { + findOne(filter: object, options?: object): Promise | null>; + find(filter: object, options?: object): { toArray(): Promise[]> }; + updateOne(filter: object, update: object, options?: object): Promise; + deleteMany(filter: object): Promise; + createIndex(spec: object): Promise; + drop(): Promise; +} + +export interface MongoDbLike { + collection(name: string): MongoCollectionLike; + collections(): Promise; +} + +/** Each queued mutation is an async function; the transaction runs them in series. */ +export type MongoTransaction = Array<() => Promise>; + +export interface MongoDBBackendOptions { + prefix?: string; + /** Store every bucket in one collection (distinguished by `_bucketname`). */ + useSingle?: boolean; + /** Use bucket names verbatim as collection names (skip sanitization). */ + useRawCollectionNames?: boolean; +} + +/** Collection that holds meta + all `allows_*` buckets when `useSingle` is on. */ +const SINGLE_COLLECTION = "resources"; + +const toArray = (value: OneOrMany): T[] => (Array.isArray(value) ? value : [value]); + +/** Field names cannot contain dots; encode keys/values before storing. */ +function encode(text: Key | StoredValue): string | number { + if (typeof text === "string") { + return encodeURIComponent(text).replace(/\./g, "%2E"); + } + return text; +} + +const decode = (text: string): string => decodeURIComponent(text); + +/** + * MongoDB storage backend (mongodb v4+ driver). + * + * Each (bucket, key) pair is a document whose field names are the set members + * (stored as `{ : true }`), since MongoDB has no native set type. + */ +export class MongoDBBackend implements Backend { + private readonly db: MongoDbLike; + private readonly prefix: string; + private readonly useSingle: boolean; + private readonly useRawCollectionNames: boolean; + + constructor(db: MongoDbLike, options: MongoDBBackendOptions = {}) { + this.db = db; + this.prefix = options.prefix ?? ""; + this.useSingle = options.useSingle ?? false; + this.useRawCollectionNames = options.useRawCollectionNames ?? false; + } + + begin(): MongoTransaction { + return []; + } + + async end(transaction: MongoTransaction): Promise { + for (const mutation of transaction) { + await mutation(); + } + } + + async clean(): Promise { + const collections = await this.db.collections(); + await Promise.all(collections.map((collection) => collection.drop().catch(() => false))); + } + + async get(bucket: string, key: Key): Promise { + const collection = this.collection(bucket); + const doc = await collection.findOne(this.filter(bucket, encode(key)), { + projection: { _bucketname: 0 }, + }); + if (!doc) { + return []; + } + return this.members(doc); + } + + async union(bucket: string, keys: Key[]): Promise { + const collection = this.collection(bucket); + const filter = this.useSingle + ? { _bucketname: bucket, key: { $in: keys.map(encode) } } + : { key: { $in: keys.map(encode) } }; + + const docs = await collection.find(filter, { projection: { _bucketname: 0 } }).toArray(); + const union = new Set(); + for (const doc of docs) { + for (const member of this.members(doc)) { + union.add(member); + } + } + return [...union]; + } + + add( + transaction: MongoTransaction, + bucket: string, + key: Key, + values: OneOrMany, + ): void { + if (key === "key") { + throw new Error("Key name 'key' is not allowed."); + } + const filter = this.filter(bucket, encode(key)); + const doc = this.buildDoc(values); + + transaction.push(async () => { + await this.collection(bucket).updateOne(filter, { $set: doc }, { upsert: true }); + }); + transaction.push(async () => { + await this.collection(bucket).createIndex({ _bucketname: 1, key: 1 }); + }); + } + + del(transaction: MongoTransaction, bucket: string, keys: OneOrMany): void { + const encoded = toArray(keys).map(encode); + const filter = this.useSingle + ? { _bucketname: bucket, key: { $in: encoded } } + : { key: { $in: encoded } }; + + transaction.push(async () => { + await this.collection(bucket).deleteMany(filter); + }); + } + + remove( + transaction: MongoTransaction, + bucket: string, + key: Key, + values: OneOrMany, + ): void { + const filter = this.filter(bucket, encode(key)); + const doc = this.buildDoc(values); + + transaction.push(async () => { + await this.collection(bucket).updateOne(filter, { $unset: doc }, { upsert: true }); + }); + } + + // --- helpers --------------------------------------------------------------- + + private collection(bucket: string): MongoCollectionLike { + const name = this.useSingle ? SINGLE_COLLECTION : bucket; + return this.db.collection(this.prefix + this.sanitizeCollectionName(name)); + } + + private filter(bucket: string, key: string | number): Record { + return this.useSingle ? { _bucketname: bucket, key } : { key }; + } + + /** Build a `{ : true }` doc from one or many values. */ + private buildDoc(values: OneOrMany): Record { + const doc: Record = {}; + for (const value of toArray(values)) { + doc[`${encode(value)}`] = true; + } + return doc; + } + + /** Decode a stored document's field names back into set members. */ + private members(doc: Record): string[] { + return Object.keys(doc) + .filter((field) => field !== "key" && field !== "_id") + .map(decode); + } + + private sanitizeCollectionName(name: string): string { + if (this.useRawCollectionNames) { + return name; + } + // Collection names cannot contain slashes or whitespace. + return decodeURIComponent(name).replace(/[/\s]/g, "_"); + } +} diff --git a/src/index.ts b/src/index.ts index e09cfe2..4846b1e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,13 @@ export { MemoryBackend } from "./backends/memory.js"; export type { MemoryTransaction } from "./backends/memory.js"; export { RedisBackend } from "./backends/redis.js"; export type { RedisClientLike, RedisMultiLike } from "./backends/redis.js"; +export { MongoDBBackend } from "./backends/mongodb.js"; +export type { + MongoCollectionLike, + MongoDbLike, + MongoDBBackendOptions, + MongoTransaction, +} from "./backends/mongodb.js"; export type { AclOptions, diff --git a/test/mongodb-acl.test.ts b/test/mongodb-acl.test.ts new file mode 100644 index 0000000..8ecb618 --- /dev/null +++ b/test/mongodb-acl.test.ts @@ -0,0 +1,37 @@ +import { type Db, MongoClient } from "mongodb"; +import { GenericContainer, type StartedTestContainer } from "testcontainers"; +import { afterAll } from "vitest"; +import { MongoDBBackend, type MongoDbLike } from "../src/backends/mongodb.js"; +import { runAclSuite } from "./shared/acl-suite.js"; + +let container: StartedTestContainer | undefined; +let client: MongoClient | undefined; + +/** Start the shared Mongo container once and reuse it for both modes. */ +async function connect(): Promise { + if (!container) { + container = await new GenericContainer("mongo:6").withExposedPorts(27017).start(); + const uri = `mongodb://${container.getHost()}:${container.getMappedPort(27017)}`; + client = new MongoClient(uri); + await client.connect(); + } + // biome-ignore lint/style/noNonNullAssertion: client is set above + return client!.db("acltest"); +} + +runAclSuite("MongoDB backend (default)", async () => { + const db = await connect(); + await db.dropDatabase(); + return new MongoDBBackend(db as unknown as MongoDbLike, { prefix: "acl_" }); +}); + +runAclSuite("MongoDB backend (useSingle)", async () => { + const db = await connect(); + await db.dropDatabase(); + return new MongoDBBackend(db as unknown as MongoDbLike, { prefix: "acl_", useSingle: true }); +}); + +afterAll(async () => { + await client?.close(); + await container?.stop(); +}); diff --git a/vitest.config.ts b/vitest.config.ts index ade1920..4b2e3ee 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -5,6 +5,7 @@ export default defineConfig({ include: ["test/**/*.test.ts"], // Integration tests spin up Docker containers via testcontainers and need headroom. testTimeout: 60_000, - hookTimeout: 120_000, + // Generous: a cold testcontainers image pull (Mongo/Redis) can take minutes. + hookTimeout: 300_000, }, }); From 7bba85c04335ee88b855cc863794286d76338f34 Mon Sep 17 00:00:00 2001 From: Nordin Date: Mon, 22 Jun 2026 11:41:20 +0800 Subject: [PATCH 10/13] =?UTF-8?q?feat:=20Phase=205=20=E2=80=94=20framework?= =?UTF-8?q?-agnostic=20Express=20middleware?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port the legacy acl.middleware + errorHandler to TypeScript: - aclMiddleware(acl, numPathComponents?, userId?, actions?) and acl.middleware(...) convenience method (familiar API preserved) - structural AclRequest/AclResponse types — no hard express dependency; works with Express/Fastify-style handlers - promise-based isAllowed; HttpError class; aclErrorHandler(json|html|text) - fixes the legacy userId=0 falsy-fallback quirk Adds 7 unit tests (mocked req/res/next): allow/deny, 401/403, session + resolver userId, numPathComponents, error handler rendering. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/acl.ts | 14 ++++ src/index.ts | 8 +++ src/middleware.ts | 123 +++++++++++++++++++++++++++++++++++ test/unit/middleware.test.ts | 99 ++++++++++++++++++++++++++++ 4 files changed, 244 insertions(+) create mode 100644 src/middleware.ts create mode 100644 test/unit/middleware.test.ts diff --git a/src/acl.ts b/src/acl.ts index 25b0383..226521a 100644 --- a/src/acl.ts +++ b/src/acl.ts @@ -1,3 +1,4 @@ +import { type AclMiddleware, type UserIdResolver, aclMiddleware } from "./middleware.js"; import type { AclOptions, AllowRule, @@ -347,6 +348,19 @@ export class Acl { return result; } + /** + * Express-style middleware that authorizes the current request against this + * Acl. See {@link aclMiddleware} for parameter semantics. Pair with + * {@link aclErrorHandler} to render the resulting 401/403 errors. + */ + middleware( + numPathComponents?: number, + userId?: UserId | UserIdResolver, + actions?: OneOrMany, + ): AclMiddleware { + return aclMiddleware(this as unknown as Acl, numPathComponents, userId, actions); + } + // --------------------------------------------------------------------------- // Private helpers // --------------------------------------------------------------------------- diff --git a/src/index.ts b/src/index.ts index 4846b1e..0a62e78 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,14 @@ export const VERSION = "1.0.0-alpha.0"; export { Acl } from "./acl.js"; export { Acl as default } from "./acl.js"; +export { aclErrorHandler, aclMiddleware, HttpError } from "./middleware.js"; +export type { + AclMiddleware, + AclNext, + AclRequest, + AclResponse, + UserIdResolver, +} from "./middleware.js"; export { MemoryBackend } from "./backends/memory.js"; export type { MemoryTransaction } from "./backends/memory.js"; export { RedisBackend } from "./backends/redis.js"; diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..dfde24c --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,123 @@ +import type { Acl } from "./acl.js"; +import type { OneOrMany, Permission, UserId } from "./types.js"; + +/** + * Minimal structural request shape the middleware needs. Compatible with + * Express (and most HTTP frameworks) without depending on their types. + */ +export interface AclRequest { + originalUrl?: string; + url?: string; + method: string; + session?: { userId?: UserId }; + user?: { id?: UserId }; +} + +export interface AclResponse { + status(code: number): { + end(body?: string): unknown; + json(body: unknown): unknown; + send(body: unknown): unknown; + }; +} + +export type AclNext = (err?: unknown) => void; + +export type AclMiddleware = (req: AclRequest, res: AclResponse, next: AclNext) => void; + +/** Resolves the userId for a request when not supplied statically. */ +export type UserIdResolver = (req: AclRequest, res: AclResponse) => UserId | undefined; + +/** Error thrown by the middleware; pair it with {@link aclErrorHandler}. */ +export class HttpError extends Error { + readonly errorCode: number; + override readonly name = "HttpError"; + + constructor(errorCode: number, message: string) { + super(message); + this.errorCode = errorCode; + } +} + +/** + * Build an Express-style middleware that authorizes the current request. + * + * @param acl The Acl instance to check against. + * @param numPathComponents How many leading URL path components form the + * resource name (default: the whole path). + * @param userId A static user id, or a resolver `(req, res) => id`. + * Defaults to `req.session.userId` / `req.user.id`. + * @param actions Permission(s) to check (default: the HTTP method). + */ +export function aclMiddleware( + acl: Acl, + numPathComponents?: number, + userId?: UserId | UserIdResolver, + actions?: OneOrMany, +): AclMiddleware { + return (req, res, next) => { + let resolvedUserId: UserId | undefined; + if (typeof userId === "function") { + resolvedUserId = userId(req, res); + } else if (userId !== undefined) { + resolvedUserId = userId; + } else { + resolvedUserId = req.session?.userId ?? req.user?.id; + } + + if (resolvedUserId === undefined || resolvedUserId === null) { + next(new HttpError(401, "User not authenticated")); + return; + } + + const fullUrl = (req.originalUrl ?? req.url ?? "").split("?")[0] ?? ""; + const resource = numPathComponents + ? fullUrl + .split("/") + .slice(0, numPathComponents + 1) + .join("/") + : fullUrl; + + const resolvedActions = actions ?? req.method.toLowerCase(); + + acl.logger?.debug(`Requesting ${resolvedActions} on ${resource} by user ${resolvedUserId}`); + + acl.isAllowed(resolvedUserId, resource, resolvedActions).then( + (allowed) => { + if (allowed) { + acl.logger?.debug(`Allowed ${resolvedActions} on ${resource} by user ${resolvedUserId}`); + next(); + } else { + acl.logger?.debug( + `Not allowed ${resolvedActions} on ${resource} by user ${resolvedUserId}`, + ); + next(new HttpError(403, "Insufficient permissions to access resource")); + } + }, + () => next(new Error("Error checking permissions to access resource")), + ); + }; +} + +/** + * Express error handler that renders {@link HttpError}s. Pass `"json"` or + * `"html"` to choose the response format (defaults to plain text). + */ +export function aclErrorHandler( + contentType?: "json" | "html", +): (err: unknown, req: AclRequest, res: AclResponse, next: AclNext) => void { + return (err, _req, res, next) => { + if (!(err instanceof HttpError) || !err.errorCode) { + next(err); + return; + } + const response = res.status(err.errorCode); + if (contentType === "json") { + response.json({ message: err.message }); + } else if (contentType === "html") { + response.send(err.message); + } else { + response.end(err.message); + } + }; +} diff --git a/test/unit/middleware.test.ts b/test/unit/middleware.test.ts new file mode 100644 index 0000000..f9816d4 --- /dev/null +++ b/test/unit/middleware.test.ts @@ -0,0 +1,99 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { Acl } from "../../src/acl.js"; +import { MemoryBackend } from "../../src/backends/memory.js"; +import { + type AclRequest, + type AclResponse, + HttpError, + aclErrorHandler, +} from "../../src/middleware.js"; + +describe("middleware", () => { + let acl: Acl; + + beforeEach(async () => { + acl = new Acl(new MemoryBackend()); + await acl.allow("admin", "/blogs", "get"); + await acl.addUserRoles("alice", "admin"); + }); + + const makeRes = (): AclResponse => ({ + status: vi.fn(() => ({ end: vi.fn(), json: vi.fn(), send: vi.fn() })), + }); + + it("calls next() with no error when allowed", async () => { + const mw = acl.middleware(undefined, "alice"); + const req = { originalUrl: "/blogs", method: "GET" } as AclRequest; + const next = vi.fn(); + mw(req, makeRes(), next); + await vi.waitFor(() => expect(next).toHaveBeenCalled()); + expect(next).toHaveBeenCalledWith(); + }); + + it("passes a 403 HttpError when not allowed", async () => { + const mw = acl.middleware(undefined, "alice"); + const req = { originalUrl: "/blogs", method: "DELETE" } as AclRequest; + const next = vi.fn(); + mw(req, makeRes(), next); + await vi.waitFor(() => expect(next).toHaveBeenCalled()); + const err = next.mock.calls[0]?.[0] as HttpError; + expect(err).toBeInstanceOf(HttpError); + expect(err.errorCode).toBe(403); + }); + + it("passes a 401 HttpError when no user can be resolved", () => { + const mw = acl.middleware(); + const req = { originalUrl: "/blogs", method: "GET" } as AclRequest; + const next = vi.fn(); + mw(req, makeRes(), next); + const err = next.mock.calls[0]?.[0] as HttpError; + expect(err).toBeInstanceOf(HttpError); + expect(err.errorCode).toBe(401); + }); + + it("resolves userId from req.session and a resolver function", async () => { + const sessionMw = acl.middleware(); + const req = { + originalUrl: "/blogs", + method: "GET", + session: { userId: "alice" }, + } as AclRequest; + const next1 = vi.fn(); + sessionMw(req, makeRes(), next1); + await vi.waitFor(() => expect(next1).toHaveBeenCalledWith()); + + const resolverMw = acl.middleware(undefined, () => "alice"); + const next2 = vi.fn(); + resolverMw({ originalUrl: "/blogs", method: "GET" } as AclRequest, makeRes(), next2); + await vi.waitFor(() => expect(next2).toHaveBeenCalledWith()); + }); + + it("limits the resource to numPathComponents", async () => { + await acl.allow("admin", "/blogs/123", "get"); + const mw = acl.middleware(1, "alice"); + // /blogs/123/comments -> resource "/blogs/123" + const req = { originalUrl: "/blogs/123/comments", method: "GET" } as AclRequest; + const next = vi.fn(); + mw(req, makeRes(), next); + await vi.waitFor(() => expect(next).toHaveBeenCalledWith()); + }); + + describe("aclErrorHandler", () => { + it("renders HttpError with the right status (plain text)", () => { + const end = vi.fn(); + const res = { status: vi.fn(() => ({ end, json: vi.fn(), send: vi.fn() })) } as AclResponse; + const next = vi.fn(); + aclErrorHandler()(new HttpError(403, "nope"), {} as AclRequest, res, next); + expect(res.status).toHaveBeenCalledWith(403); + expect(end).toHaveBeenCalledWith("nope"); + expect(next).not.toHaveBeenCalled(); + }); + + it("passes through non-HttpErrors", () => { + const next = vi.fn(); + const err = new Error("other"); + aclErrorHandler()(err, {} as AclRequest, makeRes(), next); + expect(next).toHaveBeenCalledWith(err); + }); + }); +}); From 546d4db3184cbb7252cf18b68536e7aa105e051d Mon Sep 17 00:00:00 2001 From: Nordin Date: Mon, 22 Jun 2026 11:42:26 +0800 Subject: [PATCH 11/13] =?UTF-8?q?chore:=20Phase=206=20=E2=80=94=20remove?= =?UTF-8?q?=20legacy=20ES5=20sources?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delete the fully-ported legacy implementation (lib/*.js, index.js) and the legacy mocha suite (test/runner.js, tests.js, backendtests.js). All behavior is preserved by the TypeScript rewrite under src/ and the ported vitest suites. Drop now-obsolete biome/tsconfig ignores for them. Co-Authored-By: Claude Opus 4.8 (1M context) --- biome.json | 2 +- index.js | 10 - lib/acl.js | 928 ------------------------------- lib/backend.js | 88 --- lib/contract.js | 123 ----- lib/memory-backend.js | 182 ------ lib/mongodb-backend.js | 280 ---------- lib/redis-backend.js | 182 ------ test/backendtests.js | 71 --- test/runner.js | 84 --- test/tests.js | 1195 ---------------------------------------- tsconfig.json | 2 +- 12 files changed, 2 insertions(+), 3145 deletions(-) delete mode 100644 index.js delete mode 100644 lib/acl.js delete mode 100644 lib/backend.js delete mode 100644 lib/contract.js delete mode 100644 lib/memory-backend.js delete mode 100644 lib/mongodb-backend.js delete mode 100644 lib/redis-backend.js delete mode 100644 test/backendtests.js delete mode 100644 test/runner.js delete mode 100644 test/tests.js diff --git a/biome.json b/biome.json index bedaecb..0df05eb 100644 --- a/biome.json +++ b/biome.json @@ -6,7 +6,7 @@ "useIgnoreFile": true }, "files": { - "ignore": ["dist", "node_modules", "*.json", "index.js", "lib/**", "test/*.js"] + "ignore": ["dist", "node_modules", "*.json"] }, "formatter": { "enabled": true, diff --git a/index.js b/index.js deleted file mode 100644 index ea44399..0000000 --- a/index.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = require('./lib/acl.js'); -module.exports.__defineGetter__('redisBackend', function(){ - return require('./lib/redis-backend.js'); -}); -module.exports.__defineGetter__('memoryBackend', function(){ - return require('./lib/memory-backend.js'); -}); -module.exports.__defineGetter__('mongodbBackend', function(){ - return require('./lib/mongodb-backend.js'); -}); \ No newline at end of file diff --git a/lib/acl.js b/lib/acl.js deleted file mode 100644 index c957558..0000000 --- a/lib/acl.js +++ /dev/null @@ -1,928 +0,0 @@ -/** - ACL System inspired on Zend_ACL. - - All functions accept strings, objects or arrays unless specified otherwise. - - '*' is used to express 'all' - - Database structure in Redis (using default prefix 'acl') - - Users: - - acl_roles_{userid} = set(roles) - - Roles: - - acl_roles = {roleNames} // Used to remove all the permissions associated to ONE resource. - - acl_parents_{roleName} = set(parents) - acl_resources_{roleName} = set(resourceNames) - - Permissions: - - acl_allows_{resourceName}_{roleName} = set(permissions) - - Note: user ids, role names and resource names are all case sensitive. - - Roadmap: - - Add support for locking resources. If a user has roles that gives him permissions to lock - a resource, then he can get exclusive write operation on the locked resource. - This lock should expire if the resource has not been accessed in some time. -*/ -"use strict"; - -var _ = require('lodash'), - util = require('util'), -bluebird = require('bluebird'), -contract = require('./contract'); - -contract.debug = true; - -var Acl = function (backend, logger, options){ - contract(arguments) - .params('object') - .params('object','object') - .params('object','object', 'object') - .end(); - - options = _.extend({ - buckets: { - meta: 'meta', - parents: 'parents', - permissions: 'permissions', - resources: 'resources', - roles: 'roles', - users: 'users' - } - }, options); - - this.logger = logger; - this.backend = backend; - this.options = options; - - // Promisify async methods - backend.endAsync = bluebird.promisify(backend.end); - backend.getAsync = bluebird.promisify(backend.get); - backend.cleanAsync = bluebird.promisify(backend.clean); - backend.unionAsync = bluebird.promisify(backend.union); - if (backend.unions) { - backend.unionsAsync = bluebird.promisify(backend.unions); - } -}; - -/** - addUserRoles( userId, roles, function(err) ) - - Adds roles to a given user id. - - @param {String|Number} User id. - @param {String|Array} Role(s) to add to the user id. - @param {Function} Callback called when finished. - @return {Promise} Promise resolved when finished -*/ -Acl.prototype.addUserRoles = function(userId, roles, cb){ - contract(arguments) - .params('string|number','string|array','function') - .params('string|number','string|array') - .end(); - - var transaction = this.backend.begin(); - this.backend.add(transaction, this.options.buckets.meta, 'users', userId); - this.backend.add(transaction, this.options.buckets.users, userId, roles); - - if (Array.isArray(roles)) { - var _this = this; - - roles.forEach(function(role) { - _this.backend.add(transaction, _this.options.buckets.roles, role, userId); - }); - } - else { - this.backend.add(transaction, this.options.buckets.roles, roles, userId); - } - - return this.backend.endAsync(transaction).nodeify(cb); -}; - -/** - removeUserRoles( userId, roles, function(err) ) - - Remove roles from a given user. - - @param {String|Number} User id. - @param {String|Array} Role(s) to remove to the user id. - @param {Function} Callback called when finished. - @return {Promise} Promise resolved when finished -*/ -Acl.prototype.removeUserRoles = function(userId, roles, cb){ - contract(arguments) - .params('string|number','string|array','function') - .params('string|number','string|array') - .end(); - - var transaction = this.backend.begin(); - this.backend.remove(transaction, this.options.buckets.users, userId, roles); - - if (Array.isArray(roles)) { - var _this = this; - - roles.forEach(function(role) { - _this.backend.remove(transaction, _this.options.buckets.roles, role, userId); - }); - } - else { - this.backend.remove(transaction, this.options.buckets.roles, roles, userId); - } - - return this.backend.endAsync(transaction).nodeify(cb); -}; - -/** - userRoles( userId, function(err, roles) ) - - Return all the roles from a given user. - - @param {String|Number} User id. - @param {Function} Callback called when finished. - @return {Promise} Promise resolved with an array of user roles -*/ -Acl.prototype.userRoles = function(userId, cb){ - return this.backend.getAsync(this.options.buckets.users, userId).nodeify(cb); -}; - -/** - roleUsers( roleName, function(err, users) ) - - Return all users who has a given role. - @param {String|Number} rolename. - @param {Function} Callback called when finished. - @return {Promise} Promise resolved with an array of users - */ -Acl.prototype.roleUsers = function(roleName, cb){ - return this.backend.getAsync(this.options.buckets.roles, roleName).nodeify(cb); -}; - -/** - hasRole( userId, rolename, function(err, is_in_role) ) - - Return boolean whether user is in the role - - @param {String|Number} User id. - @param {String|Number} rolename. - @param {Function} Callback called when finished. - @return {Promise} Promise resolved with boolean of whether user is in role -*/ -Acl.prototype.hasRole = function(userId, rolename, cb){ - return this.userRoles(userId).then(function(roles){ - return roles.indexOf(rolename) != -1; - }).nodeify(cb); -}; - -/** - addRoleParents( role, parents, function(err) ) - - Adds a parent or parent list to role. - - @param {String} Child role. - @param {String|Array} Parent role(s) to be added. - @param {Function} Callback called when finished. - @return {Promise} Promise resolved when finished -*/ -Acl.prototype.addRoleParents = function(role, parents, cb){ - contract(arguments) - .params('string|number','string|array','function') - .params('string|number','string|array') - .end(); - - var transaction = this.backend.begin(); - this.backend.add(transaction, this.options.buckets.meta, 'roles', role); - this.backend.add(transaction, this.options.buckets.parents, role, parents); - return this.backend.endAsync(transaction).nodeify(cb); -}; - -/** - removeRoleParents( role, parents, function(err) ) - - Removes a parent or parent list from role. - - If `parents` is not specified, removes all parents. - - @param {String} Child role. - @param {String|Array} Parent role(s) to be removed [optional]. - @param {Function} Callback called when finished [optional]. - @return {Promise} Promise resolved when finished. -*/ -Acl.prototype.removeRoleParents = function(role, parents, cb){ - contract(arguments) - .params('string', 'string|array', 'function') - .params('string', 'string|array') - .params('string', 'function') - .params('string') - .end(); - - if (!cb && _.isFunction(parents)) { - cb = parents; - parents = null; - } - - var transaction = this.backend.begin(); - if (parents) { - this.backend.remove(transaction, this.options.buckets.parents, role, parents); - } else { - this.backend.del(transaction, this.options.buckets.parents, role); - } - return this.backend.endAsync(transaction).nodeify(cb); -}; - -/** - removeRole( role, function(err) ) - - Removes a role from the system. - - @param {String} Role to be removed - @param {Function} Callback called when finished. -*/ -Acl.prototype.removeRole = function(role, cb){ - contract(arguments) - .params('string','function') - .params('string').end(); - - var _this = this; - // Note that this is not fully transactional. - return this.backend.getAsync(this.options.buckets.resources, role).then(function(resources){ - var transaction = _this.backend.begin(); - - resources.forEach(function(resource){ - var bucket = allowsBucket(resource); - _this.backend.del(transaction, bucket, role); - }); - - _this.backend.del(transaction, _this.options.buckets.resources, role); - _this.backend.del(transaction, _this.options.buckets.parents, role); - _this.backend.del(transaction, _this.options.buckets.roles, role) - _this.backend.remove(transaction, _this.options.buckets.meta, 'roles', role); - - // `users` collection keeps the removed role - // because we don't know what users have `role` assigned. - return _this.backend.endAsync(transaction); - }).nodeify(cb); -}; - -/** - removeResource( resource, function(err) ) - - Removes a resource from the system - - @param {String} Resource to be removed - @param {Function} Callback called when finished. - @return {Promise} Promise resolved when finished -*/ -Acl.prototype.removeResource = function(resource, cb){ - contract(arguments) - .params('string', 'function') - .params('string') - .end(); - - var _this = this; - return this.backend.getAsync(this.options.buckets.meta, 'roles').then(function(roles){ - var transaction = _this.backend.begin(); - _this.backend.del(transaction, allowsBucket(resource), roles); - roles.forEach(function(role){ - _this.backend.remove(transaction, _this.options.buckets.resources, role, resource); - }) - return _this.backend.endAsync(transaction); - }).nodeify(cb) -}; - -/** - allow( roles, resources, permissions, function(err) ) - - Adds the given permissions to the given roles over the given resources. - - @param {String|Array} role(s) to add permissions to. - @param {String|Array} resource(s) to add permisisons to. - @param {String|Array} permission(s) to add to the roles over the resources. - @param {Function} Callback called when finished. - - allow( permissionsArray, function(err) ) - - @param {Array} Array with objects expressing what permissions to give. - - [{roles:{String|Array}, allows:[{resources:{String|Array}, permissions:{String|Array}]] - - @param {Function} Callback called when finished. - @return {Promise} Promise resolved when finished -*/ -Acl.prototype.allow = function(roles, resources, permissions, cb){ - contract(arguments) - .params('string|array','string|array','string|array','function') - .params('string|array','string|array','string|array') - .params('array','function') - .params('array') - .end(); - - if((arguments.length == 1) || ((arguments.length===2)&&_.isObject(roles)&&_.isFunction(resources))){ - return this._allowEx(roles).nodeify(resources); - }else{ - var _this = this; - - roles = makeArray(roles); - resources = makeArray(resources); - - var transaction = _this.backend.begin(); - - _this.backend.add(transaction, _this.options.buckets.meta, 'roles', roles); - - resources.forEach(function(resource){ - roles.forEach(function(role){ - _this.backend.add(transaction, allowsBucket(resource), role, permissions); - }); - }); - - roles.forEach(function(role){ - _this.backend.add(transaction, _this.options.buckets.resources, role, resources); - }); - - return _this.backend.endAsync(transaction).nodeify(cb); - } -}; - - -Acl.prototype.removeAllow = function(role, resources, permissions, cb){ - contract(arguments) - .params('string','string|array','string|array','function') - .params('string','string|array','string|array') - .params('string','string|array','function') - .params('string','string|array') - .end(); - - resources = makeArray(resources); - if(cb || (permissions && !_.isFunction(permissions))){ - permissions = makeArray(permissions); - }else { - cb = permissions; - permissions = null; - } - - return this.removePermissions(role, resources, permissions, cb); -} - -/** - removePermissions( role, resources, permissions) - - Remove permissions from the given roles owned by the given role. - - Note: we loose atomicity when removing empty role_resources. - - @param {String} - @param {String|Array} - @param {String|Array} -*/ -Acl.prototype.removePermissions = function(role, resources, permissions, cb){ - - var _this = this; - - var transaction = _this.backend.begin(); - resources.forEach(function(resource){ - var bucket = allowsBucket(resource); - if(permissions){ - _this.backend.remove(transaction, bucket, role, permissions); - }else{ - _this.backend.del(transaction, bucket, role); - _this.backend.remove(transaction, _this.options.buckets.resources, role, resource); - } - }); - - // Remove resource from role if no rights for that role exists. - // Not fully atomic... - return _this.backend.endAsync(transaction).then(function(){ - var transaction = _this.backend.begin(); - return bluebird.all(resources.map(function(resource){ - var bucket = allowsBucket(resource); - return _this.backend.getAsync(bucket, role).then(function(permissions){ - if(permissions.length==0){ - _this.backend.remove(transaction, _this.options.buckets.resources, role, resource); - } - }); - })).then(function(){ - return _this.backend.endAsync(transaction); - }); - }).nodeify(cb); -}; - -/** - allowedPermissions( userId, resources, function(err, obj) ) - - Returns all the allowable permissions a given user have to - access the given resources. - - It returns an array of objects where every object maps a - resource name to a list of permissions for that resource. - - @param {String|Number} User id. - @param {String|Array} resource(s) to ask permissions for. - @param {Function} Callback called when finished. -*/ -Acl.prototype.allowedPermissions = function(userId, resources, cb){ - if (!userId) - return cb(null, {}); - - contract(arguments) - .params('string|number', 'string|array', 'function') - .params('string|number', 'string|array') - .end(); - - if (this.backend.unionsAsync) { - return this.optimizedAllowedPermissions(userId, resources, cb); - } - - var _this = this; - resources = makeArray(resources); - - return _this.userRoles(userId).then(function(roles){ - var result = {}; - return bluebird.all(resources.map(function(resource){ - return _this._resourcePermissions(roles, resource).then(function(permissions){ - result[resource] = permissions; - }); - })).then(function(){ - return result; - }); - }).nodeify(cb); -}; - -/** - optimizedAllowedPermissions( userId, resources, function(err, obj) ) - - Returns all the allowable permissions a given user have to - access the given resources. - - It returns a map of resource name to a list of permissions for that resource. - - This is the same as allowedPermissions, it just takes advantage of the unions - function if available to reduce the number of backend queries. - - @param {String|Number} User id. - @param {String|Array} resource(s) to ask permissions for. - @param {Function} Callback called when finished. -*/ -Acl.prototype.optimizedAllowedPermissions = function(userId, resources, cb){ - if (!userId) { - return cb(null, {}); - } - - contract(arguments) - .params('string|number', 'string|array', 'function|undefined') - .params('string|number', 'string|array') - .end(); - - resources = makeArray(resources); - var self = this; - - return this._allUserRoles(userId).then(function(roles) { - var buckets = resources.map(allowsBucket); - if (roles.length === 0) { - var emptyResult = {}; - buckets.forEach(function(bucket) { - emptyResult[bucket] = []; - }); - return bluebird.resolve(emptyResult); - } - - return self.backend.unionsAsync(buckets, roles); - }).then(function(response) { - var result = {}; - Object.keys(response).forEach(function(bucket) { - result[keyFromAllowsBucket(bucket)] = response[bucket]; - }); - - return result; - }).nodeify(cb); -}; - -/** - isAllowed( userId, resource, permissions, function(err, allowed) ) - - Checks if the given user is allowed to access the resource for the given - permissions (note: it must fulfill all the permissions). - - @param {String|Number} User id. - @param {String|Array} resource(s) to ask permissions for. - @param {String|Array} asked permissions. - @param {Function} Callback called wish the result. -*/ -Acl.prototype.isAllowed = function(userId, resource, permissions, cb){ - contract(arguments) - .params('string|number', 'string', 'string|array', 'function') - .params('string|number', 'string', 'string|array') - .end(); - - var _this = this; - - return this.backend.getAsync(this.options.buckets.users, userId).then(function(roles){ - if(roles.length){ - return _this.areAnyRolesAllowed(roles, resource, permissions); - }else{ - return false; - } - }).nodeify(cb); -}; - -/** - areAnyRolesAllowed( roles, resource, permissions, function(err, allowed) ) - - Returns true if any of the given roles have the right permissions. - - @param {String|Array} Role(s) to check the permissions for. - @param {String} resource(s) to ask permissions for. - @param {String|Array} asked permissions. - @param {Function} Callback called with the result. -*/ -Acl.prototype.areAnyRolesAllowed = function(roles, resource, permissions, cb){ - contract(arguments) - .params('string|array', 'string', 'string|array', 'function') - .params('string|array', 'string', 'string|array') - .end(); - - roles = makeArray(roles); - permissions = makeArray(permissions); - - if(roles.length===0){ - return bluebird.resolve(false).nodeify(cb); - }else{ - return this._checkPermissions(roles, resource, permissions).nodeify(cb); - } -}; - -/** - whatResources(role, function(err, {resourceName: [permissions]}) - - Returns what resources a given role or roles have permissions over. - - whatResources(role, permissions, function(err, resources) ) - - Returns what resources a role has the given permissions over. - - @param {String|Array} Roles - @param {String|Array} Permissions - @param {Function} Callback called wish the result. -*/ -Acl.prototype.whatResources = function(roles, permissions, cb){ - contract(arguments) - .params('string|array') - .params('string|array','string|array') - .params('string|array','function') - .params('string|array','string|array','function') - .end(); - - roles = makeArray(roles); - if (_.isFunction(permissions)){ - cb = permissions; - permissions = undefined; - }else if(permissions){ - permissions = makeArray(permissions); - } - - return this.permittedResources(roles, permissions, cb); -}; - -Acl.prototype.permittedResources = function(roles, permissions, cb){ - var _this = this; - var result = _.isUndefined(permissions) ? {} : []; - return this._rolesResources(roles).then(function(resources){ - return bluebird.all(resources.map(function(resource){ - return _this._resourcePermissions(roles, resource).then(function(p){ - if(permissions){ - var commonPermissions = _.intersection(permissions, p); - if(commonPermissions.length>0){ - result.push(resource); - } - }else{ - result[resource] = p; - } - }); - })).then(function(){ - return result; - }); - }).nodeify(cb); -} - -/** - clean () - - Cleans all the keys with the given prefix from redis. - - Note: this operation is not reversible!. -*/ -/* -Acl.prototype.clean = function(callback){ - var acl = this; - this.redis.keys(this.prefix+'*', function(err, keys){ - if(keys.length){ - acl.redis.del(keys, function(err){ - callback(err); - }); - }else{ - callback(); - } - }); -}; -*/ - -/** - Express Middleware - -*/ -Acl.prototype.middleware = function(numPathComponents, userId, actions){ - contract(arguments) - .params() - .params('number') - .params('number','string|number|function') - .params('number','string|number|function', 'string|array') - .end(); - - var acl = this; - - function HttpError(errorCode, msg){ - this.errorCode = errorCode; - this.message = msg; - this.name = this.constructor.name; - - Error.captureStackTrace(this, this.constructor); - this.constructor.prototype.__proto__ = Error.prototype; - } - - return function(req, res, next){ - var _userId = userId, - _actions = actions, - resource, - url; - - // call function to fetch userId - if(typeof userId === 'function'){ - _userId = userId(req, res); - } - if (!userId) { - if((req.session) && (req.session.userId)){ - _userId = req.session.userId; - }else if((req.user) && (req.user.id)){ - _userId = req.user.id; - }else{ - next(new HttpError(401, 'User not authenticated')); - return; - } - } - - // Issue #80 - Additional check - if (!_userId) { - next(new HttpError(401, 'User not authenticated')); - return; - } - - url = req.originalUrl.split('?')[0]; - if(!numPathComponents){ - resource = url; - }else{ - resource = url.split('/').slice(0,numPathComponents+1).join('/'); - } - - if(!_actions){ - _actions = req.method.toLowerCase(); - } - - acl.logger?acl.logger.debug('Requesting '+_actions+' on '+resource+' by user '+_userId):null; - - acl.isAllowed(_userId, resource, _actions, function(err, allowed){ - if (err){ - next(new Error('Error checking permissions to access resource')); - }else if(allowed === false){ - if (acl.logger) { - acl.logger.debug('Not allowed '+_actions+' on '+resource+' by user '+_userId); - acl.allowedPermissions(_userId, resource, function(err, obj){ - acl.logger.debug('Allowed permissions: '+util.inspect(obj)); - }); - } - next(new HttpError(403,'Insufficient permissions to access resource')); - }else{ - acl.logger?acl.logger.debug('Allowed '+_actions+' on '+resource+' by user '+_userId):null; - next(); - } - }); - }; -}; - -/** - Error handler for the Express middleware - - @param {String} [contentType] (html|json) defaults to plain text -*/ -Acl.prototype.middleware.errorHandler = function(contentType){ - var method = 'end'; - - if(contentType){ - switch (contentType) { - case 'json': method = 'json'; break; - case 'html': method = 'send'; break; - } - } - - return function(err, req, res, next){ - if(err.name !== 'HttpError' || !err.errorCode) return next(err); - res.status(err.errorCode)[method](err.message); - }; -}; - - -//----------------------------------------------------------------------------- -// -// Private methods -// -//----------------------------------------------------------------------------- - -// -// Same as allow but accepts a more compact input. -// -Acl.prototype._allowEx = function(objs){ - var _this = this; - objs = makeArray(objs); - - var demuxed = []; - objs.forEach(function(obj){ - var roles = obj.roles; - obj.allows.forEach(function(allow){ - demuxed.push({ - roles:roles, - resources:allow.resources, - permissions:allow.permissions}); - }); - }); - - return bluebird.reduce(demuxed, function(values, obj){ - return _this.allow(obj.roles, obj.resources, obj.permissions); - }, null); -}; - -// -// Returns the parents of the given roles -// -Acl.prototype._rolesParents = function(roles){ - return this.backend.unionAsync(this.options.buckets.parents, roles); -}; - -// -// Return all roles in the hierarchy including the given roles. -// -/* -Acl.prototype._allRoles = function(roleNames, cb){ - var _this = this, roles; - - _this._rolesParents(roleNames, function(err, parents){ - roles = _.union(roleNames, parents); - async.whilst( - function (){ - return parents.length >0; - }, - function (cb) { - _this._rolesParents(parents, function(err, result){ - if(!err){ - roles = _.union(roles, parents); - parents = result; - } - cb(err); - }); - }, - function(err){ - cb(err, roles); - } - ); - }); -}; -*/ -// -// Return all roles in the hierarchy including the given roles. -// -Acl.prototype._allRoles = function(roleNames){ - var _this = this; - - return this._rolesParents(roleNames).then(function(parents){ - if(parents.length > 0){ - return _this._allRoles(parents).then(function(parentRoles){ - return _.union(roleNames, parentRoles); - }); - }else{ - return roleNames; - } - }); -}; - -// -// Return all roles in the hierarchy of the given user. -// -Acl.prototype._allUserRoles = function(userId) { - var _this = this; - - return this.userRoles(userId).then(function(roles) { - if (roles && roles.length > 0) { - return _this._allRoles(roles); - } else { - return []; - } - }); -}; - -// -// Returns an array with resources for the given roles. -// -Acl.prototype._rolesResources = function(roles){ - var _this = this; - roles = makeArray(roles); - - return this._allRoles(roles).then(function(allRoles){ - var result = []; - - // check if bluebird.map simplifies this code - return bluebird.all(allRoles.map(function(role){ - return _this.backend.getAsync(_this.options.buckets.resources, role).then(function(resources){ - result = result.concat(resources); - }); - })).then(function(){ - return result; - }); - }); -}; - -// -// Returns the permissions for the given resource and set of roles -// -Acl.prototype._resourcePermissions = function(roles, resource){ - var _this = this; - - if(roles.length===0){ - return bluebird.resolve([]); - }else{ - return this.backend.unionAsync(allowsBucket(resource), roles).then(function(resourcePermissions){ - return _this._rolesParents(roles).then(function(parents){ - if(parents && parents.length){ - return _this._resourcePermissions(parents, resource).then(function(morePermissions){ - return _.union(resourcePermissions, morePermissions); - }); - }else{ - return resourcePermissions; - } - }); - }); - } -}; - -// -// NOTE: This function will not handle circular dependencies and result in a crash. -// -Acl.prototype._checkPermissions = function(roles, resource, permissions){ - var _this = this; - - return this.backend.unionAsync(allowsBucket(resource), roles).then(function(resourcePermissions){ - if (resourcePermissions.indexOf('*') !== -1){ - return true; - }else{ - permissions = permissions.filter(function(p){ - return resourcePermissions.indexOf(p) === -1; - }); - - if(permissions.length === 0){ - return true; - }else{ - return _this.backend.unionAsync(_this.options.buckets.parents, roles).then(function(parents){ - if(parents && parents.length){ - return _this._checkPermissions(parents, resource, permissions); - }else{ - return false; - } - }); - } - } - }); -}; - -//----------------------------------------------------------------------------- -// -// Helpers -// -//----------------------------------------------------------------------------- - -function makeArray(arr){ - return Array.isArray(arr) ? arr : [arr]; -} - -function allowsBucket(role){ - return 'allows_'+role; -} - -function keyFromAllowsBucket(str) { - return str.replace(/^allows_/, ''); -} - - -// ----------------------------------------------------------------------------------- - - -exports = module.exports = Acl; diff --git a/lib/backend.js b/lib/backend.js deleted file mode 100644 index bc6c2e4..0000000 --- a/lib/backend.js +++ /dev/null @@ -1,88 +0,0 @@ -/** - Backend Interface. - - Implement this API for providing a backend for the acl module. -*/ - -var contract = require('./contract'); - -var Backend = { - /** - Begins a transaction. - */ - begin : function(){ - // returns a transaction object - }, - - /** - Ends a transaction (and executes it) - */ - end : function(transaction, cb){ - contract(arguments).params('object', 'function').end(); - // Execute transaction - }, - - /** - Cleans the whole storage. - */ - clean : function(cb){ - contract(arguments).params('function').end(); - }, - - /** - Gets the contents at the bucket's key. - */ - get : function(bucket, key, cb){ - contract(arguments) - .params('string', 'string|number', 'function') - .end(); - }, - - /** - Gets the union of contents of the specified keys in each of the specified buckets and returns - a mapping of bucket to union. - */ - unions : function(bucket, keys, cb){ - contract(arguments) - .params('array', 'array', 'function') - .end(); - }, - - /** - Returns the union of the values in the given keys. - */ - union : function(bucket, keys, cb){ - contract(arguments) - .params('string', 'array', 'function') - .end(); - }, - - /** - Adds values to a given key inside a bucket. - */ - add : function(transaction, bucket, key, values){ - contract(arguments) - .params('object', 'string', 'string|number','string|array|number') - .end(); - }, - - /** - Delete the given key(s) at the bucket - */ - del : function(transaction, bucket, keys){ - contract(arguments) - .params('object', 'string', 'string|array') - .end(); - }, - - /** - Removes values from a given key inside a bucket. - */ - remove : function(transaction, bucket, key, values){ - contract(arguments) - .params('object', 'string', 'string|number','string|array|number') - .end(); - }, -} - -exports = module.exports = Backend; diff --git a/lib/contract.js b/lib/contract.js deleted file mode 100644 index 0760e41..0000000 --- a/lib/contract.js +++ /dev/null @@ -1,123 +0,0 @@ -/** - Design by Contract module (c) OptimalBits 2011. - - Roadmap: - - Optional parameters. ['(string)', 'array'] - - Variable number of parameters.['number','...'] - - api?: - - contract(arguments) - .params('string', 'array', '...') - .params('number') - .end() - -*/ -"use strict"; - -var noop = {}; -var util = require('util'); -var _ = require('lodash'); - -noop.params = function(){ - return this; -}; -noop.end = function(){}; - -var contract = function(args){ - if(contract.debug===true){ - contract.fulfilled = false; - contract.args = _.toArray(args); - contract.checkedParams = []; - return contract; - }else{ - return noop; - } -}; - -contract.params = function(){ - var i, len; - this.fulfilled |= checkParams(this.args, _.toArray(arguments)); - if(this.fulfilled){ - return noop; - }else{ - this.checkedParams.push(arguments); - return this; - } -} -contract.end = function(){ - if(!this.fulfilled){ - printParamsError(this.args, this.checkedParams); - throw new Error('Broke parameter contract'); - } -} - -var typeOf = function(obj){ - return Array.isArray(obj) ? 'array':typeof obj; -}; - -var checkParams = function(args, contract){ - var fulfilled, types, type, i, j; - - if(args.length !== contract.length){ - return false; - }else{ - for(i=0; i guest, jsmith => member, harry => admin, test@test.com => member', function (done) { - var acl = new Acl(this.backend) - - acl.addUserRoles('joed', 'guest', function (err) { - assert(!err) - - acl.addUserRoles('jsmith', 'member', function (err) { - assert(!err) - - acl.addUserRoles('harry', 'admin', function (err) { - assert(!err) - - acl.addUserRoles('test@test.com', 'member', function (err) { - assert(!err); - done() - }); - }) - }) - }) - }) - - it('0 => guest, 1 => member, 2 => admin', function (done) { - var acl = new Acl(this.backend) - - acl.addUserRoles(0, 'guest', function (err) { - assert(!err) - - acl.addUserRoles(1, 'member', function (err) { - assert(!err) - - acl.addUserRoles(2, 'admin', function (err) { - assert(!err); - done() - }) - }) - }) - }) - }) - - describe('read User Roles', function() { - it('run userRoles function', function(done) { - var acl = new Acl(this.backend) - acl.addUserRoles('harry', 'admin', function (err) { - if (err) return done(err); - - acl.userRoles('harry', function(err, roles) { - if (err) return done(err); - - assert.deepEqual(roles, ['admin']); - acl.hasRole('harry', 'admin', function(err, is_in_role) { - if (err) return done(err); - - assert.ok(is_in_role); - acl.hasRole('harry', 'no role', function(err, is_in_role) { - if (err) return done(err); - - assert.notOk(is_in_role) - done() - }) - }) - }) - }) - }) - }) - - describe('read Role Users', function() { - it('run roleUsers function', function(done) { - var acl = new Acl(this.backend) - acl.addUserRoles('harry', 'admin', function (err) { - if (err) return done(err); - - acl.roleUsers('admin', function(err, users) { - if (err) return done(err); - assert.include(users, 'harry') - assert.isFalse('invalid User' in users) - done(); - }) - }) - - }) - }) - - describe('allow', function () { - it('admin view/add/edit/delete users', function (done) { - var acl = new Acl(this.backend) - - acl.allow('admin', 'users', ['add','edit','view','delete'], function (err) { - assert(!err) - done(); - }) - }) - - it('foo view/edit blogs', function (done) { - var acl = new Acl(this.backend) - - acl.allow('foo', 'blogs', ['edit','view'], function (err) { - assert(!err) - done() - }) - }) - - it('bar to view/delete blogs', function (done) { - var acl = new Acl(this.backend) - - acl.allow('bar', 'blogs', ['view','delete'], function (err) { - assert(!err) - done() - }) - }) - }) - - describe('add role parents', function () { - it('add them', function (done) { - var acl = new Acl(this.backend) - - acl.addRoleParents('baz', ['foo','bar'], function (err) { - assert(!err) - done() - }) - }) - }) - - describe('add user roles', function () { - it('add them', function (done) { - var acl = new Acl(this.backend) - - acl.addUserRoles('james', 'baz', function (err) { - assert(!err) - done() - }) - }) - it('add them (numeric userId)', function (done) { - var acl = new Acl(this.backend) - - acl.addUserRoles(3, 'baz', function (err) { - assert(!err) - done() - }) - }) - }) - - describe('allow admin to do anything', function () { - it('add them', function (done) { - var acl = new Acl(this.backend) - - acl.allow('admin', ['blogs', 'forums'], '*', function (err) { - assert(!err) - done() - }) - }) - }) - - describe('Arguments in one array', function () { - it('give role fumanchu an array of resources and permissions', function (done) { - var acl = new Acl(this.backend) - - acl.allow( - [ - { - roles:'fumanchu', - allows:[ - {resources:'blogs', permissions:'get'}, - {resources:['forums','news'], permissions:['get','put','delete']}, - {resources:['/path/file/file1.txt','/path/file/file2.txt'], permissions:['get','put','delete']} - ] - } - ], - function (err) { - assert(!err) - done() - } - ) - }) - }) - - describe('Add fumanchu role to suzanne', function () { - it('do it', function (done) { - var acl = new Acl(this.backend) - acl.addUserRoles('suzanne', 'fumanchu', function (err) { - assert(!err) - done() - }) - }) - it('do it (numeric userId)', function (done) { - var acl = new Acl(this.backend) - acl.addUserRoles(4, 'fumanchu', function (err) { - assert(!err) - done() - }) - }) - }) -} - - - -exports.Allowance = function () { - describe('Allowance queries', function () { - describe('isAllowed', function () { - - it('Can joed view blogs?', function (done) { - var acl = new Acl(this.backend) - - acl.isAllowed('joed', 'blogs', 'view', function (err, allow) { - assert(!err) - assert(allow) - done() - }) - }) - - it('Can userId=0 view blogs?', function (done) { - var acl = new Acl(this.backend) - - acl.isAllowed(0, 'blogs', 'view', function (err, allow) { - assert(!err) - assert(allow) - done() - }) - }) - - it('Can joed view forums?', function (done) { - var acl = new Acl(this.backend) - - acl.isAllowed('joed', 'forums', 'view', function (err, allow) { - assert(!err) - assert(allow) - done() - }) - }) - - it('Can userId=0 view forums?', function (done) { - var acl = new Acl(this.backend) - - acl.isAllowed(0, 'forums', 'view', function (err, allow) { - assert(!err) - assert(allow) - done() - }) - }) - - it('Can joed edit forums?', function (done) { - var acl = new Acl(this.backend) - - acl.isAllowed('joed', 'forums', 'edit', function (err, allow) { - assert(!err) - assert(!allow) - done() - }) - }) - - it('Can userId=0 edit forums?', function (done) { - var acl = new Acl(this.backend) - - acl.isAllowed(0, 'forums', 'edit', function (err, allow) { - assert(!err) - assert(!allow) - done() - }) - }) - - it('Can jsmith edit forums?', function (done) { - var acl = new Acl(this.backend) - - acl.isAllowed('jsmith', 'forums', 'edit', function (err, allow) { - assert(!err) - assert(!allow) - done() - }) - }) - - it('Can jsmith edit forums?', function (done) { - var acl = new Acl(this.backend) - - acl.isAllowed('jsmith', 'forums', 'edit', function (err, allow) { - assert(!err) - assert(!allow) - done() - }) - }) - - - it('Can jsmith edit blogs?', function (done) { - var acl = new Acl(this.backend) - - acl.isAllowed('jsmith', 'blogs', 'edit', function (err, allow) { - assert(!err) - assert(allow) - done() - }) - }) - - it('Can test@test.com edit forums?', function (done) { - var acl = new Acl(this.backend) - - acl.isAllowed('test@test.com', 'forums', 'edit', function (err, allow) { - assert(!err) - assert(!allow) - done() - }) - }) - - it('Can test@test.com edit forums?', function (done) { - var acl = new Acl(this.backend) - - acl.isAllowed('test@test.com', 'forums', 'edit', function (err, allow) { - assert(!err) - assert(!allow) - done() - }) - }) - - - it('Can test@test.com edit blogs?', function (done) { - var acl = new Acl(this.backend) - - acl.isAllowed('test@test.com', 'blogs', 'edit', function (err, allow) { - assert(!err) - assert(allow) - done() - }) - }) - - it('Can userId=1 edit blogs?', function (done) { - var acl = new Acl(this.backend) - - acl.isAllowed(1, 'blogs', 'edit', function (err, allow) { - assert(!err) - assert(allow) - done() - }) - }) - - it('Can jsmith edit, delete and clone blogs?', function (done) { - var acl = new Acl(this.backend) - - acl.isAllowed('jsmith', 'blogs', ['edit','view','clone'], function (err, allow) { - assert(!err) - assert(!allow) - done() - }) - }) - - it('Can test@test.com edit, delete and clone blogs?', function (done) { - var acl = new Acl(this.backend) - - acl.isAllowed('test@test.com', 'blogs', ['edit','view','clone'], function (err, allow) { - assert(!err) - assert(!allow) - done() - }) - }) - - it('Can userId=1 edit, delete and clone blogs?', function (done) { - var acl = new Acl(this.backend) - - acl.isAllowed(1, 'blogs', ['edit','view','clone'], function (err, allow) { - assert(!err) - assert(!allow) - done() - }) - }) - - it('Can jsmith edit, clone blogs?', function (done) { - var acl = new Acl(this.backend) - - acl.isAllowed('jsmith', 'blogs', ['edit', 'clone'], function (err, allow) { - assert(!err) - assert(!allow) - done() - }) - }) - - it('Can test@test.com edit, clone blogs?', function (done) { - var acl = new Acl(this.backend) - - acl.isAllowed('test@test.com', 'blogs', ['edit', 'clone'], function (err, allow) { - assert(!err) - assert(!allow) - done() - }) - }) - - it('Can userId=1 edit, delete blogs?', function (done) { - var acl = new Acl(this.backend) - - acl.isAllowed(1, 'blogs', ['edit', 'clone'], function (err, allow) { - assert(!err) - assert(!allow) - done() - }) - }); - - it('Can james add blogs?', function (done) { - var acl = new Acl(this.backend) - - acl.isAllowed('james', 'blogs', 'add', function (err, allow) { - assert(!err) - assert(!allow) - done() - }) - }) - - it('Can userId=3 add blogs?', function (done) { - var acl = new Acl(this.backend) - - acl.isAllowed(3, 'blogs', 'add', function (err, allow) { - assert(!err) - assert(!allow) - done() - }) - }) - - it('Can suzanne add blogs?', function (done) { - var acl = new Acl(this.backend) - - acl.isAllowed('suzanne', 'blogs', 'add', function (err, allow) { - assert(!err) - assert(!allow) - done() - }) - }) - - it('Can userId=4 add blogs?', function (done) { - var acl = new Acl(this.backend) - - acl.isAllowed(4, 'blogs', 'add', function (err, allow) { - assert(!err) - assert(!allow) - done() - }) - }) - - it('Can suzanne get blogs?', function (done) { - var acl = new Acl(this.backend) - - acl.isAllowed('suzanne', 'blogs', 'get', function (err, allow) { - assert(!err) - assert(allow) - done() - }) - }) - - it('Can userId=4 get blogs?', function (done) { - var acl = new Acl(this.backend) - - acl.isAllowed(4, 'blogs', 'get', function (err, allow) { - assert(!err) - assert(allow) - done() - }) - }) - - it('Can suzanne delete and put news?', function (done) { - var acl = new Acl(this.backend) - - acl.isAllowed('suzanne', 'news', ['put','delete'], function (err, allow) { - assert(!err) - assert(allow) - done() - }) - }) - - it('Can userId=4 delete and put news?', function (done) { - var acl = new Acl(this.backend) - - acl.isAllowed(4, 'news', ['put','delete'], function (err, allow) { - assert(!err) - assert(allow) - done() - }) - }) - - - it('Can suzanne delete and put forums?', function (done) { - var acl = new Acl(this.backend) - - acl.isAllowed('suzanne', 'forums', ['put','delete'], function (err, allow) { - assert(!err) - assert(allow) - done() - }) - }) - - it('Can userId=4 delete and put forums?', function (done) { - var acl = new Acl(this.backend) - - acl.isAllowed(4, 'forums', ['put','delete'], function (err, allow) { - assert(!err) - assert(allow) - done() - }) - }) - - it('Can nobody view news?', function (done) { - var acl = new Acl(this.backend) - - acl.isAllowed('nobody', 'blogs', 'view', function (err, allow) { - assert(!err) - assert(!allow) - done() - }) - }) - - it('Can nobody view nothing?', function (done) { - var acl = new Acl(this.backend) - - acl.isAllowed('nobody', 'nothing', 'view', function (err, allow) { - assert(!err) - assert(!allow) - done() - }) - }) - }) - - describe('allowedPermissions', function () { - it('What permissions has james over blogs and forums?', function (done) { - var acl = new Acl(this.backend) - acl.allowedPermissions('james', ['blogs','forums'], function (err, permissions) { - assert(!err) - - assert.property(permissions, 'blogs') - assert.property(permissions, 'forums') - - assert.include(permissions.blogs, 'edit') - assert.include(permissions.blogs, 'delete') - assert.include(permissions.blogs, 'view') - - assert(permissions.forums.length === 0) - - done() - }) - }) - it('What permissions has userId=3 over blogs and forums?', function (done) { - var acl = new Acl(this.backend) - acl.allowedPermissions(3, ['blogs','forums'], function (err, permissions) { - assert(!err) - - assert.property(permissions, 'blogs') - assert.property(permissions, 'forums') - - assert.include(permissions.blogs, 'edit') - assert.include(permissions.blogs, 'delete') - assert.include(permissions.blogs, 'view') - - assert(permissions.forums.length === 0) - - done() - }) - }) - it('What permissions has nonsenseUser over blogs and forums?', function (done) { - var acl = new Acl(this.backend) - acl.allowedPermissions('nonsense', ['blogs','forums'], function (err, permissions) { - assert(!err) - - assert(permissions.forums.length === 0) - assert(permissions.blogs.length === 0) - - done() - }) - }) - }) - }) -} - - - - -exports.WhatResources = function () { - describe('whatResources queries', function () { - it('What resources have "bar" some rights on?', function (done) { - var acl = new Acl(this.backend) - - acl.whatResources('bar', function (err, resources) { - assert.isNull(err) - assert.include(resources.blogs, 'view') - assert.include(resources.blogs, 'delete') - done() - }) - }) - - it('What resources have "bar" view rights on?', function (done) { - var acl = new Acl(this.backend) - - acl.whatResources('bar', 'view', function (err, resources) { - assert.isNull(err) - assert.include(resources, 'blogs') - done() - }) - }) - - it('What resources have "fumanchu" some rights on?', function (done) { - var acl = new Acl(this.backend) - - acl.whatResources('fumanchu', function (err, resources) { - assert.isNull(err) - assert.include(resources.blogs, 'get') - assert.include(resources.forums, 'delete') - assert.include(resources.forums, 'get') - assert.include(resources.forums, 'put') - assert.include(resources.news, 'delete') - assert.include(resources.news, 'get') - assert.include(resources.news, 'put') - assert.include(resources['/path/file/file1.txt'], 'delete') - assert.include(resources['/path/file/file1.txt'], 'get') - assert.include(resources['/path/file/file1.txt'], 'put') - assert.include(resources['/path/file/file2.txt'], 'delete') - assert.include(resources['/path/file/file2.txt'], 'get') - assert.include(resources['/path/file/file2.txt'], 'put') - done() - }) - }) - - it('What resources have "baz" some rights on?', function (done) { - var acl = new Acl(this.backend) - - acl.whatResources('baz', function (err, resources) { - assert.isNull(err) - assert.include(resources.blogs, 'view') - assert.include(resources.blogs, 'delete') - assert.include(resources.blogs, 'edit') - done() - }) - }) - }) -} - - - -exports.PermissionRemoval= function () { - describe('removeAllow', function () { - it('Remove get permissions from resources blogs and forums from role fumanchu', function (done) { - var acl = new Acl(this.backend) - acl.removeAllow('fumanchu', ['blogs','forums'], 'get', function (err) { - assert(!err) - done() - }) - }) - - it('Remove delete and put permissions from resource news from role fumanchu', function (done) { - var acl = new Acl(this.backend) - acl.removeAllow('fumanchu', 'news', 'delete', function (err) { - assert(!err) - done() - }) - }) - - it('Remove view permissions from resource blogs from role bar', function (done) { - var acl = new Acl(this.backend); - acl.removeAllow('bar', 'blogs', 'view', function (err) { - assert(!err) - done() - }) - }) - }) - - describe('See if permissions were removed', function () { - it('What resources have "fumanchu" some rights on after removed some of them?', function (done) { - var acl = new Acl(this.backend); - acl.whatResources('fumanchu', function (err, resources) { - assert.isNull(err) - - assert.isFalse('blogs' in resources) - assert.property(resources, 'news') - assert.include(resources.news, 'get') - assert.include(resources.news, 'put') - assert.isFalse('delete' in resources.news) - - assert.property(resources, 'forums') - assert.include(resources.forums, 'delete') - assert.include(resources.forums, 'put') - done() - }) - }) - }) -} - - - - -exports.RoleRemoval = function () { - describe('removeRole', function () { - it('Remove role fumanchu', function (done) { - var acl = new Acl(this.backend) - acl.removeRole('fumanchu', function (err) { - assert(!err) - done() - }) - }) - - it('Remove role member', function (done) { - var acl = new Acl(this.backend) - acl.removeRole('member', function (err) { - assert(!err) - done() - }) - }) - - it('Remove role foo', function (done) { - var acl = new Acl(this.backend) - acl.removeRole('foo', function (err) { - assert(!err) - done() - }) - }) - }) - - describe('Was role removed?', function () { - it('What resources have "fumanchu" some rights on after removed?', function (done) { - var acl = new Acl(this.backend) - acl.whatResources('fumanchu', function (err, resources) { - assert(!err) - assert(Object.keys(resources).length === 0) - done() - }) - }) - - it('What resources have "member" some rights on after removed?', function (done) { - var acl = new Acl(this.backend) - acl.whatResources('member', function (err, resources) { - assert(!err) - assert(Object.keys(resources).length === 0) - done() - }) - }) - - describe('allowed permissions', function () { - it('What permissions has jsmith over blogs and forums?', function (done) { - var acl = new Acl(this.backend) - acl.allowedPermissions('jsmith', ['blogs','forums'], function (err, permissions) { - assert(!err) - assert(permissions.blogs.length === 0) - assert(permissions.forums.length === 0) - done() - }) - }) - - it('What permissions has test@test.com over blogs and forums?', function (done) { - var acl = new Acl(this.backend) - acl.allowedPermissions('test@test.com', ['blogs','forums'], function (err, permissions) { - assert(!err) - assert(permissions.blogs.length === 0) - assert(permissions.forums.length === 0) - done() - }) - }) - - it('What permissions has james over blogs?', function (done) { - var acl = new Acl(this.backend) - acl.allowedPermissions('james', 'blogs', function (err, permissions) { - assert(!err) - assert.property(permissions, 'blogs') - assert.include(permissions.blogs, 'delete') - done() - }) - }) - }) - }) -} - - - - - -exports.RoleParentRemoval = function () { - describe('RoleParentRemoval', function () { - before(function (done) { - var acl = new Acl(this.backend); - acl.allow('parent1', 'x', 'read1') - .then(function () { return acl.allow('parent2', 'x', 'read2'); }) - .then(function () { return acl.allow('parent3', 'x', 'read3'); }) - .then(function () { return acl.allow('parent4', 'x', 'read4'); }) - .then(function () { return acl.allow('parent5', 'x', 'read5'); }) - .then(function () { - return acl.addRoleParents('child', ['parent1', 'parent2', 'parent3', 'parent4', 'parent5']); - }) - .done(done, done); - }); - - var acl; - - beforeEach(function () { - acl = new Acl(this.backend); - }); - - it('Environment check', function (done) { - acl.whatResources('child') - .then(function (resources) { - assert.lengthOf(resources.x, 5); - assert.include(resources.x, 'read1'); - assert.include(resources.x, 'read2'); - assert.include(resources.x, 'read3'); - assert.include(resources.x, 'read4'); - assert.include(resources.x, 'read5'); - }) - .done(done, done); - }); - - it('Operation uses a callback when removing a specific parent role', function (done) { - acl.removeRoleParents('child', 'parentX', function (err) { - assert(!err); - done(); - }); - }); - - it('Operation uses a callback when removing multiple specific parent roles', function (done) { - acl.removeRoleParents('child', ['parentX', 'parentY'], function (err) { - assert(!err); - done(); - }); - }); - - it('Remove parent role "parentX" from role "child"', function (done) { - acl.removeRoleParents('child', 'parentX') - .then(function () { return acl.whatResources('child'); }) - .then(function (resources) { - assert.lengthOf(resources.x, 5); - assert.include(resources.x, 'read1'); - assert.include(resources.x, 'read2'); - assert.include(resources.x, 'read3'); - assert.include(resources.x, 'read4'); - assert.include(resources.x, 'read5'); - }) - .done(done, done); - }); - - it('Remove parent role "parent1" from role "child"', function (done) { - acl.removeRoleParents('child', 'parent1') - .then(function () { return acl.whatResources('child'); }) - .then(function (resources) { - assert.lengthOf(resources.x, 4); - assert.include(resources.x, 'read2'); - assert.include(resources.x, 'read3'); - assert.include(resources.x, 'read4'); - assert.include(resources.x, 'read5'); - }) - .done(done, done); - }); - - it('Remove parent roles "parent2" & "parent3" from role "child"', function (done) { - acl.removeRoleParents('child', ['parent2', 'parent3']) - .then(function () { return acl.whatResources('child'); }) - .then(function (resources) { - assert.lengthOf(resources.x, 2); - assert.include(resources.x, 'read4'); - assert.include(resources.x, 'read5'); - }) - .done(done, done); - }); - - it('Remove all parent roles from role "child"', function (done) { - acl.removeRoleParents('child') - .then(function () { return acl.whatResources('child'); }) - .then(function (resources) { - assert.notProperty(resources, 'x'); - }) - .done(done, done); - }); - - it('Remove all parent roles from role "child" with no parents', function (done) { - acl.removeRoleParents('child') - .then(function () { return acl.whatResources('child'); }) - .then(function (resources) { - assert.notProperty(resources, 'x'); - }) - .done(done, done); - }); - - it('Remove parent role "parent1" from role "child" with no parents', function (done) { - acl.removeRoleParents('child', 'parent1') - .then(function () { return acl.whatResources('child'); }) - .then(function (resources) { - assert.notProperty(resources, 'x'); - }) - .done(done, done); - }); - - it('Operation uses a callback when removing all parent roles', function (done) { - acl.removeRoleParents('child', function (err) { - assert(!err); - done(); - }); - }); - }); -}; - - - - - -exports.ResourceRemoval = function () { - describe('removeResource', function () { - it('Remove resource blogs', function (done) { - var acl = new Acl(this.backend) - acl.removeResource('blogs', function (err) { - assert(!err) - done() - }) - }) - - it('Remove resource users', function (done) { - var acl = new Acl(this.backend) - acl.removeResource('users', function (err) { - assert(!err) - done() - }) - }) - }) - - describe('allowedPermissions', function () { - it('What permissions has james over blogs?', function (done) { - var acl = new Acl(this.backend) - acl.allowedPermissions('james', 'blogs', function (err, permissions) { - assert.isNull(err) - assert.property(permissions, 'blogs') - assert(permissions.blogs.length === 0) - done() - }) - }) - it('What permissions has userId=4 over blogs?', function (done) { - var acl = new Acl(this.backend) - acl.allowedPermissions(4, 'blogs').then(function (permissions) { - assert.property(permissions, 'blogs') - assert(permissions.blogs.length === 0) - done() - }, done); - }) - }) - - describe('whatResources', function () { - it('What resources have "baz" some rights on after removed blogs?', function (done) { - var acl = new Acl(this.backend) - acl.whatResources('baz', function (err, resources) { - assert(!err) - assert.isObject(resources) - assert(Object.keys(resources).length === 0) - - done() - }) - }) - - it('What resources have "admin" some rights on after removed users resource?', function (done) { - var acl = new Acl(this.backend) - acl.whatResources('admin', function (err, resources) { - assert(!err) - assert.isFalse('users' in resources) - assert.isFalse('blogs' in resources) - - done() - }) - }) - }) -} - - - - - -exports.UserRoleRemoval = function () { - describe('Remove user roles', function () { - it('Remove role guest from joed', function (done) { - var acl = new Acl(this.backend) - acl.removeUserRoles('joed','guest', function (err) { - assert(!err) - done() - }) - }) - - it('Remove role guest from userId=0', function (done) { - var acl = new Acl(this.backend) - acl.removeUserRoles(0,'guest', function (err) { - assert(!err) - done() - }) - }) - it('Remove role admin from harry', function (done) { - var acl = new Acl(this.backend) - acl.removeUserRoles('harry','admin', function (err) { - assert(!err) - done() - }) - }) - - it('Remove role admin from userId=2', function (done) { - var acl = new Acl(this.backend) - acl.removeUserRoles(2,'admin', function (err) { - assert(!err) - done() - }) - }) - }) - - describe('Were roles removed?', function () { - it('What permissions has harry over forums and blogs?', function (done) { - var acl = new Acl(this.backend) - acl.allowedPermissions('harry', ['forums','blogs'], function (err, permissions) { - assert(!err) - assert.isObject(permissions) - assert(permissions.forums.length === 0) - done() - }) - it('What permissions has userId=2 over forums and blogs?', function (done) { - var acl = new Acl(this.backend) - acl.allowedPermissions(2, ['forums','blogs'], function (err, permissions) { - assert(!err) - assert.isObject(permissions) - assert(permissions.forums.length === 0) - done() - }) - }) - }) - }) -} - -exports.i55PermissionRemoval = function () { - describe('Github issue #55: removeAllow is removing all permissions.', function () { - it('Add roles/resources/permissions', function () { - var acl = new Acl(this.backend) - - return acl.addUserRoles('jannette', 'member').then(function(){ - return acl.allow('member', 'blogs', ['view', 'update']); - }).then(function(){ - return acl.isAllowed('jannette', 'blogs', 'view', function(err, allowed){ - expect(allowed).to.be.eql(true); - }) - }).then(function(){ - return acl.removeAllow('member', 'blogs', 'update'); - }).then(function(){ - return acl.isAllowed('jannette', 'blogs', 'view', function(err, allowed){ - expect(allowed).to.be.eql(true); - }); - }).then(function(){ - return acl.isAllowed('jannette', 'blogs', 'update', function(err, allowed){ - expect(allowed).to.be.eql(false); - }); - }).then(function(){ - return acl.removeAllow('member', 'blogs', 'view'); - }).then(function(){ - return acl.isAllowed('jannette', 'blogs', 'view', function(err, allowed){ - expect(allowed).to.be.eql(false); - }); - }) - }) - }) -} - -exports.i32RoleRemoval = function () { - describe('Github issue #32: Removing a role removes the entire "allows" document.', function () { - it('Add roles/resources/permissions', function (done) { - var acl = new Acl(this.backend) - - acl.allow(['role1', 'role2', 'role3'], ['res1', 'res2', 'res3'], ['perm1', 'perm2', 'perm3'], function (err) { - assert(!err) - done() - }) - }) - - it('Add user roles and parent roles', function (done) { - var acl = new Acl(this.backend) - - acl.addUserRoles('user1', 'role1', function (err) { - assert(!err) - - acl.addRoleParents('role1', 'parentRole1', function (err) { - assert(!err) - done() - }) - }) - }) - - it('Add user roles and parent roles', function (done) { - var acl = new Acl(this.backend) - - acl.addUserRoles(1, 'role1', function (err) { - assert(!err) - - acl.addRoleParents('role1', 'parentRole1', function (err) { - assert(!err) - done() - }) - }) - }) - it('Verify that roles have permissions as assigned', function(done){ - var acl = new Acl(this.backend) - - acl.whatResources('role1', function (err, res) { - assert(!err) - assert.deepEqual(res.res1.sort(), [ 'perm1', 'perm2', 'perm3' ]) - - acl.whatResources('role2', function (err, res) { - assert(!err) - assert.deepEqual(res.res1.sort(), [ 'perm1', 'perm2', 'perm3' ]) - done() - }) - }) - }) - - it('Remove role "role1"', function(done){ - var acl = new Acl(this.backend) - - acl.removeRole('role1', function (err) { - assert(!err) - done() - }) - }) - - it('Verify that "role1" has no permissions and "role2" has permissions intact', function(done){ - var acl = new Acl(this.backend) - - acl.removeRole('role1', function (err) { - assert(!err) - - acl.whatResources('role1', function (err, res) { - assert(Object.keys(res).length === 0) - - acl.whatResources('role2', function (err, res) { - assert.deepEqual(res.res1.sort(), [ 'perm1', 'perm2', 'perm3' ]) - done() - }) - }) - }) - }) - }) -} diff --git a/tsconfig.json b/tsconfig.json index 7f244c2..d4345df 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,5 +19,5 @@ "forceConsistentCasingInFileNames": true }, "include": ["src", "test"], - "exclude": ["node_modules", "dist", "lib"] + "exclude": ["node_modules", "dist"] } From 3667bcbcfc95670a661ea30c48c02c9edac7d9b6 Mon Sep 17 00:00:00 2001 From: Nordin Date: Mon, 22 Jun 2026 11:44:30 +0800 Subject: [PATCH 12/13] =?UTF-8?q?docs:=20Phase=206=20=E2=80=94=20LICENSE,?= =?UTF-8?q?=20README=20rewrite,=20CLAUDE.md=20update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LICENSE: MIT preserving Manuel Astudillo's original copyright plus the fork's; no LICENSE file existed before (terms were only in the README) - README: rewritten for acl-next — promise-only TS API, install, backends, middleware, API table, and a node_acl migration guide - CLAUDE.md: updated to the TypeScript architecture, commands, and the testcontainers-based test layout Full suite: 401 passing across memory, redis, mongo (default + useSingle), plus unit/middleware tests. Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 58 +++-- LICENSE | 23 ++ README.md | 624 +++++++++++------------------------------------------- 3 files changed, 179 insertions(+), 526 deletions(-) create mode 100644 LICENSE diff --git a/CLAUDE.md b/CLAUDE.md index b7d63d7..fabe38b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,46 +4,60 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## What this is -`acl` is a Node.js Access Control List library inspired by Zend_ACL. It models authorization as **users → roles → resources → permissions**, with role hierarchies (parents). It ships three storage backends (Redis, MongoDB, in-memory) and an Express middleware for protecting routes. +`acl-next` is a modern TypeScript Access Control List (ACL / RBAC) library — a maintained fork of optimalbits/node_acl. It models authorization as **users → roles → resources → permissions**, with role hierarchies (parents). It ships three storage backends (Redis, MongoDB, in-memory) and a framework-agnostic Express-style middleware. + +The codebase is **promise-native**, **TypeScript strict**, and has **zero runtime dependencies** (`redis` and `mongodb` are optional peer deps — install only what you use). ## Commands ```bash -npm test # full suite against all 4 backends -npx mocha test/runner.js --reporter spec # same thing directly -npx mocha test/runner.js --grep "isAllowed" # run a single test/describe block by name -npm run cover # istanbul coverage +npm run build # tsup → dist/ (ESM + CJS + .d.ts) +npm run typecheck # tsc --noEmit (strict) +npm run lint # biome check +npm run format # biome format --write +npm test # vitest run (all suites) +npx vitest run test/memory-acl.test.ts # single backend suite +npx vitest run test/unit # fast unit tests, no Docker +npx vitest run -t "isAllowed" # filter by test name ``` -**Tests require live Redis and MongoDB** on localhost (Redis `127.0.0.1:6379`, MongoDB `mongodb://localhost:27017/acltest`). Without them the Redis/MongoDB suites fail; the Memory suite still runs. [test/runner.js](test/runner.js) registers four `describe` blocks (MongoDB default, MongoDB useSingle, Redis, Memory) and runs the *same* shared test bodies from [test/tests.js](test/tests.js) and [test/backendtests.js](test/backendtests.js) against each backend's `this.backend`. So a test written once is automatically exercised on every backend. +**Test infrastructure:** the Redis and MongoDB suites use [testcontainers](https://testcontainers.com/) to spin up real databases in Docker — **Docker must be running** for them. The Memory suite and `test/unit/*` run without Docker (use these for the fast inner loop). First run pulls `redis:7-alpine` / `mongo:6` images (slow; `hookTimeout` in [vitest.config.ts](vitest.config.ts) is sized for it). ## Architecture -- **[index.js](index.js)** — entry point. Exports the `Acl` class and lazily exposes `redisBackend`, `memoryBackend`, `mongodbBackend` via getters. -- **[lib/acl.js](lib/acl.js)** — all ACL logic (the public API: `allow`, `isAllowed`, `addUserRoles`, `whatResources`, `middleware`, etc.). This is the only file with business logic; the backends are dumb storage. -- **[lib/backend.js](lib/backend.js)** — the **backend interface contract** (documentation/reference, not instantiated). Any new backend must implement these methods. -- **[lib/{redis,mongodb,memory}-backend.js](lib/)** — the three storage implementations. -- **[lib/contract.js](lib/contract.js)** — a runtime design-by-contract argument validator. +- **[src/index.ts](src/index.ts)** — public entry point; re-exports everything. Default export is `Acl`. +- **[src/acl.ts](src/acl.ts)** — the `Acl` class: all authorization logic (`allow`, `isAllowed`, `whatResources`, `allowedPermissions`, role/resource/user mutations, `middleware()`). This is the only file with business logic; backends are dumb storage. +- **[src/types.ts](src/types.ts)** — domain types and the generic `Backend` storage interface. +- **[src/backends/{memory,redis,mongodb}.ts](src/backends/)** — the three storage implementations. +- **[src/middleware.ts](src/middleware.ts)** — `aclMiddleware`, `aclErrorHandler`, `HttpError`, and structural HTTP types. ### Backend abstraction (the key concept) -`acl.js` never talks to a database directly — it talks to a backend through a small key/value-of-sets interface defined in [lib/backend.js](lib/backend.js): `get`, `union`, `unions`, `add`, `del`, `remove`, `clean`, plus a **transaction** model via `begin()` / `end(transaction, cb)`. +`acl.ts` never talks to a database directly — only to a `Backend` (see [src/types.ts](src/types.ts)): a namespaced (bucketed) key → set-of-values store with **batched writes** via a transaction. -Writes are batched: callers do `var t = backend.begin()`, queue mutations with `add`/`del`/`remove` (which push onto `t`), then `backend.end(t, cb)` commits them. In the memory backend a transaction is literally an array of closures executed on `end`; Redis/MongoDB map this onto their native multi/batch primitives. When adding or modifying behavior, preserve this batch-then-commit pattern rather than writing eagerly. +Pattern: `const t = backend.begin()` → queue mutations with `add`/`del`/`remove` (these push onto `t`, they do **not** write) → `await backend.end(t)` commits. Implementations map this onto their primitives: -Data is organized into **buckets** (namespaces): `meta`, `parents`, `permissions`, `resources`, `roles`, `users`. The header comment in [lib/acl.js](lib/acl.js) documents the exact Redis key layout (e.g. `acl_allows_{resourceName}_{roleName}`). +- **Memory**: transaction is an array of closures run on `end`. +- **Redis**: transaction is a `multi()`; mutations queue `sAdd`/`sRem`/`del`, `end` calls `exec()`. +- **MongoDB**: transaction is an array of async thunks run in series; each (bucket,key) is a document whose field names are the set members. -### Async model +Buckets: `meta`, `parents`, `permissions`, `resources`, `roles`, `users` (overridable via constructor options), plus dynamic `allows_` buckets for permissions. When adding/changing behavior, preserve the batch-then-commit pattern rather than writing eagerly. -Internally the library is promise-based (bluebird). The constructor **promisifies** the callback-style backend methods (`backend.getAsync`, `unionAsync`, etc.) so `acl.js` can use promises throughout. Public API methods accept an **optional trailing callback** but also return a promise — support both when adding/editing methods (see how existing methods end with `.nodeify(callback)`). +`Backend.unions` is **optional**; when present (Memory, Redis) `allowedPermissions` uses the bulk-query fast path, otherwise it falls back to per-resource `union` calls (MongoDB). -### contract.js +### Decoupling from drivers -Every backend method and many ACL methods begin with a `contract(arguments).params(...).end()` call that validates argument types at runtime (e.g. `'string|number'`, `'array'`). It is a no-op unless `contract.debug === true`, which [lib/acl.js](lib/acl.js) sets globally. Keep these contract guards in sync when changing a method's signature. +The Redis and MongoDB backends define **structural interfaces** (`RedisClientLike`, `MongoDbLike`, etc.) instead of importing driver types, so the package stays driver-agnostic and consumers without a given driver still type-check. tsup marks `redis`/`mongodb` as `external` so they're never bundled. ## Conventions -- Plain ES5, `"use strict"`, callback + promise dual API. No build step, no transpilation, no linter configured. -- `'*'` is the wildcard meaning "all permissions". -- User ids, role names, and resource names are **case-sensitive**. -- When adding a feature, add its test once to [test/tests.js](test/tests.js) so it runs across all backends; only put storage-specific tests in [test/backendtests.js](test/backendtests.js). +- TypeScript strict (`noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`, `verbatimModuleSyntax`). Use `import type` for type-only imports. Relative imports use the `.js` extension. +- **Promise-only** public API (the legacy callback dual-API was intentionally dropped). +- IDs (`UserId`) are coerced to **strings** in stored/returned values. +- `'*'` is the wildcard meaning "all permissions"; ids/role/resource names are case-sensitive. +- Lint/format is **Biome** (config in [biome.json](biome.json)); run `npm run lint` before committing. +- **Tests run once per backend** via the shared, ordered, stateful suite in [test/shared/acl-suite.ts](test/shared/acl-suite.ts). Add a behavioral feature there so it's exercised against all backends; put storage-specific cases in `test/unit/`. + +## History + +[MODERNIZATION.md](MODERNIZATION.md) is the rewrite plan; [TEST-BASELINE.md](TEST-BASELINE.md) records the legacy suite result (394 passing) that the rewrite preserves. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..06774c1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +The MIT License (MIT) + +Copyright (c) 2011-2013 Manuel Astudillo (original node_acl) +Copyright (c) 2026 acl-next contributors + +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/README.md b/README.md index 35a6535..62104c5 100644 --- a/README.md +++ b/README.md @@ -1,558 +1,174 @@ -# NODE ACL - Access Control Lists for Node +# acl-next -This module provides a minimalistic ACL implementation inspired by Zend_ACL. +> Modern TypeScript Access Control Lists (ACL / RBAC) for Node.js — with Redis, MongoDB and in-memory backends, and Express middleware. -When you develop a web site or application you will soon notice that sessions are not enough to protect all the -available resources. Avoiding that malicious users access other users content proves a much more -complicated task than anticipated. ACL can solve this problem in a flexible and elegant way. +A maintained, modernized fork of [optimalbits/node_acl](https://github.com/optimalbits/node_acl) (MIT). Same proven model — users → roles → resources → permissions, with role hierarchies — rebuilt as **TypeScript**, **promise-native**, and **zero runtime dependencies**. -Create roles and assign roles to users. Sometimes it may even be useful to create one role per user, -to get the finest granularity possible, while in other situations you will give the *asterisk* permission -for admin kind of functionality. +## What changed from `node_acl` -A Redis, MongoDB and In-Memory based backends are provided built-in in the module. There are other third party backends such as [*knex*](https://github.com/christophertrudel/node_acl_knex) based, [*firebase*](https://github.com/tonila/node_acl_firebase) and [*elasticsearch*](https://github.com/adnanesaghir/acl-elasticsearch-backend). There is also an alternative memory backend that supports [*regexps*](https://github.com/futurechan/node_acl-mem-regexp). +- **TypeScript**, with full type declarations. +- **Promise-only** API — the legacy callback signatures are gone (use `await`). +- **No runtime dependencies.** `bluebird`, `lodash` and `async` are removed. `redis` and `mongodb` are now **optional peer dependencies** — install only the driver you use. +- Modern drivers: **redis v4+**, **mongodb v4+**. +- IDs are normalized to **strings** in stored/returned values. +- Dual **ESM + CommonJS** build. -Follow [manast](http://twitter.com/manast) for news and updates regarding this library. +See the [Migration guide](#migration-from-node_acl) below. -## Status +## Install -[![BuildStatus](https://secure.travis-ci.org/OptimalBits/node_acl.png?branch=master)](http://travis-ci.org/OptimalBits/node_acl) -[![Dependency Status](https://david-dm.org/OptimalBits/node_acl.svg)](https://david-dm.org/OptimalBits/node_acl) -[![devDependency Status](https://david-dm.org/OptimalBits/node_acl/dev-status.svg)](https://david-dm.org/OptimalBits/node_acl#info=devDependencies) - -## Features - -- Users -- Roles -- Hierarchies -- Resources -- Express middleware for protecting resources. -- Robust implementation with good unit test coverage. - -## Installation - -Using npm: - -```javascript -npm install acl +```bash +npm install acl-next +# plus the backend driver you use (optional peer deps): +npm install redis # for RedisBackend +npm install mongodb # for MongoDBBackend +# MemoryBackend needs nothing ``` -## Documentation - -* [addUserRoles](#addUserRoles) -* [removeUserRoles](#removeUserRoles) -* [userRoles](#userRoles) -* [roleUsers](#roleUsers) -* [hasRole](#hasRole) -* [addRoleParents](#addRoleParents) -* [removeRoleParents](#removeRoleParents) -* [removeRole](#removeRole) -* [removeResource](#removeResource) -* [allow](#allow) -* [removeAllow](#removeAllow) -* [allowedPermissions](#allowedPermissions) -* [isAllowed](#isAllowed) -* [areAnyRolesAllowed](#areAnyRolesAllowed) -* [whatResources](#whatResources) -* [middleware](#middleware) -* [backend](#backend) - -## Examples - -Create your acl module by requiring it and instantiating it with a valid backend instance: - -```javascript -var acl = require('acl'); - -// Using redis backend -acl = new acl(new acl.redisBackend(redisClient, prefix)); - -// Or Using the memory backend -acl = new acl(new acl.memoryBackend()); - -// Or Using the mongodb backend -acl = new acl(new acl.mongodbBackend(dbInstance, prefix)); -``` +## Quick start -All the following functions return a promise or optionally take a callback with -an err parameter as last parameter. We omit them in the examples for simplicity. +```ts +import { Acl, MemoryBackend } from "acl-next"; -Create roles implicitly by giving them permissions: +const acl = new Acl(new MemoryBackend()); -```javascript -// guest is allowed to view blogs -acl.allow('guest', 'blogs', 'view') +// Roles get permissions over resources (roles/resources created implicitly): +await acl.allow("guest", "blogs", "view"); +await acl.allow("member", "blogs", ["edit", "view", "delete"]); -// allow function accepts arrays as any parameter -acl.allow('member', 'blogs', ['edit', 'view', 'delete']) -``` - -Users are likewise created implicitly by assigning them roles: +// Users get roles (users created implicitly): +await acl.addUserRoles("joed", "guest"); -```javascript -acl.addUserRoles('joed', 'guest') +// Query: +await acl.isAllowed("joed", "blogs", "view"); // => true +await acl.isAllowed("joed", "blogs", "edit"); // => false ``` -Hierarchies of roles can be created by assigning parents to roles: +### Role hierarchies -```javascript -acl.addRoleParents('baz', ['foo', 'bar']) +```ts +await acl.addRoleParents("baz", ["foo", "bar"]); // baz inherits foo + bar ``` -Note that the order in which you call all the functions is irrelevant (you can add parents first and assign permissions to roles later) +### Bulk permissions -```javascript -acl.allow('foo', ['blogs', 'forums', 'news'], ['view', 'delete']) +```ts +await acl.allow([ + { + roles: ["guest", "member"], + allows: [ + { resources: "blogs", permissions: "get" }, + { resources: ["forums", "news"], permissions: ["get", "put", "delete"] }, + ], + }, +]); ``` -Use the wildcard to give all permissions: +### Wildcard -```javascript -acl.allow('admin', ['blogs', 'forums'], '*') +```ts +await acl.allow("admin", ["blogs", "forums"], "*"); // all permissions ``` -Sometimes is necessary to set permissions on many different roles and resources. This would -lead to unnecessary nested callbacks for handling errors. Instead use the following: - -```javascript -acl.allow([ - { - roles:['guest', 'member'], - allows:[ - {resources:'blogs', permissions:'get'}, - {resources:['forums', 'news'], permissions:['get', 'put', 'delete']} - ] - }, - { - roles:['gold', 'silver'], - allows:[ - {resources:'cash', permissions:['sell', 'exchange']}, - {resources:['account', 'deposit'], permissions:['put', 'delete']} - ] - } -]) -``` +## Backends -You can check if a user has permissions to access a given resource with *isAllowed*: - -```javascript -acl.isAllowed('joed', 'blogs', 'view', function(err, res){ - if(res){ - console.log("User joed is allowed to view blogs") - } -}) -``` +```ts +import { Acl, RedisBackend, MongoDBBackend, MemoryBackend } from "acl-next"; +// In-memory (no deps) — great for tests / single process: +new Acl(new MemoryBackend()); -Of course arrays are also accepted in this function: +// Redis (node-redis v4+): +import { createClient } from "redis"; +const redis = createClient(); +await redis.connect(); +new Acl(new RedisBackend(redis, "acl" /* key prefix */)); -```javascript -acl.isAllowed('jsmith', 'blogs', ['edit', 'view', 'delete']) +// MongoDB (mongodb v4+): +import { MongoClient } from "mongodb"; +const client = new MongoClient("mongodb://localhost:27017"); +await client.connect(); +new Acl(new MongoDBBackend(client.db("mydb"), { prefix: "acl_", useSingle: false })); ``` -Note that all permissions must be fulfilled in order to get *true*. +Bring your own backend by implementing the `Backend` interface (see [`src/types.ts`](src/types.ts)). +## Express middleware -Sometimes is necessary to know what permissions a given user has over certain resources: - -```javascript -acl.allowedPermissions('james', ['blogs', 'forums'], function(err, permissions){ - console.log(permissions) -}) -``` - -It will return an array of resource:[permissions] like this: - -```javascript -[{'blogs' : ['get', 'delete']}, - {'forums':['get', 'put']}] -``` +```ts +import { aclErrorHandler } from "acl-next"; +// Protect a route — resource defaults to req.url, permission to req.method: +app.put("/blogs/:id", acl.middleware(), handler); -Finally, we provide a middleware for Express for easy protection of resources. +// Only the first N path components form the resource name: +app.put("/blogs/:id/comments/:commentId", acl.middleware(3), handler); -```javascript -acl.middleware() -``` - -We can protect a resource like this: +// Custom userId (value or resolver) and explicit permission: +app.put("/blogs/:id", acl.middleware(3, (req) => req.user.id, "post"), handler); -```javascript -app.put('/blogs/:id', acl.middleware(), function(req, res, next){…} +// Render the 401/403 errors it raises: +app.use(aclErrorHandler("json")); // or "html", or omit for plain text ``` -The middleware will protect the resource named by *req.url*, pick the user from *req.session.userId* and check the permission for *req.method*, so the above would be equivalent to something like this: +The middleware resolves the user from (in order): the `userId` argument, `req.session.userId`, then `req.user.id`. -```javascript -acl.isAllowed(req.session.userId, '/blogs/12345', 'put') -``` +## API -The middleware accepts 3 optional arguments, that are useful in some situations. For example, sometimes we -cannot consider the whole url as the resource: +All methods return Promises. -```javascript -app.put('/blogs/:id/comments/:commentId', acl.middleware(3), function(req, res, next){…} -``` +| Method | Description | +| --- | --- | +| `addUserRoles(userId, roles)` | Assign role(s) to a user | +| `removeUserRoles(userId, roles)` | Remove role(s) from a user | +| `userRoles(userId)` | Roles assigned to a user | +| `roleUsers(role)` | Users that have a role | +| `hasRole(userId, role)` | Whether a user has a role | +| `addRoleParents(role, parents)` | Add parent role(s) (inheritance) | +| `removeRoleParents(role, parents?)` | Remove parent role(s) (all if omitted) | +| `removeRole(role)` | Remove a role and its permissions | +| `removeResource(resource)` | Remove a resource | +| `allow(roles, resources, permissions)` / `allow(rules[])` | Grant permissions | +| `removeAllow(role, resources, permissions?)` | Revoke permissions | +| `allowedPermissions(userId, resources)` | Map of resource → permissions for a user | +| `isAllowed(userId, resource, permissions)` | Whether a user has all permissions | +| `areAnyRolesAllowed(roles, resource, permissions)` | Whether any role qualifies | +| `whatResources(roles)` / `whatResources(roles, permissions)` | Resources a role can access | +| `middleware(numPathComponents?, userId?, actions?)` | Express middleware factory | -In this case the resource will be just the three first components of the url (without the ending slash). +## Migration from `node_acl` -It is also possible to add a custom userId or check for other permissions than the method: +1. **Rename the import:** `acl` → `acl-next`. +2. **Drop callbacks, use `await`:** -```javascript -app.put('/blogs/:id/comments/:commentId', acl.middleware(3, 'joed', 'post'), function(req, res, next){…} -``` + ```ts + // before + acl.isAllowed("joed", "blogs", "view", (err, allowed) => { ... }); + // after + const allowed = await acl.isAllowed("joed", "blogs", "view"); + ``` -## Methods +3. **Constructor uses imported backends** (no more `new acl.redisBackend(...)`): - + ```ts + import { Acl, RedisBackend } from "acl-next"; + const acl = new Acl(new RedisBackend(redisClient)); + ``` +4. **MongoDB options are an object:** `new MongoDBBackend(db, { prefix, useSingle })` instead of positional args. +5. **Upgrade drivers** to `redis@4+` / `mongodb@4+`. +6. **Numeric IDs come back as strings** (e.g. `roleUsers` returns `["3"]`, not `[3]`). -### addUserRoles( userId, roles, function(err) ) +## Development -Adds roles to a given user id. - -__Arguments__ - -```javascript - userId {String|Number} User id. - roles {String|Array} Role(s) to add to the user id. - callback {Function} Callback called when finished. -``` - ---------------------------------------- - - - -### removeUserRoles( userId, roles, function(err) ) - -Remove roles from a given user. - -__Arguments__ - -```javascript - userId {String|Number} User id. - roles {String|Array} Role(s) to remove to the user id. - callback {Function} Callback called when finished. -``` - ---------------------------------------- - - - -### userRoles( userId, function(err, roles) ) - -Return all the roles from a given user. - -__Arguments__ - -```javascript - userId {String|Number} User id. - callback {Function} Callback called when finished. +```bash +npm run build # ESM + CJS + .d.ts via tsup +npm run typecheck # tsc --noEmit +npm run lint # biome +npm test # vitest (Redis/Mongo suites use testcontainers → Docker required) ``` ---------------------------------------- - - - -### roleUsers( rolename, function(err, users) ) - -Return all users who has a given role. - -__Arguments__ - -```javascript - rolename {String|Number} User id. - callback {Function} Callback called when finished. -``` - ---------------------------------------- - - - -### hasRole( userId, rolename, function(err, hasRole) ) - -Return boolean whether user has the role - -__Arguments__ - -```javascript - userId {String|Number} User id. - rolename {String|Number} role name. - callback {Function} Callback called when finished. -``` - ---------------------------------------- - - - -### addRoleParents( role, parents, function(err) ) - -Adds a parent or parent list to role. - -__Arguments__ - -```javascript - role {String} Child role. - parents {String|Array} Parent role(s) to be added. - callback {Function} Callback called when finished. -``` - ---------------------------------------- - - - -### removeRoleParents( role, parents, function(err) ) - -Removes a parent or parent list from role. - -If `parents` is not specified, removes all parents. - -__Arguments__ - -```javascript - role {String} Child role. - parents {String|Array} Parent role(s) to be removed [optional]. - callback {Function} Callback called when finished [optional]. -``` - ---------------------------------------- - - - -### removeRole( role, function(err) ) - -Removes a role from the system. - -__Arguments__ - -```javascript - role {String} Role to be removed - callback {Function} Callback called when finished. -``` - ---------------------------------------- - - -### removeResource( resource, function(err) ) - -Removes a resource from the system - -__Arguments__ - -```javascript - resource {String} Resource to be removed - callback {Function} Callback called when finished. -``` - ---------------------------------------- - - - -### allow( roles, resources, permissions, function(err) ) - -Adds the given permissions to the given roles over the given resources. - -__Arguments__ - -```javascript - roles {String|Array} role(s) to add permissions to. - resources {String|Array} resource(s) to add permisisons to. - permissions {String|Array} permission(s) to add to the roles over the resources. - callback {Function} Callback called when finished. -``` - -### allow( permissionsArray, function(err) ) - -__Arguments__ - -```javascript - permissionsArray {Array} Array with objects expressing what permissions to give. - [{roles:{String|Array}, allows:[{resources:{String|Array}, permissions:{String|Array}]] - - callback {Function} Callback called when finished. -``` - ---------------------------------------- - - - -### removeAllow( role, resources, permissions, function(err) ) - -Remove permissions from the given roles owned by the given role. - -Note: we loose atomicity when removing empty role_resources. - -__Arguments__ - -```javascript - role {String} - resources {String|Array} - permissions {String|Array} - callback {Function} -``` - ---------------------------------------- - - - -### allowedPermissions( userId, resources, function(err, obj) ) - -Returns all the allowable permissions a given user have to -access the given resources. - -It returns an array of objects where every object maps a -resource name to a list of permissions for that resource. - -__Arguments__ - -```javascript - userId {String|Number} User id. - resources {String|Array} resource(s) to ask permissions for. - callback {Function} Callback called when finished. -``` - ---------------------------------------- - - - -### isAllowed( userId, resource, permissions, function(err, allowed) ) - -Checks if the given user is allowed to access the resource for the given -permissions (note: it must fulfill all the permissions). - -__Arguments__ - -```javascript - userId {String|Number} User id. - resource {String} resource to ask permissions for. - permissions {String|Array} asked permissions. - callback {Function} Callback called with the result. -``` - ---------------------------------------- - - -### areAnyRolesAllowed( roles, resource, permissions, function(err, allowed) ) - -Returns true if any of the given roles have the right permissions. - -__Arguments__ - -```javascript - roles {String|Array} Role(s) to check the permissions for. - resource {String} resource to ask permissions for. - permissions {String|Array} asked permissions. - callback {Function} Callback called with the result. -``` - ---------------------------------------- - - -### whatResources(role, function(err, {resourceName: [permissions]}) - -Returns what resources a given role has permissions over. - -__Arguments__ - -```javascript - role {String|Array} Roles - callback {Function} Callback called with the result. -``` - -whatResources(role, permissions, function(err, resources) ) - -Returns what resources a role has the given permissions over. - -__Arguments__ - -```javascript - role {String|Array} Roles - permissions {String|Array} Permissions - callback {Function} Callback called with the result. -``` - ---------------------------------------- - - - -### middleware( [numPathComponents, userId, permissions] ) - -Middleware for express. - -To create a custom getter for userId, pass a function(req, res) which returns the userId when called (must not be async). - -__Arguments__ - -```javascript - numPathComponents {Number} number of components in the url to be considered part of the resource name. - userId {String|Number|Function} the user id for the acl system (defaults to req.session.userId) - permissions {String|Array} the permission(s) to check for (defaults to req.method.toLowerCase()) -``` - ---------------------------------------- - - - -### backend( db, [prefix] ) - -Creates a backend instance. All backends except Memory require driver or database instance. `useSingle` is only applicable to the MongoDB backend. - -__Arguments__ - -```javascript - db {Object} Database instance - prefix {String} Optional collection prefix - useSingle {Boolean} Create one collection for all resources (defaults to false) -``` - -```javascript -var mongodb = require('mongodb'); -mongodb.connect("mongodb://127.0.0.1:27017/acltest", function(error, db) { - var mongoBackend = new acl.mongodbBackend(db, 'acl_'); -}); -``` - -Creates a new MongoDB backend using database instance `db`. - -```javascript -var client = require('redis').createClient(6379, '127.0.0.1', {no_ready_check: true}); -var redisBackend = new acl.redisBackend(client); -``` - -Creates a new Redis backend using Redis client `client`. - -## Tests - -Run tests with `npm` (requires mocha): -```javascript - npm test -``` - -## Future work - -- Support for denials (deny a role a given permission) - +The Redis/MongoDB suites spin up real databases with [testcontainers](https://testcontainers.com/) (needs Docker). The Memory and unit suites run without Docker. ## License -(The MIT License) - -Copyright (c) 2011-2013 Manuel Astudillo - -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. +[MIT](LICENSE). Original work © 2011-2013 Manuel Astudillo; modernization © 2026 contributors. From 8df26edb44b0b9a28489cc805cfef22d48bd6f45 Mon Sep 17 00:00:00 2001 From: Nordin Date: Mon, 22 Jun 2026 12:39:21 +0800 Subject: [PATCH 13/13] build: make package publish-ready - package.json: repository/homepage/bugs/author metadata, publishConfig (access public), prepack + prepublishOnly (lint+typecheck+build) guards - fix dual-package types: split exports into import/require conditions so CJS resolves dist/index.d.cts (publint clean, are-the-types-wrong all green across node10/node16 CJS+ESM/bundler) - widen runtime support to Node >=16 and downlevel tsup output to node16 - add .nvmrc (20) for the dev toolchain Co-Authored-By: Claude Opus 4.8 (1M context) --- .nvmrc | 1 + package.json | 29 ++++++++++++++++++++++++----- tsup.config.ts | 2 ++ 3 files changed, 27 insertions(+), 5 deletions(-) create mode 100644 .nvmrc diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..209e3ef --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20 diff --git a/package.json b/package.json index 3f19aaf..6a47f63 100644 --- a/package.json +++ b/package.json @@ -16,18 +16,32 @@ "typescript" ], "license": "MIT", + "author": "Nordin Zahari ", "contributors": [ "Manuel Astudillo (original node_acl author)" ], + "repository": { + "type": "git", + "url": "git+https://github.com/nordinz7/node_acl.git" + }, + "homepage": "https://github.com/nordinz7/node_acl#readme", + "bugs": { + "url": "https://github.com/nordinz7/node_acl/issues" + }, "type": "module", "main": "./dist/index.cjs", "module": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "require": "./dist/index.cjs" + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } } }, "files": [ @@ -35,7 +49,7 @@ ], "sideEffects": false, "engines": { - "node": ">=18" + "node": ">=16" }, "scripts": { "build": "tsup", @@ -44,7 +58,12 @@ "test:unit": "vitest run --dir test/unit", "test:watch": "vitest", "lint": "biome check .", - "format": "biome format --write ." + "format": "biome format --write .", + "prepack": "npm run build", + "prepublishOnly": "npm run lint && npm run typecheck && npm run build" + }, + "publishConfig": { + "access": "public" }, "peerDependencies": { "mongodb": ">=5", diff --git a/tsup.config.ts b/tsup.config.ts index 906adf4..83c1d05 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -7,6 +7,8 @@ export default defineConfig({ clean: true, sourcemap: true, treeshake: true, + // Downlevel output so it runs on the widest supported Node (engines: >=16). + target: "node16", // redis / mongodb are optional peer deps — never bundle them. external: ["redis", "mongodb"], });