acl — @techteamer/acl

A tiny, dependency-free role/rule authorization library published as the npm package @techteamer/acl. It is not a service — there is no server, no Dockerfile, no network/DB/queue code. It is a reusable building block that other FaceKom services require/import to evaluate “is role X allowed to do Y?” against in-memory rule lists.

Scope of this repo

The entire repository is 3 source files (src/ACLService.js, src/ACLManager.js, src/ACLError.js), a barrel index.js, and 2 test files. Everything below is verified line-by-line from those files — there were no large subtrees to sample.

1. Purpose & role in FaceKom

  • What it is: package.json:2-4 — name @techteamer/acl, version 2.0.2, description "Access Control List (ACL)", author TechTeamer. License MIT (package.json:27). Repo https://github.com/TechTeamer/acl.git (package.json:28-31).
  • What it does (README.md:1-411): provides two public classes for evaluating access rules:
    • ACLService — a single list of roles, each role owning accept and reject rule sets. Answers isAllowed(rule, role) / areAllowed(rules, role) / anyAllowed(rules, role).
    • ACLManager — holds an ordered stack of ACLService instances with priority/fallback semantics; the ACL added later wins for a given role (README.md:37-47, src/ACLManager.js:32-39).
  • Rule grammar (README.md:52-72, enforced in src/ACLService.js):
    • plain string → accept rule (e.g. users.create)
    • !-prefixed → reject rule (e.g. !users.delete)
    • @-prefixed role or rule → ignored during import
    • * acts as a wildcard segment (compiled to .* in a regex)
    • All reject rules outrank any accept rule (README.md:54, src/ACLService.js:147-154).
  • Role in FaceKom: a shared authorization primitive. Consumers feed it a JSON config of { role: [rules...] } and ask yes/no questions. This repo contains no evidence of which FaceKom services consume it (see §9 and Unverified/gaps).

2. Tech stack & runtime

