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 barrelindex.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, version2.0.2, description"Access Control List (ACL)", authorTechTeamer. License MIT (package.json:27). Repohttps://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 owningacceptandrejectrule sets. AnswersisAllowed(rule, role)/areAllowed(rules, role)/anyAllowed(rules, role).ACLManager— holds an ordered stack ofACLServiceinstances 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 insrc/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).
- plain string → accept rule (e.g.
- 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
| Aspect | Value | Source |
|---|---|---|
| Language | JavaScript (ES modules) | package.json:32 "type":"module"; all src/*.js use import/export |
| Module system | Dual ESM + CJS published build | package.json:5-12 main/module/exports |
| Required Node | >=21.1.0 (manifest engines) | package.json:33-35 |
| Package manager | yarn used in practice (build script yarn run build, maintenance logs use yarn) but npm also documented | README.md:401, README.md:407-411; maintenance/2026-05-04.md:35 |
| Runtime deps | none — no dependencies key in manifest | package.json (only devDependencies + resolutions) |
| Build tool | TypeScript compiler (tsc) emitting .js + .d.ts from JS via allowJs | package.json:17, tsconfig*.json |
| Lint | ESLint flat config (eslint.config.mjs) | package.json:14, eslint.config.mjs |
| Test | Mocha | package.json:15-16, test/*.test.js |
Node version mismatch between manifest and CHANGELOG
package.json:33-35pinsengines.nodeto>=21.1.0, but the publishedCHANGELOG.md:4,16for 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.jsondeclares 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-9re-exportsACLService,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 bybuild. - No
binfield inpackage.json→ no CLI executable.
package.json “scripts” — every entry, verbatim
| Script | Command (package.json:14-17) | What it does |
|---|---|---|
lint | eslint . | Run ESLint over the repo using eslint.config.mjs (ignores dist/**, **/*.d.ts, node_modules/**). |
unit | mocha --exit | Run the Mocha test suite (test/*.test.js), forcing process exit when done. |
test | eslint . && mocha --exit | Lint, then run unit tests — fails fast if lint fails. This is the canonical CI/local check. |
build | rm -fr dist/* && tsc -p tsconfig.json && tsc -p tsconfig-cjs.json && ./fixup.sh | Wipe 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
startscript, no server entrypoint, no Dockerfile.
4. Top-level structure
Verified via git ls-tree -r HEAD. Meaningful entries:
| Path | What it is |
|---|---|
index.js | Barrel 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.json | Manifest (scripts, engines, devDeps). |
eslint.config.mjs | ESLint flat config. |
tsconfig.json / tsconfig-cjs.json / tsconfig-base.json | TS compiler configs for the dual build. |
fixup.sh | Post-build dual-package marker writer (see §3). |
CHANGELOG.md | Release notes (current head: 2.0.2). |
README.md | Full API documentation + usage examples. |
.gitignore / .npmignore | Ignore 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 aterrorlevel (:22-26), thencreateRulefor each non-@rule. On finish clears the result cache and logsinfo. Sets_importInProgressso per-rule cache clears are skipped until the end.createRole(role):42-55— role name must match/^[a-z0-9-_]+$/ielseerror; rejects duplicates; stores{accept:[], reject:[]}.createRule(rule, role):61-93— rule name must match/^!?[a-z0-9*-._]+$/i;!prefix routes to therejectset; 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, validatesaccessagainst/^[a-z0-9-._]+$/i, strips whitespace, checks_resultCachefirst; 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; onlevel === 'error'it throws anACLError(see gotcha below).
src/ACLManager.js (:1-107) — priority/fallback wrapper
- Holds
aclList: ACLService[](:6). import(acl):32-39— rejects non-ACLServicevalues (throws via_log('error',…)), and **unshift**es so the latest import sits at the front = highest priority.importConfig(configJSON):21-30— convenience: builds a freshACLService, imports the config, propagates the manager’s logger, thenimports it.getACLForRole(role):50-58— returns the first (highest-priority) ACL that contains the role, elsenull.isAllowed/areAllowed/anyAllowed:60-88— resolve the owning ACL viagetACLForRole, 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 managedACLService(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 ingit ls-treeand none imported in any source file). “Configuration” here means only build/lint/TS tooling plus the runtime ACL config object that a consumer passes toimport()/importConfig().
Tooling config:
tsconfig-base.json:1-24—allowJs,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/jsrecommended +typescript-eslintrecommended; pluginseslint-plugin-nandeslint-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.jsimport{ it, describe } from 'mocha';package.json:45devDepmocha ^11.7.5). - Location:
test/ACLService.test.js(:1-198) andtest/ACLManager.test.js(:1-164). They import the public API from the root barrel../index.js, using Node’s built-inassert. - How to run:
npm test # eslint . && mocha --exit (package.json:16) npm run unit # mocha --exit only (package.json:15)mocha --exitwith no config relies on Mocha’s default./testdiscovery (no.mocharcfile exists in the repo). - Coverage of behaviors (representative, all read): role/rule creation + invalid-name errors, wildcard rules, reject-overrides-accept,
importduplicate-rule error,areAllowed/anyAllowedincl. 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 loggingIn both classes,
_logends withif (level === 'error') { throw new ACLError(message) }(src/ACLService.js:238-240,src/ACLManager.js:101-103). So conditions that “log an error” — duplicate rules duringimport, invalid role/rule names, importing a non-ACLServiceinto 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
getACLForRolereturns the first ACL containing the role andisAlloweddelegates 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.replacewith a string pattern replaces only the first occurrence. So a rule likea.b.ccompiles 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.shBecause the root manifest is
"type":"module", the emitteddist/cjswould be misread as ESM without the{"type":"commonjs"}marker thatfixup.shwrites (fixup.sh:1-5). Ifbuildis reproduced manually,fixup.shmust run after bothtscpasses.
roleListorder ≠ priority order
ACLManager.roleList(src/ACLManager.js:41-44) returns a de-duplicatedSetunion across ACLs for display/enumeration; it is not a priority indicator. Priority is purely theaclListarray order (front = highest, set byunshift).
Unverified / gaps
- Consumers within FaceKom: This repo gives no evidence of which FaceKom services import
@techteamer/aclor 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
.gitattributesand no binary assets (verified viagit ls-tree -r HEAD). - CI:
.travis.ymlis referenced in.gitignore/.npmignore(.gitignore:4,.npmignore:4) but is not present in the tree; no.github/workflowsexists. Actual CI configuration is unverified from this repo. The most recent commit message referencesfkqa-331(git logHEAD74dbd1e), suggesting an external CI/QA pipeline not stored here. maintenance/2026-04-07.mdandsecurity/2026-04-07.mdwere not read line-by-line; the newer2026-05-04dated pair was read in full and is representative (dependencyyarn outdatednotes and a cleanyarn audit).
Sources
Files read in full (repo /Users/levander/coding/facekom-v2-clones/acl, branch master, HEAD 74dbd1e):
README.mdpackage.jsonindex.jsCHANGELOG.mdsrc/ACLService.jssrc/ACLManager.jssrc/ACLError.jsfixup.sheslint.config.mjstsconfig.json,tsconfig-base.json,tsconfig-cjs.json.gitignore,.npmignoretest/ACLService.test.js,test/ACLManager.test.jsmaintenance/2026-05-04.md,security/2026-05-04.mdgit ls-tree -r HEAD(authoritative file inventory),git log,git show -s(HEAD metadata)