diff --git a/packages/acl/.gitignore b/packages/acl/.gitignore index 9b65460b..439c95a5 100644 --- a/packages/acl/.gitignore +++ b/packages/acl/.gitignore @@ -2,33 +2,8 @@ .deno deno.lock deno.json._decorators - +package-lock.json # Dependencies node_modules/ # Build output build/ -out/ - -# Environment files -.env* -!src/.env/ -!src/.env/*md - -# Generated files -.dts -types/ -.D_Store -.vscode/!settings.json - -# Logs -*.Log -*.Log.* -docs-internal -systems/code-server-defaults -systems/workspace/kbot-docs -systems/.code-server/code-server-ipc.sock -systems/.code-server/User/workspaceStorage/ -systems/code-server-defaults -systems/.code-server -tests/assets/ -packages/kbot/systems/gptr/gpt-researcher diff --git a/packages/acl/README.md b/packages/acl/README.md index 730d92f5..daa497d2 100644 --- a/packages/acl/README.md +++ b/packages/acl/README.md @@ -184,7 +184,7 @@ class RedisBackend implements IBackend { ## Real-World Example — VFS Per-User Folder Permissions -This example demonstrates fine-grained access control for a virtual filesystem where each user owns a folder and can grant specific permissions to others. +Fine-grained, path-scoped access control for a virtual filesystem where each user owns a folder and can grant specific permissions to others — individually or via **groups**. ### How It Works @@ -193,20 +193,45 @@ Each user's VFS folder contains a `vfs-settings.json` that declares who can do w ```json { "owner": "3bb4cfbf-318b-44d3-a9d3-35680e738421", + "groups": [ + { + "name": "team", + "members": ["aaaaaaaa-...", "cccccccc-..."] + }, + { + "name": "viewers", + "members": ["dddddddd-...", "ffffffff-..."] + } + ], "acl": [ { - "userId": "aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb", + "group": "team", + "path": "/shared", + "permissions": ["read", "write", "list", "mkdir", "delete"] + }, + { + "group": "team", + "path": "/docs", "permissions": ["read", "list"] }, { - "userId": "cccccccc-4444-5555-6666-dddddddddddd", - "permissions": ["read", "write", "list", "mkdir", "delete"] + "group": "viewers", + "path": "/docs", + "permissions": ["read", "list"] + }, + { + "userId": "ffffffff-...", + "path": "/shared", + "permissions": ["read", "list"] } ] } ``` -The [VFS ACL bridge](src/vfs/vfs-acl.ts) loads these settings and translates them into ACL roles and grants. The [AclVfsClient](src/vfs/AclVfsClient.ts) then wraps `LocalVFS` and enforces permissions before every file operation. +- **Groups** bundle users — grant once, apply to all members. +- **Direct user grants** coexist with groups for individual overrides. +- **Path scoping** — grants apply to a specific folder and its children (`/shared`, `/docs`). +- The **owner** always gets `*` on `/` (entire tree). ### Permission Model @@ -221,9 +246,20 @@ The [VFS ACL bridge](src/vfs/vfs-acl.ts) loads these settings and translates the | `copy` | `copy` | | `*` | All of the above — auto-granted to the folder owner | -### Setup Flow +### Path-Scoped Resolution -When `loadVfsSettings` reads a user's settings file, it creates roles and grants in the ACL: +When a user accesses `/shared/reports/q1.pdf`, the guard walks the **resource chain** upward: + +``` +vfs::/shared/reports/q1.pdf ← most specific +vfs::/shared/reports +vfs::/shared ← team group matches here ✓ +vfs::/ +``` + +Access is granted if **any** level allows the operation. + +### Setup Flow ```mermaid sequenceDiagram @@ -235,51 +271,23 @@ sequenceDiagram Bridge->>Bridge: Read vfs-settings.json Bridge->>ACL: allow owner role, resource, wildcard Bridge->>ACL: addUserRoles owner, owner role - loop Each ACL entry + loop Each group ACL entry + Bridge->>ACL: allow group role, resource, permissions + loop Each group member + Bridge->>ACL: addUserRoles member, group role + end + end + loop Each direct ACL entry Bridge->>ACL: allow grant role, resource, permissions Bridge->>ACL: addUserRoles grantee, grant role end Bridge-->>App: Return settings ``` -### Allowed Request — Read-Only User Lists a Directory - -```mermaid -sequenceDiagram - participant User as Read-Only User - participant Client as AclVfsClient - participant ACL as Acl Instance - participant FS as LocalVFS - - User->>Client: readdir "." - Client->>ACL: isAllowed userId, vfs resource, "list" - ACL-->>Client: true - Client->>FS: readdir "." - FS-->>Client: File entries - Client-->>User: File entries -``` - -### Denied Request — Read-Only User Tries to Write - -```mermaid -sequenceDiagram - participant User as Read-Only User - participant Client as AclVfsClient - participant ACL as Acl Instance - - User->>Client: writefile "secret.txt", content - Client->>ACL: isAllowed userId, vfs resource, "write" - ACL-->>Client: false - Client-->>User: EACCES - lacks "write" permission - Note right of User: LocalVFS is never reached -``` - ### Code Example ```ts -import { Acl, MemoryBackend } from '@polymech/acl'; -import { loadVfsSettings } from '@polymech/acl/vfs/vfs-acl'; -import { AclVfsClient } from '@polymech/acl/vfs/AclVfsClient'; +import { Acl, MemoryBackend, AclVfsClient, loadVfsSettings } from '@polymech/acl'; // 1. Create ACL and load settings from the user's folder const acl = new Acl(new MemoryBackend()); @@ -288,43 +296,44 @@ await loadVfsSettings(acl, userDir); // 2. Create a guarded client for a specific caller const ownerId = '3bb4cfbf-318b-44d3-a9d3-35680e738421'; -const callerId = 'aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb'; // read-only +const callerId = 'aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb'; // team member const client = new AclVfsClient(acl, ownerId, callerId, { root: userDir, }); -// 3. Allowed — caller has "list" permission -const files = await client.readdir('.'); // ✓ works +// 3. Allowed — team has read+list on /docs +const files = await client.readdir('docs'); // ✓ works +const exists = await client.exists('docs/readme.txt'); // ✓ works -// 4. Allowed — caller has "read" permission -const exists = await client.exists('readme.txt'); // ✓ works +// 4. Allowed — team has full access on /shared +await client.writefile('shared/notes.txt', 'hello'); // ✓ works +await client.mkdir('shared/reports'); // ✓ works -// 5. Denied — caller lacks "write" permission -await client.writefile('hack.txt', '...'); // ✗ throws EACCES +// 5. Denied — team has no access on /private +await client.readdir('private'); // ✗ throws EACCES +await client.writefile('private/x.txt', '...'); // ✗ throws EACCES -// 6. Denied — caller lacks "mkdir" permission -await client.mkdir('new-folder'); // ✗ throws EACCES +// 6. Denied — team has read-only on /docs, no write +await client.writefile('docs/hack.txt', '...'); // ✗ throws EACCES ``` ### Test Fixtures ``` tests/ -├── config/ -│ └── vfs.json # Mount configuration -├── vfs/ -│ └── root/ -│ └── 3bb4cfbf-…/ -│ └── vfs-settings.json # Per-user ACL settings -├── vfs-acl.e2e.test.ts # 24 permission boundary tests -└── vfs-acl-fs.e2e.test.ts # 20 real filesystem tests -``` - -Run the full test suite: - -```bash -npx vitest run tests/ src/acl.test.ts # 67 tests total +├── vfs/root/ +│ ├── 3bb4cfbf-…/ +│ │ ├── vfs-settings.json # Path-scoped direct grants +│ │ ├── docs/readme.txt +│ │ ├── shared/data.txt +│ │ └── private/secret.txt +│ └── groups-test/ +│ └── vfs-settings.json # Group grants + mixed direct +├── vfs-acl.e2e.test.ts # 26 permission boundary tests +├── vfs-acl-fs.e2e.test.ts # 24 real filesystem tests +├── vfs-acl-paths.e2e.test.ts # 34 per-path nested tests +└── vfs-acl-groups.e2e.test.ts # 27 groups e2e tests ``` ## Scripts @@ -332,6 +341,7 @@ npx vitest run tests/ src/acl.test.ts # 67 tests total ```bash npm run build # Compile TypeScript npm run dev # Watch mode -npm run test:core # Run functional tests +npm run test:core # Core ACL tests (23) +npm run test:all # Full suite (134 tests) npm run lint # ESLint ``` diff --git a/packages/acl/dist-in/Acl.d.ts b/packages/acl/dist-in/Acl.d.ts new file mode 100644 index 00000000..acd274ec --- /dev/null +++ b/packages/acl/dist-in/Acl.d.ts @@ -0,0 +1,27 @@ +/** + * @polymech/acl — Core ACL class + * + * Zend_ACL-inspired role-based access control. + * Pure ESM, async/await, zero lodash/bluebird. + */ +import type { Logger } from 'pino'; +import type { AclGrant, AclOptions, AclResult, IBackend, IAcl, Value, Values } from './interfaces.js'; +export declare class Acl implements IAcl { + #private; + constructor(backend: IBackend, logger?: Logger, options?: AclOptions); + allow(rolesOrGrants: Values | AclGrant[], resources?: Values, permissions?: Values): Promise; + addUserRoles(userId: Value, roles: Values): Promise; + removeUserRoles(userId: Value, roles: Values): Promise; + userRoles(userId: Value): Promise>; + roleUsers(role: Value): Promise>; + hasRole(userId: Value, role: string): Promise>; + addRoleParents(role: string, parents: Values): Promise; + removeRoleParents(role: string, parents?: Values): Promise; + removeRole(role: string): Promise; + removeResource(resource: string): Promise; + removeAllow(role: string, resources: Values, permissions?: Values): Promise; + allowedPermissions(userId: Value, resources: Values): Promise>>; + isAllowed(userId: Value, resource: string, permissions: Values): Promise>; + areAnyRolesAllowed(roles: Values, resource: string, permissions: Values): Promise>; + whatResources(roles: Values, permissions?: Values): Promise | string[]>>; +} diff --git a/packages/acl/dist-in/Acl.js b/packages/acl/dist-in/Acl.js new file mode 100644 index 00000000..09edd996 --- /dev/null +++ b/packages/acl/dist-in/Acl.js @@ -0,0 +1,452 @@ +import { ok, okVoid, err } from './interfaces.js'; +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- +const toArray = (v) => Array.isArray(v) ? v : [v]; +const allowsBucket = (resource) => `allows_${resource}`; +const keyFromAllowsBucket = (str) => str.replace(/^allows_/, ''); +/** Set-based union of two arrays (deduped). */ +const union = (a, b) => [...new Set([...a, ...b])]; +/** Items in `a` that are not in `b`. */ +const difference = (a, b) => { + const set = new Set(b); + return a.filter((x) => !set.has(x)); +}; +/** Intersection of `a` and `b`. */ +const intersect = (a, b) => { + const set = new Set(b); + return a.filter((x) => set.has(x)); +}; +/** + * Validate that none of the values are empty/whitespace-only. + * Returns an AclErr if validation fails, otherwise undefined. + */ +const validateNonEmpty = (value, label) => { + const arr = Array.isArray(value) ? value : [value]; + for (const v of arr) { + const s = String(v).trim(); + if (!s) + return err('INVALID_INPUT', `${label} cannot be empty`); + } + return undefined; +}; +// --------------------------------------------------------------------------- +// Default bucket names +// --------------------------------------------------------------------------- +const DEFAULT_BUCKETS = { + meta: 'meta', + parents: 'parents', + permissions: 'permissions', + resources: 'resources', + roles: 'roles', + users: 'users', +}; +// --------------------------------------------------------------------------- +// ACL +// --------------------------------------------------------------------------- +export class Acl { + #backend; + #buckets; + #logger; + constructor(backend, logger, options) { + this.#backend = backend; + this.#logger = logger; + this.#buckets = { ...DEFAULT_BUCKETS, ...options?.buckets }; + } + // ------------------------------------------------------------------------- + // allow + // ------------------------------------------------------------------------- + async allow(rolesOrGrants, resources, permissions) { + try { + // Overload: allow(grants[]) + if (Array.isArray(rolesOrGrants) && rolesOrGrants.length > 0 && typeof rolesOrGrants[0] === 'object') { + return this.#allowBatch(rolesOrGrants); + } + const roles = toArray(rolesOrGrants); + const res = toArray(resources); + const perms = toArray(permissions); + const v1 = validateNonEmpty(roles, 'Role'); + if (v1) + return v1; + const v2 = validateNonEmpty(res, 'Resource'); + if (v2) + return v2; + const v3 = validateNonEmpty(perms, 'Permission'); + if (v3) + return v3; + const tx = await this.#backend.begin(); + await this.#backend.add(tx, this.#buckets.meta, 'roles', roles); + for (const resource of res) { + for (const role of roles) { + await this.#backend.add(tx, allowsBucket(String(resource)), role, perms); + } + } + for (const role of roles) { + await this.#backend.add(tx, this.#buckets.resources, role, res); + } + await this.#backend.end(tx); + this.#logger?.debug({ roles, resources: res, permissions: perms }, 'allow'); + return okVoid; + } + catch (e) { + return err('BACKEND_ERROR', e.message); + } + } + async #allowBatch(grants) { + for (const g of grants) { + for (const a of g.allows) { + const result = await this.allow(g.roles, a.resources, a.permissions); + if (!result.ok) + return result; + } + } + return okVoid; + } + // ------------------------------------------------------------------------- + // User ↔ Role + // ------------------------------------------------------------------------- + async addUserRoles(userId, roles) { + try { + const v1 = validateNonEmpty(userId, 'User ID'); + if (v1) + return v1; + const v2 = validateNonEmpty(roles, 'Role'); + if (v2) + return v2; + const rolesArr = toArray(roles); + const tx = await this.#backend.begin(); + await this.#backend.add(tx, this.#buckets.meta, 'users', userId); + await this.#backend.add(tx, this.#buckets.users, userId, roles); + for (const role of rolesArr) { + await this.#backend.add(tx, this.#buckets.roles, role, userId); + } + await this.#backend.end(tx); + return okVoid; + } + catch (e) { + return err('BACKEND_ERROR', e.message); + } + } + async removeUserRoles(userId, roles) { + try { + const v1 = validateNonEmpty(userId, 'User ID'); + if (v1) + return v1; + const v2 = validateNonEmpty(roles, 'Role'); + if (v2) + return v2; + const rolesArr = toArray(roles); + const tx = await this.#backend.begin(); + await this.#backend.remove(tx, this.#buckets.users, userId, roles); + for (const role of rolesArr) { + await this.#backend.remove(tx, this.#buckets.roles, role, userId); + } + await this.#backend.end(tx); + return okVoid; + } + catch (e) { + return err('BACKEND_ERROR', e.message); + } + } + async userRoles(userId) { + try { + const data = await this.#backend.get(this.#buckets.users, userId); + return ok(data); + } + catch (e) { + return err('BACKEND_ERROR', e.message); + } + } + async roleUsers(role) { + try { + const data = await this.#backend.get(this.#buckets.roles, role); + return ok(data); + } + catch (e) { + return err('BACKEND_ERROR', e.message); + } + } + async hasRole(userId, role) { + try { + const result = await this.userRoles(userId); + if (!result.ok) + return result; + return ok(result.data.includes(role)); + } + catch (e) { + return err('BACKEND_ERROR', e.message); + } + } + // ------------------------------------------------------------------------- + // Role hierarchy + // ------------------------------------------------------------------------- + async addRoleParents(role, parents) { + try { + const v1 = validateNonEmpty(role, 'Role'); + if (v1) + return v1; + const v2 = validateNonEmpty(parents, 'Parent role'); + if (v2) + return v2; + const tx = await this.#backend.begin(); + await this.#backend.add(tx, this.#buckets.meta, 'roles', role); + await this.#backend.add(tx, this.#buckets.parents, role, parents); + await this.#backend.end(tx); + return okVoid; + } + catch (e) { + return err('BACKEND_ERROR', e.message); + } + } + async removeRoleParents(role, parents) { + try { + const tx = await this.#backend.begin(); + if (parents) { + await this.#backend.remove(tx, this.#buckets.parents, role, parents); + } + else { + await this.#backend.del(tx, this.#buckets.parents, role); + } + await this.#backend.end(tx); + return okVoid; + } + catch (e) { + return err('BACKEND_ERROR', e.message); + } + } + async removeRole(role) { + try { + const v = validateNonEmpty(role, 'Role'); + if (v) + return v; + const resources = await this.#backend.get(this.#buckets.resources, role); + const tx = await this.#backend.begin(); + for (const resource of resources) { + await this.#backend.del(tx, allowsBucket(resource), role); + } + await this.#backend.del(tx, this.#buckets.resources, role); + await this.#backend.del(tx, this.#buckets.parents, role); + await this.#backend.del(tx, this.#buckets.roles, role); + await this.#backend.remove(tx, this.#buckets.meta, 'roles', role); + await this.#backend.end(tx); + return okVoid; + } + catch (e) { + return err('BACKEND_ERROR', e.message); + } + } + // ------------------------------------------------------------------------- + // Resources + // ------------------------------------------------------------------------- + async removeResource(resource) { + try { + const v = validateNonEmpty(resource, 'Resource'); + if (v) + return v; + const roles = await this.#backend.get(this.#buckets.meta, 'roles'); + const tx = await this.#backend.begin(); + await this.#backend.del(tx, allowsBucket(resource), roles); + for (const role of roles) { + await this.#backend.remove(tx, this.#buckets.resources, role, resource); + } + await this.#backend.end(tx); + return okVoid; + } + catch (e) { + return err('BACKEND_ERROR', e.message); + } + } + // ------------------------------------------------------------------------- + // Remove allow / permissions + // ------------------------------------------------------------------------- + async removeAllow(role, resources, permissions) { + try { + const v1 = validateNonEmpty(role, 'Role'); + if (v1) + return v1; + const v2 = validateNonEmpty(resources, 'Resource'); + if (v2) + return v2; + const res = toArray(resources); + const perms = permissions ? toArray(permissions) : undefined; + await this.#removePermissions(role, res, perms); + return okVoid; + } + catch (e) { + return err('BACKEND_ERROR', e.message); + } + } + async #removePermissions(role, resources, permissions) { + const tx = await this.#backend.begin(); + for (const resource of resources) { + const bucket = allowsBucket(resource); + if (permissions) { + await this.#backend.remove(tx, bucket, role, permissions); + } + else { + await this.#backend.del(tx, bucket, role); + await this.#backend.remove(tx, this.#buckets.resources, role, resource); + } + } + await this.#backend.end(tx); + // Clean up resources with empty permission sets (not fully atomic) + const tx2 = await this.#backend.begin(); + for (const resource of resources) { + const bucket = allowsBucket(resource); + const remaining = await this.#backend.get(bucket, role); + if (remaining.length === 0) { + await this.#backend.remove(tx2, this.#buckets.resources, role, resource); + } + } + await this.#backend.end(tx2); + } + // ------------------------------------------------------------------------- + // Permission queries + // ------------------------------------------------------------------------- + async allowedPermissions(userId, resources) { + try { + if (!userId) + return ok({}); + const res = toArray(resources); + const rolesResult = await this.userRoles(userId); + if (!rolesResult.ok) + return rolesResult; + const roles = rolesResult.data; + const result = {}; + for (const resource of res) { + result[resource] = await this.#resourcePermissions(roles, resource); + } + return ok(result); + } + catch (e) { + return err('BACKEND_ERROR', e.message); + } + } + async isAllowed(userId, resource, permissions) { + try { + const v1 = validateNonEmpty(userId, 'User ID'); + if (v1) + return v1; + const v2 = validateNonEmpty(resource, 'Resource'); + if (v2) + return v2; + const v3 = validateNonEmpty(permissions, 'Permission'); + if (v3) + return v3; + const roles = await this.#backend.get(this.#buckets.users, userId); + if (roles.length === 0) + return ok(false); + return this.areAnyRolesAllowed(roles, resource, permissions); + } + catch (e) { + return err('BACKEND_ERROR', e.message); + } + } + async areAnyRolesAllowed(roles, resource, permissions) { + try { + const rolesArr = toArray(roles); + if (rolesArr.length === 0) + return ok(false); + const permsArr = toArray(permissions); + const allowed = await this.#checkPermissions(rolesArr, resource, permsArr); + return ok(allowed); + } + catch (e) { + return err('BACKEND_ERROR', e.message); + } + } + // ------------------------------------------------------------------------- + // What resources + // ------------------------------------------------------------------------- + async whatResources(roles, permissions) { + try { + const rolesArr = toArray(roles); + const permsArr = permissions ? toArray(permissions) : undefined; + const data = await this.#permittedResources(rolesArr, permsArr); + return ok(data); + } + catch (e) { + return err('BACKEND_ERROR', e.message); + } + } + async #permittedResources(roles, permissions) { + const resources = await this.#rolesResources(roles); + if (!permissions) { + const result = {}; + for (const resource of resources) { + result[resource] = await this.#resourcePermissions(roles, resource); + } + return result; + } + const result = []; + for (const resource of resources) { + const perms = await this.#resourcePermissions(roles, resource); + if (intersect(permissions, perms).length > 0) { + result.push(resource); + } + } + return result; + } + // ------------------------------------------------------------------------- + // Private helpers + // ------------------------------------------------------------------------- + async #rolesParents(roles) { + return this.#backend.union(this.#buckets.parents, roles); + } + async #allRoles(roleNames) { + const parents = await this.#rolesParents(roleNames); + if (parents.length > 0) { + const parentRoles = await this.#allRoles(parents); + return union(roleNames, parentRoles); + } + return roleNames; + } + async #allUserRoles(userId) { + const result = await this.userRoles(userId); + const roles = result.ok ? result.data : []; + if (roles.length > 0) { + return this.#allRoles(roles); + } + return []; + } + async #rolesResources(roles) { + const allRoles = await this.#allRoles(roles); + let result = []; + for (const role of allRoles) { + const resources = await this.#backend.get(this.#buckets.resources, role); + result = result.concat(resources); + } + return [...new Set(result)]; + } + async #resourcePermissions(roles, resource) { + if (roles.length === 0) + return []; + const perms = await this.#backend.union(allowsBucket(resource), roles); + const parents = await this.#rolesParents(roles); + if (parents.length > 0) { + const parentPerms = await this.#resourcePermissions(parents, resource); + return union(perms, parentPerms); + } + return perms; + } + /** + * Recursively checks whether the given roles satisfy ALL requested permissions + * for a resource, walking up the parent hierarchy. + * + * NOTE: Does not handle circular parent graphs — will infinite-loop. + */ + async #checkPermissions(roles, resource, permissions) { + const resourcePerms = await this.#backend.union(allowsBucket(resource), roles); + if (resourcePerms.includes('*')) + return true; + const remaining = difference(permissions, resourcePerms); + if (remaining.length === 0) + return true; + const parents = await this.#backend.union(this.#buckets.parents, roles); + if (parents.length > 0) { + return this.#checkPermissions(parents, resource, remaining); + } + return false; + } +} +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiQWNsLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL0FjbC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFrQkEsT0FBTyxFQUFFLEVBQUUsRUFBRSxNQUFNLEVBQUUsR0FBRyxFQUFFLE1BQU0saUJBQWlCLENBQUM7QUFFbEQsOEVBQThFO0FBQzlFLFVBQVU7QUFDViw4RUFBOEU7QUFFOUUsTUFBTSxPQUFPLEdBQUcsQ0FBSSxDQUFVLEVBQU8sRUFBRSxDQUNuQyxLQUFLLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFFL0IsTUFBTSxZQUFZLEdBQUcsQ0FBQyxRQUFnQixFQUFVLEVBQUUsQ0FDOUMsVUFBVSxRQUFRLEVBQUUsQ0FBQztBQUV6QixNQUFNLG1CQUFtQixHQUFHLENBQUMsR0FBVyxFQUFVLEVBQUUsQ0FDaEQsR0FBRyxDQUFDLE9BQU8sQ0FBQyxVQUFVLEVBQUUsRUFBRSxDQUFDLENBQUM7QUFFaEMsK0NBQStDO0FBQy9DLE1BQU0sS0FBSyxHQUFHLENBQUksQ0FBTSxFQUFFLENBQU0sRUFBTyxFQUFFLENBQ3JDLENBQUMsR0FBRyxJQUFJLEdBQUcsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxFQUFFLEdBQUcsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBRS9CLHdDQUF3QztBQUN4QyxNQUFNLFVBQVUsR0FBRyxDQUFJLENBQU0sRUFBRSxDQUFNLEVBQU8sRUFBRTtJQUMxQyxNQUFNLEdBQUcsR0FBRyxJQUFJLEdBQUcsQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUN2QixPQUFPLENBQUMsQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBQ3hDLENBQUMsQ0FBQztBQUVGLG1DQUFtQztBQUNuQyxNQUFNLFNBQVMsR0FBRyxD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 \ No newline at end of file diff --git a/packages/acl/dist-in/acl.test.d.ts b/packages/acl/dist-in/acl.test.d.ts new file mode 100644 index 00000000..cb0ff5c3 --- /dev/null +++ b/packages/acl/dist-in/acl.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/acl/dist-in/acl.test.js b/packages/acl/dist-in/acl.test.js new file mode 100644 index 00000000..888eba37 --- /dev/null +++ b/packages/acl/dist-in/acl.test.js @@ -0,0 +1,225 @@ +/** + * @polymech/acl — Functional tests + * + * Exercises the full ACL API against the MemoryBackend. + */ +import { describe, it, expect, beforeEach } from 'vitest'; +import { Acl } from './Acl.js'; +import { MemoryBackend } from './data/MemoryBackend.js'; +/** Unwrap AclResult — asserts ok and returns data. */ +function d(result) { + if (!result.ok) + throw new Error(`Expected ok result, got ${result.code}: ${result.message}`); + return result.data; +} +describe('Acl (MemoryBackend)', () => { + let acl; + beforeEach(async () => { + const backend = new MemoryBackend(); + acl = new Acl(backend); + }); + // ------------------------------------------------------------------------- + // Roles ↔ Users + // ------------------------------------------------------------------------- + describe('addUserRoles / userRoles', () => { + it('assigns a single role to a user', async () => { + await acl.addUserRoles('user1', 'admin'); + const roles = d(await acl.userRoles('user1')); + expect(roles).toContain('admin'); + }); + it('assigns multiple roles', async () => { + await acl.addUserRoles('user2', ['editor', 'viewer']); + const roles = d(await acl.userRoles('user2')); + expect(roles).toEqual(expect.arrayContaining(['editor', 'viewer'])); + }); + }); + describe('removeUserRoles', () => { + it('removes a role from a user', async () => { + await acl.addUserRoles('user1', ['admin', 'editor']); + await acl.removeUserRoles('user1', 'admin'); + const roles = d(await acl.userRoles('user1')); + expect(roles).not.toContain('admin'); + expect(roles).toContain('editor'); + }); + }); + describe('roleUsers', () => { + it('returns users for a given role', async () => { + await acl.addUserRoles('u1', 'admin'); + await acl.addUserRoles('u2', 'admin'); + const users = d(await acl.roleUsers('admin')); + expect(users).toEqual(expect.arrayContaining(['u1', 'u2'])); + }); + }); + describe('hasRole', () => { + it('returns true when user has the role', async () => { + await acl.addUserRoles('u1', 'admin'); + expect(d(await acl.hasRole('u1', 'admin'))).toBe(true); + }); + it('returns false when user lacks the role', async () => { + await acl.addUserRoles('u1', 'editor'); + expect(d(await acl.hasRole('u1', 'admin'))).toBe(false); + }); + }); + // ------------------------------------------------------------------------- + // Permissions + // ------------------------------------------------------------------------- + describe('allow / isAllowed', () => { + it('grants and checks a simple permission', async () => { + await acl.addUserRoles('u1', 'editor'); + await acl.allow('editor', 'posts', 'edit'); + expect(d(await acl.isAllowed('u1', 'posts', 'edit'))).toBe(true); + expect(d(await acl.isAllowed('u1', 'posts', 'delete'))).toBe(false); + }); + it('grants multiple permissions at once', async () => { + await acl.addUserRoles('u1', 'admin'); + await acl.allow('admin', 'posts', ['create', 'edit', 'delete']); + expect(d(await acl.isAllowed('u1', 'posts', 'create'))).toBe(true); + expect(d(await acl.isAllowed('u1', 'posts', 'delete'))).toBe(true); + }); + it('supports wildcard * permission', async () => { + await acl.addUserRoles('u1', 'superadmin'); + await acl.allow('superadmin', 'everything', '*'); + expect(d(await acl.isAllowed('u1', 'everything', 'anything'))).toBe(true); + expect(d(await acl.isAllowed('u1', 'everything', 'whatever'))).toBe(true); + }); + }); + describe('allow — batch grant syntax', () => { + it('grants via AclGrant[] array', async () => { + await acl.addUserRoles('u1', 'member'); + await acl.allow([ + { + roles: 'member', + allows: [ + { resources: 'profile', permissions: ['read', 'update'] }, + { resources: 'feed', permissions: 'read' }, + ], + }, + ]); + expect(d(await acl.isAllowed('u1', 'profile', 'read'))).toBe(true); + expect(d(await acl.isAllowed('u1', 'profile', 'update'))).toBe(true); + expect(d(await acl.isAllowed('u1', 'feed', 'read'))).toBe(true); + expect(d(await acl.isAllowed('u1', 'feed', 'write'))).toBe(false); + }); + }); + // ------------------------------------------------------------------------- + // Role hierarchy + // ------------------------------------------------------------------------- + describe('addRoleParents — hierarchical permissions', () => { + it('inherits permissions from parent roles', async () => { + await acl.allow('viewer', 'docs', 'read'); + await acl.allow('editor', 'docs', 'write'); + await acl.addRoleParents('editor', 'viewer'); + await acl.addUserRoles('u1', 'editor'); + // editor can write (own) AND read (inherited from viewer) + expect(d(await acl.isAllowed('u1', 'docs', 'write'))).toBe(true); + expect(d(await acl.isAllowed('u1', 'docs', 'read'))).toBe(true); + }); + it('supports multi-level hierarchy', async () => { + await acl.allow('base', 'res', 'read'); + await acl.allow('mid', 'res', 'write'); + await acl.allow('top', 'res', 'admin'); + await acl.addRoleParents('mid', 'base'); + await acl.addRoleParents('top', 'mid'); + await acl.addUserRoles('u1', 'top'); + expect(d(await acl.isAllowed('u1', 'res', 'admin'))).toBe(true); + expect(d(await acl.isAllowed('u1', 'res', 'write'))).toBe(true); + expect(d(await acl.isAllowed('u1', 'res', 'read'))).toBe(true); + }); + }); + describe('removeRoleParents', () => { + it('cuts parent inheritance', async () => { + await acl.allow('viewer', 'docs', 'read'); + await acl.allow('editor', 'docs', 'write'); + await acl.addRoleParents('editor', 'viewer'); + await acl.addUserRoles('u1', 'editor'); + // Before removal: editor inherits viewer's read + expect(d(await acl.isAllowed('u1', 'docs', 'read'))).toBe(true); + await acl.removeRoleParents('editor', 'viewer'); + // After removal: editor only has write + expect(d(await acl.isAllowed('u1', 'docs', 'read'))).toBe(false); + expect(d(await acl.isAllowed('u1', 'docs', 'write'))).toBe(true); + }); + }); + // ------------------------------------------------------------------------- + // Removal + // ------------------------------------------------------------------------- + describe('removeRole', () => { + it('removes the role and its permissions', async () => { + await acl.allow('temp', 'stuff', 'do'); + await acl.addUserRoles('u1', 'temp'); + expect(d(await acl.isAllowed('u1', 'stuff', 'do'))).toBe(true); + await acl.removeRole('temp'); + // areAnyRolesAllowed should fail now for 'temp' + expect(d(await acl.areAnyRolesAllowed('temp', 'stuff', 'do'))).toBe(false); + }); + }); + describe('removeAllow', () => { + it('removes specific permissions', async () => { + await acl.allow('editor', 'posts', ['read', 'write', 'delete']); + await acl.addUserRoles('u1', 'editor'); + await acl.removeAllow('editor', 'posts', 'delete'); + expect(d(await acl.isAllowed('u1', 'posts', 'delete'))).toBe(false); + expect(d(await acl.isAllowed('u1', 'posts', 'read'))).toBe(true); + }); + it('removes all permissions for a resource when no perms specified', async () => { + await acl.allow('editor', 'posts', ['read', 'write']); + await acl.addUserRoles('u1', 'editor'); + await acl.removeAllow('editor', 'posts'); + expect(d(await acl.isAllowed('u1', 'posts', 'read'))).toBe(false); + expect(d(await acl.isAllowed('u1', 'posts', 'write'))).toBe(false); + }); + }); + describe('removeResource', () => { + it('removes the resource from all roles', async () => { + await acl.allow('admin', 'secrets', 'read'); + await acl.addUserRoles('u1', 'admin'); + expect(d(await acl.isAllowed('u1', 'secrets', 'read'))).toBe(true); + await acl.removeResource('secrets'); + expect(d(await acl.isAllowed('u1', 'secrets', 'read'))).toBe(false); + }); + }); + // ------------------------------------------------------------------------- + // Queries + // ------------------------------------------------------------------------- + describe('allowedPermissions', () => { + it('returns a map of resource → permissions', async () => { + await acl.addUserRoles('u1', 'editor'); + await acl.allow('editor', 'posts', ['read', 'write']); + await acl.allow('editor', 'comments', 'moderate'); + const perms = d(await acl.allowedPermissions('u1', ['posts', 'comments', 'settings'])); + expect(perms['posts']).toEqual(expect.arrayContaining(['read', 'write'])); + expect(perms['comments']).toContain('moderate'); + expect(perms['settings']).toEqual([]); + }); + it('returns {} for falsy userId', async () => { + const perms = d(await acl.allowedPermissions('', ['posts'])); + expect(perms).toEqual({}); + }); + }); + describe('areAnyRolesAllowed', () => { + it('returns true if at least one role has the permission', async () => { + await acl.allow('a', 'res', 'read'); + expect(d(await acl.areAnyRolesAllowed(['a', 'b'], 'res', 'read'))).toBe(true); + }); + it('returns false for empty roles', async () => { + expect(d(await acl.areAnyRolesAllowed([], 'res', 'read'))).toBe(false); + }); + }); + describe('whatResources', () => { + it('without permissions — returns resource→permissions map', async () => { + await acl.allow('editor', 'posts', ['read', 'write']); + await acl.allow('editor', 'pages', 'read'); + const result = d(await acl.whatResources('editor')); + expect(Object.keys(result)).toEqual(expect.arrayContaining(['posts', 'pages'])); + expect(result['posts']).toEqual(expect.arrayContaining(['read', 'write'])); + }); + it('with permissions — returns matching resource names', async () => { + await acl.allow('editor', 'posts', ['read', 'write']); + await acl.allow('editor', 'pages', 'read'); + const result = d(await acl.whatResources('editor', 'write')); + expect(result).toContain('posts'); + expect(result).not.toContain('pages'); + }); + }); +}); +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYWNsLnRlc3QuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvYWNsLnRlc3QudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7R0FJRztBQUNILE9BQU8sRUFBRSxRQUFRLEVBQUUsRUFBRSxFQUFFLE1BQU0sRUFBRSxVQUFVLEVBQUUsTUFBTSxRQUFRLENBQUM7QUFDMUQsT0FBTyxFQUFFLEdBQUcsRUFBRSxNQUFNLFVBQVUsQ0FBQztBQUMvQixPQUFPLEVBQUUsYUFBYSxFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFHeEQsc0RBQXNEO0FBQ3RELFNBQVMsQ0FBQyxDQUFJLE1BQW9CO0lBQzlCLElBQUksQ0FBQyxNQUFNLENBQUMsRUFBRTtRQUFFLE1BQU0sSUFBSSxLQUFLLENBQUMsMkJBQTJCLE1BQU0sQ0FBQyxJQUFJLEtBQUssTUFBTSxDQUFDLE9BQU8sRUFBRSxDQUFDLENBQUM7SUFDN0YsT0FBTyxNQUFNLENBQUMsSUFBSSxDQUFDO0FBQ3ZCLENBQUM7QUFFRCxRQUFRLENBQUMscUJBQXFCLEVBQUUsR0FBRyxFQUFFO0lBQ2pDLElBQUksR0FBUSxDQUFDO0lBRWIsVUFBVSxDQUFDLEtBQUssSUFBSSxFQUFFO1FBQ2xCLE1BQU0sT0FBTyxHQUFHLElBQUksYUFBYSxFQUFFLENBQUM7UUFDcEMsR0FBRyxHQUFHLElBQUksR0FBRyxDQUFDLE9BQU8sQ0FBQyxDQUFDO0lBQzNCLENBQUMsQ0FBQyxDQUFDO0lBRUgsNEVBQTRFO0lBQzVFLGdCQUFnQjtJQUNoQiw0RUFBNEU7SUFFNUUsUUFBUSxDQUFDLDBCQUEwQixFQUFFLEdBQUcsRUFBRTtRQUN0QyxFQUFFLENBQUMsaUNBQWlDLEVBQUUsS0FBSyxJQUFJLEVBQUU7WUFDN0MsTUFBTSxHQUFHLENBQUMsWUFBWSxDQUFDLE9BQU8sRUFBRSxPQUFPLENBQUMsQ0FBQztZQUN6QyxNQUFNLEtBQUssR0FBRyxDQUFDLENBQUMsTUFBTSxHQUFHLENBQUMsU0FBUyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUM7WUFDOUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDLFNBQVMsQ0FBQyxPQUFPLENBQUMsQ0FBQztRQUNyQyxDQUFDLENBQUMsQ0FBQztRQUVILEVBQUUsQ0FBQyx3QkFBd0IsRUFBRSxLQUFLLElBQUksRUFBRTtZQUNwQyxNQUFNLEdBQUcsQ0FBQyxZQUFZLENBQUMsT0FBTyxFQUFFLENBQUMsUUFBUSxFQUFFLFFBQVEsQ0FBQyxDQUFDLENBQUM7WUFDdEQsTUFBTSxLQUFLLEdBQUcsQ0FBQyxDQUFDLE1BQU0sR0FBRyxDQUFDLFNBQVMsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDO1lBQzlDLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQyxPQUFPLENBQUMsTUFBTSxDQUFDLGVBQWUsQ0FBQyxDQUFDLFFBQVEsRUFBRSxRQUFRLENBQUMsQ0FBQyxDQUFDLENBQUM7UUFDeEUsQ0FBQyxDQUFDLENBQUM7SUFDUCxDQUFDLENBQUMsQ0FBQztJQUVILFFBQVEsQ0FBQyxpQkFBaUIsRUFBRSxHQUFHLEVBQUU7UUFDN0IsRUFBRSxDQUFDLDRCQUE0QixFQUFFLEtBQUssSUFBSSxFQUFFO1lBQ3hDLE1BQU0sR0FBRyxDQUFDLFlBQVksQ0FBQyxPQUFPLEVBQUUsQ0FBQyxPQUFPLEVBQUUsUUFBUSxDQUFDLENBQUMsQ0FBQztZQUNyRCxNQUFNLEdBQUcsQ0FBQyxlQUFlLENBQUMsT0FBTyxFQUFFLE9BQU8sQ0FBQyxDQUFDO1lBQzVDLE1BQU0sS0FBSyxHQUFHLENBQUMsQ0FBQyxNQUFNLEdBQUcsQ0FBQyxTQUFTLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQztZQUM5QyxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUMsR0FBRyxDQUFDLFNBQVMsQ0FBQyxPQUFPLENBQUMsQ0FBQztZQUNyQyxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUMsU0FBUyxDQUFDLFFBQVEsQ0FBQyxDQUFDO1FBQ3RDLENBQUMsQ0FBQyxDQUFDO0lBQ1AsQ0FBQyxDQUFDLENBQUM7SUFFSCxRQUFRLENBQUMsV0FBVyxFQUFFLEdBQUcsRUFBRTtRQUN2QixFQUFFLENBQUMsZ0NBQWdDLEVBQUUsS0FBSyxJQUFJLEVBQUU7WUFDNUMsTUFBTSxHQUFHLENBQUMsWUFBWSxDQUFDLElBQUksRUFBRSxPQUFPLENBQUMsQ0FBQztZQUN0QyxNQUFNLEdBQUcsQ0FBQyxZQUFZLENBQUMsSUFBSSxFQUFFLE9BQU8sQ0FBQyxDQUFDO1lBQ3RDLE1BQU0sS0FBSyxHQUFHLENBQUMsQ0FBQyxNQUFNLEdBQUcsQ0FBQyxTQUFTLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQztZQUM5QyxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUMsT0FBTyxDQUFDLE1BQU0sQ0FBQyxlQUFlLENBQUMsQ0FBQyxJQUFJLEVBQUUsSUFBSSxDQUFDLENBQUMsQ0FBQyxDQUFDO1FBQ2hFLENBQUMsQ0FBQyxDQUFDO0lBQ1AsQ0FBQyxDQUFDLENBQUM7SUFFSCxRQUFRLENBQUMsU0FBUyxFQUFFLEdBQUcsRUFBRTtRQUNyQixFQUFFLENBQUMscUNBQXFDLEVBQUUsS0FBSyxJQUFJLEVBQUU7WUFDakQsTUFBTSxHQUFHLENBQUMsWUFBWSxDQUFDLElBQUksRUFBRSxPQUFPLENBQUMsQ0FBQztZQUN0QyxNQUFNLENBQUMsQ0FBQyxDQUFDLE1BQU0sR0FBRyxDQUFDLE9BQU8sQ0FBQyxJQUFJLEVBQUUsT0FBTyxDQUFDLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUMzRCxDQUFDLENBQUMsQ0FBQztRQUVILEVBQUUsQ0FBQyx3Q0FBd0MsRUFBRSxLQUFLLElBQUksRUFBRTtZQUNwRCxNQUFNLEdBQUcsQ0FBQyxZQUFZLENBQUMsSUFBSSxFQUFFLFFBQVEsQ0FBQyxDQUFDO1lBQ3ZDLE1BQU0sQ0FBQyxDQUFDLENBQUMsTUFBTSxHQUFHLENBQUMsT0FBTyxDQUFDLElBQUksRUFBRSxPQUFPLENBQUMsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDO1FBQzVELENBQUMsQ0FBQyxDQUFDO0lBQ1AsQ0FBQyxDQUFDLENBQUM7SUFFSCw0RUFBNEU7SUFDNUUsY0FBYztJQUNkLDRFQUE0RTtJQUU1RSxRQUFRLENBQUMsbUJBQW1CLEVBQUUsR0FBRyxFQUFFO1FBQy9CLEVBQUUsQ0FBQyx1Q0FBdUMsRUFBRSxLQUFLLElBQUksRUFBRTtZQUNuRCxNQUFNLEdBQUcsQ0FBQyxZQUFZLENBQUMsSUFBSSxFQUFFLFFBQVEsQ0FBQyxDQUFDO1lBQ3ZDLE1BQU0sR0FBRyxDQUFDLEtBQUssQ0FBQyxRQUFRLEVBQUUsT0FBTyxFQUFFLE1BQU0sQ0FBQyxDQUFDO1lBRTNDLE1BQU0sQ0FBQyxDQUFDLENBQUMsTUFBTSxHQUFHLENBQUMsU0FBUyxDQUFDLElBQUksRUFBRSxPQUFPLEVBQUUsTUFBTSxDQUFDLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQztZQUNqRSxNQUFNLENBQUMsQ0FBQyxDQUFDLE1BQU0sR0FBRyxDQUFDLFNBQVMsQ0FBQyxJQUFJLEVBQUUsT0FBTyxFQUFFLFFBQVEsQ0FBQyxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUM7UUFDeEUsQ0FBQyxDQUFDLENBQUM7UUFFSCxFQUFFLENBQUMscUNBQXFDLEVBQUUsS0FBSyxJQUFJLEVBQUU7WUFDakQsTUFBTSxHQUFHLENBQUMsWUFBWSxDQUFDLElBQUksRUFBRSxPQUFPLENBQUMsQ0FBQztZQUN0QyxNQUFNLEdBQUcsQ0FBQyxLQUFLLENBQUMsT0FBTyxFQUFFLE9BQU8sRUFBRSxDQUFDLFFBQVEsRUFBRSxNQUFNLEVBQUUsUUFBUSxDQUFDLENBQUMsQ0FBQztZQUVoRSxNQUFNLENBQUMsQ0FBQyxDQUFDLE1BQU0sR0FBRyxDQUFDLFNBQVMsQ0FBQyxJQUFJLEVBQUUsT0FBTyxFQUFFLFFBQVEsQ0FBQyxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7WUFDbkUsTUFBTSxDQUFDLENBQUMsQ0FBQyxNQUFNLEdBQUcsQ0FBQyxTQUFTLENBQUMsSUFBSSxFQUFFLE9BQU8sRUFBRSxRQUFRLENBQUMsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDO1FBQ3ZFLENBQUMsQ0FBQyxDQUFDO1FBRUgsRUFBRSxDQUFDLGdDQUFnQyxFQUFFLEtBQUssSUFBSSxFQUFFO1lBQzVDLE1BQU0sR0FBRyxDQUFDLFlBQVksQ0FBQyxJQUFJLEVBQUUsWUFBWSxDQUFDLENBQUM7WUFDM0MsTUFBTSxHQUFHLENBQUMsS0FBSyxDQUFDLFlBQVksRUFBRSxZQUFZLEVBQUUsR0FBRyxDQUFDLENBQUM7WUFFakQsTUFBTSxDQUFDLENBQUMsQ0FBQyxNQUFNLEdBQUcsQ0FBQyxTQUFTLENBQUMsSUFBSSxFQUFFLFlBQVksRUFBRSxVQUFVLENBQUMsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDO1lBQzFFLE1BQU0sQ0FBQyxDQUFDLENBQUMsTUFBTSxHQUFHLENBQUMsU0FBUyxDQUFDLElBQUksRUFBRSxZQUFZLEVBQUUsVUFBVSxDQUFDLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUM5RSxDQUFDLENBQUMsQ0FBQztJQUNQLENBQUMsQ0FBQyxDQUFDO0lBRUgsUUFBUSxDQUFDLDRCQUE0QixFQUFFLEdBQUcsRUFBRTtRQUN4QyxFQUFFLENBQUMsNkJBQTZCLEVBQUUsS0FBSyxJQUFJLEVBQUU7WUFDekMsTUFBTSxHQUFHLENBQUMsWUFBWSxDQUFDLElBQUksRUFBRSxRQUFRLENBQUMsQ0FBQztZQUN2QyxNQUFNLEdBQUcsQ0FBQyxLQUFLLENBQUM7Z0JBQ1o7b0JBQ0ksS0FBSyxFQUFFLFFBQVE7b0JBQ2YsTUFBTSxFQUFFO3dCQUNKLEVBQUUsU0FBUyxFQUFFLFNBQVMsRUFBRSxXQUFXLEVBQUUsQ0FBQyxNQUFNLEVBQUUsUUFBUSxDQUFDLEVBQUU7d0JBQ3pELEVBQUUsU0FBUyxFQUFFLE1BQU0sRUFBRSxXQUFXLEVBQUUsTUFBTSxFQUFFO3FCQUM3QztpQkFDSjthQUNKLENBQUMsQ0FBQztZQUVILE1BQU0sQ0FBQyxDQUFDLENBQUMsTUFBTSxHQUFHLENBQUMsU0FBUyxDQUFDLElBQUksRUFBRSxTQUFTLEVBQUUsTUFBTSxDQUFDLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQztZQUNuRSxNQUFNLENBQUMsQ0FBQyxDQUFDLE1BQU0sR0FBRyxDQUFDLFNBQVMsQ0FBQyxJQUFJLEVBQUUsU0FBUyxFQUFFLFFBQVEsQ0FBQyxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7WUFDckUsTUFBTSxDQUFDLENBQUMsQ0FBQyxNQUFNLEdBQUcsQ0FBQyxTQUFTLENBQUMsSUFBSSxFQUFFLE1BQU0sRUFBRSxNQUFNLENBQUMsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDO1lBQ2hFLE1BQU0sQ0FBQyxDQUFDLENBQUMsTUFBTSxHQUFHLENBQUMsU0FBUyxDQUFDLElBQUksRUFBRSxNQUFNLEVBQUUsT0FBTyxDQUFDLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQztRQUN0RSxDQUFDLENBQUMsQ0FBQztJQUNQLENBQUMsQ0FBQyxDQUFDO0lBRUgsNEVBQTRFO0lBQzVFLGlCQUFpQjtJQUNqQiw0RUFBNEU7SUFFNUUsUUFBUSxDQUFDLDJDQUEyQyxFQUFFLEdBQUcsRUFBRTtRQUN2RCxFQUFFLENBQUMsd0NBQXdDLEVBQUUsS0FBSyxJQUFJLEVBQUU7WUFDcEQsTUFBTSxHQUFHLENBQUMsS0FBSyxDQUFDLFFBQVEsRUFBRSxNQUFNLEVBQUUsTUFBTSxDQUFDLENBQUM7WUFDMUMsTUFBTSxHQUFHLENBQUMsS0FBSyxDQUFDLFFBQVEsRUFBRSxNQUFNLEVBQUUsT0FBTyxDQUFDLENBQUM7WUFDM0MsTUFBTSxHQUFHLENBQUMsY0FBYyxDQUFDLFFBQVEsRUFBRSxRQUFRLENBQUMsQ0FBQztZQUM3QyxNQUFNLEdBQUcsQ0FBQyxZQUFZLENBQUMsSUFBSSxFQUFFLFFBQVEsQ0FBQyxDQUFDO1lBRXZDLDBEQUEwRDtZQUMxRCxNQUFNLENBQUMsQ0FBQyxDQUFDLE1BQU0sR0FBRyxDQUFDLFNBQVMsQ0FBQyxJQUFJLEVBQUUsTUFBTSxFQUFFLE9BQU8sQ0FBQyxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7WUFDakUsTUFBTSxDQUFDLENBQUMsQ0FBQyxNQUFNLEdBQUcsQ0FBQyxTQUFTLENBQUMsSUFBSSxFQUFFLE1BQU0sRUFBRSxNQUFNLENBQUMsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDO1FBQ3BFLENBQUMsQ0FBQyxDQUFDO1FBRUgsRUFBRSxDQUFDLGdDQUFnQyxFQUFFLEtBQUssSUFBSSxFQUFFO1lBQzVDLE1BQU0sR0FBRyxDQUFDLEtBQUssQ0FBQyxNQUFNLEVBQUUsS0FBSyxFQUFFLE1BQU0sQ0FBQyxDQUFDO1lBQ3ZDLE1BQU0sR0FBRyxDQUFDLEtBQUssQ0FBQyxLQUFLLEVBQUUsS0FBSyxFQUFFLE9BQU8sQ0FBQyxDQUFDO1lBQ3ZDLE1BQU0sR0FBRyxDQUFDLEtBQUssQ0FBQyxLQUFLLEVBQUUsS0FBSyxFQUFFLE9BQU8sQ0FBQyxDQUFDO1lBQ3ZDLE1BQU0sR0FBRyxDQUFDLGNBQWMsQ0FBQyxLQUFLLEVBQUUsTUFBTSxDQUFDLENBQUM7WUFDeEMsTUFBTSxHQUFHLENBQUMsY0FBYyxDQUFDLEtBQUssRUFBRSxLQUFLLENBQUMsQ0FBQztZQUN2QyxNQUFNLEdBQUcsQ0FBQyxZQUFZLENBQUMsSUFBSSxFQUFFLEtBQUssQ0FBQyxDQUFDO1lBRXBDLE1BQU0sQ0FBQyxDQUFDLENBQUMsTUFBTSxHQUFHLENBQUMsU0FBUyxDQUFDLElBQUksRUFBRSxLQUFLLEVBQUUsT0FBTyxDQUFDLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQztZQUNoRSxNQUFNLENBQUMsQ0FBQyxDQUFDLE1BQU0sR0FBRyxDQUFDLFNBQVMsQ0FBQyxJQUFJLEVBQUUsS0FBSyxFQUFFLE9BQU8sQ0FBQyxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7WUFDaEUsTUFBTSxDQUFDLENBQUMsQ0FBQyxNQUFNLEdBQUcsQ0FBQyxTQUFTLENBQUMsSUFBSSxFQUFFLEtBQUssRUFBRSxNQUFNLENBQUMsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDO1FBQ25FLENBQUMsQ0FBQyxDQUFDO0lBQ1AsQ0FBQyxDQUFDLENBQUM7SUFFSCxRQUFRLENBQUMsbUJBQW1CLEVBQUUsR0FBRyxFQUFFO1FBQy9CLEVBQUUsQ0FBQyx5QkFBeUIsRUFBRSxLQUFLLElBQUksRUFBRTtZQUNyQyxNQUFNLEdBQUcsQ0FBQyxLQUFLLENBQUMsUUFBUSxFQUFFLE1BQU0sRUFBRSxNQUFNLENBQUMsQ0FBQztZQUMxQyxNQUFNLEdBQUcsQ0FBQyxLQUFLLENBQUMsUUFBUSxFQUFFLE1BQU0sRUFBRSxPQUFPLENBQUMsQ0FBQztZQUMzQyxNQUFNLEdBQUcsQ0FBQyxjQUFjLENBQUMsUUFBUSxFQUFFLFFBQVEsQ0FBQyxDQUFDO1lBQzdDLE1BQU0sR0FBRyxDQUFDLFlBQVksQ0FBQyxJQUFJLEVBQUUsUUFBUSxDQUFDLENBQUM7WUFFdkMsZ0RBQWdEO1lBQ2hELE1BQU0sQ0FBQyxDQUFDLENBQUMsTUFBTSxHQUFHLENBQUMsU0FBUyxDQUFDLElBQUksRUFBRSxNQUFNLEVBQUUsTUFBTSxDQUFDLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQztZQUVoRSxNQUFNLEdBQUcsQ0FBQyxpQkFBaUIsQ0FBQyxRQUFRLEVBQUUsUUFBUSxDQUFDLENBQUM7WUFFaEQsdUNBQXVDO1lBQ3ZDLE1BQU0sQ0FBQyxDQUFDLENBQUMsTUFBTSxHQUFHLENBQUMsU0FBUyxDQUFDLElBQUksRUFBRSxNQUFNLEVBQUUsTUFBTSxDQUFDLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQztZQUNqRSxNQUFNLENBQUMsQ0FBQyxDQUFDLE1BQU0sR0FBRyxDQUFDLFNBQVMsQ0FBQyxJQUFJLEVBQUUsTUFBTSxFQUFFLE9BQU8sQ0FBQyxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7UUFDckUsQ0FBQyxDQUFDLENBQUM7SUFDUCxDQUFDLENBQUMsQ0FBQztJQUVILDRFQUE0RTtJQUM1RSxVQUFVO0lBQ1YsNEVBQTRFO0lBRTVFLFFBQVEsQ0FBQyxZQUFZLEVBQUUsR0FBRyxFQUFFO1FBQ3hCLEVBQUUsQ0FBQyxzQ0FBc0MsRUFBRSxLQUFLLElBQUksRUFBRTtZQUNsRCxNQUFNLEdBQUcsQ0FBQyxLQUFLLENBQUMsTUFBTSxFQUFFLE9BQU8sRUFBRSxJQUFJLENBQUMsQ0FBQztZQUN2QyxNQUFNLEdBQUcsQ0FBQyxZQUFZLENBQUMsSUFBSSxFQUFFLE1BQU0sQ0FBQyxDQUFDO1lBQ3JDLE1BQU0sQ0FBQyxDQUFDLENBQUMsTUFBTSxHQUFHLENBQUMsU0FBUyxDQUFDLElBQUksRUFBRSxPQUFPLEVBQUUsSUFBSSxDQUFDLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQztZQUUvRCxNQUFNLEdBQUcsQ0FBQyxVQUFVLENBQUMsTUFBTSxDQUFDLENBQUM7WUFDN0IsZ0RBQWdEO1lBQ2hELE1BQU0sQ0FBQyxDQUFDLENBQUMsTUFBTSxHQUFHLENBQUMsa0JBQWtCLENBQUMsTUFBTSxFQUFFLE9BQU8sRUFBRSxJQUFJLENBQUMsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDO1FBQy9FLENBQUMsQ0FBQyxDQUFDO0lBQ1AsQ0FBQyxDQUFDLENBQUM7SUFFSCxRQUFRLENBQUMsYUFBYSxFQUFFLEdBQUcsRUFBRTtRQUN6QixFQUFFLENBQUMsOEJBQThCLEVBQUUsS0FBSyxJQUFJLEVBQUU7WUFDMUMsTUFBTSxHQUFHLENBQUMsS0FBSyxDQUFDLFFBQVEsRUFBRSxPQUFPLEVBQUUsQ0FBQyxNQUFNLEVBQUUsT0FBTyxFQUFFLFFBQVEsQ0FBQyxDQUFDLENBQUM7WUFDaEUsTUFBTSxHQUFHLENBQUMsWUFBWSxDQUFDLElBQUksRUFBRSxRQUFRLENBQUMsQ0FBQztZQUV2QyxNQUFNLEdBQUcsQ0FBQyxXQUFXLENBQUMsUUFBUSxFQUFFLE9BQU8sRUFBRSxRQUFRLENBQUMsQ0FBQztZQUNuRCxNQUFNLENBQUMsQ0FBQyxDQUFDLE1BQU0sR0FBRyxDQUFDLFNBQVMsQ0FBQyxJQUFJLEVBQUUsT0FBTyxFQUFFLFFBQVEsQ0FBQyxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUM7WUFDcEUsTUFBTSxDQUFDLENBQUMsQ0FBQyxNQUFNLEdBQUcsQ0FBQyxTQUFTLENBQUMsSUFBSSxFQUFFLE9BQU8sRUFBRSxNQUFNLENBQUMsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDO1FBQ3JFLENBQUMsQ0FBQyxDQUFDO1FBRUgsRUFBRSxDQUFDLGdFQUFnRSxFQUFFLEtBQUssSUFBSSxFQUFFO1lBQzVFLE1BQU0sR0FBRyxDQUFDLEtBQUssQ0FBQyxRQUFRLEVBQUUsT0FBTyxFQUFFLENBQUMsTUFBTSxFQUFFLE9BQU8sQ0FBQyxDQUFDLENBQUM7WUFDdEQsTUFBTSxHQUFHLENBQUMsWUFBWSxDQUFDLElBQUksRUFBRSxRQUFRLENBQUMsQ0FBQztZQUV2QyxNQUFNLEdBQUcsQ0FBQyxXQUFXLENBQUMsUUFBUSxFQUFFLE9BQU8sQ0FBQyxDQUFDO1lBQ3pDLE1BQU0sQ0FBQyxDQUFDLENBQUMsTUFBTSxHQUFHLENBQUMsU0FBUyxDQUFDLElBQUksRUFBRSxPQUFPLEVBQUUsTUFBTSxDQUFDLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQztZQUNsRSxNQUFNLENBQUMsQ0FBQyxDQUFDLE1BQU0sR0FBRyxDQUFDLFNBQVMsQ0FBQyxJQUFJLEVBQUUsT0FBTyxFQUFFLE9BQU8sQ0FBQyxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUM7UUFDdkUsQ0FBQyxDQUFDLENBQUM7SUFDUCxDQUFDLENBQUMsQ0FBQztJQUVILFFBQVEsQ0FBQyxnQkFBZ0IsRUFBRSxHQUFHLEVBQUU7UUFDNUIsRUFBRSxDQUFDLHFDQUFxQyxFQUFFLEtBQUssSUFBSSxFQUFFO1lBQ2pELE1BQU0sR0FBRyxDQUFDLEtBQUssQ0FBQyxPQUFPLEVBQUUsU0FBUyxFQUFFLE1BQU0sQ0FBQyxDQUFDO1lBQzVDLE1BQU0sR0FBRyxDQUFDLFlBQVksQ0FBQyxJQUFJLEVBQUUsT0FBTyxDQUFDLENBQUM7WUFDdEMsTUFBTSxDQUFDLENBQUMsQ0FBQyxNQUFNLEdBQUcsQ0FBQyxTQUFTLENBQUMsSUFBSSxFQUFFLFNBQVMsRUFBRSxNQUFNLENBQUMsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDO1lBRW5FLE1BQU0sR0FBRyxDQUFDLGNBQWMsQ0FBQyxTQUFTLENBQUMsQ0FBQztZQUNwQyxNQUFNLENBQUMsQ0FBQyxDQUFDLE1BQU0sR0FBRyxDQUFDLFNBQVMsQ0FBQyxJQUFJLEVBQUUsU0FBUyxFQUFFLE1BQU0sQ0FBQyxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUM7UUFDeEUsQ0FBQyxDQUFDLENBQUM7SUFDUCxDQUFDLENBQUMsQ0FBQztJQUVILDRFQUE0RTtJQUM1RSxVQUFVO0lBQ1YsNEVBQTRFO0lBRTVFLFFBQVEsQ0FBQyxvQkFBb0IsRUFBRSxHQUFHLEVBQUU7UUFDaEMsRUFBRSxDQUFDLHlDQUF5QyxFQUFFLEtBQUssSUFBSSxFQUFFO1lBQ3JELE1BQU0sR0FBRyxDQUFDLFlBQVksQ0FBQyxJQUFJLEVBQUUsUUFBUSxDQUFDLENBQUM7WUFDdkMsTUFBTSxHQUFHLENBQUMsS0FBSyxDQUFDLFFBQVEsRUFBRSxPQUFPLEVBQUUsQ0FBQyxNQUFNLEVBQUUsT0FBTyxDQUFDLENBQUMsQ0FBQztZQUN0RCxNQUFNLEdBQUcsQ0FBQyxLQUFLLENBQUMsUUFBUSxFQUFFLFVBQVUsRUFBRSxVQUFVLENBQUMsQ0FBQztZQUVsRCxNQUFNLEtBQUssR0FBRyxDQUFDLENBQUMsTUFBTSxHQUFHLENBQUMsa0JBQWtCLENBQUMsSUFBSSxFQUFFLENBQUMsT0FBTyxFQUFFLFVBQVUsRUFBRSxVQUFVLENBQUMsQ0FBQyxDQUFDLENBQUM7WUFDdkYsTUFBTSxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxNQUFNLENBQUMsZUFBZSxDQUFDLENBQUMsTUFBTSxFQUFFLE9BQU8sQ0FBQyxDQUFDLENBQUMsQ0FBQztZQUMxRSxNQUFNLENBQUMsS0FBSyxDQUFDLFVBQVUsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLFVBQVUsQ0FBQyxDQUFDO1lBQ2hELE1BQU0sQ0FBQyxLQUFLLENBQUMsVUFBVSxDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUMsRUFBRSxDQUFDLENBQUM7UUFDMUMsQ0FBQyxDQUFDLENBQUM7UUFFSCxFQUFFLENBQUMsNkJBQTZCLEVBQUUsS0FBSyxJQUFJLEVBQUU7WUFDekMsTUFBTSxLQUFLLEdBQUcsQ0FBQyxDQUFDLE1BQU0sR0FBRyxDQUFDLGtCQUFrQixDQUFDLEVBQUUsRUFBRSxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsQ0FBQztZQUM3RCxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUMsT0FBTyxDQUFDLEVBQUUsQ0FBQyxDQUFDO1FBQzlCLENBQUMsQ0FBQyxDQUFDO0lBQ1AsQ0FBQyxDQUFDLENBQUM7SUFFSCxRQUFRLENBQUMsb0JBQW9CLEVBQUUsR0FBRyxFQUFFO1FBQ2hDLEVBQUUsQ0FBQyxzREFBc0QsRUFBRSxLQUFLLElBQUksRUFBRTtZQUNsRSxNQUFNLEdBQUcsQ0FBQyxLQUFLLENBQUMsR0FBRyxFQUFFLEtBQUssRUFBRSxNQUFNLENBQUMsQ0FBQztZQUNwQyxNQUFNLENBQUMsQ0FBQyxDQUFDLE1BQU0sR0FBRyxDQUFDLGtCQUFrQixDQUFDLENBQUMsR0FBRyxFQUFFLEdBQUcsQ0FBQyxFQUFFLEtBQUssRUFBRSxNQUFNLENBQUMsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDO1FBQ2xGLENBQUMsQ0FBQyxDQUFDO1FBRUgsRUFBRSxDQUFDLCtCQUErQixFQUFFLEtBQUssSUFBSSxFQUFFO1lBQzNDLE1BQU0sQ0FBQyxDQUFDLENBQUMsTUFBTSxHQUFHLENBQUMsa0JBQWtCLENBQUMsRUFBRSxFQUFFLEtBQUssRUFBRSxNQUFNLENBQUMsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDO1FBQzNFLENBQUMsQ0FBQyxDQUFDO0lBQ1AsQ0FBQyxDQUFDLENBQUM7SUFFSCxRQUFRLENBQUMsZUFBZSxFQUFFLEdBQUcsRUFBRTtRQUMzQixFQUFFLENBQUMsd0RBQXdELEVBQUUsS0FBSyxJQUFJLEVBQUU7WUFDcEUsTUFBTSxHQUFHLENBQUMsS0FBSyxDQUFDLFFBQVEsRUFBRSxPQUFPLEVBQUUsQ0FBQyxNQUFNLEVBQUUsT0FBTyxDQUFDLENBQUMsQ0FBQztZQUN0RCxNQUFNLEdBQUcsQ0FBQyxLQUFLLENBQUMsUUFBUSxFQUFFLE9BQU8sRUFBRSxNQUFNLENBQUMsQ0FBQztZQUUzQyxNQUFNLE1BQU0sR0FBRyxDQUFDLENBQUMsTUFBTSxHQUFHLENBQUMsYUFBYSxDQUFDLFFBQVEsQ0FBQyxDQUE2QixDQUFDO1lBQ2hGLE1BQU0sQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsT0FBTyxDQUFDLE1BQU0sQ0FBQyxlQUFlLENBQUMsQ0FBQyxPQUFPLEVBQUUsT0FBTyxDQUFDLENBQUMsQ0FBQyxDQUFDO1lBQ2hGLE1BQU0sQ0FBQyxNQUFNLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUMsTUFBTSxDQUFDLGVBQWUsQ0FBQyxDQUFDLE1BQU0sRUFBRSxPQUFPLENBQUMsQ0FBQyxDQUFDLENBQUM7UUFDL0UsQ0FBQyxDQUFDLENBQUM7UUFFSCxFQUFFLENBQUMsb0RBQW9ELEVBQUUsS0FBSyxJQUFJLEVBQUU7WUFDaEUsTUFBTSxHQUFHLENBQUMsS0FBSyxDQUFDLFFBQVEsRUFBRSxPQUFPLEVBQUUsQ0FBQyxNQUFNLEVBQUUsT0FBTyxDQUFDLENBQUMsQ0FBQztZQUN0RCxNQUFNLEdBQUcsQ0FBQyxLQUFLLENBQUMsUUFBUSxFQUFFLE9BQU8sRUFBRSxNQUFNLENBQUMsQ0FBQztZQUUzQyxNQUFNLE1BQU0sR0FBRyxDQUFDLENBQUMsTUFBTSxHQUFHLENBQUMsYUFBYSxDQUFDLFFBQVEsRUFBRSxPQUFPLENBQUMsQ0FBYSxDQUFDO1lBQ3pFLE1BQU0sQ0FBQyxNQUFNLENBQUMsQ0FBQyxTQUFTLENBQUMsT0FBTyxDQUFDLENBQUM7WUFDbEMsTUFBTSxDQUFDLE1BQU0sQ0FBQyxDQUFDLEdBQUcsQ0FBQyxTQUFTLENBQUMsT0FBTyxDQUFDLENBQUM7UUFDMUMsQ0FBQyxDQUFDLENBQUM7SUFDUCxDQUFDLENBQUMsQ0FBQztBQUNQLENBQUMsQ0FBQyxDQUFDIn0= \ No newline at end of file diff --git a/packages/acl/dist-in/data/FileBackend.d.ts b/packages/acl/dist-in/data/FileBackend.d.ts new file mode 100644 index 00000000..f55fc50d --- /dev/null +++ b/packages/acl/dist-in/data/FileBackend.d.ts @@ -0,0 +1,10 @@ +import { MemoryBackend } from './MemoryBackend.js'; +import type { IFileStore } from '../interfaces.js'; +export declare class FileBackend extends MemoryBackend implements IFileStore { + #private; + constructor(filePath: string); + /** Load stored ACL data from disk into memory. */ + read(path?: string): void; + /** Persist current ACL data to disk. */ + write(path?: string): void; +} diff --git a/packages/acl/dist-in/data/FileBackend.js b/packages/acl/dist-in/data/FileBackend.js new file mode 100644 index 00000000..1d746ce9 --- /dev/null +++ b/packages/acl/dist-in/data/FileBackend.js @@ -0,0 +1,44 @@ +/** + * @polymech/acl — File-backed storage + * + * Extends MemoryBackend with JSON read/write via node:fs. + * Intended for dev/testing use, not production. + */ +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { dirname } from 'node:path'; +import { MemoryBackend } from './MemoryBackend.js'; +export class FileBackend extends MemoryBackend { + #path; + constructor(filePath) { + super(); + this.#path = filePath; + } + /** Load stored ACL data from disk into memory. */ + read(path) { + const target = path ?? this.#path; + try { + const raw = readFileSync(target, 'utf8'); + this.buckets = JSON.parse(raw); + } + catch (err) { + const e = err; + if (e.code === 'ENOENT') { + mkdirSync(dirname(target), { recursive: true }); + return; + } + if (err instanceof SyntaxError) { + // Corrupt JSON → reset + writeFileSync(target, '', { mode: 0o600 }); + return; + } + throw err; + } + } + /** Persist current ACL data to disk. */ + write(path) { + const target = path ?? this.#path; + mkdirSync(dirname(target), { recursive: true }); + writeFileSync(target, JSON.stringify(this.buckets, null, 2), { mode: 0o600 }); + } +} +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiRmlsZUJhY2tlbmQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvZGF0YS9GaWxlQmFja2VuZC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7R0FLRztBQUNILE9BQU8sRUFBRSxTQUFTLEVBQUUsWUFBWSxFQUFFLGFBQWEsRUFBYyxNQUFNLFNBQVMsQ0FBQztBQUM3RSxPQUFPLEVBQUUsT0FBTyxFQUFFLE1BQU0sV0FBVyxDQUFDO0FBQ3BDLE9BQU8sRUFBRSxhQUFhLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQztBQUduRCxNQUFNLE9BQU8sV0FBWSxTQUFRLGFBQWE7SUFDakMsS0FBSyxDQUFTO0lBRXZCLFlBQVksUUFBZ0I7UUFDeEIsS0FBSyxFQUFFLENBQUM7UUFDUixJQUFJLENBQUMsS0FBSyxHQUFHLFFBQVEsQ0FBQztJQUMxQixDQUFDO0lBRUQsa0RBQWtEO0lBQ2xELElBQUksQ0FBQyxJQUFhO1FBQ2QsTUFBTSxNQUFNLEdBQUcsSUFBSSxJQUFJLElBQUksQ0FBQyxLQUFLLENBQUM7UUFDbEMsSUFBSSxDQUFDO1lBQ0QsTUFBTSxHQUFHLEdBQUcsWUFBWSxDQUFDLE1BQU0sRUFBRSxNQUFNLENBQUMsQ0FBQztZQUN6QyxJQUFJLENBQUMsT0FBTyxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUM7UUFDbkMsQ0FBQztRQUFDLE9BQU8sR0FBWSxFQUFFLENBQUM7WUFDcEIsTUFBTSxDQUFDLEdBQUcsR0FBNEIsQ0FBQztZQUN2QyxJQUFJLENBQUMsQ0FBQyxJQUFJLEtBQUssUUFBUSxFQUFFLENBQUM7Z0JBQ3RCLFNBQVMsQ0FBQyxPQUFPLENBQUMsTUFBTSxDQUFDLEVBQUUsRUFBRSxTQUFTLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQztnQkFDaEQsT0FBTztZQUNYLENBQUM7WUFDRCxJQUFJLEdBQUcsWUFBWSxXQUFXLEVBQUUsQ0FBQztnQkFDN0IsdUJBQXVCO2dCQUN2QixhQUFhLENBQUMsTUFBTSxFQUFFLEVBQUUsRUFBRSxFQUFFLElBQUksRUFBRSxLQUFLLEVBQUUsQ0FBQyxDQUFDO2dCQUMzQyxPQUFPO1lBQ1gsQ0FBQztZQUNELE1BQU0sR0FBRyxDQUFDO1FBQ2QsQ0FBQztJQUNMLENBQUM7SUFFRCx3Q0FBd0M7SUFDeEMsS0FBSyxDQUFDLElBQWE7UUFDZixNQUFNLE1BQU0sR0FBRyxJQUFJLElBQUksSUFBSSxDQUFDLEtBQUssQ0FBQztRQUNsQyxTQUFTLENBQUMsT0FBTyxDQUFDLE1BQU0sQ0FBQyxFQUFFLEVBQUUsU0FBUyxFQUFFLElBQUksRUFBRSxDQUFDLENBQUM7UUFDaEQsYUFBYSxDQUFDLE1BQU0sRUFBRSxJQUFJLENBQUMsU0FBUyxDQUFDLElBQUksQ0FBQyxPQUFPLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQyxFQUFFLEVBQUUsSUFBSSxFQUFFLEtBQUssRUFBRSxDQUFDLENBQUM7SUFDbEYsQ0FBQztDQUNKIn0= \ No newline at end of file diff --git a/packages/acl/dist-in/data/MemoryBackend.d.ts b/packages/acl/dist-in/data/MemoryBackend.d.ts new file mode 100644 index 00000000..1237fa9a --- /dev/null +++ b/packages/acl/dist-in/data/MemoryBackend.d.ts @@ -0,0 +1,25 @@ +/** + * @polymech/acl — In-memory backend + * + * Transaction = array of deferred mutations, executed on `end()`. + * All reads are synchronous (wrapped as async for the interface). + */ +import type { IBackend, Value, Values } from '../interfaces.js'; +type Transaction = (() => void)[]; +type BucketStore = Record>; +export declare class MemoryBackend implements IBackend { + #private; + /** Expose raw data (used by FileBackend for serialisation). */ + get buckets(): BucketStore; + set buckets(data: BucketStore); + begin(): Transaction; + end(transaction: Transaction): Promise; + clean(): Promise; + get(bucket: string, key: Value): Promise; + union(bucket: string, keys: Value[]): Promise; + unions(buckets: string[], keys: Value[]): Promise>; + add(transaction: Transaction, bucket: string, key: Value, values: Values): void; + del(transaction: Transaction, bucket: string, keys: Values): void; + remove(transaction: Transaction, bucket: string, key: Value, values: Values): void; +} +export {}; diff --git a/packages/acl/dist-in/data/MemoryBackend.js b/packages/acl/dist-in/data/MemoryBackend.js new file mode 100644 index 00000000..dc0d44ef --- /dev/null +++ b/packages/acl/dist-in/data/MemoryBackend.js @@ -0,0 +1,119 @@ +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- +function toArray(v) { + return Array.isArray(v) ? v : [v]; +} +function setUnion(a, b) { + return [...new Set([...a, ...b])]; +} +function setDifference(a, b) { + const set = new Set(b); + return a.filter((x) => !set.has(x)); +} +// --------------------------------------------------------------------------- +// MemoryBackend +// --------------------------------------------------------------------------- +export class MemoryBackend { + #buckets = {}; + /** Expose raw data (used by FileBackend for serialisation). */ + get buckets() { + return this.#buckets; + } + set buckets(data) { + this.#buckets = data; + } + // -- Transaction lifecycle ------------------------------------------------ + begin() { + return []; + } + async end(transaction) { + for (const fn of transaction) { + fn(); + } + } + async clean() { + this.#buckets = {}; + } + // -- Reads ---------------------------------------------------------------- + async get(bucket, key) { + return this.#buckets[bucket]?.[key] ?? []; + } + async union(bucket, keys) { + const b = this.#findBucket(bucket); + if (!b) + return []; + const result = []; + for (const key of keys) { + const vals = b[String(key)]; + if (vals) + result.push(...vals); + } + return [...new Set(result)]; + } + async unions(buckets, keys) { + const result = {}; + for (const bucket of buckets) { + const b = this.#buckets[bucket]; + if (!b) { + result[bucket] = []; + continue; + } + const merged = []; + for (const key of keys) { + const vals = b[String(key)]; + if (vals) + merged.push(...vals); + } + result[bucket] = [...new Set(merged)]; + } + return result; + } + // -- Writes (queued in transaction) --------------------------------------- + add(transaction, bucket, key, values) { + const vals = toArray(values).map(String); + transaction.push(() => { + this.#buckets[bucket] ??= {}; + const existing = this.#buckets[bucket][String(key)] ?? []; + this.#buckets[bucket][String(key)] = setUnion(existing, vals); + }); + } + del(transaction, bucket, keys) { + const keysArr = toArray(keys).map(String); + transaction.push(() => { + const b = this.#buckets[bucket]; + if (!b) + return; + for (const k of keysArr) { + delete b[k]; + } + }); + } + remove(transaction, bucket, key, values) { + const vals = toArray(values).map(String); + transaction.push(() => { + const b = this.#buckets[bucket]; + if (!b) + return; + const old = b[String(key)]; + if (old) { + b[String(key)] = setDifference(old, vals); + } + }); + } + // -- Internal ------------------------------------------------------------- + /** + * Find a bucket by exact name or by regex match (legacy compat). + */ + #findBucket(name) { + if (this.#buckets[name]) + return this.#buckets[name]; + for (const key of Object.keys(this.#buckets)) { + if (new RegExp(`^${key}$`).test(name)) { + return this.#buckets[key]; + } + } + return undefined; + } +} +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiTWVtb3J5QmFja2VuZC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9kYXRhL01lbW9yeUJhY2tlbmQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBV0EsOEVBQThFO0FBQzlFLFVBQVU7QUFDViw4RUFBOEU7QUFFOUUsU0FBUyxPQUFPLENBQUMsQ0FBUztJQUN0QixPQUFPLEtBQUssQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUN0QyxDQUFDO0FBRUQsU0FBUyxRQUFRLENBQUMsQ0FBVyxFQUFFLENBQVc7SUFDdEMsT0FBTyxDQUFDLEdBQUcsSUFBSSxHQUFHLENBQUMsQ0FBQyxHQUFHLENBQUMsRUFBRSxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUN0QyxDQUFDO0FBRUQsU0FBUyxhQUFhLENBQUMsQ0FBVyxFQUFFLENBQVc7SUFDM0MsTUFBTSxHQUFHLEdBQUcsSUFBSSxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDdkIsT0FBTyxDQUFDLENBQUMsTUFBTSxDQUFDLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUN4QyxDQUFDO0FBRUQsOEVBQThFO0FBQzlFLGdCQUFnQjtBQUNoQiw4RUFBOEU7QUFFOUUsTUFBTSxPQUFPLGFBQWE7SUFDdEIsUUFBUSxHQUFnQixFQUFFLENBQUM7SUFFM0IsK0RBQStEO0lBQy9ELElBQUksT0FBTztRQUNQLE9BQU8sSUFBSSxDQUFDLFFBQVEsQ0FBQztJQUN6QixDQUFDO0lBRUQsSUFBSSxPQUFPLENBQUMsSUFBaUI7UUFDekIsSUFBSSxDQUFDLFFBQVEsR0FBRyxJQUFJLENBQUM7SUFDekIsQ0FBQztJQUVELDRFQUE0RTtJQUU1RSxLQUFLO1FBQ0QsT0FBTyxFQUFFLENBQUM7SUFDZCxD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 \ No newline at end of file diff --git a/packages/acl/dist-in/index.d.ts b/packages/acl/dist-in/index.d.ts new file mode 100644 index 00000000..6cb72396 --- /dev/null +++ b/packages/acl/dist-in/index.d.ts @@ -0,0 +1,14 @@ +/** + * @polymech/acl — Public API + */ +export { Acl } from './Acl.js'; +export { MemoryBackend } from './data/MemoryBackend.js'; +export { FileBackend } from './data/FileBackend.js'; +export type { IAcl, IBackend, IFileStore, AclGrant, AclAllow, AclOptions, AclErrorCode, AclOk, AclErr, AclResult, BucketNames, Value, Values, } from './interfaces.js'; +export { ok, okVoid, err } from './interfaces.js'; +export { AclVfsClient } from './vfs/AclVfsClient.js'; +export { DecoratedVfsClient } from './vfs/DecoratedVfsClient.js'; +export { loadVfsSettings, vfsResource, resourceChain } from './vfs/vfs-acl.js'; +export type { VfsSettings, VfsAclEntry, VfsGroup } from './vfs/vfs-acl.js'; +export { DefaultSanitizers } from './vfs/sanitizers.js'; +export { assertNonEmpty, cleanPath, pathSegments, normalisePath, cleanPermission, cleanPermissions, isUuid, cleanUuid, cleanId, cleanGroupName, sanitizeSubpath, sanitizeWritePath, sanitizeFilename, } from './vfs/sanitizers.js'; diff --git a/packages/acl/dist-in/index.js b/packages/acl/dist-in/index.js new file mode 100644 index 00000000..06a0d8da --- /dev/null +++ b/packages/acl/dist-in/index.js @@ -0,0 +1,14 @@ +/** + * @polymech/acl — Public API + */ +export { Acl } from './Acl.js'; +export { MemoryBackend } from './data/MemoryBackend.js'; +export { FileBackend } from './data/FileBackend.js'; +export { ok, okVoid, err } from './interfaces.js'; +// VFS +export { AclVfsClient } from './vfs/AclVfsClient.js'; +export { DecoratedVfsClient } from './vfs/DecoratedVfsClient.js'; +export { loadVfsSettings, vfsResource, resourceChain } from './vfs/vfs-acl.js'; +export { DefaultSanitizers } from './vfs/sanitizers.js'; +export { assertNonEmpty, cleanPath, pathSegments, normalisePath, cleanPermission, cleanPermissions, isUuid, cleanUuid, cleanId, cleanGroupName, sanitizeSubpath, sanitizeWritePath, sanitizeFilename, } from './vfs/sanitizers.js'; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7O0dBRUc7QUFDSCxPQUFPLEVBQUUsR0FBRyxFQUFFLE1BQU0sVUFBVSxDQUFDO0FBQy9CLE9BQU8sRUFBRSxhQUFhLEVBQUUsTUFBTSx5QkFBeUIsQ0FBQztBQUN4RCxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFnQnBELE9BQU8sRUFBRSxFQUFFLEVBQUUsTUFBTSxFQUFFLEdBQUcsRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBRWxELE1BQU07QUFDTixPQUFPLEVBQUUsWUFBWSxFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFDckQsT0FBTyxFQUFFLGtCQUFrQixFQUFFLE1BQU0sNkJBQTZCLENBQUM7QUFDakUsT0FBTyxFQUFFLGVBQWUsRUFBRSxXQUFXLEVBQUUsYUFBYSxFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFFL0UsT0FBTyxFQUFFLGlCQUFpQixFQUFFLE1BQU0scUJBQXFCLENBQUM7QUFDeEQsT0FBTyxFQUNILGNBQWMsRUFDZCxTQUFTLEVBQUUsWUFBWSxFQUFFLGFBQWEsRUFDdEMsZUFBZSxFQUFFLGdCQUFnQixFQUNqQyxNQUFNLEVBQUUsU0FBUyxFQUFFLE9BQU8sRUFBRSxjQUFjLEVBQzFDLGVBQWUsRUFBRSxpQkFBaUIsRUFBRSxnQkFBZ0IsR0FDdkQsTUFBTSxxQkFBcUIsQ0FBQyJ9 \ No newline at end of file diff --git a/packages/acl/dist-in/interfaces.d.ts b/packages/acl/dist-in/interfaces.d.ts new file mode 100644 index 00000000..de2e6339 --- /dev/null +++ b/packages/acl/dist-in/interfaces.d.ts @@ -0,0 +1,80 @@ +/** + * @polymech/acl — Type definitions + * + * Pure ESM, zero external dependencies. + * All methods are async (native Promise). + */ +export type Value = string | number; +export type Values = Value | Value[]; +export type AclErrorCode = 'OK' | 'INVALID_INPUT' | 'NOT_FOUND' | 'BACKEND_ERROR'; +export interface AclOk { + readonly ok: true; + readonly code: 'OK'; + readonly data: T; +} +export interface AclErr { + readonly ok: false; + readonly code: Exclude; + readonly message: string; +} +export type AclResult = AclOk | AclErr; +export declare const ok: (data: T) => AclOk; +export declare const okVoid: AclOk; +export declare const err: (code: AclErr["code"], message: string) => AclErr; +export interface BucketNames { + readonly meta: string; + readonly parents: string; + readonly permissions: string; + readonly resources: string; + readonly roles: string; + readonly users: string; +} +export interface AclOptions { + buckets?: Partial; +} +/** + * Transaction-based storage backend. + * + * `T` is the transaction type (e.g. `(() => void)[]` for in-memory). + */ +export interface IBackend { + begin(): T | Promise; + end(transaction: T): Promise; + clean(): Promise; + get(bucket: string, key: Value): Promise; + union(bucket: string, keys: Value[]): Promise; + unions(buckets: string[], keys: Value[]): Promise>; + add(transaction: T, bucket: string, key: Value, values: Values): void | Promise; + del(transaction: T, bucket: string, keys: Values): void | Promise; + remove(transaction: T, bucket: string, key: Value, values: Values): void | Promise; +} +export interface IAcl { + allow(roles: Values, resources: Values, permissions: Values): Promise; + allow(grants: AclGrant[]): Promise; + addUserRoles(userId: Value, roles: Values): Promise; + removeUserRoles(userId: Value, roles: Values): Promise; + userRoles(userId: Value): Promise>; + roleUsers(role: Value): Promise>; + hasRole(userId: Value, role: string): Promise>; + addRoleParents(role: string, parents: Values): Promise; + removeRoleParents(role: string, parents?: Values): Promise; + removeRole(role: string): Promise; + removeResource(resource: string): Promise; + removeAllow(role: string, resources: Values, permissions?: Values): Promise; + allowedPermissions(userId: Value, resources: Values): Promise>>; + isAllowed(userId: Value, resource: string, permissions: Values): Promise>; + areAnyRolesAllowed(roles: Values, resource: string, permissions: Values): Promise>; + whatResources(roles: Values, permissions?: Values): Promise | string[]>>; +} +export interface AclGrant { + roles: Values; + allows: AclAllow[]; +} +export interface AclAllow { + resources: Values; + permissions: Values; +} +export interface IFileStore { + read(path?: string): void | Promise; + write(path?: string): void | Promise; +} diff --git a/packages/acl/dist-in/interfaces.js b/packages/acl/dist-in/interfaces.js new file mode 100644 index 00000000..2ac93823 --- /dev/null +++ b/packages/acl/dist-in/interfaces.js @@ -0,0 +1,11 @@ +/** + * @polymech/acl — Type definitions + * + * Pure ESM, zero external dependencies. + * All methods are async (native Promise). + */ +// Result constructors +export const ok = (data) => ({ ok: true, code: 'OK', data }); +export const okVoid = Object.freeze({ ok: true, code: 'OK', data: undefined }); +export const err = (code, message) => ({ ok: false, code, message }); +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW50ZXJmYWNlcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9pbnRlcmZhY2VzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7OztHQUtHO0FBaUNILHNCQUFzQjtBQUN0QixNQUFNLENBQUMsTUFBTSxFQUFFLEdBQUcsQ0FBSSxJQUFPLEVBQVksRUFBRSxDQUFDLENBQUMsRUFBRSxFQUFFLEVBQUUsSUFBSSxFQUFFLElBQUksRUFBRSxJQUFJLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQztBQUM3RSxNQUFNLENBQUMsTUFBTSxNQUFNLEdBQWdCLE1BQU0sQ0FBQyxNQUFNLENBQUMsRUFBRSxFQUFFLEVBQUUsSUFBSSxFQUFFLElBQUksRUFBRSxJQUFJLEVBQUUsSUFBSSxFQUFFLFNBQVMsRUFBRSxDQUFnQixDQUFDO0FBQzNHLE1BQU0sQ0FBQyxNQUFNLEdBQUcsR0FBRyxDQUFDLElBQW9CLEVBQUUsT0FBZSxFQUFVLEVBQUUsQ0FBQyxDQUFDLEVBQUUsRUFBRSxFQUFFLEtBQUssRUFBRSxJQUFJLEVBQUUsT0FBTyxFQUFFLENBQUMsQ0FBQyJ9 \ No newline at end of file diff --git a/packages/acl/dist-in/vfs/AclVfsClient.d.ts b/packages/acl/dist-in/vfs/AclVfsClient.d.ts new file mode 100644 index 00000000..575aee3d --- /dev/null +++ b/packages/acl/dist-in/vfs/AclVfsClient.d.ts @@ -0,0 +1,46 @@ +/** + * ACL-Guarded VFS Client + * + * Wraps `LocalVFS` and checks every operation against an `Acl` instance + * before delegating. Throws EACCES if the caller lacks the required permission. + * + * Path-aware: walks the resource chain from the target path up to the root + * and grants access if ANY ancestor resource allows the permission. + * + * Permission mapping: + * readfile / stat → "read" + * readdir → "list" + * writefile / mkfile → "write" + * mkdir / mkdirP → "mkdir" + * rmfile / rmdir → "delete" + * rename → "rename" + * copy → "copy" + */ +import type { ReadStream } from 'node:fs'; +import type { Acl } from '../Acl.js'; +import type { INode } from './fs/VFS.js'; +import { type IDefaultParameters } from './fs/Local.js'; +export declare class AclVfsClient { + #private; + /** + * @param acl Populated Acl instance (call `loadVfsSettings` first) + * @param ownerId UUID of the folder owner + * @param callerId UUID of the user performing the operation + * @param fsOpts LocalVFS options (must include `root`) + */ + constructor(acl: Acl, ownerId: string, callerId: string, fsOpts: IDefaultParameters); + stat(path: string): Promise; + readdir(path: string): Promise; + readfile(path: string, options?: Record): Promise<{ + stream: ReadStream; + meta: unknown; + }>; + exists(path: string): Promise; + writefile(path: string, content: string | Buffer, options?: Record): Promise; + mkfile(path: string): Promise; + mkdir(path: string): Promise; + rmfile(path: string): Promise; + rmdir(path: string): Promise; + rename(from: string, to: string): Promise; + copy(from: string, to: string): Promise; +} diff --git a/packages/acl/dist-in/vfs/AclVfsClient.js b/packages/acl/dist-in/vfs/AclVfsClient.js new file mode 100644 index 00000000..3b982986 --- /dev/null +++ b/packages/acl/dist-in/vfs/AclVfsClient.js @@ -0,0 +1,88 @@ +import { LocalVFS } from './fs/Local.js'; +import { resourceChain } from './vfs-acl.js'; +import { cleanUuid, sanitizeSubpath } from './sanitizers.js'; +export class AclVfsClient { + #acl; + #local; + #ownerId; + #callerId; + /** + * @param acl Populated Acl instance (call `loadVfsSettings` first) + * @param ownerId UUID of the folder owner + * @param callerId UUID of the user performing the operation + * @param fsOpts LocalVFS options (must include `root`) + */ + constructor(acl, ownerId, callerId, fsOpts) { + this.#acl = acl; + this.#local = new LocalVFS(fsOpts); + this.#ownerId = cleanUuid(ownerId); + this.#callerId = cleanUuid(callerId); + } + // ── Guards ────────────────────────────────────────────────────── + /** + * Walk the resource chain from most-specific path to root. + * If ANY level grants the permission, access is allowed. + * This means a grant on `/` covers the entire tree. + */ + async #guard(permission, path) { + const safePath = sanitizeSubpath(path); + const chain = resourceChain(this.#ownerId, safePath); + for (const resource of chain) { + const result = await this.#acl.isAllowed(this.#callerId, resource, permission); + if (result.ok && result.data) + return; + } + const err = new Error(`EACCES: user '${this.#callerId}' lacks '${permission}' on path '${path}'`); + err.code = 'EACCES'; + throw err; + } + // ── Read operations ───────────────────────────────────────────── + async stat(path) { + await this.#guard('read', path); + return this.#local.stat(path); + } + async readdir(path) { + await this.#guard('list', path); + return this.#local.readdir(path); + } + async readfile(path, options) { + await this.#guard('read', path); + return this.#local.readfile(path, options); + } + async exists(path) { + await this.#guard('read', path); + return this.#local.exists(path); + } + // ── Write operations ──────────────────────────────────────────── + async writefile(path, content, options) { + await this.#guard('write', path); + return this.#local.writefile(path, content, options); + } + async mkfile(path) { + await this.#guard('write', path); + return this.#local.mkfile(path); + } + async mkdir(path) { + await this.#guard('mkdir', path); + return this.#local.mkdir(path, { recursive: true }); + } + // ── Delete operations ─────────────────────────────────────────── + async rmfile(path) { + await this.#guard('delete', path); + return this.#local.rmfile(path); + } + async rmdir(path) { + await this.#guard('delete', path); + return this.#local.rmdir(path); + } + // ── Move / Copy ───────────────────────────────────────────────── + async rename(from, to) { + await this.#guard('rename', from); + return this.#local.rename(from, to); + } + async copy(from, to) { + await this.#guard('copy', from); + return this.#local.copy(from, to); + } +} +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiQWNsVmZzQ2xpZW50LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3Zmcy9BY2xWZnNDbGllbnQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBcUJBLE9BQU8sRUFBRSxRQUFRLEVBQTJCLE1BQU0sZUFBZSxDQUFDO0FBQ2xFLE9BQU8sRUFBRSxhQUFhLEVBQUUsTUFBTSxjQUFjLENBQUM7QUFDN0MsT0FBTyxFQUFFLFNBQVMsRUFBRSxlQUFlLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUU3RCxNQUFNLE9BQU8sWUFBWTtJQUNaLElBQUksQ0FBTTtJQUNWLE1BQU0sQ0FBVztJQUNqQixRQUFRLENBQVM7SUFDakIsU0FBUyxDQUFTO0lBRTNCOzs7OztPQUtHO0lBQ0gsWUFBWSxHQUFRLEVBQUUsT0FBZSxFQUFFLFFBQWdCLEVBQUUsTUFBMEI7UUFDL0UsSUFBSSxDQUFDLElBQUksR0FBRyxHQUFHLENBQUM7UUFDaEIsSUFBSSxDQUFDLE1BQU0sR0FBRyxJQUFJLFFBQVEsQ0FBQyxNQUFNLENBQUMsQ0FBQztRQUNuQyxJQUFJLENBQUMsUUFBUSxHQUFHLFNBQVMsQ0FBQyxPQUFPLENBQUMsQ0FBQztRQUNuQyxJQUFJLENBQUMsU0FBUyxHQUFHLFNBQVMsQ0FBQyxRQUFRLENBQUMsQ0FBQztJQUN6QyxDQUFDO0lBRUQsbUVBQW1FO0lBRW5FOzs7O09BSUc7SUFDSCxLQUFLLENBQUMsTUFBTSxDQUFDLFVBQWtCLEVBQUUsSUFBWTtRQUN6QyxNQUFNLFFBQVEsR0FBRyxlQUFlLENBQUMsSUFBSSxDQUFDLENBQUM7UUFDdkMsTUFBTSxLQUFLLEdBQUcsYUFBYSxDQUFDLElBQUksQ0FBQyxRQUFRLEVBQUUsUUFBUSxDQUFDLENBQUM7UUFFckQsS0FBSyxNQUFNLFFBQVEsSUFBSSxLQUFLLEVBQUUsQ0FBQztZQUMzQixNQUFNLE1BQU0sR0FBRyxNQUFNLElBQUksQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDLElBQUksQ0FBQyxTQUFTLEVB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 \ No newline at end of file diff --git a/packages/acl/dist-in/vfs/DecoratedVfsClient.d.ts b/packages/acl/dist-in/vfs/DecoratedVfsClient.d.ts new file mode 100644 index 00000000..89fc7dc2 --- /dev/null +++ b/packages/acl/dist-in/vfs/DecoratedVfsClient.d.ts @@ -0,0 +1,46 @@ +/** + * ACL-Guarded VFS Client — Decorator Edition 🎭 + * + * Same behaviour as AclVfsClient, but uses TC39 Stage-3 method decorators + * to declare permission guards declaratively instead of imperative #guard() calls. + * + * Usage: + * @aclGuard('read') → checks "read" permission on the first arg (path) + * @aclGuard('list') → checks "list" permission + * @aclGuard('write') → checks "write" permission + * …etc + * + * The decorator extracts `path` from the first argument and walks + * the resource chain from that path up to "/" — same as AclVfsClient. + */ +import type { ReadStream } from 'node:fs'; +import type { Acl } from '../Acl.js'; +import type { INode } from './fs/VFS.js'; +import { LocalVFS, type IDefaultParameters } from './fs/Local.js'; +/** + * Decorator-based ACL VFS client. + * + * Properties are public (not #private) so the decorator can access them. + * This is the trade-off: decorators can't reach private fields. + */ +export declare class DecoratedVfsClient { + readonly acl: Acl; + readonly local: LocalVFS; + readonly ownerId: string; + readonly callerId: string; + constructor(acl: Acl, ownerId: string, callerId: string, fsOpts: IDefaultParameters); + stat(path: string): Promise; + readdir(path: string): Promise; + readfile(path: string, options?: Record): Promise<{ + stream: ReadStream; + meta: unknown; + }>; + exists(path: string): Promise; + writefile(path: string, content: string | Buffer, options?: Record): Promise; + mkfile(path: string): Promise; + mkdir(path: string): Promise; + rmfile(path: string): Promise; + rmdir(path: string): Promise; + rename(from: string, to: string): Promise; + copy(from: string, to: string): Promise; +} diff --git a/packages/acl/dist-in/vfs/DecoratedVfsClient.js b/packages/acl/dist-in/vfs/DecoratedVfsClient.js new file mode 100644 index 00000000..94f691d7 --- /dev/null +++ b/packages/acl/dist-in/vfs/DecoratedVfsClient.js @@ -0,0 +1,100 @@ +import { LocalVFS } from './fs/Local.js'; +import { resourceChain } from './vfs-acl.js'; +import { cleanUuid, sanitizeSubpath } from './sanitizers.js'; +// --------------------------------------------------------------------------- +// Decorator factory +// --------------------------------------------------------------------------- +/** + * TC39 Stage-3 method decorator. + * + * Intercepts the method call, extracts the first argument as `path`, + * walks the resource chain, and throws EACCES if denied. + */ +function aclGuard(permission) { + return function (target, context) { + const methodName = String(context.name); + return async function (...args) { + const path = sanitizeSubpath(args[0]); + const chain = resourceChain(this.ownerId, path); + for (const resource of chain) { + const result = await this.acl.isAllowed(this.callerId, resource, permission); + if (result.ok && result.data) { + return target.call(this, ...args); + } + } + const err = new Error(`EACCES: user '${this.callerId}' lacks '${permission}' on path '${path}' [${methodName}]`); + err.code = 'EACCES'; + throw err; + }; + }; +} +// --------------------------------------------------------------------------- +// Client +// --------------------------------------------------------------------------- +/** + * Decorator-based ACL VFS client. + * + * Properties are public (not #private) so the decorator can access them. + * This is the trade-off: decorators can't reach private fields. + */ +export class DecoratedVfsClient { + acl; + local; + ownerId; + callerId; + constructor(acl, ownerId, callerId, fsOpts) { + this.acl = acl; + this.local = new LocalVFS(fsOpts); + this.ownerId = cleanUuid(ownerId); + this.callerId = cleanUuid(callerId); + } + // ── Read ──────────────────────────────────────────────────────── + @aclGuard('read') + async stat(path) { + return this.local.stat(path); + } + @aclGuard('list') + async readdir(path) { + return this.local.readdir(path); + } + @aclGuard('read') + async readfile(path, options) { + return this.local.readfile(path, options); + } + @aclGuard('read') + async exists(path) { + return this.local.exists(path); + } + // ── Write ─────────────────────────────────────────────────────── + @aclGuard('write') + async writefile(path, content, options) { + return this.local.writefile(path, content, options); + } + @aclGuard('write') + async mkfile(path) { + return this.local.mkfile(path); + } + @aclGuard('mkdir') + async mkdir(path) { + return this.local.mkdir(path, { recursive: true }); + } + // ── Delete ────────────────────────────────────────────────────── + @aclGuard('delete') + async rmfile(path) { + return this.local.rmfile(path); + } + @aclGuard('delete') + async rmdir(path) { + return this.local.rmdir(path); + } + // ── Move / Copy ───────────────────────────────────────────────── + @aclGuard('rename') + async rename(from, to) { + return this.local.rename(from, to); + } + @aclGuard('copy') + async copy(from, to) { + return this.local.copy(from, to); + } +} +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiRGVjb3JhdGVkVmZzQ2xpZW50LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3Zmcy9EZWNvcmF0ZWRWZnNDbGllbnQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBa0JBLE9BQU8sRUFBRSxRQUFRLEVBQTJCLE1BQU0sZUFBZSxDQUFDO0FBQ2xFLE9BQU8sRUFBRSxhQUFhLEVBQUUsTUFBTSxjQUFjLENBQUM7QUFDN0MsT0FBTyxFQUFFLFNBQVMsRUFBRSxlQUFlLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUU3RCw4RUFBOEU7QUFDOUUsb0JBQW9CO0FBQ3BCLDhFQUE4RTtBQUU5RTs7Ozs7R0FLRztBQUNILFNBQVMsUUFBUSxDQUFDLFVBQWtCO0lBQ2hDLE9BQU8sVUFDSCxNQUEyQyxFQUMzQyxPQUE0RTtRQUU1RSxNQUFNLFVBQVUsR0FBRyxNQUFNLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDO1FBRXhDLE9BQU8sS0FBSyxXQUFvQixHQUFHLElBQU87WUFDdEMsTUFBTSxJQUFJLEdBQUcsZUFBZSxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO1lBQ3RDLE1BQU0sS0FBSyxHQUFHLGFBQWEsQ0FBQyxJQUFJLENBQUMsT0FBTyxFQUFFLElBQUksQ0FBQyxDQUFDO1lBRWhELEtBQUssTUFBTSxRQUFRLElBQUksS0FBSyxFQUFFLENBQUM7Z0JBQzNCLE1BQU0sTUFBTSxHQUFHLE1BQU0sSUFBSSxDQUFDLEdBQUcsQ0FBQyxTQUFTLENBQUMsSUFBSSxDQUFDLFFBQVEsRUFBRSxRQUFRLEVBQUUsVUFBVSxDQUFDLENBQUM7Z0JBQzdFLElBQUksTUFBTSxDQUFDLEVBQUUsSUFBSSxNQUFNLENBQUMsSUFBSSxFQUFFLENBQUM7b0JBQzNCLE9BQU8sTUFBTSxDQUFDLElBQUksQ0FBQyxJQUFJLEVBQUUsR0FBRyxJQUFJLENBQUMsQ0FBQztnQkFDdEMsQ0FBQztZQUNMLENBQUM7WUFFRCxNQUFNLEdBQUcsR0FB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 \ No newline at end of file diff --git a/packages/acl/dist-in/vfs/fs/Local.d.ts b/packages/acl/dist-in/vfs/fs/Local.d.ts new file mode 100644 index 00000000..9cdcf66f --- /dev/null +++ b/packages/acl/dist-in/vfs/fs/Local.d.ts @@ -0,0 +1,49 @@ +import { type ReadStream } from 'fs'; +import type { INode } from './VFS.js'; +import type { FileResource } from './Resource.js'; +export interface IDefaultParameters { + readonly root: string; + nopty?: boolean; + local?: boolean; + metapath?: boolean; + defaultEnv?: any; + umask?: string | number; + checkSymlinks?: boolean; + wsmetapath?: boolean; + testing?: boolean; +} +export declare class LocalVFS { + private fsOptions; + private root; + private base; + private umask; + private ig; + constructor(fsOptions: IDefaultParameters, _resource?: FileResource); + /** + * Check if a relative path is ignored by .gitignore rules. + */ + isIgnored(relativePath: string): boolean; + /** + * SECURITY: Resolve a relative path to an absolute path within the jail. + * Layer 2 of defense-in-depth — uses sanitizeSubpath for input validation, + * then enforces the root boundary via realpath + prefix check. + */ + private resolvePath; + stat(path: string, _options?: any): Promise; + private createStatEntry; + readfile(path: string, options?: any): Promise<{ + stream: ReadStream; + meta: any; + }>; + writefile(path: string, content: string | Buffer, options?: any): Promise; + readdir(path: string, _options?: any): Promise; + mkfile(path: string, options?: any): Promise; + mkdir(path: string, options?: any): Promise; + mkdirP(path: string, options?: any): Promise; + rmfile(path: string, _options?: any): Promise; + rmdir(path: string, _options?: any): Promise; + rename(from: string, to: string, _options?: any): Promise; + copy(from: string, to: string, _options?: any): Promise; + exists(path: string): Promise; + private calcEtag; +} diff --git a/packages/acl/dist-in/vfs/fs/Local.js b/packages/acl/dist-in/vfs/fs/Local.js new file mode 100644 index 00000000..6b08baa7 --- /dev/null +++ b/packages/acl/dist-in/vfs/fs/Local.js @@ -0,0 +1,217 @@ +import { join, resolve as pathResolve, normalize as pathNormalize, dirname, basename, sep as pathSep, } from 'path'; +import { createReadStream, readFileSync, existsSync } from 'fs'; +import { sanitizeSubpath } from '../path-sanitizer.js'; +import { realpath, lstat, stat, readdir, mkdir, rm, rename, cp, writeFile, access } from 'fs/promises'; +import ignoreModule from 'ignore'; +import Mime from 'mime'; +// Handle CJS/ESM interop — ignore exports { default: fn, isPathValid: fn } +const createIgnore = (typeof ignoreModule === 'function' ? ignoreModule : ignoreModule.default); +export class LocalVFS { + fsOptions; + root; + base; + umask; + ig = null; + constructor(fsOptions, _resource) { + if (!fsOptions.root) { + throw new Error('root is a required option'); + } + this.fsOptions = fsOptions; + this.root = pathNormalize(fsOptions.root); + if (pathSep === '/' && !this.root.startsWith('/')) { + throw new Error('root path must start in /'); + } + if (!this.root.endsWith(pathSep)) { + this.root += pathSep; + } + if (pathSep === '\\') { + this.root = this.root.replace(/\\/g, '/'); + } + this.base = this.root.substr(0, this.root.length - 1); + this.umask = this.fsOptions.umask || Number.parseInt('0750', 8); + // Load .gitignore if present in mount root + const gitignorePath = join(this.root, '.gitignore'); + if (existsSync(gitignorePath)) { + const patterns = readFileSync(gitignorePath, 'utf-8'); + this.ig = createIgnore().add(patterns); + } + } + /** + * Check if a relative path is ignored by .gitignore rules. + */ + isIgnored(relativePath) { + if (!this.ig) + return false; + // Normalize: strip leading slash, use forward slashes + const clean = relativePath.replace(/\\/g, '/').replace(/^\//, ''); + if (!clean) + return false; + return this.ig.ignores(clean); + } + /** + * SECURITY: Resolve a relative path to an absolute path within the jail. + * Layer 2 of defense-in-depth — uses sanitizeSubpath for input validation, + * then enforces the root boundary via realpath + prefix check. + */ + async resolvePath(path, options) { + // Sanitize input through the shared module (strips traversal, null bytes, etc.) + // The sanitizer returns '' for root-directory references like '' or '.' + const sanitized = sanitizeSubpath(path); + // Join with root + let resolved = join(this.root, sanitized); + // Resolve symlinks to prevent symlink escape (default: on) + const checkSymlinks = options?.checkSymlinks !== false; + if (checkSymlinks) { + try { + resolved = await realpath(resolved); + } + catch { + // File may not exist yet (e.g. for write) — normalize instead + resolved = pathResolve(resolved); + } + } + else { + resolved = pathResolve(resolved); + } + // Normalize slashes (Windows compat) + resolved = resolved.replace(/\\/g, '/'); + // Enforce root jail — resolved path MUST be root or start with root/ + const norm = (s) => { + let n = s.replace(/\\/g, '/').replace(/\/+$/, ''); + if (pathSep === '\\') + n = n.toLowerCase(); + return n; + }; + const resolvedNorm = norm(resolved); + const rootNorm = norm(this.root); + if (resolvedNorm !== rootNorm && !resolvedNorm.startsWith(rootNorm + '/')) { + const err = new Error(`EACCESS: '${resolved}' escapes root '${this.root}'`); + err.code = 'EACCESS'; + throw err; + } + return resolved; + } + async stat(path, _options) { + const dir = await this.resolvePath(dirname(path)); + const file = basename(path); + path = join(dir, file); + return this.createStatEntry(file, path); + } + async createStatEntry(file, fullpath) { + const s = await lstat(fullpath); + const relativePath = '/' + fullpath.replace(/\\/g, '/').replace(this.root, ''); + const entry = { + name: file, + path: relativePath, + mime: Mime.getType(file) || '', + mtime: s.mtime.valueOf(), + size: s.size, + parent: dirname(relativePath), + type: '', + }; + if (s.isDirectory()) { + entry.mime = 'inode/directory'; + } + return entry; + } + async readfile(path, options = {}) { + const realp = await this.resolvePath(path); + const s = await stat(realp); + if (s.isDirectory()) { + const err = new Error('EISDIR: Requested resource is a directory'); + err.code = 'EISDIR'; + throw err; + } + const meta = { + size: s.size, + etag: this.calcEtag(s), + }; + if (options.head) { + return { stream: null, meta }; + } + const stream = createReadStream(realp, options); + return { stream, meta }; + } + async writefile(path, content, options = {}) { + const realp = await this.resolvePath(path); + // Ensure parent directory exists (equivalent to fs-extra outputFile) + await mkdir(dirname(realp), { recursive: true }); + await writeFile(realp, content, options); + } + async readdir(path, _options) { + const realp = await this.resolvePath(path); + const s = await stat(realp); + if (!s.isDirectory()) { + const err = new Error('ENOTDIR: Requested resource is not a directory'); + err.code = 'ENOTDIR'; + throw err; + } + const files = await readdir(realp); + // Filter out .gitignore'd entries + const filtered = this.ig + ? files.filter(file => { + // Build relative path from mount root for ignore matching + // Normalize both sides: strip trailing slashes and use forward slashes + const realpNorm = realp.replace(/\\/g, '/').replace(/\/+$/, ''); + const rootNorm = this.root.replace(/\/+$/, ''); + let relDir = realpNorm.startsWith(rootNorm) + ? realpNorm.substring(rootNorm.length).replace(/^\//, '') + : ''; + const relPath = relDir ? `${relDir}/${file}` : file; + return !this.ig.ignores(relPath); + }) + : files; + const entries = await Promise.all(filtered.map((file) => this.createStatEntry(file, join(realp, file)))); + return entries; + } + async mkfile(path, options = {}) { + await this.writefile(path, '', options); + } + async mkdir(path, options = {}) { + const realp = await this.resolvePath(path); + await mkdir(realp, options); + } + async mkdirP(path, options = {}) { + const realp = await this.resolvePath(path, { checkSymlinks: false }); + await mkdir(realp, { ...options, recursive: true }); + } + async rmfile(path, _options) { + const realp = await this.resolvePath(path); + await rm(realp, { recursive: true, force: true }); + } + async rmdir(path, _options = {}) { + const realp = await this.resolvePath(path); + await rm(realp, { recursive: true, force: true }); + } + async rename(from, to, _options = {}) { + const frompath = await this.resolvePath(from); + const topath = await this.resolvePath(to); + await rename(frompath, topath); + } + async copy(from, to, _options = {}) { + const frompath = await this.resolvePath(from); + const topath = await this.resolvePath(to); + await cp(frompath, topath, { recursive: true }); + } + async exists(path) { + try { + const realp = await this.resolvePath(path); + await access(realp); + return true; + } + catch (_err) { + return false; + } + } + calcEtag(s) { + return ((s.isFile() ? '' : 'W/') + + '"' + + (s.ino || 0).toString(36) + + '-' + + s.size.toString(36) + + '-' + + s.mtime.valueOf().toString(36) + + '"'); + } +} +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiTG9jYWwuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvdmZzL2ZzL0xvY2FsLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFDSCxJQUFJLEVBQ0osT0FBTyxJQUFJLFdBQVcsRUFDdEIsU0FBUyxJQUFJLGFBQWEsRUFDMUIsT0FBTyxFQUNQLFFBQVEsRUFDUixHQUFHLElBQUksT0FBTyxHQUNqQixNQUFNLE1BQU0sQ0FBQztBQUVkLE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSxZQUFZLEVBQUUsVUFBVSxFQUErQixNQUFNLElBQUksQ0FBQztBQUM3RixPQUFPLEVBQUUsZUFBZSxFQUFFLE1BQU0sc0JBQXNCLENBQUM7QUFDdkQsT0FBTyxFQUFFLFFBQVEsRUFBRSxLQUFLLEVBQUUsSUFBSSxFQUFFLE9BQU8sRUFBRSxLQUFLLEVBQUUsRUFBRSxFQUFFLE1BQU0sRUFBRSxFQUFFLEVBQUUsU0FBUyxFQUFFLE1BQU0sRUFBRSxNQUFNLGFBQWEsQ0FBQztBQUN2RyxPQUFPLFlBQVksTUFBTSxRQUFRLENBQUM7QUFJbEMsT0FBTyxJQUFJLE1BQU0sTUFBTSxDQUFDO0FBRXhCLDJFQUEyRTtBQUMzRSxNQUFNLFlBQVksR0FBYyxDQUFDLE9BQU8sWUFBWSxLQUFLLFVBQVUsQ0FBQyxDQUFDLENBQUMsWUFBWSxDQUFDLENBQUMsQ0FBRSxZQUFvQixDQUFDLE9BQU8sQ0FBQyxDQUFDO0FBY3BILE1BQU0sT0FBTyxRQUFRO0lBQ1QsU0FBUyxDQUFxQjtJQUM5QixJQUFJLENBQVM7SUFDYixJQUFJLENBQVM7SUFDYixLQUFLLENBQVM7SUFDZCxFQUFFLEdBQVEsSUFBSSxDQUFDO0lBRXZCLFlBQVksU0FBNkIsRUFBRSxTQUF3QjtRQUMvRCxJQUFJLENBQUMsU0FBUyxDQUFDLElBQUksRUFBRSxDQUFDO1lBQ2xCLE1BQU0sSUFBSSxLQUFLLENBQUMsMkJBQTJCLENBQUMsQ0FBQztRQUNqRCxDQUFDO1FBRUQsSUFBSSxDQUFD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 \ No newline at end of file diff --git a/packages/acl/dist-in/vfs/fs/Resource.d.ts b/packages/acl/dist-in/vfs/fs/Resource.d.ts new file mode 100644 index 00000000..02e49115 --- /dev/null +++ b/packages/acl/dist-in/vfs/fs/Resource.d.ts @@ -0,0 +1,55 @@ +export interface Hash { + [id: string]: T; +} +export interface List { + [index: number]: T; + length: number; +} +/** + * Interface of the simple literal object with any string keys. + */ +export interface IObjectLiteral { + [key: string]: any; +} +/** + * Represents some Type of the Object. + */ +export type ObjectType = (new () => T) | ((...args: never[]) => unknown); +/** + * Same as Partial but goes deeper and makes Partial all its properties and sub-properties. + */ +export type DeepPartial = { + [P in keyof T]?: DeepPartial; +}; +export interface IDelimitter { + begin: '%'; + end: '%'; +} +export declare enum EResourceType { + JS_HEADER_INCLUDE, + JS_HEADER_SCRIPT_TAG, + CSS, + FILE_PROXY +} +export interface IResource { + type?: EResourceType; + name?: string; + url?: string; + enabled?: boolean; + label?: string; +} +export type IResourceProperty = IObjectLiteral & {}; +export interface IFileResource { + readOnly?: boolean; + label?: string; + path?: string; + vfs?: string; + options?: IObjectLiteral; +} +export declare function DefaultDelimitter(): IDelimitter; +export interface IResourceDriven { + configPath?: string | null; + relativeVariables: any; + absoluteVariables: any; +} +export type FileResource = IResource & IFileResource; diff --git a/packages/acl/dist-in/vfs/fs/Resource.js b/packages/acl/dist-in/vfs/fs/Resource.js new file mode 100644 index 00000000..46076aff --- /dev/null +++ b/packages/acl/dist-in/vfs/fs/Resource.js @@ -0,0 +1,14 @@ +export var EResourceType; +(function (EResourceType) { + EResourceType[EResourceType["JS_HEADER_INCLUDE"] = 'JS-HEADER-INCLUDE'] = "JS_HEADER_INCLUDE"; + EResourceType[EResourceType["JS_HEADER_SCRIPT_TAG"] = 'JS-HEADER-SCRIPT-TAG'] = "JS_HEADER_SCRIPT_TAG"; + EResourceType[EResourceType["CSS"] = 'CSS'] = "CSS"; + EResourceType[EResourceType["FILE_PROXY"] = 'FILE_PROXY'] = "FILE_PROXY"; +})(EResourceType || (EResourceType = {})); +export function DefaultDelimitter() { + return { + begin: '%', + end: '%' + }; +} +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiUmVzb3VyY2UuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvdmZzL2ZzL1Jlc291cmNlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQThCQSxNQUFNLENBQU4sSUFBWSxhQUtYO0FBTEQsV0FBWSxhQUFhO0lBQ3hCLG1EQUFvQixtQkFBMEIsdUJBQUEsQ0FBQTtJQUM5QyxzREFBdUIsc0JBQTZCLDBCQUFBLENBQUE7SUFDcEQscUNBQU0sS0FBWSxTQUFBLENBQUE7SUFDbEIsNENBQWEsWUFBbUIsZ0JBQUEsQ0FBQTtBQUNqQyxDQUFDLEVBTFcsYUFBYSxLQUFiLGFBQWEsUUFLeEI7QUFvQkQsTUFBTSxVQUFVLGlCQUFpQjtJQUNoQyxPQUFPO1FBQ04sS0FBSyxFQUFFLEdBQUc7UUFDVixHQUFHLEVBQUUsR0FBRztLQUNSLENBQUM7QUFDSCxDQUFDIn0= \ No newline at end of file diff --git a/packages/acl/dist-in/vfs/fs/VFS.d.ts b/packages/acl/dist-in/vfs/fs/VFS.d.ts new file mode 100644 index 00000000..d7d919f4 --- /dev/null +++ b/packages/acl/dist-in/vfs/fs/VFS.d.ts @@ -0,0 +1,115 @@ +/** + * Node types + * + * @export + * @enum {string} + */ +export declare enum ENodeType { + FILE, + DIR, + SYMLINK, + OTHER, + BLOCK +} +/** + * General features of a VFS + * + * @export + * @enum {number} + */ +export declare enum ECapabilties { + VERSIONED = 0,// VCS + CHANGE_MESSAGE = 1,// changes require additional comments + META = 2,// more meta data per node + MIME = 3,// VFS has native methods to determine the mime type + AUTHORS = 4,// VFS nodes can have different owners/authors, used by VCS + META_TREE = 5,// VFS has non INode tree nodes (VCS branches, tags, commits,..) + ROOT = 6,// VFS can have an root path prefix, eg. the user's home directory, + REMOTE_CONNECTION = 7 +} +/** + * Supported file operations + * + * @export + * @enum {number} + */ +export declare enum EOperations { + LS = 0, + RENAME = 1, + COPY = 2, + DELETE = 3, + MOVE = 4, + GET = 5, + SET = 6 +} +/** + * General presentation structure for clients + * + * @export + * @interface INode + */ +export interface INode { + name: string; + path: string; + size: number; + mtime?: number; + mime?: string; + parent: string; + mount?: string; + children?: INode[]; + owner?: any; + _EX?: boolean; + isDir?: boolean; + directory?: boolean; + fileType?: string; + sizeBytes?: number; + type: string; +} +export type INodeEx = INode & { + err: any; + linkStatErr: any | null; + link: null; + linkErr: null; + linkStat: null; +}; +export interface VFS_PATH { + mount: string; + path: string; +} +export interface IMount { + name: string; + type: string; + path: string; +} +export interface IVFSConfig { + configPath: string; + mounts: IMount[]; +} +/** + * + * These flags are used to build the result, adaptive. + * @TODO: sync with dgrid#configureColumn + * @export + * @enum {number} + */ +export declare enum NODE_FIELDS { + SHOW_ISDIR = 1602, + SHOW_OWNER = 1604, + SHOW_MIME = 1608, + SHOW_SIZE = 1616, + SHOW_PERMISSIONS = 1632, + SHOW_TIME = 1633, + SHOW_FOLDER_SIZE = 1634, + SHOW_FOLDER_HIDDEN = 1635, + SHOW_TYPE = 1636, + SHOW_MEDIA_INFO = 1637 +} +export declare class MountManager { + private mounts; + constructor(mounts: IMount[]); + findByName(name: string): IMount | undefined; + resolve(vfsPath: string): { + mount: IMount; + path: string; + }; +} diff --git a/packages/acl/dist-in/vfs/fs/VFS.js b/packages/acl/dist-in/vfs/fs/VFS.js new file mode 100644 index 00000000..c697391d --- /dev/null +++ b/packages/acl/dist-in/vfs/fs/VFS.js @@ -0,0 +1,88 @@ +/** + * Node types + * + * @export + * @enum {string} + */ +export var ENodeType; +(function (ENodeType) { + ENodeType[ENodeType["FILE"] = 'file'] = "FILE"; + ENodeType[ENodeType["DIR"] = 'dir'] = "DIR"; + ENodeType[ENodeType["SYMLINK"] = 'symlink'] = "SYMLINK"; + ENodeType[ENodeType["OTHER"] = 'other'] = "OTHER"; + ENodeType[ENodeType["BLOCK"] = 'block'] = "BLOCK"; +})(ENodeType || (ENodeType = {})); +/** + * General features of a VFS + * + * @export + * @enum {number} + */ +export var ECapabilties; +(function (ECapabilties) { + ECapabilties[ECapabilties["VERSIONED"] = 0] = "VERSIONED"; + ECapabilties[ECapabilties["CHANGE_MESSAGE"] = 1] = "CHANGE_MESSAGE"; + ECapabilties[ECapabilties["META"] = 2] = "META"; + ECapabilties[ECapabilties["MIME"] = 3] = "MIME"; + ECapabilties[ECapabilties["AUTHORS"] = 4] = "AUTHORS"; + ECapabilties[ECapabilties["META_TREE"] = 5] = "META_TREE"; + ECapabilties[ECapabilties["ROOT"] = 6] = "ROOT"; + ECapabilties[ECapabilties["REMOTE_CONNECTION"] = 7] = "REMOTE_CONNECTION"; // VFS has a remote connection +})(ECapabilties || (ECapabilties = {})); +/** + * Supported file operations + * + * @export + * @enum {number} + */ +export var EOperations; +(function (EOperations) { + EOperations[EOperations["LS"] = 0] = "LS"; + EOperations[EOperations["RENAME"] = 1] = "RENAME"; + EOperations[EOperations["COPY"] = 2] = "COPY"; + EOperations[EOperations["DELETE"] = 3] = "DELETE"; + EOperations[EOperations["MOVE"] = 4] = "MOVE"; + EOperations[EOperations["GET"] = 5] = "GET"; + EOperations[EOperations["SET"] = 6] = "SET"; +})(EOperations || (EOperations = {})); +/** + * + * These flags are used to build the result, adaptive. + * @TODO: sync with dgrid#configureColumn + * @export + * @enum {number} + */ +export var NODE_FIELDS; +(function (NODE_FIELDS) { + NODE_FIELDS[NODE_FIELDS["SHOW_ISDIR"] = 1602] = "SHOW_ISDIR"; + NODE_FIELDS[NODE_FIELDS["SHOW_OWNER"] = 1604] = "SHOW_OWNER"; + NODE_FIELDS[NODE_FIELDS["SHOW_MIME"] = 1608] = "SHOW_MIME"; + NODE_FIELDS[NODE_FIELDS["SHOW_SIZE"] = 1616] = "SHOW_SIZE"; + NODE_FIELDS[NODE_FIELDS["SHOW_PERMISSIONS"] = 1632] = "SHOW_PERMISSIONS"; + NODE_FIELDS[NODE_FIELDS["SHOW_TIME"] = 1633] = "SHOW_TIME"; + // @TODO: re-impl. du -ahs/x for windows + NODE_FIELDS[NODE_FIELDS["SHOW_FOLDER_SIZE"] = 1634] = "SHOW_FOLDER_SIZE"; + NODE_FIELDS[NODE_FIELDS["SHOW_FOLDER_HIDDEN"] = 1635] = "SHOW_FOLDER_HIDDEN"; + NODE_FIELDS[NODE_FIELDS["SHOW_TYPE"] = 1636] = "SHOW_TYPE"; + NODE_FIELDS[NODE_FIELDS["SHOW_MEDIA_INFO"] = 1637] = "SHOW_MEDIA_INFO"; +})(NODE_FIELDS || (NODE_FIELDS = {})); +export class MountManager { + mounts; + constructor(mounts) { + this.mounts = mounts; + } + findByName(name) { + return this.mounts.find(m => m.name === name); + } + resolve(vfsPath) { + const parts = vfsPath.split(':'); + const mountName = parts[0] ?? ''; + const path = parts.slice(1).join(':'); + const mount = this.findByName(mountName); + if (!mount) { + throw new Error(`Mount not found: ${mountName}`); + } + return { mount, path }; + } +} +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiVkZTLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL3Zmcy9mcy9WRlMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQ0E7Ozs7O0dBS0c7QUFDSCxNQUFNLENBQU4sSUFBWSxTQU1YO0FBTkQsV0FBWSxTQUFTO0lBQ3BCLDhCQUFPLE1BQWEsVUFBQSxDQUFBO0lBQ3BCLDZCQUFNLEtBQVksU0FBQSxDQUFBO0lBQ2xCLGlDQUFVLFNBQWdCLGFBQUEsQ0FBQTtJQUMxQiwrQkFBUSxPQUFjLFdBQUEsQ0FBQTtJQUN0QiwrQkFBUSxPQUFjLFdBQUEsQ0FBQTtBQUN2QixDQUFDLEVBTlcsU0FBUyxLQUFULFNBQVMsUUFNcEI7QUFDRDs7Ozs7R0FLRztBQUNILE1BQU0sQ0FBTixJQUFZLFlBU1g7QUFURCxXQUFZLFlBQVk7SUFDdkIseURBQWEsQ0FBQTtJQUNiLG1FQUFrQixDQUFBO0lBQ2xCLCtDQUFRLENBQUE7SUFDUiwrQ0FBUSxDQUFBO0lBQ1IscURBQVcsQ0FBQTtJQUNYLHlEQUFhLENBQUE7SUFDYiwrQ0FBUSxDQUFBO0lBQ1IseUVBQXFCLENBQUEsQ0FBQyw4QkFBOEI7QUFDckQsQ0FBQyxFQVRXLFlBQVksS0FBWixZQUFZLFFBU3ZCO0FBQ0Q7Ozs7O0dBS0c7QUFDSCxNQUFNLENBQU4sSUFBWSxXQVFYO0FBUkQsV0FBWSxXQUFXO0lBQ3RCLHlDQUFNLENBQUE7SUFDTixpREFBVSxDQUFBO0lBQ1YsNkNBQVEsQ0FBQTtJQUNSLGlEQUFVLENBQUE7SUFDViw2Q0FBUSxDQUFBO0lBQ1IsMkNBQU8sQ0FBQTtJQUNQLDJDQUFPLENBQUE7QUFDUixDQUFDLEVBUlcsV0FBVyxLQUFYLFdBQVcsUUFRdEI7QUFvREQ7Ozs7OztHQU1HO0FBQ0gsTUFBTSxDQUFOLElBQVksV0FZWDtBQVpELFdBQVksV0FBVztJQUN0Qiw0REFBaUIsQ0FBQTtJQUNqQiw0REFBaUIsQ0FBQTtJQUNqQiwwREFBZ0IsQ0FBQTtJQUNoQiwwREFBZ0IsQ0FBQTtJQUNoQix3RUFBdUIsQ0FBQTtJQUN2QiwwREFBZ0IsQ0FBQTtJQUNoQix3Q0FBd0M7SUFDeEMsd0VBQXVCLENBQUE7SUFDdkIsNEVBQXlCLENBQUE7SUFDekIsMERBQWdCLENBQUE7SUFDaEIsc0VBQXNCLENBQUE7QUFDdkIsQ0FBQyxFQVpXLFdBQVcsS0FBWCxXQUFXLFFBWXRCO0FBRUQsTUFBTSxPQUFPLFlBQVk7SUFDaEIsTUFBTSxDQUFXO0lBRXpCLFlBQVksTUFBZ0I7UUFDM0IsSUFBSSxDQUFDLE1BQU0sR0FBRyxNQUFNLENBQUM7SUFDdEIsQ0FBQztJQUVELFVBQVUsQ0FBQyxJQUFZO1FBQ3RCLE9BQU8sSUFBSSxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsSUFBSSxLQUFLLElBQUksQ0FBQyxDQUFDO0lBQy9DLENBQUM7SUFFRCxPQUFPLENBQUMsT0FBZTtRQUN0QixNQUFNLEtBQUssR0FBRyxPQUFPLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDO1FBQ2pDLE1BQU0sU0FBUyxHQUFHLEtBQUssQ0FBQyxDQUFDLENBQUMsSUFBSSxFQUFFLENBQUM7UUFDakMsTUFBTSxJQUFJLEdBQUcsS0FBSyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUM7UUFDdEMsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLFVBQVUsQ0FBQyxTQUFTLENBQUMsQ0FBQztRQUN6QyxJQUFJLENBQUMsS0FBSyxFQUFFLENBQUM7WUFDWixNQUFNLElBQUksS0FBSyxDQUFDLG9CQUFvQixTQUFTLEVBQUUsQ0FBQyxDQUFDO1FBQ2xELENBQUM7UUFDRCxPQUFPLEVBQUUsS0FBSyxFQUFFLElBQUksRUFBRSxDQUFDO0lBQ3hCLENBQUM7Q0FDRCJ9 \ No newline at end of file diff --git a/packages/acl/dist-in/vfs/fs/index.d.ts b/packages/acl/dist-in/vfs/fs/index.d.ts new file mode 100644 index 00000000..fd1d04c1 --- /dev/null +++ b/packages/acl/dist-in/vfs/fs/index.d.ts @@ -0,0 +1,2 @@ +export * from './Local.js'; +export * from './VFS.js'; diff --git a/packages/acl/dist-in/vfs/fs/index.js b/packages/acl/dist-in/vfs/fs/index.js new file mode 100644 index 00000000..21da123e --- /dev/null +++ b/packages/acl/dist-in/vfs/fs/index.js @@ -0,0 +1,3 @@ +export * from './Local.js'; +export * from './VFS.js'; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvdmZzL2ZzL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLGNBQWMsWUFBWSxDQUFDO0FBQzNCLGNBQWMsVUFBVSxDQUFDIn0= \ No newline at end of file diff --git a/packages/acl/dist-in/vfs/path-sanitizer.d.ts b/packages/acl/dist-in/vfs/path-sanitizer.d.ts new file mode 100644 index 00000000..c596d47c --- /dev/null +++ b/packages/acl/dist-in/vfs/path-sanitizer.d.ts @@ -0,0 +1,53 @@ +/** + * VFS Path Sanitizer — Defense-in-depth path validation. + * + * Combines filename sanitization (illegal chars, Windows reserved names, control chars) + * with path traversal prevention (encoded traversal, parent directory escape, null bytes, + * home directory escape, absolute path injection, double-encoding attacks). + * + * Usage: + * import { sanitizeSubpath, sanitizeFilename } from './path-sanitizer.js'; + * + * // For VFS subpaths (relative paths from mount root) + * const safe = sanitizeSubpath(userInput); // throws on violation + * + * // For individual filenames + * const name = sanitizeFilename(userInput); // strips illegal chars + */ +/** + * Sanitize and validate a VFS subpath (relative path from mount root). + * Rejects or strips dangerous patterns. Returns a clean, normalized relative path. + * + * Throws EFORBIDDEN on: + * - Null bytes + * - Double-encoded sequences (%25) + * - Home directory escape (~/) + * - Absolute paths (/foo, C:\foo) + * - Path traversal after full normalization + */ +export declare function sanitizeSubpath(input: string): string; +/** + * Sanitize every segment of a write-path by stripping unsafe characters and + * truncating overly long names. + * + * **Allowed characters**: `a-z A-Z 0-9 _ - (space)` + * + * Rules: + * - All characters outside the allowlist are stripped. + * - Directory segments have dots stripped entirely. + * - File segments (last) preserve the **last** dot + extension; extra dots are stripped. + * - Each segment is truncated to {@link MAX_SEGMENT_LENGTH} characters. + * - When `isDirectory` is true, ALL segments are treated as directories (no dots). + * + * Call this AFTER sanitizeSubpath — operates on the already-cleaned relative path. + * Returns the sanitized path (may differ from input). + */ +export declare function sanitizeWritePath(subpath: string, opts?: { + isDirectory?: boolean; +}): string; +/** + * Sanitize a single filename (not a path — no slashes allowed). + * Strips illegal characters, control characters, Windows reserved names. + * Returns the sanitized filename, truncated to 255 bytes. + */ +export declare function sanitizeFilename(input: string, replacement?: string): string; diff --git a/packages/acl/dist-in/vfs/path-sanitizer.js b/packages/acl/dist-in/vfs/path-sanitizer.js new file mode 100644 index 00000000..7dd9d3c7 --- /dev/null +++ b/packages/acl/dist-in/vfs/path-sanitizer.js @@ -0,0 +1,208 @@ +/** + * VFS Path Sanitizer — Defense-in-depth path validation. + * + * Combines filename sanitization (illegal chars, Windows reserved names, control chars) + * with path traversal prevention (encoded traversal, parent directory escape, null bytes, + * home directory escape, absolute path injection, double-encoding attacks). + * + * Usage: + * import { sanitizeSubpath, sanitizeFilename } from './path-sanitizer.js'; + * + * // For VFS subpaths (relative paths from mount root) + * const safe = sanitizeSubpath(userInput); // throws on violation + * + * // For individual filenames + * const name = sanitizeFilename(userInput); // strips illegal chars + */ +import { normalize } from 'path'; +// --- Filename sanitization regexes --- +/** Characters illegal in filenames on most filesystems */ +const ILLEGAL_CHARS_RE = /[/?<>\\:*|"]/g; +/** ASCII/Unicode control characters */ +const CONTROL_CHARS_RE = /[\x00-\x1f\x80-\x9f]/g; +/** Reserved filenames: just dots (., .., ...) */ +const RESERVED_DOTS_RE = /^\.+$/; +/** Windows reserved device names (CON, PRN, AUX, NUL, COM1-9, LPT1-9) */ +const WINDOWS_RESERVED_RE = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i; +/** Windows trailing dots/spaces (not allowed) */ +const WINDOWS_TRAILING_RE = /[. ]+$/; +// --- Path traversal regexes --- +/** Encoded traversal sequences to decode before checking */ +const DECODE_PATTERNS = [ + { regex: /%2e/gi, replacement: '.' }, + { regex: /%2f/gi, replacement: '/' }, + { regex: /%5c/gi, replacement: '\\' }, +]; +/** Parent directory traversal: /../, \..\, or standalone .. */ +const PARENT_DIR_RE = /[/\\]\.\.[/\\]/g; +/** Characters not allowed in VFS paths */ +const NOT_ALLOWED_RE = /[:$!'"`@+|=]/g; +// --- Error helper --- +function forbidden(reason) { + const err = new Error(`EFORBIDDEN: ${reason}`); + err.code = 'EFORBIDDEN'; + throw err; +} +// --- Public API --- +/** + * Sanitize and validate a VFS subpath (relative path from mount root). + * Rejects or strips dangerous patterns. Returns a clean, normalized relative path. + * + * Throws EFORBIDDEN on: + * - Null bytes + * - Double-encoded sequences (%25) + * - Home directory escape (~/) + * - Absolute paths (/foo, C:\foo) + * - Path traversal after full normalization + */ +export function sanitizeSubpath(input) { + if (typeof input !== 'string') { + forbidden('input must be a string'); + } + // 0. Empty path is valid (root directory) + if (!input || input.trim() === '') + return ''; + // ── TAMPER DETECTION (reject immediately) ────────────────────── + // 1. Reject null bytes immediately + if (input.includes('\0')) { + forbidden('null byte in path'); + } + // 2. Reject double-encoded sequences (%25 = encoded %) + if (/%25/i.test(input)) { + forbidden('double-encoded path segment'); + } + // 3. Reject home directory escape + if (input.startsWith('~/') || input === '~') { + forbidden('home directory access denied'); + } + // 4. Decode traversal-encoded sequences to inspect the real intent + let decoded = input; + for (const { regex, replacement } of DECODE_PATTERNS) { + decoded = decoded.replace(regex, replacement); + } + // 5. Strip not-allowed characters (=, $, etc.) to reveal hidden traversal + // e.g. ..=%5c → ..=\ → after strip = → ..\ → traversal! + const stripped = decoded.replace(NOT_ALLOWED_RE, ''); + // 6. Normalize backslashes to forward slashes for uniform detection + const normalized = stripped.replace(/[\\]/g, '/'); + // 7. DETECT TRAVERSAL — any ".." segment in the fully-decoded/stripped path + // is a tampering attempt, regardless of encoding tricks + if (PARENT_DIR_RE.test(normalized + '/') || // /../ mid-path + normalized.startsWith('../') || // ../ at start + normalized === '..' || // bare .. + normalized.endsWith('/..') || // /.. at end + /\/\.\.\//g.test('/' + normalized + '/')) { // catch-all + forbidden('path traversal'); + } + // 8. Strip leading slashes (common from URL path extraction, not an attack) + const cleaned = normalized.replace(/^\/+/, ''); + // 9. Reject absolute paths (Windows drive letters like C:\) + if (/^[a-zA-Z]:/.test(cleaned)) { + forbidden('absolute path'); + } + // ── NORMALIZATION (clean paths only reach here) ──────────────── + let sanitized = cleaned; + // 10. Collapse multiple slashes + sanitized = sanitized.replace(/\/+/g, '/'); + // 10. Use path.normalize for OS-level normalization + sanitized = normalize(sanitized); + // 11. Backslash-to-forward-slash (Windows normalize may re-introduce) + sanitized = sanitized.replace(/[\\]/g, '/'); + // 12. Strip leading/trailing slashes (must be relative) + sanitized = sanitized.replace(/^\/+/, '').replace(/\/+$/, ''); + // 13. Handle "." result (root directory reference) + sanitized = sanitized === '.' ? '' : sanitized; + // 14. FINAL GUARD — if any ".." segments survived, reject + const segments = sanitized.split('/'); + if (segments.some(s => s === '..')) { + forbidden('path traversal survived normalization'); + } + return sanitized; +} +// --- Write-operation name sanitization --- +/** Strip everything except alphanumeric, hyphens, underscores, spaces */ +const UNSAFE_CHARS_RE = /[^a-zA-Z0-9_\- ]/g; +/** Max length for a single path segment (leaves room within Windows 260-char path limit) */ +const MAX_SEGMENT_LENGTH = 200; +/** + * Sanitize every segment of a write-path by stripping unsafe characters and + * truncating overly long names. + * + * **Allowed characters**: `a-z A-Z 0-9 _ - (space)` + * + * Rules: + * - All characters outside the allowlist are stripped. + * - Directory segments have dots stripped entirely. + * - File segments (last) preserve the **last** dot + extension; extra dots are stripped. + * - Each segment is truncated to {@link MAX_SEGMENT_LENGTH} characters. + * - When `isDirectory` is true, ALL segments are treated as directories (no dots). + * + * Call this AFTER sanitizeSubpath — operates on the already-cleaned relative path. + * Returns the sanitized path (may differ from input). + */ +export function sanitizeWritePath(subpath, opts) { + if (!subpath) + return ''; + const segments = subpath.split('/'); + const allDirs = opts?.isDirectory ?? false; + const cleaned = []; + for (let i = 0; i < segments.length; i++) { + let seg = segments[i]; + if (!seg) + continue; + const isLast = i === segments.length - 1; + if (isLast && !allDirs) { + // --- File segment: preserve last extension --- + const lastDot = seg.lastIndexOf('.'); + if (lastDot > 0) { + const name = seg.substring(0, lastDot).replace(UNSAFE_CHARS_RE, '').replace(/\./g, ''); + const ext = seg.substring(lastDot + 1).replace(UNSAFE_CHARS_RE, ''); + seg = ext ? `${name}.${ext}` : name; + } + else { + seg = seg.replace(UNSAFE_CHARS_RE, ''); + } + } + else { + // --- Directory segment: strip dots AND unsafe chars --- + seg = seg.replace(UNSAFE_CHARS_RE, '').replace(/\./g, ''); + } + // Trim leading/trailing whitespace + seg = seg.trim(); + // Truncate to max length + if (seg.length > MAX_SEGMENT_LENGTH) { + seg = seg.substring(0, MAX_SEGMENT_LENGTH).trim(); + } + // Skip empty segments (result of stripping all chars) + if (seg) + cleaned.push(seg); + } + return cleaned.join('/'); +} +/** + * Sanitize a single filename (not a path — no slashes allowed). + * Strips illegal characters, control characters, Windows reserved names. + * Returns the sanitized filename, truncated to 255 bytes. + */ +export function sanitizeFilename(input, replacement = '') { + if (typeof input !== 'string') { + throw new Error('Input must be a string'); + } + let sanitized = input + .replace(ILLEGAL_CHARS_RE, replacement) + .replace(CONTROL_CHARS_RE, replacement) + .replace(RESERVED_DOTS_RE, replacement) + .replace(WINDOWS_RESERVED_RE, replacement) + .replace(WINDOWS_TRAILING_RE, replacement); + // Truncate to 255 bytes (UTF-8 safe) + const encoder = new TextEncoder(); + const encoded = encoder.encode(sanitized); + if (encoded.length > 255) { + const decoder = new TextDecoder(); + sanitized = decoder.decode(encoded.slice(0, 255)); + // Remove any truncated multi-byte char fragments + sanitized = sanitized.replace(/\uFFFD$/, ''); + } + return sanitized; +} +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGF0aC1zYW5pdGl6ZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvdmZzL3BhdGgtc2FuaXRpemVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7Ozs7Ozs7Ozs7R0FlRztBQUVILE9BQU8sRUFBRSxTQUFTLEVBQVEsTUFBTSxNQUFNLENBQUM7QUFFdkMsd0NBQXdDO0FBRXhDLDBEQUEwRDtBQUMxRCxNQUFNLGdCQUFnQixHQUFHLGVBQWUsQ0FBQztBQUV6Qyx1Q0FBdUM7QUFDdkMsTUFBTSxnQkFBZ0IsR0FBRyx1QkFBdUIsQ0FBQztBQUVqRCxpREFBaUQ7QUFDakQsTUFBTSxnQkFBZ0IsR0FBRyxPQUFPLENBQUM7QUFFakMseUVBQXlFO0FBQ3pFLE1BQU0sbUJBQW1CLEdBQUcsK0NBQStDLENBQUM7QUFFNUUsaURBQWlEO0FBQ2pELE1BQU0sbUJBQW1CLEdBQUcsUUFBUSxDQUFDO0FBRXJDLGlDQUFpQztBQUVqQyw0REFBNEQ7QUFDNUQsTUFBTSxlQUFlLEdBQUc7SUFDcEIsRUFBRSxLQUFLLEVBQUUsT0FBTyxFQUFFLFdBQVcsRUFBRSxHQUFHLEVBQUU7SUFDcEMsRUFBRSxLQUFLLEVBQUUsT0FBTyxFQUFFLFdBQVcsRUFBRSxHQUFHLEVBQUU7SUFDcEMsRUFBRSxLQUFLLEVBQUUsT0FBTyxFQUFFLFdBQVcsRUFBRSxJQUFJLEVBQUU7Q0FDeEMsQ0FBQztBQUVGLCtEQUErRDtBQUMvRCxNQUFNLGFBQWEsR0FBRyxpQkFBaUIsQ0FBQztBQUV4QywwQ0FBMEM7QUFDMUMsTUFBTSxjQUFjLEdBQUcsZUFBZSxDQUFDO0FBRXZDLHVCQUF1QjtBQUV2QixTQUFTLFNBQVMsQ0FBQyxNQUFjO0lBQzdCLE1BQU0sR0FBRyxHQUFRLElBQUksS0FBSyxDQUFDLGVBQWUsTUFBTSxFQUFFLENBQUMsQ0FBQztJQUNwRCxHQUFHLENBQUMsSUFBSSxHQUFHLFlBQVksQ0FBQztJQUN4QixNQUFNLEdBQUcsQ0FBQztBQUNkLENB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 \ No newline at end of file diff --git a/packages/acl/dist-in/vfs/sanitizers.d.ts b/packages/acl/dist-in/vfs/sanitizers.d.ts new file mode 100644 index 00000000..460ec296 --- /dev/null +++ b/packages/acl/dist-in/vfs/sanitizers.d.ts @@ -0,0 +1,105 @@ +/** + * Default input sanitizers for ACL. + * + * Reusable, pure functions — no side effects, no I/O. + * Exported as the `DefaultSanitizers` namespace for convenient access. + * + * Used by: + * - Core Acl class (assertNonEmpty) + * - VFS ACL bridge (cleanUuid, cleanGroupName, normalisePath, cleanPermissions) + * - AclVfsClient / DecoratedVfsClient (cleanUuid, sanitizeSubpath) + */ +/** + * Normalise a subpath for VFS use. + * + * - Converts backslashes to forward slashes + * - Strips leading and trailing slashes + * - Collapses consecutive slashes + * + * @example cleanPath('\\docs\\sub/') → 'docs/sub' + * @example cleanPath('/') → '' + * @example cleanPath('') → '' + */ +export declare function cleanPath(raw: string): string; +/** + * Split a cleaned path into individual segments. + * + * @example pathSegments('docs/sub/file.txt') → ['docs', 'sub', 'file.txt'] + * @example pathSegments('') → [] + */ +export declare function pathSegments(raw: string): string[]; +/** + * Normalise a path into its absolute VFS form (leading slash, no trailing). + * + * @example normalisePath('docs/sub/') → '/docs/sub' + * @example normalisePath('') → '/' + * @example normalisePath('\\a\\b') → '/a/b' + */ +export declare function normalisePath(raw: string): string; +/** + * Validate and normalise a permission name. + * + * - Lowercased + * - Trimmed + * - Rejects empty strings + * + * @throws Error if the permission is empty after trimming. + */ +export declare function cleanPermission(raw: string): string; +/** + * Validate and normalise permission arrays. + */ +export declare function cleanPermissions(raw: string[]): string[]; +/** + * Test whether a string is a valid UUID. + * + * @example isUuid('3bb4cfbf-318b-44d3-a9d3-35680e738421') → true + * @example isUuid('not-a-uuid') → false + */ +export declare function isUuid(value: string): boolean; +/** + * Validate and normalise a UUID string (lowercased, trimmed). + * + * @throws Error if the value is not a valid UUID. + */ +export declare function cleanUuid(raw: string): string; +/** + * Validate a user/owner identifier — must be a valid UUID. + * + * @throws Error if the identifier is not a valid UUID. + */ +export declare function cleanId(raw: string): string; +/** + * Validate a group name (non-empty, lowercased, no colons). + * + * @throws Error if the name is empty or contains reserved characters. + */ +export declare function cleanGroupName(raw: string): string; +/** + * Reject empty/whitespace-only identifiers. + * Works with single values or arrays of values (string | number). + * + * @throws Error with a descriptive label if any value is empty. + * + * @example assertNonEmpty('admin', 'Role') // ok + * @example assertNonEmpty('', 'Role') // throws "Role cannot be empty" + * @example assertNonEmpty(['a', ''], 'Role') // throws "Role cannot be empty" + */ +export declare function assertNonEmpty(value: string | number | (string | number)[], label: string): void; +export { sanitizeSubpath, sanitizeWritePath, sanitizeFilename } from './path-sanitizer.js'; +import { sanitizeSubpath, sanitizeWritePath, sanitizeFilename } from './path-sanitizer.js'; +export declare const DefaultSanitizers: { + readonly assertNonEmpty: typeof assertNonEmpty; + readonly cleanPath: typeof cleanPath; + readonly pathSegments: typeof pathSegments; + readonly normalisePath: typeof normalisePath; + readonly cleanPermission: typeof cleanPermission; + readonly cleanPermissions: typeof cleanPermissions; + readonly isUuid: typeof isUuid; + readonly cleanUuid: typeof cleanUuid; + readonly cleanId: typeof cleanId; + readonly cleanGroupName: typeof cleanGroupName; + readonly sanitizeSubpath: typeof sanitizeSubpath; + readonly sanitizeWritePath: typeof sanitizeWritePath; + readonly sanitizeFilename: typeof sanitizeFilename; +}; diff --git a/packages/acl/dist-in/vfs/sanitizers.js b/packages/acl/dist-in/vfs/sanitizers.js new file mode 100644 index 00000000..e890c755 --- /dev/null +++ b/packages/acl/dist-in/vfs/sanitizers.js @@ -0,0 +1,163 @@ +/** + * Default input sanitizers for ACL. + * + * Reusable, pure functions — no side effects, no I/O. + * Exported as the `DefaultSanitizers` namespace for convenient access. + * + * Used by: + * - Core Acl class (assertNonEmpty) + * - VFS ACL bridge (cleanUuid, cleanGroupName, normalisePath, cleanPermissions) + * - AclVfsClient / DecoratedVfsClient (cleanUuid, sanitizeSubpath) + */ +/** + * Normalise a subpath for VFS use. + * + * - Converts backslashes to forward slashes + * - Strips leading and trailing slashes + * - Collapses consecutive slashes + * + * @example cleanPath('\\docs\\sub/') → 'docs/sub' + * @example cleanPath('/') → '' + * @example cleanPath('') → '' + */ +export function cleanPath(raw) { + return raw + .replace(/\\/g, '/') + .replace(/\/+/g, '/') + .replace(/^\/+/, '') + .replace(/\/+$/, ''); +} +/** + * Split a cleaned path into individual segments. + * + * @example pathSegments('docs/sub/file.txt') → ['docs', 'sub', 'file.txt'] + * @example pathSegments('') → [] + */ +export function pathSegments(raw) { + const clean = cleanPath(raw); + return clean ? clean.split('/') : []; +} +/** + * Normalise a path into its absolute VFS form (leading slash, no trailing). + * + * @example normalisePath('docs/sub/') → '/docs/sub' + * @example normalisePath('') → '/' + * @example normalisePath('\\a\\b') → '/a/b' + */ +export function normalisePath(raw) { + const clean = cleanPath(raw); + return clean ? `/${clean}` : '/'; +} +/** + * Validate and normalise a permission name. + * + * - Lowercased + * - Trimmed + * - Rejects empty strings + * + * @throws Error if the permission is empty after trimming. + */ +export function cleanPermission(raw) { + const p = raw.trim().toLowerCase(); + if (!p) + throw new Error('Permission name cannot be empty'); + return p; +} +/** + * Validate and normalise permission arrays. + */ +export function cleanPermissions(raw) { + return raw.map(cleanPermission); +} +// --------------------------------------------------------------------------- +// UUID validation +// --------------------------------------------------------------------------- +/** Standard UUID v1–v5 pattern (case-insensitive, lowercased on output). */ +const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +/** + * Test whether a string is a valid UUID. + * + * @example isUuid('3bb4cfbf-318b-44d3-a9d3-35680e738421') → true + * @example isUuid('not-a-uuid') → false + */ +export function isUuid(value) { + return UUID_RE.test(value.trim()); +} +/** + * Validate and normalise a UUID string (lowercased, trimmed). + * + * @throws Error if the value is not a valid UUID. + */ +export function cleanUuid(raw) { + const id = raw.trim().toLowerCase(); + if (!UUID_RE.test(id)) { + throw new Error(`Invalid UUID: '${raw}'`); + } + return id; +} +/** + * Validate a user/owner identifier — must be a valid UUID. + * + * @throws Error if the identifier is not a valid UUID. + */ +export function cleanId(raw) { + return cleanUuid(raw); +} +/** + * Validate a group name (non-empty, lowercased, no colons). + * + * @throws Error if the name is empty or contains reserved characters. + */ +export function cleanGroupName(raw) { + const name = raw.trim().toLowerCase(); + if (!name) + throw new Error('Group name cannot be empty'); + if (name.includes(':')) + throw new Error(`Group name cannot contain ':': ${name}`); + return name; +} +// --------------------------------------------------------------------------- +// Generic assertions (used by core Acl) +// --------------------------------------------------------------------------- +/** + * Reject empty/whitespace-only identifiers. + * Works with single values or arrays of values (string | number). + * + * @throws Error with a descriptive label if any value is empty. + * + * @example assertNonEmpty('admin', 'Role') // ok + * @example assertNonEmpty('', 'Role') // throws "Role cannot be empty" + * @example assertNonEmpty(['a', ''], 'Role') // throws "Role cannot be empty" + */ +export function assertNonEmpty(value, label) { + const arr = Array.isArray(value) ? value : [value]; + for (const v of arr) { + const s = String(v).trim(); + if (!s) + throw new Error(`${label} cannot be empty`); + } +} +// --------------------------------------------------------------------------- +// Re-export path-sanitizer functions for unified access +// --------------------------------------------------------------------------- +export { sanitizeSubpath, sanitizeWritePath, sanitizeFilename } from './path-sanitizer.js'; +// --------------------------------------------------------------------------- +// Namespace re-export for convenience +// --------------------------------------------------------------------------- +import { sanitizeSubpath, sanitizeWritePath, sanitizeFilename } from './path-sanitizer.js'; +export const DefaultSanitizers = { + assertNonEmpty, + cleanPath, + pathSegments, + normalisePath, + cleanPermission, + cleanPermissions, + isUuid, + cleanUuid, + cleanId, + cleanGroupName, + sanitizeSubpath, + sanitizeWritePath, + sanitizeFilename, +}; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2FuaXRpemVycy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy92ZnMvc2FuaXRpemVycy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7Ozs7OztHQVVHO0FBRUg7Ozs7Ozs7Ozs7R0FVRztBQUNILE1BQU0sVUFBVSxTQUFTLENBQUMsR0FBVztJQUNqQyxPQUFPLEdBQUc7U0FDTCxPQUFPLENBQUMsS0FBSyxFQUFFLEdBQUcsQ0FBQztTQUNuQixPQUFPLENBQUMsTUFBTSxFQUFFLEdBQUcsQ0FBQztTQUNwQixPQUFPLENBQUMsTUFBTSxFQUFFLEVBQUUsQ0FBQztTQUNuQixPQUFPLENBQUMsTUFBTSxFQUFFLEVBQUUsQ0FBQyxDQUFDO0FBQzdCLENBQUM7QUFFRDs7Ozs7R0FLRztBQUNILE1BQU0sVUFBVSxZQUFZLENBQUMsR0FBVztJQUNwQyxNQUFNLEtBQUssR0FBRyxTQUFTLENBQUMsR0FBRyxDQUFDLENBQUM7SUFDN0IsT0FBTyxLQUFLLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQztBQUN6QyxDQUFDO0FBRUQ7Ozs7OztHQU1HO0FBQ0gsTUFBTSxVQUFVLGFBQWEsQ0FBQyxHQUFXO0lBQ3JDLE1BQU0sS0FBSyxHQUFHLFNBQVMsQ0FBQyxHQUFHLENBQUMsQ0FBQztJQUM3QixPQUFPLEtBQUssQ0FBQyxDQUFDLENBQUMsSUFBSSxLQUFLLEVBQUUsQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDO0FBQ3JDLENBQUM7QUFFRDs7Ozs7Ozs7R0FRRztBQUNILE1BQU0sVUFBVSxlQUFlLENBQUMsR0FBVztJQUN2QyxNQUFNLENBQUMsR0FBRyxHQUFHLENBQUMsSUFBSSxFQUFFLENBQUMsV0FBVyxFQUFFLENBQUM7SUFDbkMsSUFBSSxDQUFDLENBQUM7UUFBRSxNQUFNLElBQUksS0FBSyxDQUFDLGlDQUFpQyxDQUFDLENBQUM7SUFDM0QsT0FB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 \ No newline at end of file diff --git a/packages/acl/dist-in/vfs/vfs-acl.d.ts b/packages/acl/dist-in/vfs/vfs-acl.d.ts new file mode 100644 index 00000000..3b82a7be --- /dev/null +++ b/packages/acl/dist-in/vfs/vfs-acl.d.ts @@ -0,0 +1,43 @@ +import type { Acl } from '../Acl.js'; +export interface VfsGroup { + name: string; + members: string[]; +} +export interface VfsAclEntry { + /** Direct user grant */ + userId?: string; + /** Group grant */ + group?: string; + /** Scoped path — defaults to "/" (entire folder). */ + path?: string; + permissions: string[]; +} +export interface VfsSettings { + owner: string; + groups?: VfsGroup[]; + acl: VfsAclEntry[]; +} +/** + * Canonical resource name for a path inside a user's VFS folder. + * + * vfsResource('aaa', '/') → 'vfs:aaa:/' + * vfsResource('aaa', '/docs') → 'vfs:aaa:/docs' + */ +export declare function vfsResource(ownerId: string, resourcePath?: string): string; +/** + * Build the list of resources to check, from most-specific to root. + * + * resourceChain('aaa', 'docs/sub/file.txt') + * → ['vfs:aaa:/docs/sub/file.txt', 'vfs:aaa:/docs/sub', 'vfs:aaa:/docs', 'vfs:aaa:/'] + * + * The guard walks this chain top-down; if ANY level allows, access is granted. + */ +export declare function resourceChain(ownerId: string, subpath: string): string[]; +/** + * Load `vfs-settings.json` from a user's VFS folder and apply the ACL rules. + * + * - The **owner** always gets `*` on `/` (entire tree). + * - Each ACL entry grants permissions scoped to `entry.path` (default: `/`). + * - Group entries resolve members from the `groups[]` array. + */ +export declare function loadVfsSettings(acl: Acl, userDir: string): Promise; diff --git a/packages/acl/dist-in/vfs/vfs-acl.js b/packages/acl/dist-in/vfs/vfs-acl.js new file mode 100644 index 00000000..bb615b6f --- /dev/null +++ b/packages/acl/dist-in/vfs/vfs-acl.js @@ -0,0 +1,108 @@ +/** + * VFS ACL Bridge + * + * Reads per-user `vfs-settings.json` files and populates an Acl instance + * with fine-grained, path-scoped permissions for each user's VFS folder. + * + * Supports: + * - Direct user grants: { userId, path, permissions } + * - Group grants: { group, path, permissions } + * + * Resource naming: `vfs::` — e.g. `vfs:3bb4cfbf-...:/docs` + * + * Permissions: read | write | list | mkdir | delete | rename | copy + */ +import { readFileSync, existsSync } from 'node:fs'; +import { join } from 'node:path'; +import { pathSegments, normalisePath, cleanGroupName, cleanId, cleanPermissions } from './sanitizers.js'; +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- +/** + * Canonical resource name for a path inside a user's VFS folder. + * + * vfsResource('aaa', '/') → 'vfs:aaa:/' + * vfsResource('aaa', '/docs') → 'vfs:aaa:/docs' + */ +export function vfsResource(ownerId, resourcePath = '/') { + return `vfs:${ownerId}:${normalisePath(resourcePath)}`; +} +/** + * Build the list of resources to check, from most-specific to root. + * + * resourceChain('aaa', 'docs/sub/file.txt') + * → ['vfs:aaa:/docs/sub/file.txt', 'vfs:aaa:/docs/sub', 'vfs:aaa:/docs', 'vfs:aaa:/'] + * + * The guard walks this chain top-down; if ANY level allows, access is granted. + */ +export function resourceChain(ownerId, subpath) { + const segments = pathSegments(subpath); + const chain = []; + // Build from most-specific to least-specific + for (let i = segments.length; i > 0; i--) { + chain.push(vfsResource(ownerId, '/' + segments.slice(0, i).join('/'))); + } + // Always include root + chain.push(vfsResource(ownerId, '/')); + return chain; +} +// --------------------------------------------------------------------------- +// Loader +// --------------------------------------------------------------------------- +/** + * Load `vfs-settings.json` from a user's VFS folder and apply the ACL rules. + * + * - The **owner** always gets `*` on `/` (entire tree). + * - Each ACL entry grants permissions scoped to `entry.path` (default: `/`). + * - Group entries resolve members from the `groups[]` array. + */ +export async function loadVfsSettings(acl, userDir) { + const settingsPath = join(userDir, 'vfs-settings.json'); + if (!existsSync(settingsPath)) + return null; + const raw = readFileSync(settingsPath, 'utf8'); + const settings = JSON.parse(raw); + // Validate owner + const safeOwner = cleanId(settings.owner); + // Helper: unwrap result or throw + const unwrap = (result) => { + if (!result.ok) + throw new Error(result.message); + }; + // Owner role — full access on entire tree + const ownerRole = `owner:${safeOwner}`; + unwrap(await acl.allow(ownerRole, vfsResource(safeOwner, '/'), '*')); + unwrap(await acl.addUserRoles(safeOwner, ownerRole)); + // Index groups (validate member IDs) + const groupMembers = new Map(); + for (const group of settings.groups ?? []) { + const safeName = cleanGroupName(group.name); + const safeMembers = group.members.map(m => cleanId(m)); + groupMembers.set(safeName, safeMembers); + } + // Process ACL entries + for (const entry of settings.acl) { + const resourcePath = normalisePath(entry.path ?? '/'); + const resource = vfsResource(safeOwner, resourcePath); + const safePerms = cleanPermissions(entry.permissions); + if (entry.group) { + // Group grant + const safeGroup = cleanGroupName(entry.group); + const groupRole = `group:${safeOwner}:${safeGroup}`; + unwrap(await acl.allow(groupRole, resource, safePerms)); + const members = groupMembers.get(safeGroup) ?? []; + for (const memberId of members) { + unwrap(await acl.addUserRoles(memberId, groupRole)); + } + } + else if (entry.userId) { + // Direct user grant + const safeId = cleanId(entry.userId); + const grantRole = `vfs-grant:${safeOwner}:${safeId}:${resourcePath}`; + unwrap(await acl.allow(grantRole, resource, safePerms)); + unwrap(await acl.addUserRoles(safeId, grantRole)); + } + } + return settings; +} +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmZzLWFjbC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy92ZnMvdmZzLWFjbC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7Ozs7Ozs7OztHQWFHO0FBQ0gsT0FBTyxFQUFFLFlBQVksRUFBRSxVQUFVLEVBQUUsTUFBTSxTQUFTLENBQUM7QUFDbkQsT0FBTyxFQUFFLElBQUksRUFBRSxNQUFNLFdBQVcsQ0FBQztBQUVqQyxPQUFPLEVBQUUsWUFBWSxFQUFFLGFBQWEsRUFBRSxjQUFjLEVBQUUsT0FBTyxFQUFFLGdCQUFnQixFQUFFLE1BQU0saUJBQWlCLENBQUM7QUEyQnpHLDhFQUE4RTtBQUM5RSxVQUFVO0FBQ1YsOEVBQThFO0FBRTlFOzs7OztHQUtHO0FBQ0gsTUFBTSxVQUFVLFdBQVcsQ0FBQyxPQUFlLEVBQUUsWUFBWSxHQUFHLEdBQUc7SUFDM0QsT0FBTyxPQUFPLE9BQU8sSUFBSSxhQUFhLENBQUMsWUFBWSxDQUFDLEVBQUUsQ0FBQztBQUMzRCxDQUFDO0FBRUQ7Ozs7Ozs7R0FPRztBQUNILE1BQU0sVUFBVSxhQUFhLENBQUMsT0FBZSxFQUFFLE9BQWU7SUFDMUQsTUFBTSxRQUFRLEdBQUcsWUFBWSxDQUFDLE9BQU8sQ0FBQyxDQUFDO0lBRXZDLE1BQU0sS0FBSyxHQUFhLEVBQUUsQ0FBQztJQUUzQiw2Q0FBNkM7SUFDN0MsS0FBSyxJQUFJLENBQUMsR0FBRyxRQUFRLENBQUMsTUFBTSxFQUFFLENBQUMsR0FBRyxDQUFDLEVBQUUsQ0FBQyxFQUFFLEVBQUUsQ0FBQztRQUN2QyxLQUFLLENBQUMsSUFBSSxDQUFDLFdBQVcsQ0FBQyxPQUFPLEVBQUUsR0FBRyxHQUFHLFFBQVEsQ0FBQyxLQUFLLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDM0UsQ0FBQztJQUVELHNCQUFzQjtJQUN0QixLQUFLLENBQUMsSUFBSSxDQUFDLFdBQVcs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 \ No newline at end of file diff --git a/packages/acl/docs/groups.md b/packages/acl/docs/groups.md new file mode 100644 index 00000000..b9fb3add --- /dev/null +++ b/packages/acl/docs/groups.md @@ -0,0 +1,256 @@ +# Groups — Design Proposal + +## Problem + +Today, VFS permissions are granted **per-user**: + +```json +{ + "acl": [ + { "userId": "aaa-...", "path": "/docs", "permissions": ["read", "list"] }, + { "userId": "bbb-...", "path": "/docs", "permissions": ["read", "list"] }, + { "userId": "ccc-...", "path": "/docs", "permissions": ["read", "list"] } + ] +} +``` + +If 50 users need the same access, you write 50 entries. Changing the permission set means updating all 50. This doesn't scale. + +## Proposal: User Groups + +Introduce a **group** — a named set of users. Permissions are granted to groups rather than individual user IDs. + +### Data Model + +```mermaid +erDiagram + GROUP ||--o{ GROUP_MEMBER : contains + GROUP ||--o{ VFS_PERMISSION : receives + USER ||--o{ GROUP_MEMBER : belongs_to + USER ||--o{ VFS_PERMISSION : owns + + GROUP { + uuid id PK + uuid owner_id FK + string name + string description + } + + GROUP_MEMBER { + uuid group_id FK + uuid user_id FK + } + + VFS_PERMISSION { + uuid owner_id FK + uuid group_id FK "nullable — group grant" + uuid grantee_id FK "nullable — direct grant" + string resource_path + string[] permissions + } +``` + +A permission row targets **either** a `group_id` or a `grantee_id` — never both. + +### VFS Settings (JSON) + +```json +{ + "owner": "3bb4cfbf-...", + "groups": [ + { + "name": "team", + "members": ["aaa-...", "bbb-...", "ccc-..."] + }, + { + "name": "viewers", + "members": ["ddd-...", "eee-..."] + } + ], + "acl": [ + { "group": "team", "path": "/shared", "permissions": ["read", "write", "list", "mkdir", "delete"] }, + { "group": "viewers", "path": "/docs", "permissions": ["read", "list"] }, + { "userId": "fff-...", "path": "/private/partner", "permissions": ["read", "list"] } + ] +} +``` + +Groups and direct user grants coexist. Direct grants take precedence for individual overrides. + +--- + +## ACL Mapping + +### How Groups Become Roles + +Each group becomes a role in the ACL. All members of the group inherit that role: + +```mermaid +sequenceDiagram + participant Loader as VFS ACL Bridge + participant ACL as Acl Instance + + Note over Loader: For each group + Loader->>ACL: allow "group:team", resource, permissions + loop Each member of team + Loader->>ACL: addUserRoles member, "group:team" + end + + Note over Loader: For each direct grant + Loader->>ACL: allow "vfs-grant:owner:user", resource, permissions + Loader->>ACL: addUserRoles user, "vfs-grant:owner:user" +``` + +### Role Naming Convention + +| Type | Role Name | Example | +|------|-----------|---------| +| Owner | `owner:` | `owner:3bb4-...` | +| Group | `group::` | `group:3bb4-...:team` | +| Direct | `vfs-grant:::` | `vfs-grant:3bb4-...:fff-...:/private` | + +--- + +## Implementation + +### TypeScript Types + +```ts +interface VfsGroup { + name: string; + members: string[]; +} + +interface VfsAclEntry { + /** Direct user grant */ + userId?: string; + /** Group grant */ + group?: string; + /** Scoped path — defaults to "/" */ + path?: string; + permissions: string[]; +} + +interface VfsSettings { + owner: string; + groups?: VfsGroup[]; + acl: VfsAclEntry[]; +} +``` + +### Loader Changes + +```ts +export async function loadVfsSettings(acl: Acl, userDir: string): Promise { + // ... read settings ... + + // 1. Owner — wildcard on / + const ownerRole = `owner:${settings.owner}`; + await acl.allow(ownerRole, vfsResource(settings.owner, '/'), '*'); + await acl.addUserRoles(settings.owner, ownerRole); + + // 2. Register groups + const groupMembers = new Map(); + for (const group of settings.groups ?? []) { + groupMembers.set(group.name, group.members); + } + + // 3. Process ACL entries + for (const entry of settings.acl) { + const resourcePath = entry.path ?? '/'; + const resource = vfsResource(settings.owner, resourcePath); + + if (entry.group) { + // Group grant + const role = `group:${settings.owner}:${entry.group}`; + await acl.allow(role, resource, entry.permissions); + + const members = groupMembers.get(entry.group) ?? []; + for (const memberId of members) { + await acl.addUserRoles(memberId, role); + } + } else if (entry.userId) { + // Direct grant + const role = `vfs-grant:${settings.owner}:${entry.userId}:${resourcePath}`; + await acl.allow(role, resource, entry.permissions); + await acl.addUserRoles(entry.userId, role); + } + } + + return settings; +} +``` + +### AclVfsClient + +**No changes needed.** The client only calls `acl.isAllowed(callerId, resource, permission)` — it doesn't care whether the permission was granted via a group or a direct entry. The ACL resolves it transparently through `addUserRoles`. + +--- + +## Postgres Schema + +```sql +create table public.vfs_groups ( + id uuid primary key default gen_random_uuid(), + owner_id uuid not null references public.users(id) on delete cascade, + name text not null, + description text, + + unique (owner_id, name) +); + +create table public.vfs_group_members ( + group_id uuid not null references public.vfs_groups(id) on delete cascade, + user_id uuid not null references public.users(id) on delete cascade, + + primary key (group_id, user_id) +); + +-- Extend vfs_permissions to support group grants +alter table public.vfs_permissions + add column group_id uuid references public.vfs_groups(id) on delete cascade; + +-- Either group_id or grantee_id must be set, never both +alter table public.vfs_permissions + add constraint grant_target_check + check ( + (group_id is not null and grantee_id is null) + or (group_id is null and grantee_id is not null) + ); +``` + +--- + +## Permission Resolution Order + +When checking `isAllowed(userId, resource, permission)`: + +```mermaid +flowchart TD + A[isAllowed userId resource permission] --> B{Is owner?} + B -- yes --> C[ALLOW - wildcard] + B -- no --> D{Direct grant exists?} + D -- yes, allowed --> C + D -- no --> E{Any group grants?} + E -- yes --> F{User is member of group?} + F -- yes, allowed --> C + F -- no --> G[Walk parent path] + E -- no --> G + G --> H{Reached root?} + H -- no --> D + H -- yes --> I[DENY] +``` + +This is already handled by the existing `resourceChain` + `addUserRoles` — no new resolution logic needed. + +--- + +## Open Questions + +1. **Nested groups?** — A group containing other groups (e.g. `all-staff` inherits members from `team-a` + `team-b`). This maps to `addRoleParents` but adds complexity. Recommend **no** for v1. + +2. **Group-scoped deny?** — Explicitly deny a permission for a group. The current ACL doesn't support deny rules (allow-only). Recommend deferring. + +3. **Who can manage groups?** — Only the folder owner, or delegates? RLS naturally restricts to `owner_id = auth.uid()`, but a `group_admin` role could be added later. + +4. **Max group size?** — For the in-memory backend, large groups (1000+ members) are fine. For the DB loader, a single `SELECT ... JOIN` handles it. No practical limit for v1. diff --git a/packages/acl/eslint.config.js b/packages/acl/eslint.config.js new file mode 100644 index 00000000..7969654d --- /dev/null +++ b/packages/acl/eslint.config.js @@ -0,0 +1,45 @@ +import tseslint from 'typescript-eslint'; + +/** @type {import('eslint').Linter.Config[]} */ +export default tseslint.config( + { files: ['src/**/*.ts'] }, + ...tseslint.configs.recommendedTypeChecked, + ...tseslint.configs.stylisticTypeChecked, + { + languageOptions: { + parserOptions: { + projectService: true, + }, + }, + rules: { + // Aligned with monorepo root config + '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/consistent-type-definitions': 'off', + '@typescript-eslint/consistent-indexed-object-style': 'off', + '@typescript-eslint/array-type': 'off', + '@typescript-eslint/prefer-for-of': 'off', + '@typescript-eslint/no-empty-function': 'off', + '@typescript-eslint/no-floating-promises': 'off', + '@typescript-eslint/require-await': 'off', + '@typescript-eslint/await-thenable': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/dot-notation': 'off', + '@typescript-eslint/prefer-nullish-coalescing': 'off', + '@typescript-eslint/prefer-optional-chain': 'off', + '@typescript-eslint/no-unnecessary-type-assertion': 'off', + '@typescript-eslint/restrict-template-expressions': 'off', + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-return': 'off', + '@typescript-eslint/no-unsafe-argument': 'off', + '@typescript-eslint/no-base-to-string': 'off', + '@typescript-eslint/no-redundant-type-constituents': 'off', + '@typescript-eslint/only-throw-error': 'off', + '@typescript-eslint/prefer-promise-reject-errors': 'off', + '@typescript-eslint/unbound-method': 'off', + 'prefer-const': 'off', + }, + }, +); diff --git a/packages/acl/package-lock.json b/packages/acl/package-lock.json index beadd627..f62ee060 100644 --- a/packages/acl/package-lock.json +++ b/packages/acl/package-lock.json @@ -11,7 +11,8 @@ "dependencies": { "ignore": "^7.0.5", "mime": "^4.1.0", - "pino": "^9.6.0" + "pino": "^9.6.0", + "typescript-eslint": "^8.56.0" }, "bin": { "pm-acl": "dist-in/main.js" @@ -19,10 +20,9 @@ "devDependencies": { "@repo/typescript-config": "file:../typescript-config", "@types/node": "22.10.2", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "@typescript-eslint/parser": "^6.21.0", "@vitest/coverage-v8": "^2.1.8", "@vitest/ui": "2.1.9", + "eslint": "^9.39.2", "typescript": "^5.7.2", "vitest": "^2.1.8" } @@ -624,7 +624,6 @@ "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", - "dev": true, "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" @@ -643,31 +642,89 @@ "version": "4.12.2", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", + "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -677,7 +734,6 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -688,7 +744,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 4" @@ -698,7 +753,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -708,60 +762,65 @@ } }, "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", - "dev": true, + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", - "dev": true, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" }, "engines": { - "node": ">=10.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" } }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "license": "Apache-2.0", "dependencies": { - "brace-expansion": "^1.1.7" + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" }, "engines": { - "node": "*" + "node": ">=18.18.0" } }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=12.22" @@ -771,13 +830,18 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", - "dev": true, - "license": "BSD-3-Clause" + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, "node_modules/@isaacs/cliui": { "version": "8.0.2", @@ -931,44 +995,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@pinojs/redact": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", @@ -1351,14 +1377,12 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, "license": "MIT" }, "node_modules/@types/node": { @@ -1372,229 +1396,56 @@ "undici-types": "~6.20.0" } }, - "node_modules/@types/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", - "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", - "dev": true, + "node_modules/@typescript-eslint/project-service": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz", + "integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==", "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/type-utils": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.4", - "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "@typescript-eslint/tsconfig-utils": "^8.56.0", + "@typescript-eslint/types": "^8.56.0", + "debug": "^4.4.3" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, + "node_modules/@typescript-eslint/project-service/node_modules/@typescript-eslint/types": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", + "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", "license": "MIT", "engines": { - "node": ">= 4" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/parser": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", - "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4" - }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz", + "integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==", + "license": "MIT", "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", - "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", - "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "semver": "^7.5.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "dev": true, - "license": "ISC" - }, "node_modules/@vitest/coverage-v8": { "version": "2.1.9", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.9.tgz", @@ -1768,7 +1619,6 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, "license": "MIT", "peer": true, "bin": { @@ -1782,7 +1632,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -1792,7 +1641,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -1819,7 +1667,6 @@ "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" @@ -1835,19 +1682,8 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -1871,32 +1707,17 @@ "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/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -1911,7 +1732,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -1938,7 +1758,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -1965,7 +1784,6 @@ "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" @@ -1978,21 +1796,18 @@ "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/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, "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", @@ -2007,7 +1822,6 @@ "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" @@ -2035,35 +1849,8 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, "license": "MIT" }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2128,7 +1915,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -2138,75 +1924,76 @@ } }, "node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", - "dev": true, + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "license": "MIT", "peer": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -2216,7 +2003,6 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2229,18 +2015,28 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/eslint/node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 4" @@ -2250,7 +2046,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -2260,18 +2055,29 @@ } }, "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.2.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -2281,7 +2087,6 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" @@ -2294,7 +2099,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" @@ -2307,7 +2111,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -2327,7 +2130,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -2347,63 +2149,20 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, "license": "MIT" }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, "node_modules/fflate": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", @@ -2412,36 +2171,21 @@ "license": "MIT" }, "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "license": "MIT", "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" + "node": ">=16.0.0" } }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, "license": "MIT", "dependencies": { "locate-path": "^6.0.0", @@ -2455,25 +2199,22 @@ } }, "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "license": "MIT", "dependencies": { "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "keyv": "^4.5.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16" } }, "node_modules/flatted": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, "license": "ISC" }, "node_modules/foreground-child": { @@ -2493,13 +2234,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2515,33 +2249,10 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "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": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -2550,89 +2261,22 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globby/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2658,7 +2302,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -2675,36 +2318,15 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8.19" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "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-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -2724,7 +2346,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -2733,31 +2354,10 @@ "node": ">=0.10.0" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "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/istanbul-lib-coverage": { @@ -2834,7 +2434,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -2847,28 +2446,24 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, "license": "MIT" }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, "license": "MIT", "dependencies": { "json-buffer": "3.0.1" @@ -2878,7 +2473,6 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", @@ -2892,7 +2486,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, "license": "MIT", "dependencies": { "p-locate": "^5.0.0" @@ -2908,7 +2501,6 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, "license": "MIT" }, "node_modules/loupe": { @@ -2963,30 +2555,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/mime": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/mime/-/mime-4.1.0.tgz", @@ -3002,22 +2570,6 @@ "node": ">=16" } }, - "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -3042,7 +2594,6 @@ "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/nanoid": { @@ -3068,7 +2619,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, "license": "MIT" }, "node_modules/on-exit-leak-free": { @@ -3080,21 +2630,10 @@ "node": ">=14.0.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/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, "license": "MIT", "dependencies": { "deep-is": "^0.1.3", @@ -3112,7 +2651,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" @@ -3128,7 +2666,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, "license": "MIT", "dependencies": { "p-limit": "^3.0.2" @@ -3151,7 +2688,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -3164,27 +2700,15 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.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" @@ -3207,16 +2731,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/pathe": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", @@ -3241,19 +2755,6 @@ "dev": true, "license": "ISC" }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/pino": { "version": "9.14.0", "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", @@ -3324,7 +2825,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8.0" @@ -3350,33 +2850,11 @@ "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/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "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/quick-format-unescaped": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", @@ -3396,40 +2874,11 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" } }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/rollup": { "version": "4.57.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", @@ -3475,30 +2924,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "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": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/safe-stable-stringify": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", @@ -3512,7 +2937,6 @@ "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3525,7 +2949,6 @@ "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" @@ -3538,7 +2961,6 @@ "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" @@ -3579,16 +3001,6 @@ "node": ">=18" } }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/sonic-boom": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", @@ -3693,7 +3105,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3706,7 +3117,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -3768,13 +3178,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, - "license": "MIT" - }, "node_modules/thread-stream": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", @@ -3802,7 +3205,6 @@ "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -3819,7 +3221,6 @@ "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" @@ -3837,7 +3238,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, "license": "MIT", "peer": true, "engines": { @@ -3877,19 +3277,6 @@ "node": ">=14.0.0" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -3900,24 +3287,10 @@ "node": ">=6" } }, - "node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" @@ -3926,24 +3299,10 @@ "node": ">= 0.8.0" } }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "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", "peer": true, "bin": { @@ -3954,6 +3313,242 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.0.tgz", + "integrity": "sha512-c7toRLrotJ9oixgdW7liukZpsnq5CZ7PuKztubGYlNppuTqhIoWfhgHo/7EU0v06gS2l/x0i2NEFK1qMIf0rIg==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.56.0", + "@typescript-eslint/parser": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/utils": "8.56.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz", + "integrity": "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==", + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/type-utils": "8.56.0", + "@typescript-eslint/utils": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.56.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/parser": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz", + "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/scope-manager": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz", + "integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/type-utils": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz", + "integrity": "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/utils": "8.56.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/types": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", + "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz", + "integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.56.0", + "@typescript-eslint/tsconfig-utils": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/utils": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz", + "integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==", + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz", + "integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/typescript-eslint/node_modules/eslint-visitor-keys": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz", + "integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==", + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/typescript-eslint/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typescript-eslint/node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/undici-types": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", @@ -3965,7 +3560,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -4126,7 +3720,6 @@ "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" @@ -4159,7 +3752,6 @@ "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4184,18 +3776,10 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "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/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" diff --git a/packages/acl/package.json b/packages/acl/package.json index 3bbe01c8..2a3479db 100644 --- a/packages/acl/package.json +++ b/packages/acl/package.json @@ -25,7 +25,8 @@ "build": "tsc", "dev": "tsc -p . --watch", "test:core": "vitest run src/acl.test.ts", - "lint": "eslint src --ext .ts" + "test:all": "vitest run tests/ src/acl.test.ts", + "lint": "eslint src" }, "dependencies": { "ignore": "^7.0.5", @@ -35,11 +36,11 @@ "devDependencies": { "@repo/typescript-config": "file:../typescript-config", "@types/node": "22.10.2", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "@typescript-eslint/parser": "^6.21.0", "@vitest/coverage-v8": "^2.1.8", "@vitest/ui": "2.1.9", + "eslint": "^9.39.2", "typescript": "^5.7.2", + "typescript-eslint": "^8.56.0", "vitest": "^2.1.8" } } \ No newline at end of file diff --git a/packages/acl/src/Acl.ts b/packages/acl/src/Acl.ts index 9bde999f..2ac6eaff 100644 --- a/packages/acl/src/Acl.ts +++ b/packages/acl/src/Acl.ts @@ -6,47 +6,59 @@ */ import type { Logger } from 'pino'; import type { + AclErr, AclGrant, AclOptions, + AclResult, BucketNames, IBackend, IAcl, Value, Values, } from './interfaces.js'; +import { ok, okVoid, err } from './interfaces.js'; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- -function toArray(v: T | T[]): T[] { - return Array.isArray(v) ? v : [v]; -} +const toArray = (v: T | T[]): T[] => + Array.isArray(v) ? v : [v]; -function allowsBucket(resource: string): string { - return `allows_${resource}`; -} +const allowsBucket = (resource: string): string => + `allows_${resource}`; -function keyFromAllowsBucket(str: string): string { - return str.replace(/^allows_/, ''); -} +const keyFromAllowsBucket = (str: string): string => + str.replace(/^allows_/, ''); /** Set-based union of two arrays (deduped). */ -function union(a: T[], b: T[]): T[] { - return [...new Set([...a, ...b])]; -} +const union = (a: T[], b: T[]): T[] => + [...new Set([...a, ...b])]; /** Items in `a` that are not in `b`. */ -function difference(a: T[], b: T[]): T[] { +const difference = (a: T[], b: T[]): T[] => { const set = new Set(b); return a.filter((x) => !set.has(x)); -} +}; /** Intersection of `a` and `b`. */ -function intersect(a: T[], b: T[]): T[] { +const intersect = (a: T[], b: T[]): T[] => { const set = new Set(b); return a.filter((x) => set.has(x)); -} +}; + +/** + * Validate that none of the values are empty/whitespace-only. + * Returns an AclErr if validation fails, otherwise undefined. + */ +const validateNonEmpty = (value: Value | Values, label: string): AclErr | undefined => { + const arr = Array.isArray(value) ? value : [value]; + for (const v of arr) { + const s = String(v).trim(); + if (!s) return err('INVALID_INPUT', `${label} cannot be empty`); + } + return undefined; +}; // --------------------------------------------------------------------------- // Default bucket names @@ -80,163 +92,249 @@ export class Acl implements IAcl { // allow // ------------------------------------------------------------------------- - async allow(rolesOrGrants: Values | AclGrant[], resources?: Values, permissions?: Values): Promise { - // Overload: allow(grants[]) - if (Array.isArray(rolesOrGrants) && rolesOrGrants.length > 0 && typeof rolesOrGrants[0] === 'object') { - return this.#allowBatch(rolesOrGrants as AclGrant[]); - } - - const roles = toArray(rolesOrGrants as Values) as string[]; - const res = toArray(resources!); - const perms = toArray(permissions!); - - const tx = this.#backend.begin(); - this.#backend.add(tx, this.#buckets.meta, 'roles', roles); - - for (const resource of res) { - for (const role of roles) { - this.#backend.add(tx, allowsBucket(String(resource)), role, perms); + async allow(rolesOrGrants: Values | AclGrant[], resources?: Values, permissions?: Values): Promise { + try { + // Overload: allow(grants[]) + if (Array.isArray(rolesOrGrants) && rolesOrGrants.length > 0 && typeof rolesOrGrants[0] === 'object') { + return this.#allowBatch(rolesOrGrants as AclGrant[]); } - } - for (const role of roles) { - this.#backend.add(tx, this.#buckets.resources, role, res); - } - await this.#backend.end(tx); - this.#logger?.debug({ roles, resources: res, permissions: perms }, 'allow'); + const roles = toArray(rolesOrGrants as Values) as string[]; + const res = toArray(resources!); + const perms = toArray(permissions!); + + const v1 = validateNonEmpty(roles, 'Role'); + if (v1) return v1; + const v2 = validateNonEmpty(res, 'Resource'); + if (v2) return v2; + const v3 = validateNonEmpty(perms, 'Permission'); + if (v3) return v3; + + const tx = await this.#backend.begin(); + await this.#backend.add(tx, this.#buckets.meta, 'roles', roles); + + for (const resource of res) { + for (const role of roles) { + await this.#backend.add(tx, allowsBucket(String(resource)), role, perms); + } + } + for (const role of roles) { + await this.#backend.add(tx, this.#buckets.resources, role, res); + } + + await this.#backend.end(tx); + this.#logger?.debug({ roles, resources: res, permissions: perms }, 'allow'); + return okVoid; + } catch (e) { + return err('BACKEND_ERROR', (e as Error).message); + } } - async #allowBatch(grants: AclGrant[]): Promise { - const flat: { roles: Values; resources: Values; permissions: Values }[] = []; + async #allowBatch(grants: AclGrant[]): Promise { for (const g of grants) { for (const a of g.allows) { - flat.push({ roles: g.roles, resources: a.resources, permissions: a.permissions }); + const result = await this.allow(g.roles, a.resources, a.permissions); + if (!result.ok) return result; } } - for (const item of flat) { - await this.allow(item.roles, item.resources, item.permissions); - } + return okVoid; } // ------------------------------------------------------------------------- // User ↔ Role // ------------------------------------------------------------------------- - async addUserRoles(userId: Value, roles: Values): Promise { - const rolesArr = toArray(roles); - const tx = this.#backend.begin(); - this.#backend.add(tx, this.#buckets.meta, 'users', userId); - this.#backend.add(tx, this.#buckets.users, userId, roles); + async addUserRoles(userId: Value, roles: Values): Promise { + try { + const v1 = validateNonEmpty(userId, 'User ID'); + if (v1) return v1; + const v2 = validateNonEmpty(roles, 'Role'); + if (v2) return v2; - for (const role of rolesArr) { - this.#backend.add(tx, this.#buckets.roles, role, userId); + const rolesArr = toArray(roles); + const tx = await this.#backend.begin(); + await this.#backend.add(tx, this.#buckets.meta, 'users', userId); + await this.#backend.add(tx, this.#buckets.users, userId, roles); + + for (const role of rolesArr) { + await this.#backend.add(tx, this.#buckets.roles, role, userId); + } + await this.#backend.end(tx); + return okVoid; + } catch (e) { + return err('BACKEND_ERROR', (e as Error).message); } - await this.#backend.end(tx); } - async removeUserRoles(userId: Value, roles: Values): Promise { - const rolesArr = toArray(roles); - const tx = this.#backend.begin(); - this.#backend.remove(tx, this.#buckets.users, userId, roles); + async removeUserRoles(userId: Value, roles: Values): Promise { + try { + const v1 = validateNonEmpty(userId, 'User ID'); + if (v1) return v1; + const v2 = validateNonEmpty(roles, 'Role'); + if (v2) return v2; - for (const role of rolesArr) { - this.#backend.remove(tx, this.#buckets.roles, role, userId); + const rolesArr = toArray(roles); + const tx = await this.#backend.begin(); + await this.#backend.remove(tx, this.#buckets.users, userId, roles); + + for (const role of rolesArr) { + await this.#backend.remove(tx, this.#buckets.roles, role, userId); + } + await this.#backend.end(tx); + return okVoid; + } catch (e) { + return err('BACKEND_ERROR', (e as Error).message); } - await this.#backend.end(tx); } - async userRoles(userId: Value): Promise { - return this.#backend.get(this.#buckets.users, userId); + async userRoles(userId: Value): Promise> { + try { + const data = await this.#backend.get(this.#buckets.users, userId); + return ok(data); + } catch (e) { + return err('BACKEND_ERROR', (e as Error).message); + } } - async roleUsers(role: Value): Promise { - return this.#backend.get(this.#buckets.roles, role); + async roleUsers(role: Value): Promise> { + try { + const data = await this.#backend.get(this.#buckets.roles, role); + return ok(data); + } catch (e) { + return err('BACKEND_ERROR', (e as Error).message); + } } - async hasRole(userId: Value, role: string): Promise { - const roles = await this.userRoles(userId); - return roles.includes(role); + async hasRole(userId: Value, role: string): Promise> { + try { + const result = await this.userRoles(userId); + if (!result.ok) return result; + return ok(result.data.includes(role)); + } catch (e) { + return err('BACKEND_ERROR', (e as Error).message); + } } // ------------------------------------------------------------------------- // Role hierarchy // ------------------------------------------------------------------------- - async addRoleParents(role: string, parents: Values): Promise { - const tx = this.#backend.begin(); - this.#backend.add(tx, this.#buckets.meta, 'roles', role); - this.#backend.add(tx, this.#buckets.parents, role, parents); - await this.#backend.end(tx); + async addRoleParents(role: string, parents: Values): Promise { + try { + const v1 = validateNonEmpty(role, 'Role'); + if (v1) return v1; + const v2 = validateNonEmpty(parents, 'Parent role'); + if (v2) return v2; + + const tx = await this.#backend.begin(); + await this.#backend.add(tx, this.#buckets.meta, 'roles', role); + await this.#backend.add(tx, this.#buckets.parents, role, parents); + await this.#backend.end(tx); + return okVoid; + } catch (e) { + return err('BACKEND_ERROR', (e as Error).message); + } } - async removeRoleParents(role: string, parents?: Values): Promise { - const tx = this.#backend.begin(); - if (parents) { - this.#backend.remove(tx, this.#buckets.parents, role, parents); - } else { - this.#backend.del(tx, this.#buckets.parents, role); + async removeRoleParents(role: string, parents?: Values): Promise { + try { + const tx = await this.#backend.begin(); + if (parents) { + await this.#backend.remove(tx, this.#buckets.parents, role, parents); + } else { + await this.#backend.del(tx, this.#buckets.parents, role); + } + await this.#backend.end(tx); + return okVoid; + } catch (e) { + return err('BACKEND_ERROR', (e as Error).message); } - await this.#backend.end(tx); } - async removeRole(role: string): Promise { - const resources = await this.#backend.get(this.#buckets.resources, role); + async removeRole(role: string): Promise { + try { + const v = validateNonEmpty(role, 'Role'); + if (v) return v; - const tx = this.#backend.begin(); - for (const resource of resources) { - this.#backend.del(tx, allowsBucket(resource), role); + const resources = await this.#backend.get(this.#buckets.resources, role); + + const tx = await this.#backend.begin(); + for (const resource of resources) { + await this.#backend.del(tx, allowsBucket(resource), role); + } + await this.#backend.del(tx, this.#buckets.resources, role); + await this.#backend.del(tx, this.#buckets.parents, role); + await this.#backend.del(tx, this.#buckets.roles, role); + await this.#backend.remove(tx, this.#buckets.meta, 'roles', role); + await this.#backend.end(tx); + return okVoid; + } catch (e) { + return err('BACKEND_ERROR', (e as Error).message); } - this.#backend.del(tx, this.#buckets.resources, role); - this.#backend.del(tx, this.#buckets.parents, role); - this.#backend.del(tx, this.#buckets.roles, role); - this.#backend.remove(tx, this.#buckets.meta, 'roles', role); - await this.#backend.end(tx); } // ------------------------------------------------------------------------- // Resources // ------------------------------------------------------------------------- - async removeResource(resource: string): Promise { - const roles = await this.#backend.get(this.#buckets.meta, 'roles'); - const tx = this.#backend.begin(); - this.#backend.del(tx, allowsBucket(resource), roles); - for (const role of roles) { - this.#backend.remove(tx, this.#buckets.resources, role, resource); + async removeResource(resource: string): Promise { + try { + const v = validateNonEmpty(resource, 'Resource'); + if (v) return v; + + const roles = await this.#backend.get(this.#buckets.meta, 'roles'); + const tx = await this.#backend.begin(); + await this.#backend.del(tx, allowsBucket(resource), roles); + for (const role of roles) { + await this.#backend.remove(tx, this.#buckets.resources, role, resource); + } + await this.#backend.end(tx); + return okVoid; + } catch (e) { + return err('BACKEND_ERROR', (e as Error).message); } - await this.#backend.end(tx); } // ------------------------------------------------------------------------- // Remove allow / permissions // ------------------------------------------------------------------------- - async removeAllow(role: string, resources: Values, permissions?: Values): Promise { - const res = toArray(resources); - const perms = permissions ? toArray(permissions) : undefined; - await this.#removePermissions(role, res as string[], perms as string[] | undefined); + async removeAllow(role: string, resources: Values, permissions?: Values): Promise { + try { + const v1 = validateNonEmpty(role, 'Role'); + if (v1) return v1; + const v2 = validateNonEmpty(resources, 'Resource'); + if (v2) return v2; + + const res = toArray(resources); + const perms = permissions ? toArray(permissions) : undefined; + await this.#removePermissions(role, res as string[], perms as string[] | undefined); + return okVoid; + } catch (e) { + return err('BACKEND_ERROR', (e as Error).message); + } } async #removePermissions(role: string, resources: string[], permissions?: string[]): Promise { - const tx = this.#backend.begin(); + const tx = await this.#backend.begin(); for (const resource of resources) { const bucket = allowsBucket(resource); if (permissions) { - this.#backend.remove(tx, bucket, role, permissions); + await this.#backend.remove(tx, bucket, role, permissions); } else { - this.#backend.del(tx, bucket, role); - this.#backend.remove(tx, this.#buckets.resources, role, resource); + await this.#backend.del(tx, bucket, role); + await this.#backend.remove(tx, this.#buckets.resources, role, resource); } } await this.#backend.end(tx); // Clean up resources with empty permission sets (not fully atomic) - const tx2 = this.#backend.begin(); + const tx2 = await this.#backend.begin(); for (const resource of resources) { const bucket = allowsBucket(resource); const remaining = await this.#backend.get(bucket, role); if (remaining.length === 0) { - this.#backend.remove(tx2, this.#buckets.resources, role, resource); + await this.#backend.remove(tx2, this.#buckets.resources, role, resource); } } await this.#backend.end(tx2); @@ -246,40 +344,68 @@ export class Acl implements IAcl { // Permission queries // ------------------------------------------------------------------------- - async allowedPermissions(userId: Value, resources: Values): Promise> { - if (!userId) return {}; + async allowedPermissions(userId: Value, resources: Values): Promise>> { + try { + if (!userId) return ok({}); - const res = toArray(resources) as string[]; - const roles = await this.userRoles(userId); - const result: Record = {}; + const res = toArray(resources) as string[]; + const rolesResult = await this.userRoles(userId); + if (!rolesResult.ok) return rolesResult; - for (const resource of res) { - result[resource] = await this.#resourcePermissions(roles, resource); + const roles = rolesResult.data; + const result: Record = {}; + + for (const resource of res) { + result[resource] = await this.#resourcePermissions(roles, resource); + } + return ok(result); + } catch (e) { + return err('BACKEND_ERROR', (e as Error).message); } - return result; } - async isAllowed(userId: Value, resource: string, permissions: Values): Promise { - const roles = await this.#backend.get(this.#buckets.users, userId); - if (roles.length === 0) return false; - return this.areAnyRolesAllowed(roles, resource, permissions); + async isAllowed(userId: Value, resource: string, permissions: Values): Promise> { + try { + const v1 = validateNonEmpty(userId, 'User ID'); + if (v1) return v1; + const v2 = validateNonEmpty(resource, 'Resource'); + if (v2) return v2; + const v3 = validateNonEmpty(permissions, 'Permission'); + if (v3) return v3; + + const roles = await this.#backend.get(this.#buckets.users, userId); + if (roles.length === 0) return ok(false); + return this.areAnyRolesAllowed(roles, resource, permissions); + } catch (e) { + return err('BACKEND_ERROR', (e as Error).message); + } } - async areAnyRolesAllowed(roles: Values, resource: string, permissions: Values): Promise { - const rolesArr = toArray(roles) as string[]; - if (rolesArr.length === 0) return false; - const permsArr = toArray(permissions) as string[]; - return this.#checkPermissions(rolesArr, resource, permsArr); + async areAnyRolesAllowed(roles: Values, resource: string, permissions: Values): Promise> { + try { + const rolesArr = toArray(roles) as string[]; + if (rolesArr.length === 0) return ok(false); + const permsArr = toArray(permissions) as string[]; + const allowed = await this.#checkPermissions(rolesArr, resource, permsArr); + return ok(allowed); + } catch (e) { + return err('BACKEND_ERROR', (e as Error).message); + } } // ------------------------------------------------------------------------- // What resources // ------------------------------------------------------------------------- - async whatResources(roles: Values, permissions?: Values): Promise | string[]> { - const rolesArr = toArray(roles) as string[]; - const permsArr = permissions ? toArray(permissions) as string[] : undefined; - return this.#permittedResources(rolesArr, permsArr); + async whatResources(roles: Values, permissions?: Values): Promise | string[]>> { + try { + const rolesArr = toArray(roles) as string[]; + const permsArr = permissions ? toArray(permissions) as string[] : undefined; + const data = await this.#permittedResources(rolesArr, permsArr); + return ok(data); + } catch (e) { + return err('BACKEND_ERROR', (e as Error).message); + } } async #permittedResources(roles: string[], permissions?: string[]): Promise | string[]> { @@ -321,7 +447,8 @@ export class Acl implements IAcl { } async #allUserRoles(userId: Value): Promise { - const roles = await this.userRoles(userId); + const result = await this.userRoles(userId); + const roles = result.ok ? result.data : []; if (roles.length > 0) { return this.#allRoles(roles); } diff --git a/packages/acl/src/acl.test.ts b/packages/acl/src/acl.test.ts index 209534a1..6325fd64 100644 --- a/packages/acl/src/acl.test.ts +++ b/packages/acl/src/acl.test.ts @@ -6,6 +6,13 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { Acl } from './Acl.js'; import { MemoryBackend } from './data/MemoryBackend.js'; +import type { AclResult } from './interfaces.js'; + +/** Unwrap AclResult — asserts ok and returns data. */ +function d(result: AclResult): T { + if (!result.ok) throw new Error(`Expected ok result, got ${result.code}: ${result.message}`); + return result.data; +} describe('Acl (MemoryBackend)', () => { let acl: Acl; @@ -22,13 +29,13 @@ describe('Acl (MemoryBackend)', () => { describe('addUserRoles / userRoles', () => { it('assigns a single role to a user', async () => { await acl.addUserRoles('user1', 'admin'); - const roles = await acl.userRoles('user1'); + const roles = d(await acl.userRoles('user1')); expect(roles).toContain('admin'); }); it('assigns multiple roles', async () => { await acl.addUserRoles('user2', ['editor', 'viewer']); - const roles = await acl.userRoles('user2'); + const roles = d(await acl.userRoles('user2')); expect(roles).toEqual(expect.arrayContaining(['editor', 'viewer'])); }); }); @@ -37,7 +44,7 @@ describe('Acl (MemoryBackend)', () => { it('removes a role from a user', async () => { await acl.addUserRoles('user1', ['admin', 'editor']); await acl.removeUserRoles('user1', 'admin'); - const roles = await acl.userRoles('user1'); + const roles = d(await acl.userRoles('user1')); expect(roles).not.toContain('admin'); expect(roles).toContain('editor'); }); @@ -47,7 +54,7 @@ describe('Acl (MemoryBackend)', () => { it('returns users for a given role', async () => { await acl.addUserRoles('u1', 'admin'); await acl.addUserRoles('u2', 'admin'); - const users = await acl.roleUsers('admin'); + const users = d(await acl.roleUsers('admin')); expect(users).toEqual(expect.arrayContaining(['u1', 'u2'])); }); }); @@ -55,12 +62,12 @@ describe('Acl (MemoryBackend)', () => { describe('hasRole', () => { it('returns true when user has the role', async () => { await acl.addUserRoles('u1', 'admin'); - expect(await acl.hasRole('u1', 'admin')).toBe(true); + expect(d(await acl.hasRole('u1', 'admin'))).toBe(true); }); it('returns false when user lacks the role', async () => { await acl.addUserRoles('u1', 'editor'); - expect(await acl.hasRole('u1', 'admin')).toBe(false); + expect(d(await acl.hasRole('u1', 'admin'))).toBe(false); }); }); @@ -73,24 +80,24 @@ describe('Acl (MemoryBackend)', () => { await acl.addUserRoles('u1', 'editor'); await acl.allow('editor', 'posts', 'edit'); - expect(await acl.isAllowed('u1', 'posts', 'edit')).toBe(true); - expect(await acl.isAllowed('u1', 'posts', 'delete')).toBe(false); + expect(d(await acl.isAllowed('u1', 'posts', 'edit'))).toBe(true); + expect(d(await acl.isAllowed('u1', 'posts', 'delete'))).toBe(false); }); it('grants multiple permissions at once', async () => { await acl.addUserRoles('u1', 'admin'); await acl.allow('admin', 'posts', ['create', 'edit', 'delete']); - expect(await acl.isAllowed('u1', 'posts', 'create')).toBe(true); - expect(await acl.isAllowed('u1', 'posts', 'delete')).toBe(true); + expect(d(await acl.isAllowed('u1', 'posts', 'create'))).toBe(true); + expect(d(await acl.isAllowed('u1', 'posts', 'delete'))).toBe(true); }); it('supports wildcard * permission', async () => { await acl.addUserRoles('u1', 'superadmin'); await acl.allow('superadmin', 'everything', '*'); - expect(await acl.isAllowed('u1', 'everything', 'anything')).toBe(true); - expect(await acl.isAllowed('u1', 'everything', 'whatever')).toBe(true); + expect(d(await acl.isAllowed('u1', 'everything', 'anything'))).toBe(true); + expect(d(await acl.isAllowed('u1', 'everything', 'whatever'))).toBe(true); }); }); @@ -107,10 +114,10 @@ describe('Acl (MemoryBackend)', () => { }, ]); - expect(await acl.isAllowed('u1', 'profile', 'read')).toBe(true); - expect(await acl.isAllowed('u1', 'profile', 'update')).toBe(true); - expect(await acl.isAllowed('u1', 'feed', 'read')).toBe(true); - expect(await acl.isAllowed('u1', 'feed', 'write')).toBe(false); + expect(d(await acl.isAllowed('u1', 'profile', 'read'))).toBe(true); + expect(d(await acl.isAllowed('u1', 'profile', 'update'))).toBe(true); + expect(d(await acl.isAllowed('u1', 'feed', 'read'))).toBe(true); + expect(d(await acl.isAllowed('u1', 'feed', 'write'))).toBe(false); }); }); @@ -126,8 +133,8 @@ describe('Acl (MemoryBackend)', () => { await acl.addUserRoles('u1', 'editor'); // editor can write (own) AND read (inherited from viewer) - expect(await acl.isAllowed('u1', 'docs', 'write')).toBe(true); - expect(await acl.isAllowed('u1', 'docs', 'read')).toBe(true); + expect(d(await acl.isAllowed('u1', 'docs', 'write'))).toBe(true); + expect(d(await acl.isAllowed('u1', 'docs', 'read'))).toBe(true); }); it('supports multi-level hierarchy', async () => { @@ -138,9 +145,9 @@ describe('Acl (MemoryBackend)', () => { await acl.addRoleParents('top', 'mid'); await acl.addUserRoles('u1', 'top'); - expect(await acl.isAllowed('u1', 'res', 'admin')).toBe(true); - expect(await acl.isAllowed('u1', 'res', 'write')).toBe(true); - expect(await acl.isAllowed('u1', 'res', 'read')).toBe(true); + expect(d(await acl.isAllowed('u1', 'res', 'admin'))).toBe(true); + expect(d(await acl.isAllowed('u1', 'res', 'write'))).toBe(true); + expect(d(await acl.isAllowed('u1', 'res', 'read'))).toBe(true); }); }); @@ -152,13 +159,13 @@ describe('Acl (MemoryBackend)', () => { await acl.addUserRoles('u1', 'editor'); // Before removal: editor inherits viewer's read - expect(await acl.isAllowed('u1', 'docs', 'read')).toBe(true); + expect(d(await acl.isAllowed('u1', 'docs', 'read'))).toBe(true); await acl.removeRoleParents('editor', 'viewer'); // After removal: editor only has write - expect(await acl.isAllowed('u1', 'docs', 'read')).toBe(false); - expect(await acl.isAllowed('u1', 'docs', 'write')).toBe(true); + expect(d(await acl.isAllowed('u1', 'docs', 'read'))).toBe(false); + expect(d(await acl.isAllowed('u1', 'docs', 'write'))).toBe(true); }); }); @@ -170,11 +177,11 @@ describe('Acl (MemoryBackend)', () => { it('removes the role and its permissions', async () => { await acl.allow('temp', 'stuff', 'do'); await acl.addUserRoles('u1', 'temp'); - expect(await acl.isAllowed('u1', 'stuff', 'do')).toBe(true); + expect(d(await acl.isAllowed('u1', 'stuff', 'do'))).toBe(true); await acl.removeRole('temp'); // areAnyRolesAllowed should fail now for 'temp' - expect(await acl.areAnyRolesAllowed('temp', 'stuff', 'do')).toBe(false); + expect(d(await acl.areAnyRolesAllowed('temp', 'stuff', 'do'))).toBe(false); }); }); @@ -184,8 +191,8 @@ describe('Acl (MemoryBackend)', () => { await acl.addUserRoles('u1', 'editor'); await acl.removeAllow('editor', 'posts', 'delete'); - expect(await acl.isAllowed('u1', 'posts', 'delete')).toBe(false); - expect(await acl.isAllowed('u1', 'posts', 'read')).toBe(true); + expect(d(await acl.isAllowed('u1', 'posts', 'delete'))).toBe(false); + expect(d(await acl.isAllowed('u1', 'posts', 'read'))).toBe(true); }); it('removes all permissions for a resource when no perms specified', async () => { @@ -193,8 +200,8 @@ describe('Acl (MemoryBackend)', () => { await acl.addUserRoles('u1', 'editor'); await acl.removeAllow('editor', 'posts'); - expect(await acl.isAllowed('u1', 'posts', 'read')).toBe(false); - expect(await acl.isAllowed('u1', 'posts', 'write')).toBe(false); + expect(d(await acl.isAllowed('u1', 'posts', 'read'))).toBe(false); + expect(d(await acl.isAllowed('u1', 'posts', 'write'))).toBe(false); }); }); @@ -202,10 +209,10 @@ describe('Acl (MemoryBackend)', () => { it('removes the resource from all roles', async () => { await acl.allow('admin', 'secrets', 'read'); await acl.addUserRoles('u1', 'admin'); - expect(await acl.isAllowed('u1', 'secrets', 'read')).toBe(true); + expect(d(await acl.isAllowed('u1', 'secrets', 'read'))).toBe(true); await acl.removeResource('secrets'); - expect(await acl.isAllowed('u1', 'secrets', 'read')).toBe(false); + expect(d(await acl.isAllowed('u1', 'secrets', 'read'))).toBe(false); }); }); @@ -219,14 +226,14 @@ describe('Acl (MemoryBackend)', () => { await acl.allow('editor', 'posts', ['read', 'write']); await acl.allow('editor', 'comments', 'moderate'); - const perms = await acl.allowedPermissions('u1', ['posts', 'comments', 'settings']); + const perms = d(await acl.allowedPermissions('u1', ['posts', 'comments', 'settings'])); expect(perms['posts']).toEqual(expect.arrayContaining(['read', 'write'])); expect(perms['comments']).toContain('moderate'); expect(perms['settings']).toEqual([]); }); it('returns {} for falsy userId', async () => { - const perms = await acl.allowedPermissions('', ['posts']); + const perms = d(await acl.allowedPermissions('', ['posts'])); expect(perms).toEqual({}); }); }); @@ -234,11 +241,11 @@ describe('Acl (MemoryBackend)', () => { describe('areAnyRolesAllowed', () => { it('returns true if at least one role has the permission', async () => { await acl.allow('a', 'res', 'read'); - expect(await acl.areAnyRolesAllowed(['a', 'b'], 'res', 'read')).toBe(true); + expect(d(await acl.areAnyRolesAllowed(['a', 'b'], 'res', 'read'))).toBe(true); }); it('returns false for empty roles', async () => { - expect(await acl.areAnyRolesAllowed([], 'res', 'read')).toBe(false); + expect(d(await acl.areAnyRolesAllowed([], 'res', 'read'))).toBe(false); }); }); @@ -247,7 +254,7 @@ describe('Acl (MemoryBackend)', () => { await acl.allow('editor', 'posts', ['read', 'write']); await acl.allow('editor', 'pages', 'read'); - const result = await acl.whatResources('editor') as Record; + const result = d(await acl.whatResources('editor')) as Record; expect(Object.keys(result)).toEqual(expect.arrayContaining(['posts', 'pages'])); expect(result['posts']).toEqual(expect.arrayContaining(['read', 'write'])); }); @@ -256,7 +263,7 @@ describe('Acl (MemoryBackend)', () => { await acl.allow('editor', 'posts', ['read', 'write']); await acl.allow('editor', 'pages', 'read'); - const result = await acl.whatResources('editor', 'write') as string[]; + const result = d(await acl.whatResources('editor', 'write')) as string[]; expect(result).toContain('posts'); expect(result).not.toContain('pages'); }); diff --git a/packages/acl/src/index.ts b/packages/acl/src/index.ts index 8b784005..e15dd6d6 100644 --- a/packages/acl/src/index.ts +++ b/packages/acl/src/index.ts @@ -11,13 +11,26 @@ export type { AclGrant, AclAllow, AclOptions, + AclErrorCode, + AclOk, + AclErr, + AclResult, BucketNames, Value, Values, } from './interfaces.js'; +export { ok, okVoid, err } from './interfaces.js'; // VFS export { AclVfsClient } from './vfs/AclVfsClient.js'; export { DecoratedVfsClient } from './vfs/DecoratedVfsClient.js'; export { loadVfsSettings, vfsResource, resourceChain } from './vfs/vfs-acl.js'; -export type { VfsSettings, VfsAclEntry } from './vfs/vfs-acl.js'; +export type { VfsSettings, VfsAclEntry, VfsGroup } from './vfs/vfs-acl.js'; +export { DefaultSanitizers } from './vfs/sanitizers.js'; +export { + assertNonEmpty, + cleanPath, pathSegments, normalisePath, + cleanPermission, cleanPermissions, + isUuid, cleanUuid, cleanId, cleanGroupName, + sanitizeSubpath, sanitizeWritePath, sanitizeFilename, +} from './vfs/sanitizers.js'; diff --git a/packages/acl/src/interfaces.ts b/packages/acl/src/interfaces.ts index a2b54a30..6bb336ec 100644 --- a/packages/acl/src/interfaces.ts +++ b/packages/acl/src/interfaces.ts @@ -12,6 +12,35 @@ export type Value = string | number; export type Values = Value | Value[]; +// --------------------------------------------------------------------------- +// Result types +// --------------------------------------------------------------------------- + +export type AclErrorCode = + | 'OK' + | 'INVALID_INPUT' + | 'NOT_FOUND' + | 'BACKEND_ERROR'; + +export interface AclOk { + readonly ok: true; + readonly code: 'OK'; + readonly data: T; +} + +export interface AclErr { + readonly ok: false; + readonly code: Exclude; + readonly message: string; +} + +export type AclResult = AclOk | AclErr; + +// Result constructors +export const ok = (data: T): AclOk => ({ ok: true, code: 'OK', data }); +export const okVoid: AclOk = Object.freeze({ ok: true, code: 'OK', data: undefined }) as AclOk; +export const err = (code: AclErr['code'], message: string): AclErr => ({ ok: false, code, message }); + // --------------------------------------------------------------------------- // Bucket naming // --------------------------------------------------------------------------- @@ -43,7 +72,7 @@ export interface AclOptions { * `T` is the transaction type (e.g. `(() => void)[]` for in-memory). */ export interface IBackend { - begin(): T; + begin(): T | Promise; end(transaction: T): Promise; clean(): Promise; @@ -51,9 +80,9 @@ export interface IBackend { union(bucket: string, keys: Value[]): Promise; unions(buckets: string[], keys: Value[]): Promise>; - add(transaction: T, bucket: string, key: Value, values: Values): void; - del(transaction: T, bucket: string, keys: Values): void; - remove(transaction: T, bucket: string, key: Value, values: Values): void; + add(transaction: T, bucket: string, key: Value, values: Values): void | Promise; + del(transaction: T, bucket: string, keys: Values): void | Promise; + remove(transaction: T, bucket: string, key: Value, values: Values): void | Promise; } // --------------------------------------------------------------------------- @@ -61,27 +90,27 @@ export interface IBackend { // --------------------------------------------------------------------------- export interface IAcl { - allow(roles: Values, resources: Values, permissions: Values): Promise; - allow(grants: AclGrant[]): Promise; + allow(roles: Values, resources: Values, permissions: Values): Promise; + allow(grants: AclGrant[]): Promise; - addUserRoles(userId: Value, roles: Values): Promise; - removeUserRoles(userId: Value, roles: Values): Promise; - userRoles(userId: Value): Promise; - roleUsers(role: Value): Promise; - hasRole(userId: Value, role: string): Promise; + addUserRoles(userId: Value, roles: Values): Promise; + removeUserRoles(userId: Value, roles: Values): Promise; + userRoles(userId: Value): Promise>; + roleUsers(role: Value): Promise>; + hasRole(userId: Value, role: string): Promise>; - addRoleParents(role: string, parents: Values): Promise; - removeRoleParents(role: string, parents?: Values): Promise; - removeRole(role: string): Promise; - removeResource(resource: string): Promise; + addRoleParents(role: string, parents: Values): Promise; + removeRoleParents(role: string, parents?: Values): Promise; + removeRole(role: string): Promise; + removeResource(resource: string): Promise; - removeAllow(role: string, resources: Values, permissions?: Values): Promise; + removeAllow(role: string, resources: Values, permissions?: Values): Promise; - allowedPermissions(userId: Value, resources: Values): Promise>; - isAllowed(userId: Value, resource: string, permissions: Values): Promise; - areAnyRolesAllowed(roles: Values, resource: string, permissions: Values): Promise; + allowedPermissions(userId: Value, resources: Values): Promise>>; + isAllowed(userId: Value, resource: string, permissions: Values): Promise>; + areAnyRolesAllowed(roles: Values, resource: string, permissions: Values): Promise>; - whatResources(roles: Values, permissions?: Values): Promise | string[]>; + whatResources(roles: Values, permissions?: Values): Promise | string[]>>; } // --------------------------------------------------------------------------- @@ -103,6 +132,6 @@ export interface AclAllow { // --------------------------------------------------------------------------- export interface IFileStore { - read(path?: string): void; - write(path?: string): void; + read(path?: string): void | Promise; + write(path?: string): void | Promise; } diff --git a/packages/acl/src/vfs/AclVfsClient.ts b/packages/acl/src/vfs/AclVfsClient.ts index 1f1ec2d6..e8f7cb31 100644 --- a/packages/acl/src/vfs/AclVfsClient.ts +++ b/packages/acl/src/vfs/AclVfsClient.ts @@ -21,6 +21,7 @@ import type { Acl } from '../Acl.js'; import type { INode } from './fs/VFS.js'; import { LocalVFS, type IDefaultParameters } from './fs/Local.js'; import { resourceChain } from './vfs-acl.js'; +import { cleanUuid, sanitizeSubpath } from './sanitizers.js'; export class AclVfsClient { readonly #acl: Acl; @@ -37,8 +38,8 @@ export class AclVfsClient { constructor(acl: Acl, ownerId: string, callerId: string, fsOpts: IDefaultParameters) { this.#acl = acl; this.#local = new LocalVFS(fsOpts); - this.#ownerId = ownerId; - this.#callerId = callerId; + this.#ownerId = cleanUuid(ownerId); + this.#callerId = cleanUuid(callerId); } // ── Guards ────────────────────────────────────────────────────── @@ -49,11 +50,12 @@ export class AclVfsClient { * This means a grant on `/` covers the entire tree. */ async #guard(permission: string, path: string): Promise { - const chain = resourceChain(this.#ownerId, path); + const safePath = sanitizeSubpath(path); + const chain = resourceChain(this.#ownerId, safePath); for (const resource of chain) { - const allowed = await this.#acl.isAllowed(this.#callerId, resource, permission); - if (allowed) return; + const result = await this.#acl.isAllowed(this.#callerId, resource, permission); + if (result.ok && result.data) return; } const err = new Error( diff --git a/packages/acl/src/vfs/DecoratedVfsClient.ts b/packages/acl/src/vfs/DecoratedVfsClient.ts index 050cae56..19312dd5 100644 --- a/packages/acl/src/vfs/DecoratedVfsClient.ts +++ b/packages/acl/src/vfs/DecoratedVfsClient.ts @@ -18,6 +18,7 @@ import type { Acl } from '../Acl.js'; import type { INode } from './fs/VFS.js'; import { LocalVFS, type IDefaultParameters } from './fs/Local.js'; import { resourceChain } from './vfs-acl.js'; +import { cleanUuid, sanitizeSubpath } from './sanitizers.js'; // --------------------------------------------------------------------------- // Decorator factory @@ -37,12 +38,12 @@ function aclGuard(permission: string) { const methodName = String(context.name); return async function (this: T, ...args: A): Promise { - const path = args[0]; + const path = sanitizeSubpath(args[0]); const chain = resourceChain(this.ownerId, path); for (const resource of chain) { - const allowed = await this.acl.isAllowed(this.callerId, resource, permission); - if (allowed) { + const result = await this.acl.isAllowed(this.callerId, resource, permission); + if (result.ok && result.data) { return target.call(this, ...args); } } @@ -75,8 +76,8 @@ export class DecoratedVfsClient { constructor(acl: Acl, ownerId: string, callerId: string, fsOpts: IDefaultParameters) { this.acl = acl; this.local = new LocalVFS(fsOpts); - this.ownerId = ownerId; - this.callerId = callerId; + this.ownerId = cleanUuid(ownerId); + this.callerId = cleanUuid(callerId); } // ── Read ──────────────────────────────────────────────────────── diff --git a/packages/acl/src/vfs/fs/Local.ts b/packages/acl/src/vfs/fs/Local.ts index d4722ba8..0c4430b2 100644 --- a/packages/acl/src/vfs/fs/Local.ts +++ b/packages/acl/src/vfs/fs/Local.ts @@ -38,7 +38,7 @@ export class LocalVFS { private umask: number; private ig: any = null; - constructor(fsOptions: IDefaultParameters, resource?: FileResource) { + constructor(fsOptions: IDefaultParameters, _resource?: FileResource) { if (!fsOptions.root) { throw new Error('root is a required option'); } @@ -46,10 +46,10 @@ export class LocalVFS { this.fsOptions = fsOptions; this.root = pathNormalize(fsOptions.root); - if (pathSep === '/' && this.root[0] !== '/') { + if (pathSep === '/' && !this.root.startsWith('/')) { throw new Error('root path must start in /'); } - if (this.root[this.root.length - 1] !== pathSep) { + if (!this.root.endsWith(pathSep)) { this.root += pathSep; } @@ -125,7 +125,7 @@ export class LocalVFS { return resolved; } - async stat(path: string, options?: any): Promise { + async stat(path: string, _options?: any): Promise { const dir = await this.resolvePath(dirname(path)); const file = basename(path); path = join(dir, file); @@ -182,7 +182,7 @@ export class LocalVFS { await writeFile(realp, content, options); } - async readdir(path: string, options?: any): Promise { + async readdir(path: string, _options?: any): Promise { const realp = await this.resolvePath(path); const s = await stat(realp); if (!s.isDirectory()) { @@ -228,38 +228,36 @@ export class LocalVFS { await mkdir(realp, { ...options, recursive: true }); } - async rmfile(path: string, options?: any): Promise { + async rmfile(path: string, _options?: any): Promise { const realp = await this.resolvePath(path); await rm(realp, { recursive: true, force: true }); } - async rmdir(path: string, options: any = {}): Promise { + async rmdir(path: string, _options: any = {}): Promise { const realp = await this.resolvePath(path); await rm(realp, { recursive: true, force: true }); } - async rename(from: string, to: string, options: any = {}): Promise { + async rename(from: string, to: string, _options: any = {}): Promise { const frompath = await this.resolvePath(from); const topath = await this.resolvePath(to); await rename(frompath, topath); } - async copy(from: string, to: string, options: any = {}): Promise { + async copy(from: string, to: string, _options: any = {}): Promise { const frompath = await this.resolvePath(from); const topath = await this.resolvePath(to); await cp(frompath, topath, { recursive: true }); } - exists(path: string): Promise { - return new Promise(async (resolve) => { - try { - const realp = await this.resolvePath(path); - await access(realp); - resolve(true); - } catch (err) { - resolve(false); - } - }); + async exists(path: string): Promise { + try { + const realp = await this.resolvePath(path); + await access(realp); + return true; + } catch (_err) { + return false; + } } private calcEtag(s: Stats): string { diff --git a/packages/acl/src/vfs/fs/Resource.ts b/packages/acl/src/vfs/fs/Resource.ts index ac3bd88d..01f19800 100644 --- a/packages/acl/src/vfs/fs/Resource.ts +++ b/packages/acl/src/vfs/fs/Resource.ts @@ -1,8 +1,6 @@ -// tslint:disable-next-line:interface-name export interface Hash { - [ id: string ]: T; + [id: string]: T; } -// tslint:disable-next-line:interface-name export interface List { [index: number]: T; length: number; @@ -16,7 +14,7 @@ export interface IObjectLiteral { /** * Represents some Type of the Object. */ -export type ObjectType = { new (): T } | Function; +export type ObjectType = (new () => T) | ((...args: never[]) => unknown); /** * Same as Partial but goes deeper and makes Partial all its properties and sub-properties. */ @@ -31,10 +29,10 @@ export interface IDelimitter { export enum EResourceType { - JS_HEADER_INCLUDE = 'JS-HEADER-INCLUDE', - JS_HEADER_SCRIPT_TAG = 'JS-HEADER-SCRIPT-TAG', - CSS = 'CSS', - FILE_PROXY = 'FILE_PROXY' + JS_HEADER_INCLUDE = 'JS-HEADER-INCLUDE' as any, + JS_HEADER_SCRIPT_TAG = 'JS-HEADER-SCRIPT-TAG' as any, + CSS = 'CSS' as any, + FILE_PROXY = 'FILE_PROXY' as any } export interface IResource { diff --git a/packages/acl/src/vfs/fs/VFS.ts b/packages/acl/src/vfs/fs/VFS.ts index 30511ede..085d6032 100644 --- a/packages/acl/src/vfs/fs/VFS.ts +++ b/packages/acl/src/vfs/fs/VFS.ts @@ -6,11 +6,11 @@ * @enum {string} */ export enum ENodeType { - FILE = 'file', - DIR = 'dir', - SYMLINK = 'symlink', - OTHER = 'other', - BLOCK = 'block' + FILE = 'file' as any, + DIR = 'dir' as any, + SYMLINK = 'symlink' as any, + OTHER = 'other' as any, + BLOCK = 'block' as any } /** * General features of a VFS @@ -77,7 +77,6 @@ export type INodeEx = INode & { linkStat: null; }; -// tslint:disable-next-line:interface-name export interface VFS_PATH { mount: string; path: string; diff --git a/packages/acl/src/vfs/path-sanitizer.ts b/packages/acl/src/vfs/path-sanitizer.ts index 819d302c..ec15fe53 100644 --- a/packages/acl/src/vfs/path-sanitizer.ts +++ b/packages/acl/src/vfs/path-sanitizer.ts @@ -226,7 +226,7 @@ export function sanitizeWritePath(subpath: string, opts?: { isDirectory?: boolea * Strips illegal characters, control characters, Windows reserved names. * Returns the sanitized filename, truncated to 255 bytes. */ -export function sanitizeFilename(input: string, replacement: string = ''): string { +export function sanitizeFilename(input: string, replacement = ''): string { if (typeof input !== 'string') { throw new Error('Input must be a string'); } diff --git a/packages/acl/src/vfs/sanitizers.ts b/packages/acl/src/vfs/sanitizers.ts new file mode 100644 index 00000000..512390e9 --- /dev/null +++ b/packages/acl/src/vfs/sanitizers.ts @@ -0,0 +1,176 @@ +/** + * Default input sanitizers for ACL. + * + * Reusable, pure functions — no side effects, no I/O. + * Exported as the `DefaultSanitizers` namespace for convenient access. + * + * Used by: + * - Core Acl class (assertNonEmpty) + * - VFS ACL bridge (cleanUuid, cleanGroupName, normalisePath, cleanPermissions) + * - AclVfsClient / DecoratedVfsClient (cleanUuid, sanitizeSubpath) + */ + +/** + * Normalise a subpath for VFS use. + * + * - Converts backslashes to forward slashes + * - Strips leading and trailing slashes + * - Collapses consecutive slashes + * + * @example cleanPath('\\docs\\sub/') → 'docs/sub' + * @example cleanPath('/') → '' + * @example cleanPath('') → '' + */ +export function cleanPath(raw: string): string { + return raw + .replace(/\\/g, '/') + .replace(/\/+/g, '/') + .replace(/^\/+/, '') + .replace(/\/+$/, ''); +} + +/** + * Split a cleaned path into individual segments. + * + * @example pathSegments('docs/sub/file.txt') → ['docs', 'sub', 'file.txt'] + * @example pathSegments('') → [] + */ +export function pathSegments(raw: string): string[] { + const clean = cleanPath(raw); + return clean ? clean.split('/') : []; +} + +/** + * Normalise a path into its absolute VFS form (leading slash, no trailing). + * + * @example normalisePath('docs/sub/') → '/docs/sub' + * @example normalisePath('') → '/' + * @example normalisePath('\\a\\b') → '/a/b' + */ +export function normalisePath(raw: string): string { + const clean = cleanPath(raw); + return clean ? `/${clean}` : '/'; +} + +/** + * Validate and normalise a permission name. + * + * - Lowercased + * - Trimmed + * - Rejects empty strings + * + * @throws Error if the permission is empty after trimming. + */ +export function cleanPermission(raw: string): string { + const p = raw.trim().toLowerCase(); + if (!p) throw new Error('Permission name cannot be empty'); + return p; +} + +/** + * Validate and normalise permission arrays. + */ +export function cleanPermissions(raw: string[]): string[] { + return raw.map(cleanPermission); +} + +// --------------------------------------------------------------------------- +// UUID validation +// --------------------------------------------------------------------------- + +/** Standard UUID v1–v5 pattern (case-insensitive, lowercased on output). */ +const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +/** + * Test whether a string is a valid UUID. + * + * @example isUuid('3bb4cfbf-318b-44d3-a9d3-35680e738421') → true + * @example isUuid('not-a-uuid') → false + */ +export function isUuid(value: string): boolean { + return UUID_RE.test(value.trim()); +} + +/** + * Validate and normalise a UUID string (lowercased, trimmed). + * + * @throws Error if the value is not a valid UUID. + */ +export function cleanUuid(raw: string): string { + const id = raw.trim().toLowerCase(); + if (!UUID_RE.test(id)) { + throw new Error(`Invalid UUID: '${raw}'`); + } + return id; +} + +/** + * Validate a user/owner identifier — must be a valid UUID. + * + * @throws Error if the identifier is not a valid UUID. + */ +export function cleanId(raw: string): string { + return cleanUuid(raw); +} + +/** + * Validate a group name (non-empty, lowercased, no colons). + * + * @throws Error if the name is empty or contains reserved characters. + */ +export function cleanGroupName(raw: string): string { + const name = raw.trim().toLowerCase(); + if (!name) throw new Error('Group name cannot be empty'); + if (name.includes(':')) throw new Error(`Group name cannot contain ':': ${name}`); + return name; +} + +// --------------------------------------------------------------------------- +// Generic assertions (used by core Acl) +// --------------------------------------------------------------------------- + +/** + * Reject empty/whitespace-only identifiers. + * Works with single values or arrays of values (string | number). + * + * @throws Error with a descriptive label if any value is empty. + * + * @example assertNonEmpty('admin', 'Role') // ok + * @example assertNonEmpty('', 'Role') // throws "Role cannot be empty" + * @example assertNonEmpty(['a', ''], 'Role') // throws "Role cannot be empty" + */ +export function assertNonEmpty(value: string | number | (string | number)[], label: string): void { + const arr = Array.isArray(value) ? value : [value]; + for (const v of arr) { + const s = String(v).trim(); + if (!s) throw new Error(`${label} cannot be empty`); + } +} + +// --------------------------------------------------------------------------- +// Re-export path-sanitizer functions for unified access +// --------------------------------------------------------------------------- + +export { sanitizeSubpath, sanitizeWritePath, sanitizeFilename } from './path-sanitizer.js'; + +// --------------------------------------------------------------------------- +// Namespace re-export for convenience +// --------------------------------------------------------------------------- + +import { sanitizeSubpath, sanitizeWritePath, sanitizeFilename } from './path-sanitizer.js'; + +export const DefaultSanitizers = { + assertNonEmpty, + cleanPath, + pathSegments, + normalisePath, + cleanPermission, + cleanPermissions, + isUuid, + cleanUuid, + cleanId, + cleanGroupName, + sanitizeSubpath, + sanitizeWritePath, + sanitizeFilename, +} as const; diff --git a/packages/acl/src/vfs/vfs-acl.ts b/packages/acl/src/vfs/vfs-acl.ts index 321cfb9e..ba024271 100644 --- a/packages/acl/src/vfs/vfs-acl.ts +++ b/packages/acl/src/vfs/vfs-acl.ts @@ -4,6 +4,10 @@ * Reads per-user `vfs-settings.json` files and populates an Acl instance * with fine-grained, path-scoped permissions for each user's VFS folder. * + * Supports: + * - Direct user grants: { userId, path, permissions } + * - Group grants: { group, path, permissions } + * * Resource naming: `vfs::` — e.g. `vfs:3bb4cfbf-...:/docs` * * Permissions: read | write | list | mkdir | delete | rename | copy @@ -11,13 +15,22 @@ import { readFileSync, existsSync } from 'node:fs'; import { join } from 'node:path'; import type { Acl } from '../Acl.js'; +import { pathSegments, normalisePath, cleanGroupName, cleanId, cleanPermissions } from './sanitizers.js'; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- +export interface VfsGroup { + name: string; + members: string[]; +} + export interface VfsAclEntry { - userId: string; + /** Direct user grant */ + userId?: string; + /** Group grant */ + group?: string; /** Scoped path — defaults to "/" (entire folder). */ path?: string; permissions: string[]; @@ -25,6 +38,7 @@ export interface VfsAclEntry { export interface VfsSettings { owner: string; + groups?: VfsGroup[]; acl: VfsAclEntry[]; } @@ -39,7 +53,7 @@ export interface VfsSettings { * vfsResource('aaa', '/docs') → 'vfs:aaa:/docs' */ export function vfsResource(ownerId: string, resourcePath = '/'): string { - return `vfs:${ownerId}:${resourcePath}`; + return `vfs:${ownerId}:${normalisePath(resourcePath)}`; } /** @@ -51,8 +65,7 @@ export function vfsResource(ownerId: string, resourcePath = '/'): string { * The guard walks this chain top-down; if ANY level allows, access is granted. */ export function resourceChain(ownerId: string, subpath: string): string[] { - const clean = subpath.replace(/\\/g, '/').replace(/^\/+/, '').replace(/\/+$/, ''); - const segments = clean ? clean.split('/') : []; + const segments = pathSegments(subpath); const chain: string[] = []; @@ -76,6 +89,7 @@ export function resourceChain(ownerId: string, subpath: string): string[] { * * - The **owner** always gets `*` on `/` (entire tree). * - Each ACL entry grants permissions scoped to `entry.path` (default: `/`). + * - Group entries resolve members from the `groups[]` array. */ export async function loadVfsSettings(acl: Acl, userDir: string): Promise { const settingsPath = join(userDir, 'vfs-settings.json'); @@ -84,18 +98,50 @@ export async function loadVfsSettings(acl: Acl, userDir: string): Promise { + if (!result.ok) throw new Error((result as { message: string }).message); + }; + + // Owner role — full access on entire tree + const ownerRole = `owner:${safeOwner}`; + unwrap(await acl.allow(ownerRole, vfsResource(safeOwner, '/'), '*')); + unwrap(await acl.addUserRoles(safeOwner, ownerRole)); + + // Index groups (validate member IDs) + const groupMembers = new Map(); + for (const group of settings.groups ?? []) { + const safeName = cleanGroupName(group.name); + const safeMembers = group.members.map(m => cleanId(m)); + groupMembers.set(safeName, safeMembers); + } + + // Process ACL entries for (const entry of settings.acl) { - const resourcePath = entry.path ?? '/'; - const resource = vfsResource(settings.owner, resourcePath); - const grantRole = `vfs-grant:${settings.owner}:${entry.userId}:${resourcePath}`; - await acl.allow(grantRole, resource, entry.permissions); - await acl.addUserRoles(entry.userId, grantRole); + const resourcePath = normalisePath(entry.path ?? '/'); + const resource = vfsResource(safeOwner, resourcePath); + const safePerms = cleanPermissions(entry.permissions); + + if (entry.group) { + // Group grant + const safeGroup = cleanGroupName(entry.group); + const groupRole = `group:${safeOwner}:${safeGroup}`; + unwrap(await acl.allow(groupRole, resource, safePerms)); + + const members = groupMembers.get(safeGroup) ?? []; + for (const memberId of members) { + unwrap(await acl.addUserRoles(memberId, groupRole)); + } + } else if (entry.userId) { + // Direct user grant + const safeId = cleanId(entry.userId); + const grantRole = `vfs-grant:${safeOwner}:${safeId}:${resourcePath}`; + unwrap(await acl.allow(grantRole, resource, safePerms)); + unwrap(await acl.addUserRoles(safeId, grantRole)); + } } return settings; diff --git a/packages/acl/tests/vfs-acl-decorated.e2e.test.ts b/packages/acl/tests/vfs-acl-decorated.e2e.test.ts new file mode 100644 index 00000000..bb632d1a --- /dev/null +++ b/packages/acl/tests/vfs-acl-decorated.e2e.test.ts @@ -0,0 +1,166 @@ +/** + * DecoratedVfsClient — e2e test + * + * Verifies that the decorator-based client behaves identically + * to the imperative AclVfsClient using the same path-scoped fixture. + * + * Uses: tests/vfs/root/3bb4cfbf-…/vfs-settings.json + * - User aaa → read+list on / + * - User ccc → full on /shared, read+list on /docs + */ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { resolve, join } from 'node:path'; +import { existsSync, rmSync, mkdirSync, readFileSync } from 'node:fs'; +import { Acl } from '../src/Acl.js'; +import { MemoryBackend } from '../src/data/MemoryBackend.js'; +import { loadVfsSettings } from '../src/vfs/vfs-acl.js'; +import { DecoratedVfsClient } from '../src/vfs/DecoratedVfsClient.js'; + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +const OWNER = '3bb4cfbf-318b-44d3-a9d3-35680e738421'; +const READER = 'aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb'; +const COLLAB = 'cccccccc-4444-5555-6666-dddddddddddd'; +const NOBODY = '99999999-0000-0000-0000-ffffffffffff'; + +const USER_DIR = resolve(import.meta.dirname!, 'vfs/root', OWNER); +const SANDBOX = '_decorated_sandbox'; + +function client(acl: Acl, callerId: string): DecoratedVfsClient { + return new DecoratedVfsClient(acl, OWNER, callerId, { root: USER_DIR }); +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +describe('DecoratedVfsClient — e2e', () => { + let acl: Acl; + + beforeAll(async () => { + acl = new Acl(new MemoryBackend()); + await loadVfsSettings(acl, USER_DIR); + + for (const sub of ['shared', 'docs', 'private']) { + mkdirSync(join(USER_DIR, sub, SANDBOX), { recursive: true }); + } + }); + + afterAll(() => { + for (const sub of ['shared', 'docs', 'private']) { + const p = join(USER_DIR, sub, SANDBOX); + if (existsSync(p)) rmSync(p, { recursive: true, force: true }); + } + }); + + // ===================================================================== + // Owner — full access + // ===================================================================== + + describe('owner', () => { + it('can list /docs', async () => { + const c = client(acl, OWNER); + const entries = await c.readdir('docs'); + expect(entries.map(e => e.name)).toContain('readme.txt'); + }); + + it('can write in /private', async () => { + const c = client(acl, OWNER); + await c.writefile(`private/${SANDBOX}/decorated.txt`, 'decorator works'); + expect(readFileSync(join(USER_DIR, 'private', SANDBOX, 'decorated.txt'), 'utf8')) + .toBe('decorator works'); + }); + + it('can mkdir in /shared', async () => { + const c = client(acl, OWNER); + await c.mkdir(`shared/${SANDBOX}/dec-dir`); + expect(existsSync(join(USER_DIR, 'shared', SANDBOX, 'dec-dir'))).toBe(true); + }); + + it('can delete in /private', async () => { + const c = client(acl, OWNER); + await c.writefile(`private/${SANDBOX}/del.txt`, 'bye'); + await c.rmfile(`private/${SANDBOX}/del.txt`); + expect(existsSync(join(USER_DIR, 'private', SANDBOX, 'del.txt'))).toBe(false); + }); + }); + + // ===================================================================== + // Reader — read+list on / + // ===================================================================== + + describe('reader (read+list on /)', () => { + it('can list /docs', async () => { + const c = client(acl, READER); + const entries = await c.readdir('docs'); + expect(entries.map(e => e.name)).toContain('readme.txt'); + }); + + it('can check existence in /shared', async () => { + const c = client(acl, READER); + expect(await c.exists('shared/data.txt')).toBe(true); + }); + + it('CANNOT write in /shared', async () => { + const c = client(acl, READER); + await expect(c.writefile(`shared/${SANDBOX}/nope.txt`, 'x')).rejects.toThrow('EACCES'); + }); + + it('CANNOT mkdir in /docs', async () => { + const c = client(acl, READER); + await expect(c.mkdir(`docs/${SANDBOX}/nope`)).rejects.toThrow('EACCES'); + }); + }); + + // ===================================================================== + // Collaborator — full on /shared, read on /docs + // ===================================================================== + + describe('collaborator (full on /shared)', () => { + it('can write in /shared', async () => { + const c = client(acl, COLLAB); + await c.writefile(`shared/${SANDBOX}/collab.txt`, 'collab'); + expect(readFileSync(join(USER_DIR, 'shared', SANDBOX, 'collab.txt'), 'utf8')) + .toBe('collab'); + }); + + it('can list /docs', async () => { + const c = client(acl, COLLAB); + const entries = await c.readdir('docs'); + expect(entries.map(e => e.name)).toContain('readme.txt'); + }); + + it('CANNOT write in /docs', async () => { + const c = client(acl, COLLAB); + await expect(c.writefile(`docs/${SANDBOX}/nope.txt`, 'x')).rejects.toThrow('EACCES'); + }); + + it('CANNOT list /private', async () => { + const c = client(acl, COLLAB); + await expect(c.readdir('private')).rejects.toThrow('EACCES'); + }); + }); + + // ===================================================================== + // Stranger — blocked + // ===================================================================== + + describe('stranger — EACCES on everything', () => { + it('CANNOT list /shared', async () => { + const c = client(acl, NOBODY); + await expect(c.readdir('shared')).rejects.toThrow('EACCES'); + }); + + it('CANNOT read /docs', async () => { + const c = client(acl, NOBODY); + await expect(c.exists('docs/readme.txt')).rejects.toThrow('EACCES'); + }); + + it('CANNOT write anywhere', async () => { + const c = client(acl, NOBODY); + await expect(c.writefile(`shared/${SANDBOX}/x.txt`, 'x')).rejects.toThrow('EACCES'); + }); + }); +}); diff --git a/packages/acl/tests/vfs-acl-edge.e2e.test.ts b/packages/acl/tests/vfs-acl-edge.e2e.test.ts new file mode 100644 index 00000000..7884476e --- /dev/null +++ b/packages/acl/tests/vfs-acl-edge.e2e.test.ts @@ -0,0 +1,189 @@ +/** + * VFS ACL — Edge cases + * + * Fixture: tests/vfs/root/edge-cases/vfs-settings.json + * + * 1. Empty group — "empty-group" has members: [] → full on /shared + * Nobody should get access, but the loader must not crash. + * + * 2. Undefined group ref — "ghost-group" is referenced in ACL + * but NOT defined in groups[]. Loader should handle gracefully. + * + * 3. Multi-group user — user "aaa" is in both "multi-a" and "multi-b". + * "multi-a" grants read+list on /shared. + * "multi-b" grants write+mkdir on /shared and read+list on /docs. + * User should get the UNION: read+list+write+mkdir on /shared AND read+list on /docs. + */ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { resolve, join } from 'node:path'; +import { existsSync, rmSync, mkdirSync, writeFileSync } from 'node:fs'; +import { Acl } from '../src/Acl.js'; +import { MemoryBackend } from '../src/data/MemoryBackend.js'; +import { loadVfsSettings, vfsResource } from '../src/vfs/vfs-acl.js'; +import { AclVfsClient } from '../src/vfs/AclVfsClient.js'; +import type { AclResult } from '../src/interfaces.js'; + +/** Unwrap AclResult — asserts ok and returns data. */ +function d(result: AclResult): T { + if (!result.ok) throw new Error(`Expected ok, got ${result.code}: ${result.message}`); + return result.data; +} + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +const OWNER = '3bb4cfbf-318b-44d3-a9d3-35680e738421'; +const USER_A = 'aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb'; // in multi-a + multi-b +const NOBODY = '99999999-0000-0000-0000-ffffffffffff'; + +const USER_DIR = resolve(import.meta.dirname!, 'vfs/root/edge-cases'); +const SANDBOX = '_edge_sandbox'; + +const SHARED = vfsResource(OWNER, '/shared'); +const DOCS = vfsResource(OWNER, '/docs'); + +function client(acl: Acl, callerId: string): AclVfsClient { + return new AclVfsClient(acl, OWNER, callerId, { root: USER_DIR }); +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +describe('VFS ACL — edge cases', () => { + let acl: Acl; + + beforeAll(async () => { + acl = new Acl(new MemoryBackend()); + + // Create folder structure + seed files + for (const sub of ['shared', 'docs']) { + mkdirSync(join(USER_DIR, sub, SANDBOX), { recursive: true }); + } + writeFileSync(join(USER_DIR, 'shared', 'data.txt'), 'shared'); + writeFileSync(join(USER_DIR, 'docs', 'readme.txt'), 'docs'); + + await loadVfsSettings(acl, USER_DIR); + }); + + afterAll(() => { + for (const sub of ['shared', 'docs']) { + const p = join(USER_DIR, sub, SANDBOX); + if (existsSync(p)) rmSync(p, { recursive: true, force: true }); + } + for (const f of ['shared/data.txt', 'docs/readme.txt']) { + const p = join(USER_DIR, f); + if (existsSync(p)) rmSync(p); + } + }); + + // ================================================================= + // 1. Empty group — members: [] + // ================================================================= + + describe('empty group — nobody gets access', () => { + it('loader does not crash', () => { + // If we got here, loadVfsSettings succeeded + expect(true).toBe(true); + }); + + it('stranger has no access to /shared (despite empty group having full perms)', async () => { + expect(d(await acl.isAllowed(NOBODY, SHARED, 'read'))).toBe(false); + expect(d(await acl.isAllowed(NOBODY, SHARED, 'write'))).toBe(false); + }); + + it('stranger cannot list /shared via client', async () => { + const c = client(acl, NOBODY); + await expect(c.readdir('shared')).rejects.toThrow('EACCES'); + }); + }); + + // ================================================================= + // 2. Undefined group ref — "ghost-group" not in groups[] + // ================================================================= + + describe('undefined group ref — graceful handling', () => { + it('loader does not crash on unknown group name', () => { + expect(true).toBe(true); + }); + + it('nobody gets the ghost-group permissions on /docs', async () => { + // The ghost-group granted read+list on /docs, but has no members + expect(d(await acl.isAllowed(NOBODY, DOCS, 'read'))).toBe(false); + }); + }); + + // ================================================================= + // 3. Multi-group user — permissions merge + // ================================================================= + + describe('multi-group user (aaa) — permissions merge across groups', () => { + // From multi-a: read + list on /shared + // From multi-b: write + mkdir on /shared, read + list on /docs + + it('has read on /shared (from multi-a)', async () => { + expect(d(await acl.isAllowed(USER_A, SHARED, 'read'))).toBe(true); + }); + + it('has list on /shared (from multi-a)', async () => { + expect(d(await acl.isAllowed(USER_A, SHARED, 'list'))).toBe(true); + }); + + it('has write on /shared (from multi-b)', async () => { + expect(d(await acl.isAllowed(USER_A, SHARED, 'write'))).toBe(true); + }); + + it('has mkdir on /shared (from multi-b)', async () => { + expect(d(await acl.isAllowed(USER_A, SHARED, 'mkdir'))).toBe(true); + }); + + it('does NOT have delete on /shared (neither group grants it)', async () => { + expect(d(await acl.isAllowed(USER_A, SHARED, 'delete'))).toBe(false); + }); + + it('has read+list on /docs (from multi-b)', async () => { + expect(d(await acl.isAllowed(USER_A, DOCS, 'read'))).toBe(true); + expect(d(await acl.isAllowed(USER_A, DOCS, 'list'))).toBe(true); + }); + + it('does NOT have write on /docs', async () => { + expect(d(await acl.isAllowed(USER_A, DOCS, 'write'))).toBe(false); + }); + + // Real FS ops + it('can list /shared via client', async () => { + const c = client(acl, USER_A); + const entries = await c.readdir('shared'); + expect(entries.map(e => e.name)).toContain('data.txt'); + }); + + it('can write in /shared via client', async () => { + const c = client(acl, USER_A); + await c.writefile(`shared/${SANDBOX}/merged.txt`, 'from both groups'); + expect(existsSync(join(USER_DIR, 'shared', SANDBOX, 'merged.txt'))).toBe(true); + }); + + it('can mkdir in /shared via client', async () => { + const c = client(acl, USER_A); + await c.mkdir(`shared/${SANDBOX}/merged-dir`); + expect(existsSync(join(USER_DIR, 'shared', SANDBOX, 'merged-dir'))).toBe(true); + }); + + it('CANNOT delete in /shared via client', async () => { + const c = client(acl, USER_A); + await expect(c.rmfile(`shared/${SANDBOX}/merged.txt`)).rejects.toThrow('EACCES'); + }); + + it('can list /docs via client', async () => { + const c = client(acl, USER_A); + const entries = await c.readdir('docs'); + expect(entries.map(e => e.name)).toContain('readme.txt'); + }); + + it('CANNOT write in /docs via client', async () => { + const c = client(acl, USER_A); + await expect(c.writefile(`docs/${SANDBOX}/nope.txt`, 'x')).rejects.toThrow('EACCES'); + }); + }); +}); diff --git a/packages/acl/tests/vfs-acl-groups.e2e.test.ts b/packages/acl/tests/vfs-acl-groups.e2e.test.ts new file mode 100644 index 00000000..70331273 --- /dev/null +++ b/packages/acl/tests/vfs-acl-groups.e2e.test.ts @@ -0,0 +1,276 @@ +/** + * VFS ACL — Groups e2e test + * + * Fixture: tests/vfs/root/groups-test/vfs-settings.json + * + * Groups: + * "team" → [aaa, ccc] + * "viewers" → [ddd, fff] + * + * ACL: + * group:"team" → /shared → read, write, list, mkdir, delete + * group:"team" → /docs → read, list + * group:"viewers" → /docs → read, list + * userId: fff → /shared → read, list (direct override for one viewer) + * + * Expected: + * - aaa (team member): full on /shared, read on /docs, nothing on /private + * - ccc (team member): same as aaa + * - ddd (viewer only): read /docs, nothing on /shared, nothing on /private + * - fff (viewer + direct): read /docs, read+list /shared (via direct), nothing on /private + * - stranger: nothing anywhere + */ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { resolve, join } from 'node:path'; +import { existsSync, rmSync, mkdirSync, readFileSync } from 'node:fs'; +import { Acl } from '../src/Acl.js'; +import { MemoryBackend } from '../src/data/MemoryBackend.js'; +import { loadVfsSettings, vfsResource } from '../src/vfs/vfs-acl.js'; +import { AclVfsClient } from '../src/vfs/AclVfsClient.js'; +import type { AclResult } from '../src/interfaces.js'; + +/** Unwrap AclResult — asserts ok and returns data. */ +function d(result: AclResult): T { + if (!result.ok) throw new Error(`Expected ok, got ${result.code}: ${result.message}`); + return result.data; +} + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +const OWNER = '3bb4cfbf-318b-44d3-a9d3-35680e738421'; +const TEAM_A = 'aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb'; +const TEAM_C = 'cccccccc-4444-5555-6666-dddddddddddd'; +const VIEW_D = 'dddddddd-7777-8888-9999-eeeeeeeeeeee'; +const VIEW_F = 'ffffffff-aaaa-bbbb-cccc-111111111111'; // also has direct grant +const NOBODY = '99999999-0000-0000-0000-ffffffffffff'; + +const USER_DIR = resolve(import.meta.dirname!, 'vfs/root/groups-test'); +const SANDBOX = '_group_sandbox'; + +function client(acl: Acl, callerId: string): AclVfsClient { + return new AclVfsClient(acl, OWNER, callerId, { root: USER_DIR }); +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +describe('VFS ACL — Groups e2e', () => { + let acl: Acl; + + beforeAll(async () => { + acl = new Acl(new MemoryBackend()); + + // Create folder structure + for (const sub of ['shared', 'docs', 'private']) { + mkdirSync(join(USER_DIR, sub, SANDBOX), { recursive: true }); + } + + // Seed files + const fs = await import('node:fs'); + fs.writeFileSync(join(USER_DIR, 'docs', 'readme.txt'), 'hello docs'); + fs.writeFileSync(join(USER_DIR, 'shared', 'data.txt'), 'shared data'); + fs.writeFileSync(join(USER_DIR, 'private', 'secret.txt'), 'top secret'); + + await loadVfsSettings(acl, USER_DIR); + }); + + afterAll(() => { + for (const sub of ['shared', 'docs', 'private']) { + const p = join(USER_DIR, sub, SANDBOX); + if (existsSync(p)) rmSync(p, { recursive: true, force: true }); + } + // Clean seeded files + for (const f of ['docs/readme.txt', 'shared/data.txt', 'private/secret.txt']) { + const p = join(USER_DIR, f); + if (existsSync(p)) rmSync(p); + } + }); + + // ================================================================= + // Raw ACL checks (permission boundary) + // ================================================================= + + describe('raw ACL checks', () => { + const SHARED = vfsResource(OWNER, '/shared'); + const DOCS = vfsResource(OWNER, '/docs'); + const PRIVATE = vfsResource(OWNER, '/private'); + const ROOT = vfsResource(OWNER, '/'); + + it('owner has wildcard on everything', async () => { + expect(d(await acl.isAllowed(OWNER, ROOT, 'read'))).toBe(true); + expect(d(await acl.isAllowed(OWNER, ROOT, 'write'))).toBe(true); + expect(d(await acl.isAllowed(OWNER, ROOT, 'delete'))).toBe(true); + expect(d(await acl.isAllowed(OWNER, ROOT, 'some-future-perm'))).toBe(true); + }); + + it('team member (aaa) — full on /shared', async () => { + expect(d(await acl.isAllowed(TEAM_A, SHARED, 'read'))).toBe(true); + expect(d(await acl.isAllowed(TEAM_A, SHARED, 'write'))).toBe(true); + expect(d(await acl.isAllowed(TEAM_A, SHARED, 'list'))).toBe(true); + expect(d(await acl.isAllowed(TEAM_A, SHARED, 'mkdir'))).toBe(true); + expect(d(await acl.isAllowed(TEAM_A, SHARED, 'delete'))).toBe(true); + }); + + it('team member (aaa) — read+list on /docs', async () => { + expect(d(await acl.isAllowed(TEAM_A, DOCS, 'read'))).toBe(true); + expect(d(await acl.isAllowed(TEAM_A, DOCS, 'list'))).toBe(true); + expect(d(await acl.isAllowed(TEAM_A, DOCS, 'write'))).toBe(false); + }); + + it('team member (aaa) — no access to /private', async () => { + expect(d(await acl.isAllowed(TEAM_A, PRIVATE, 'read'))).toBe(false); + }); + + it('team member (ccc) — same as aaa', async () => { + expect(d(await acl.isAllowed(TEAM_C, SHARED, 'write'))).toBe(true); + expect(d(await acl.isAllowed(TEAM_C, DOCS, 'list'))).toBe(true); + expect(d(await acl.isAllowed(TEAM_C, PRIVATE, 'read'))).toBe(false); + }); + + it('viewer (ddd) — read+list on /docs only', async () => { + expect(d(await acl.isAllowed(VIEW_D, DOCS, 'read'))).toBe(true); + expect(d(await acl.isAllowed(VIEW_D, DOCS, 'list'))).toBe(true); + expect(d(await acl.isAllowed(VIEW_D, DOCS, 'write'))).toBe(false); + expect(d(await acl.isAllowed(VIEW_D, SHARED, 'read'))).toBe(false); + expect(d(await acl.isAllowed(VIEW_D, PRIVATE, 'read'))).toBe(false); + }); + + it('viewer+direct (fff) — read /docs via group + read+list /shared via direct', async () => { + // From viewers group + expect(d(await acl.isAllowed(VIEW_F, DOCS, 'read'))).toBe(true); + expect(d(await acl.isAllowed(VIEW_F, DOCS, 'list'))).toBe(true); + // From direct grant + expect(d(await acl.isAllowed(VIEW_F, SHARED, 'read'))).toBe(true); + expect(d(await acl.isAllowed(VIEW_F, SHARED, 'list'))).toBe(true); + // Not granted + expect(d(await acl.isAllowed(VIEW_F, SHARED, 'write'))).toBe(false); + expect(d(await acl.isAllowed(VIEW_F, PRIVATE, 'read'))).toBe(false); + }); + + it('stranger — nothing anywhere', async () => { + expect(d(await acl.isAllowed(NOBODY, SHARED, 'read'))).toBe(false); + expect(d(await acl.isAllowed(NOBODY, DOCS, 'list'))).toBe(false); + expect(d(await acl.isAllowed(NOBODY, PRIVATE, 'read'))).toBe(false); + expect(d(await acl.isAllowed(NOBODY, ROOT, 'read'))).toBe(false); + }); + }); + + // ================================================================= + // Real filesystem through AclVfsClient + // ================================================================= + + describe('team member (aaa) — real FS ops', () => { + it('can list /shared', async () => { + const c = client(acl, TEAM_A); + const entries = await c.readdir('shared'); + expect(entries.map(e => e.name)).toContain('data.txt'); + }); + + it('can write in /shared', async () => { + const c = client(acl, TEAM_A); + await c.writefile(`shared/${SANDBOX}/team.txt`, 'from team'); + expect(readFileSync(join(USER_DIR, 'shared', SANDBOX, 'team.txt'), 'utf8')).toBe('from team'); + }); + + it('can mkdir in /shared', async () => { + const c = client(acl, TEAM_A); + await c.mkdir(`shared/${SANDBOX}/team-dir`); + expect(existsSync(join(USER_DIR, 'shared', SANDBOX, 'team-dir'))).toBe(true); + }); + + it('can delete in /shared', async () => { + const c = client(acl, TEAM_A); + await c.writefile(`shared/${SANDBOX}/del.txt`, 'bye'); + await c.rmfile(`shared/${SANDBOX}/del.txt`); + expect(existsSync(join(USER_DIR, 'shared', SANDBOX, 'del.txt'))).toBe(false); + }); + + it('can list /docs (read-only)', async () => { + const c = client(acl, TEAM_A); + const entries = await c.readdir('docs'); + expect(entries.map(e => e.name)).toContain('readme.txt'); + }); + + it('CANNOT write in /docs', async () => { + const c = client(acl, TEAM_A); + await expect(c.writefile(`docs/${SANDBOX}/nope.txt`, 'x')).rejects.toThrow('EACCES'); + }); + + it('CANNOT list /private', async () => { + const c = client(acl, TEAM_A); + await expect(c.readdir('private')).rejects.toThrow('EACCES'); + }); + }); + + describe('viewer (ddd) — real FS ops', () => { + it('can list /docs', async () => { + const c = client(acl, VIEW_D); + const entries = await c.readdir('docs'); + expect(entries.map(e => e.name)).toContain('readme.txt'); + }); + + it('can check existence in /docs', async () => { + const c = client(acl, VIEW_D); + expect(await c.exists('docs/readme.txt')).toBe(true); + }); + + it('CANNOT list /shared', async () => { + const c = client(acl, VIEW_D); + await expect(c.readdir('shared')).rejects.toThrow('EACCES'); + }); + + it('CANNOT write in /docs', async () => { + const c = client(acl, VIEW_D); + await expect(c.writefile(`docs/${SANDBOX}/nope.txt`, 'x')).rejects.toThrow('EACCES'); + }); + }); + + describe('viewer+direct (fff) — mixed grants FS ops', () => { + it('can list /docs (from group)', async () => { + const c = client(acl, VIEW_F); + const entries = await c.readdir('docs'); + expect(entries.map(e => e.name)).toContain('readme.txt'); + }); + + it('can list /shared (from direct grant)', async () => { + const c = client(acl, VIEW_F); + const entries = await c.readdir('shared'); + expect(entries.map(e => e.name)).toContain('data.txt'); + }); + + it('can check existence in /shared (from direct grant)', async () => { + const c = client(acl, VIEW_F); + expect(await c.exists('shared/data.txt')).toBe(true); + }); + + it('CANNOT write in /shared (direct only grants read+list)', async () => { + const c = client(acl, VIEW_F); + await expect(c.writefile(`shared/${SANDBOX}/nope.txt`, 'x')).rejects.toThrow('EACCES'); + }); + + it('CANNOT access /private', async () => { + const c = client(acl, VIEW_F); + await expect(c.readdir('private')).rejects.toThrow('EACCES'); + }); + }); + + describe('stranger — blocked on everything', () => { + it('CANNOT list /shared', async () => { + const c = client(acl, NOBODY); + await expect(c.readdir('shared')).rejects.toThrow('EACCES'); + }); + + it('CANNOT read /docs', async () => { + const c = client(acl, NOBODY); + await expect(c.exists('docs/readme.txt')).rejects.toThrow('EACCES'); + }); + + it('CANNOT write anywhere', async () => { + const c = client(acl, NOBODY); + await expect(c.writefile(`shared/${SANDBOX}/x.txt`, 'x')).rejects.toThrow('EACCES'); + }); + }); +}); diff --git a/packages/acl/tests/vfs-acl.e2e.test.ts b/packages/acl/tests/vfs-acl.e2e.test.ts index 3e5dd0be..19b7dcbc 100644 --- a/packages/acl/tests/vfs-acl.e2e.test.ts +++ b/packages/acl/tests/vfs-acl.e2e.test.ts @@ -15,6 +15,13 @@ import { resolve } from 'node:path'; import { Acl } from '../src/Acl.js'; import { MemoryBackend } from '../src/data/MemoryBackend.js'; import { loadVfsSettings, vfsResource } from '../src/vfs/vfs-acl.js'; +import type { AclResult } from '../src/interfaces.js'; + +/** Unwrap AclResult — asserts ok and returns data. */ +function d(result: AclResult): T { + if (!result.ok) throw new Error(`Expected ok, got ${result.code}: ${result.message}`); + return result.data; +} // --------------------------------------------------------------------------- // Constants @@ -53,13 +60,13 @@ describe('VFS ACL — e2e', () => { describe(`owner (${OWNER_ID})`, () => { it('can do anything on /', async () => { - expect(await acl.isAllowed(OWNER_ID, ROOT, 'read')).toBe(true); - expect(await acl.isAllowed(OWNER_ID, ROOT, 'write')).toBe(true); - expect(await acl.isAllowed(OWNER_ID, ROOT, 'list')).toBe(true); - expect(await acl.isAllowed(OWNER_ID, ROOT, 'mkdir')).toBe(true); - expect(await acl.isAllowed(OWNER_ID, ROOT, 'delete')).toBe(true); - expect(await acl.isAllowed(OWNER_ID, ROOT, 'rename')).toBe(true); - expect(await acl.isAllowed(OWNER_ID, ROOT, 'copy')).toBe(true); + expect(d(await acl.isAllowed(OWNER_ID, ROOT, 'read'))).toBe(true); + expect(d(await acl.isAllowed(OWNER_ID, ROOT, 'write'))).toBe(true); + expect(d(await acl.isAllowed(OWNER_ID, ROOT, 'list'))).toBe(true); + expect(d(await acl.isAllowed(OWNER_ID, ROOT, 'mkdir'))).toBe(true); + expect(d(await acl.isAllowed(OWNER_ID, ROOT, 'delete'))).toBe(true); + expect(d(await acl.isAllowed(OWNER_ID, ROOT, 'rename'))).toBe(true); + expect(d(await acl.isAllowed(OWNER_ID, ROOT, 'copy'))).toBe(true); }); }); @@ -69,27 +76,27 @@ describe('VFS ACL — e2e', () => { describe(`read-only user (${READ_ONLY_USER})`, () => { it('can read on /', async () => { - expect(await acl.isAllowed(READ_ONLY_USER, ROOT, 'read')).toBe(true); + expect(d(await acl.isAllowed(READ_ONLY_USER, ROOT, 'read'))).toBe(true); }); it('can list on /', async () => { - expect(await acl.isAllowed(READ_ONLY_USER, ROOT, 'list')).toBe(true); + expect(d(await acl.isAllowed(READ_ONLY_USER, ROOT, 'list'))).toBe(true); }); it('CANNOT write on /', async () => { - expect(await acl.isAllowed(READ_ONLY_USER, ROOT, 'write')).toBe(false); + expect(d(await acl.isAllowed(READ_ONLY_USER, ROOT, 'write'))).toBe(false); }); it('CANNOT mkdir on /', async () => { - expect(await acl.isAllowed(READ_ONLY_USER, ROOT, 'mkdir')).toBe(false); + expect(d(await acl.isAllowed(READ_ONLY_USER, ROOT, 'mkdir'))).toBe(false); }); it('CANNOT delete on /', async () => { - expect(await acl.isAllowed(READ_ONLY_USER, ROOT, 'delete')).toBe(false); + expect(d(await acl.isAllowed(READ_ONLY_USER, ROOT, 'delete'))).toBe(false); }); it('CANNOT rename on /', async () => { - expect(await acl.isAllowed(READ_ONLY_USER, ROOT, 'rename')).toBe(false); + expect(d(await acl.isAllowed(READ_ONLY_USER, ROOT, 'rename'))).toBe(false); }); }); @@ -102,61 +109,61 @@ describe('VFS ACL — e2e', () => { describe(`full-grant user (${FULL_GRANT_USER})`, () => { it('can read on /shared', async () => { - expect(await acl.isAllowed(FULL_GRANT_USER, SHARED, 'read')).toBe(true); + expect(d(await acl.isAllowed(FULL_GRANT_USER, SHARED, 'read'))).toBe(true); }); it('can write on /shared', async () => { - expect(await acl.isAllowed(FULL_GRANT_USER, SHARED, 'write')).toBe(true); + expect(d(await acl.isAllowed(FULL_GRANT_USER, SHARED, 'write'))).toBe(true); }); it('can list on /shared', async () => { - expect(await acl.isAllowed(FULL_GRANT_USER, SHARED, 'list')).toBe(true); + expect(d(await acl.isAllowed(FULL_GRANT_USER, SHARED, 'list'))).toBe(true); }); it('can mkdir on /shared', async () => { - expect(await acl.isAllowed(FULL_GRANT_USER, SHARED, 'mkdir')).toBe(true); + expect(d(await acl.isAllowed(FULL_GRANT_USER, SHARED, 'mkdir'))).toBe(true); }); it('can delete on /shared', async () => { - expect(await acl.isAllowed(FULL_GRANT_USER, SHARED, 'delete')).toBe(true); + expect(d(await acl.isAllowed(FULL_GRANT_USER, SHARED, 'delete'))).toBe(true); }); it('CANNOT rename on /shared (not granted)', async () => { - expect(await acl.isAllowed(FULL_GRANT_USER, SHARED, 'rename')).toBe(false); + expect(d(await acl.isAllowed(FULL_GRANT_USER, SHARED, 'rename'))).toBe(false); }); it('CANNOT copy on /shared (not granted)', async () => { - expect(await acl.isAllowed(FULL_GRANT_USER, SHARED, 'copy')).toBe(false); + expect(d(await acl.isAllowed(FULL_GRANT_USER, SHARED, 'copy'))).toBe(false); }); // /docs — read + list only it('can read on /docs', async () => { - expect(await acl.isAllowed(FULL_GRANT_USER, DOCS, 'read')).toBe(true); + expect(d(await acl.isAllowed(FULL_GRANT_USER, DOCS, 'read'))).toBe(true); }); it('can list on /docs', async () => { - expect(await acl.isAllowed(FULL_GRANT_USER, DOCS, 'list')).toBe(true); + expect(d(await acl.isAllowed(FULL_GRANT_USER, DOCS, 'list'))).toBe(true); }); it('CANNOT write on /docs', async () => { - expect(await acl.isAllowed(FULL_GRANT_USER, DOCS, 'write')).toBe(false); + expect(d(await acl.isAllowed(FULL_GRANT_USER, DOCS, 'write'))).toBe(false); }); // /private — no grant at all it('CANNOT read on /private', async () => { - expect(await acl.isAllowed(FULL_GRANT_USER, PRIVATE, 'read')).toBe(false); + expect(d(await acl.isAllowed(FULL_GRANT_USER, PRIVATE, 'read'))).toBe(false); }); it('CANNOT list on /private', async () => { - expect(await acl.isAllowed(FULL_GRANT_USER, PRIVATE, 'list')).toBe(false); + expect(d(await acl.isAllowed(FULL_GRANT_USER, PRIVATE, 'list'))).toBe(false); }); // root "/" — no grant it('CANNOT read on / (no root grant)', async () => { - expect(await acl.isAllowed(FULL_GRANT_USER, ROOT, 'read')).toBe(false); + expect(d(await acl.isAllowed(FULL_GRANT_USER, ROOT, 'read'))).toBe(false); }); }); @@ -167,16 +174,16 @@ describe('VFS ACL — e2e', () => { describe(`stranger (${STRANGER_USER})`, () => { for (const res of [ROOT, SHARED, DOCS, PRIVATE]) { it(`CANNOT read on ${res}`, async () => { - expect(await acl.isAllowed(STRANGER_USER, res, 'read')).toBe(false); + expect(d(await acl.isAllowed(STRANGER_USER, res, 'read'))).toBe(false); }); } it('CANNOT write on /shared', async () => { - expect(await acl.isAllowed(STRANGER_USER, SHARED, 'write')).toBe(false); + expect(d(await acl.isAllowed(STRANGER_USER, SHARED, 'write'))).toBe(false); }); it('CANNOT list on /docs', async () => { - expect(await acl.isAllowed(STRANGER_USER, DOCS, 'list')).toBe(false); + expect(d(await acl.isAllowed(STRANGER_USER, DOCS, 'list'))).toBe(false); }); }); }); diff --git a/packages/acl/tests/vfs/root/edge-cases/vfs-settings.json b/packages/acl/tests/vfs/root/edge-cases/vfs-settings.json new file mode 100644 index 00000000..45b91990 --- /dev/null +++ b/packages/acl/tests/vfs/root/edge-cases/vfs-settings.json @@ -0,0 +1,66 @@ +{ + "owner": "3bb4cfbf-318b-44d3-a9d3-35680e738421", + "groups": [ + { + "name": "empty-group", + "members": [] + }, + { + "name": "multi-a", + "members": [ + "aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb" + ] + }, + { + "name": "multi-b", + "members": [ + "aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb" + ] + } + ], + "acl": [ + { + "group": "empty-group", + "path": "/shared", + "permissions": [ + "read", + "write", + "list", + "mkdir", + "delete" + ] + }, + { + "group": "ghost-group", + "path": "/docs", + "permissions": [ + "read", + "list" + ] + }, + { + "group": "multi-a", + "path": "/shared", + "permissions": [ + "read", + "list" + ] + }, + { + "group": "multi-b", + "path": "/shared", + "permissions": [ + "write", + "mkdir" + ] + }, + { + "group": "multi-b", + "path": "/docs", + "permissions": [ + "read", + "list" + ] + } + ] +} \ No newline at end of file diff --git a/packages/acl/tests/vfs/root/groups-test/vfs-settings.json b/packages/acl/tests/vfs/root/groups-test/vfs-settings.json new file mode 100644 index 00000000..fc5b9cfe --- /dev/null +++ b/packages/acl/tests/vfs/root/groups-test/vfs-settings.json @@ -0,0 +1,56 @@ +{ + "owner": "3bb4cfbf-318b-44d3-a9d3-35680e738421", + "groups": [ + { + "name": "team", + "members": [ + "aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb", + "cccccccc-4444-5555-6666-dddddddddddd" + ] + }, + { + "name": "viewers", + "members": [ + "dddddddd-7777-8888-9999-eeeeeeeeeeee", + "ffffffff-aaaa-bbbb-cccc-111111111111" + ] + } + ], + "acl": [ + { + "group": "team", + "path": "/shared", + "permissions": [ + "read", + "write", + "list", + "mkdir", + "delete" + ] + }, + { + "group": "team", + "path": "/docs", + "permissions": [ + "read", + "list" + ] + }, + { + "group": "viewers", + "path": "/docs", + "permissions": [ + "read", + "list" + ] + }, + { + "userId": "ffffffff-aaaa-bbbb-cccc-111111111111", + "path": "/shared", + "permissions": [ + "read", + "list" + ] + } + ] +} \ No newline at end of file diff --git a/packages/acl/tsconfig.json b/packages/acl/tsconfig.json index 1d71ac13..35448ce4 100644 --- a/packages/acl/tsconfig.json +++ b/packages/acl/tsconfig.json @@ -7,6 +7,11 @@ "composite": false, "inlineSourceMap": true, "strict": true, + "alwaysStrict": true, + "allowUnusedLabels": false, + "noUnusedParameters": true, + "noImplicitReturns": true, + "allowUnreachableCode": false, "noUncheckedIndexedAccess": true, "paths": { "@/*": [