AspectValueSource
LanguageJavaScript (ES modules)package.json:32 "type":"module"; all src/*.js use import/export
Module systemDual ESM + CJS published buildpackage.json:5-12 main/module/exports
Required Node>=21.1.0 (manifest engines)package.json:33-35
Package manageryarn used in practice (build script yarn run build, maintenance logs use yarn) but npm also documentedREADME.md:401, README.md:407-411; maintenance/2026-05-04.md:35
Runtime depsnone — no dependencies key in manifestpackage.json (only devDependencies + resolutions)
Build toolTypeScript compiler (tsc) emitting .js + .d.ts from JS via allowJspackage.json:17, tsconfig*.json
LintESLint flat config (eslint.config.mjs)package.json:14, eslint.config.mjs
TestMochapackage.json:15-16, test/*.test.js

Node version mismatch between manifest and CHANGELOG

package.json:33-35 pins engines.node to >=21.1.0, but the published CHANGELOG.md:4,16 for v2.0.x says minimum Node was bumped to ^18.18.0 || >=20.9.0. The manifest is the authoritative current value (21.1.0+); the CHANGELOG entries describe earlier releases.

devDependency versions: manifest vs. installed

package.json declares aspirational ranges (e.g. eslint ^10.4.0, typescript ^6.0.3, @types/node ^25.9.0), but the maintenance log shows what was actually installed/locked at the last run: eslint 8.57.1, typescript 5.9.3, @types/node 20.19.39, typescript-eslint 7.18.0 (maintenance/2026-05-04.md:39-46,58-64). Those upgrades are tracked as pending — “Migration needed” (maintenance/2026-05-04.md:5-27).

3. Build & run

This is a library, so “run” means lint + test; “build” means compile the dist bundle.

Entrypoints

  • Dev / source barrel: index.js:1-9 re-exports ACLService, ACLError, ACLManager.
  • Published entry (consumers): dist/cjs/index.js (require) / dist/mjs/index.js (import) — package.json:5-12. dist/ is git-ignored (.gitignore:8) and generated by build.
  • No bin field in package.json → no CLI executable.

package.json “scripts” — every entry, verbatim

ScriptCommand (package.json:14-17)What it does
linteslint .Run ESLint over the repo using eslint.config.mjs (ignores dist/**, **/*.d.ts, node_modules/**).
unitmocha --exitRun the Mocha test suite (test/*.test.js), forcing process exit when done.
testeslint . && mocha --exitLint, then run unit tests — fails fast if lint fails. This is the canonical CI/local check.
buildrm -fr dist/* && tsc -p tsconfig.json && tsc -p tsconfig-cjs.json && ./fixup.shWipe dist/, compile the ESM build (→ dist/mjs), compile the CJS build (→ dist/cjs), then run fixup.sh.

fixup.sh (fixup.sh:1-12)

Writes two tiny package.json marker files into the build output so Node resolves each folder with the correct module type:

  • dist/cjs/package.json{ "type": "commonjs" }
  • dist/mjs/package.json{ "type": "module" }

This is the standard dual-package trick that lets the root "type":"module" project still ship a working CommonJS require path.

Start commands

npm install            # or: yarn
npm test               # eslint . && mocha --exit   (README.md:407-411)
yarn run build         # produce dist/ (README.md:398-402)

No service start command exists. There is no start script, no server entrypoint, no Dockerfile.

4. Top-level structure

Verified via git ls-tree -r HEAD. Meaningful entries:

PathWhat it is
index.jsBarrel module; exports the 3 public classes (index.js:1-9).
src/Library source (3 files; see §6).
test/Mocha specs: ACLService.test.js, ACLManager.test.js.
maintenance/Dated dependency-maintenance notes (2026-04-07.md, 2026-05-04.md) — yarn outdated before/after logs + “pending” rationale.
security/Dated yarn audit reports (2026-04-07.md, 2026-05-04.md) — both show 0 vulnerabilities (security/2026-05-04.md:11).
package.jsonManifest (scripts, engines, devDeps).
eslint.config.mjsESLint flat config.
tsconfig.json / tsconfig-cjs.json / tsconfig-base.jsonTS compiler configs for the dual build.
fixup.shPost-build dual-package marker writer (see §3).
CHANGELOG.mdRelease notes (current head: 2.0.2).
README.mdFull API documentation + usage examples.
.gitignore / .npmignoreIgnore rules (dist, node_modules, lockfiles, IDE, .travis.yml, etc.).

5. First-party bin/ helper scripts

None. There is no bin/ directory and no bin field in package.json (verified via git ls-tree -r HEAD and reading package.json). The only first-party shell script in the repo is fixup.sh (documented in §3).

6. Key modules & entities

src/ACLService.js (:1-243) — the core engine

Single-list ACL. State (constructor :2-8): logger=false, _ruleCache: Map<role, {accept:[], reject:[]}>, _resultCache: Map<searchKey, bool>, _importInProgress.

Key behaviors verified from source:

  • import(configJSON) :10-40 — for each role key: skips @-prefixed roles (:13-16), creates the role if new, detects duplicate rules in the array and logs them at error level (:22-26), then createRule for each non-@ rule. On finish clears the result cache and logs info. Sets _importInProgress so per-rule cache clears are skipped until the end.
  • createRole(role) :42-55 — role name must match /^[a-z0-9-_]+$/i else error; rejects duplicates; stores {accept:[], reject:[]}.
  • createRule(rule, role) :61-93 — rule name must match /^!?[a-z0-9*-._]+$/i; ! prefix routes to the reject set; the rule string is compiled to a case-insensitive RegExp via '^' + expRule.replace('.', '\\.').replace('*', '.*') + '$' (:77); rejects an already-present identical regex.
  • isAllowed(access, role) :112-162 — validates args, requires the role to exist, validates access against /^[a-z0-9-._]+$/i, strips whitespace, checks _resultCache first; otherwise tests accept rules (any match → true) then reject rules (any match → overrides to false), caches and returns.
  • areAllowed(list, role) :164-197 — true only if every access in the list is allowed; caches under a &-joined sorted key; empty list → false.
  • anyAllowed(list, role) :199-229 — true if any access is allowed; caches under a |-joined sorted key.
  • clear() / clearResultCache() / get roleList :95-110.
  • _log(level, message) :231-241 — dispatches to a logger callback or {level: fn} object; on level === 'error' it throws an ACLError (see gotcha below).

src/ACLManager.js (:1-107) — priority/fallback wrapper

  • Holds aclList: ACLService[] (:6).
  • import(acl) :32-39 — rejects non-ACLService values (throws via _log('error',…)), and **unshift**es so the latest import sits at the front = highest priority.
  • importConfig(configJSON) :21-30 — convenience: builds a fresh ACLService, imports the config, propagates the manager’s logger, then imports it.
  • getACLForRole(role) :50-58 — returns the first (highest-priority) ACL that contains the role, else null.
  • isAllowed / areAllowed / anyAllowed :60-88 — resolve the owning ACL via getACLForRole, then delegate. No fallback once a role is found (see gotcha).
  • set logger :9-15 — assigning a logger to the manager propagates it to every managed ACLService (README.md:366).
  • roleList :41-44 — flattened unique union of roles across all ACLs.

src/ACLError.js (:1-3)

class ACLError extends Error {} — a marker error subclass so callers can instanceof-match ACL failures. Used by both classes’ _log and asserted throughout the tests.

7. Configuration

No app config / env vars / schemas

This library has no config files, no environment variables, no getconfig/AJV/dotenv usage (none present in git ls-tree and none imported in any source file). “Configuration” here means only build/lint/TS tooling plus the runtime ACL config object that a consumer passes to import()/importConfig().

Tooling config:

  • tsconfig-base.json :1-24allowJs, declaration:true, strict:true, moduleResolution:"bundler", lib:["esnext"], types:["node"], include:["src","index.js"], exclude:["node_modules","dist"].
  • tsconfig.json :1-9 — extends base; module:"esnext", target:"esnext", outDir:"dist/mjs" (the ESM build).
  • tsconfig-cjs.json :1-9 — extends base; module:"commonjs", target:"es2022", outDir:"dist/cjs" (the CJS build).
  • eslint.config.mjs :1-42 — flat config; extends @eslint/js recommended + typescript-eslint recommended; plugins eslint-plugin-n and eslint-plugin-promise; ecmaVersion:2020, sourceType:"module", Node globals. Notable rules: curly:["error","all"], no-console:"error", no-debugger:"error", radix:"error", no-empty-function:"error", n/no-deprecated-api:"warn".

Runtime ACL config shape (consumer-supplied, README.md:101-116, src/ACLService.js:10-40):

{ "<role>": ["accept.rule", "!reject.rule", "wildcard.*", "@ignored"] , ... }

8. Tests

  • Framework: Mocha (test/*.test.js import { it, describe } from 'mocha'; package.json:45 devDep mocha ^11.7.5).
  • Location: test/ACLService.test.js (:1-198) and test/ACLManager.test.js (:1-164). They import the public API from the root barrel ../index.js, using Node’s built-in assert.
  • How to run:
    npm test     # eslint . && mocha --exit   (package.json:16)
    npm run unit # mocha --exit only          (package.json:15)
    mocha --exit with no config relies on Mocha’s default ./test discovery (no .mocharc file exists in the repo).
  • Coverage of behaviors (representative, all read): role/rule creation + invalid-name errors, wildcard rules, reject-overrides-accept, import duplicate-rule error, areAllowed/anyAllowed incl. empty-list → false, cache clearing (ACLService.test.js); manager priority/fallback — higher-precedence ACL wins and does not fall back once the role is found, but does fall back when the role is absent from higher ACLs, plus non-ACL import rejection (ACLManager.test.js:90-121).

9. Dependencies on other FaceKom services

None evidenced in code. Verified: zero runtime dependencies (package.json), and no src file imports anything beyond sibling files (import … from './…'). There is no RabbitMQ/amqp, no HTTP client/server, no DB/Redis, no env access. This package is a pure in-process library; any coupling to FaceKom services exists only in the (external) consumers that install it.

10. Verified gotchas

_log('error', …) always throws — error-level logging is not just logging

In both classes, _log ends with if (level === 'error') { throw new ACLError(message) } (src/ACLService.js:238-240, src/ACLManager.js:101-103). So conditions that “log an error” — duplicate rules during import, invalid role/rule names, importing a non-ACLService into the manager — throw synchronously. Tests rely on this (ACLService.test.js:127-131, ACLManager.test.js:62-68). Wrap imports in try/catch if a bad config must not crash the caller.

ACLManager has no fallback after a role is found

getACLForRole returns the first ACL containing the role and isAllowed delegates only to that one (src/ACLManager.js:50-68). If the highest-priority ACL has the role but is missing the specific rule, the result is false — it will NOT consult lower-priority ACLs that might allow it (README.md:46-47, ACLManager.test.js:109-111). Plan your priority layering accordingly.

Regex compilation only escapes/expands the FIRST . and *

Rule→regex uses expRule.replace('.', '\\.').replace('*', '.*') (src/ACLService.js:77). String.replace with a string pattern replaces only the first occurrence. So a rule like a.b.c compiles with only its first . escaped (^a\.b.c$), meaning later .s act as regex “any char”. Multi-dot / multi-wildcard rules do not behave as fully-literal segment matches. (Behavior verified from source; no test exercises a multi-dot rule.)

Dual-build correctness depends on fixup.sh

Because the root manifest is "type":"module", the emitted dist/cjs would be misread as ESM without the {"type":"commonjs"} marker that fixup.sh writes (fixup.sh:1-5). If build is reproduced manually, fixup.sh must run after both tsc passes.

roleList order ≠ priority order

ACLManager.roleList (src/ACLManager.js:41-44) returns a de-duplicated Set union across ACLs for display/enumeration; it is not a priority indicator. Priority is purely the aclList array order (front = highest, set by unshift).

Unverified / gaps

  • Consumers within FaceKom: This repo gives no evidence of which FaceKom services import @techteamer/acl or what role/rule configs they ship. Determining that requires reading the other repos (not in scope here). See sibling docs such as architecture-overview and customization-architecture for service inventory.
  • Git-LFS / binaries: none — repo has no .gitattributes and no binary assets (verified via git ls-tree -r HEAD).
  • CI: .travis.yml is referenced in .gitignore/.npmignore (.gitignore:4, .npmignore:4) but is not present in the tree; no .github/workflows exists. Actual CI configuration is unverified from this repo. The most recent commit message references fkqa-331 (git log HEAD 74dbd1e), suggesting an external CI/QA pipeline not stored here.
  • maintenance/2026-04-07.md and security/2026-04-07.md were not read line-by-line; the newer 2026-05-04 dated pair was read in full and is representative (dependency yarn outdated notes and a clean yarn audit).

Sources

Files read in full (repo /Users/levander/coding/facekom-v2-clones/acl, branch master, HEAD 74dbd1e):

  • README.md
  • package.json
  • index.js
  • CHANGELOG.md
  • src/ACLService.js
  • src/ACLManager.js
  • src/ACLError.js
  • fixup.sh
  • eslint.config.mjs
  • tsconfig.json, tsconfig-base.json, tsconfig-cjs.json
  • .gitignore, .npmignore
  • test/ACLService.test.js, test/ACLManager.test.js
  • maintenance/2026-05-04.md, security/2026-05-04.md
  • git ls-tree -r HEAD (authoritative file inventory), git log, git show -s (HEAD metadata)