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/.nvmrc b/.nvmrc new file mode 100644 index 0000000..209e3ef --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..fabe38b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,63 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +`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 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 +``` + +**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 + +- **[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.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. + +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: + +- **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. + +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. + +`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). + +### Decoupling from drivers + +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 + +- 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/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. 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. 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. diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..0df05eb --- /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"] + }, + "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/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=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..6a47f63 100644 --- a/package.json +++ b/package.json @@ -1,32 +1,90 @@ { - "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" ], - "repository": "git://github.com/optimalbits/node_acl.git", - "author": "Manuel Astudillo ", - "homepage": "https://github.com/optimalbits/node_acl", - "engines": { - "node": ">= 0.10" + "license": "MIT", + "author": "Nordin Zahari ", + "contributors": [ + "Manuel Astudillo (original node_acl author)" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/nordinz7/node_acl.git" }, - "main": "./index.js", - "dependencies": { - "async": "^2.1.4", - "bluebird": "^3.0.2", - "lodash": "^4.17.3", - "mongodb": "^2.0.47", - "redis": "^2.2.5" + "homepage": "https://github.com/nordinz7/node_acl#readme", + "bugs": { + "url": "https://github.com/nordinz7/node_acl/issues" }, - "devDependencies": { - "mocha": "^3.2.0", - "chai": "^3.4.0" + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + } + }, + "files": [ + "dist" + ], + "sideEffects": false, + "engines": { + "node": ">=16" }, "scripts": { - "test": "mocha test/runner.js --reporter spec", - "cover": "istanbul cover -- _mocha test/runner.js --reporter spec" + "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 .", + "prepack": "npm run build", + "prepublishOnly": "npm run lint && npm run typecheck && npm run build" + }, + "publishConfig": { + "access": "public" + }, + "peerDependencies": { + "mongodb": ">=5", + "redis": ">=4" + }, + "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/acl.ts b/src/acl.ts new file mode 100644 index 0000000..226521a --- /dev/null +++ b/src/acl.ts @@ -0,0 +1,466 @@ +import { type AclMiddleware, type UserIdResolver, aclMiddleware } from "./middleware.js"; +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; + } + + /** + * 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 + // --------------------------------------------------------------------------- + + /** 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/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/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/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 new file mode 100644 index 0000000..0a62e78 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,50 @@ +/** + * 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"; + +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"; +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, + AllowRule, + Backend, + Buckets, + Key, + Logger, + OneOrMany, + Permission, + Resource, + Role, + StoredValue, + UserId, +} from "./types.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/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; +} diff --git a/test/backendtests.js b/test/backendtests.js deleted file mode 100644 index 476651f..0000000 --- a/test/backendtests.js +++ /dev/null @@ -1,71 +0,0 @@ -var chai = require('chai'); -var assert = chai.assert; -var expect = chai.expect; -var _ = require('lodash'); - -var testData = { - key1: ["1", "2", "3"], - key2: ["3", "2", "4"], - key3: ["3", "4", "5"] -}; -var buckets = ['bucket1', 'bucket2']; - -exports.unions = function() { - describe('unions', function() { - before(function(done) { - var backend = this.backend; - if (!backend.unions) { - this.skip(); - } - - backend.clean(function() { - var transaction = backend.begin(); - Object.keys(testData).forEach(function(key) { - buckets.forEach(function(bucket) { - backend.add(transaction, bucket, key, testData[key]); - }); - }); - backend.end(transaction, done); - }); - }); - - after(function(done) { - this.backend.clean(done); - }); - - it('should respond with an appropriate map', function(done) { - var expected = { - 'bucket1': ["1", "2", "3", "4", "5"], - 'bucket2': ["1", "2", "3", "4", "5"] - }; - this.backend.unions(buckets, Object.keys(testData), function(err, result) { - expect(err).to.be.null; - expect(result).to.be.eql(expected); - done(); - }); - }); - - it('should get only the specified keys', function(done) { - var expected = { - 'bucket1': ['1', '2', '3'], - 'bucket2': ['1', '2', '3'] - } - this.backend.unions(buckets, ['key1'], function(err, result) { - expect(err).to.be.null; - expect(result).to.be.eql(expected); - done(); - }); - }); - - it('should only get the specified buckets', function(done) { - var expected = { - 'bucket1': ['1', '2', '3'] - }; - this.backend.unions(['bucket1'], ['key1'], function(err, result) { - expect(err).to.be.null; - expect(result).to.be.eql(expected); - done(); - }); - }); - }); -}; 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/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/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(); +}); diff --git a/test/runner.js b/test/runner.js deleted file mode 100644 index 3092900..0000000 --- a/test/runner.js +++ /dev/null @@ -1,84 +0,0 @@ -var Acl = require('../') - , tests = require('./tests') - , backendTests = require('./backendtests'); - -describe('MongoDB - Default', function () { - before(function (done) { - var self = this - , mongodb = require('mongodb') - - mongodb.connect('mongodb://localhost:27017/acltest',function(error, db) { - db.dropDatabase(function () { - self.backend = new Acl.mongodbBackend(db, "acl") - done() - }) - }) - }) - - run() -}); - - -describe('MongoDB - useSingle', function () { - before(function (done) { - var self = this - , mongodb = require('mongodb') - - mongodb.connect('mongodb://localhost:27017/acltest',function(error, db) { - db.dropDatabase(function () { - self.backend = new Acl.mongodbBackend(db, "acl", true) - done() - }) - }) - }) - - run() -}); - -describe('Redis', function () { - before(function (done) { - var self = this - , options = { - host: '127.0.0.1', - port: 6379, - password: null - } - , Redis = require('redis') - - - var redis = Redis.createClient(options.port, options.host, {no_ready_check: true} ) - - function start(){ - self.backend = new Acl.redisBackend(redis) - done() - } - - if (options.password) { - redis.auth(options.password, start) - } else { - start() - } - }) - - run() -}) - - -describe('Memory', function () { - before(function () { - var self = this - self.backend = new Acl.memoryBackend() - }) - - run() -}) - -function run() { - Object.keys(tests).forEach(function (test) { - tests[test]() - }) - - Object.keys(backendTests).forEach(function (test) { - backendTests[test]() - }); -} 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"]); + }); + }); + }); +} 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/test/tests.js b/test/tests.js deleted file mode 100644 index 3f186f3..0000000 --- a/test/tests.js +++ /dev/null @@ -1,1195 +0,0 @@ -var Acl = require('../') - , assert = require('chai').assert - , expect = require('chai').expect; - -exports.Constructor = function () { - describe('constructor', function () { - it('should use default `buckets` names', function () { - var acl = new Acl(this.backend) - - expect(acl.options.buckets.meta).to.equal('meta') - expect(acl.options.buckets.parents).to.equal('parents') - expect(acl.options.buckets.permissions).to.equal('permissions') - expect(acl.options.buckets.resources).to.equal('resources') - expect(acl.options.buckets.roles).to.equal('roles') - expect(acl.options.buckets.users).to.equal('users') - }) - - it('should use given `buckets` names', function () { - var acl = new Acl(this.backend, null, { - buckets: { - meta: 'Meta', - parents: 'Parents', - permissions: 'Permissions', - resources: 'Resources', - roles: 'Roles', - users: 'Users' - } - }) - - expect(acl.options.buckets.meta).to.equal('Meta') - expect(acl.options.buckets.parents).to.equal('Parents') - expect(acl.options.buckets.permissions).to.equal('Permissions') - expect(acl.options.buckets.resources).to.equal('Resources') - expect(acl.options.buckets.roles).to.equal('Roles') - expect(acl.options.buckets.users).to.equal('Users') - }) - }) -} - -exports.Allows = function () { - describe('allow', function () { - - this.timeout(10000); - - it('guest to view blogs', function (done) { - var acl = new Acl(this.backend) - - acl.allow('guest', 'blogs', 'view', function (err) { - assert(!err) - done() - }) - }) - - it('guest to view forums', function (done) { - var acl = new Acl(this.backend) - - acl.allow('guest', 'forums', 'view', function (err) { - assert(!err) - done() - }) - }) - - it('member to view/edit/delete blogs', function (done) { - var acl = new Acl(this.backend) - - acl.allow('member', 'blogs', ['edit','view', 'delete'], function (err) { - assert(!err) - done() - }) - }) - }) - - describe('Add user roles', function () { - it('joed => 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/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([]); + }); +}); 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); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d4345df --- /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"] +} diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..83c1d05 --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["esm", "cjs"], + dts: true, + 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"], +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..4b2e3ee --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,11 @@ +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, + // Generous: a cold testcontainers image pull (Mongo/Redis) can take minutes. + hookTimeout: 300_000, + }, +});