npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@i4e/invest4edu-access-core

v0.43.4

Published

Shared access-control primitives for NeoFindesk: tenant keystone, role capabilities, reportee tree, entity/role vocabulary, mobile app identity, feature flags, and the unified access engine (registry schema, snapshot resolver, visibleWhen).

Readme

@i4e/invest4edu-access-core

Shared tenant-isolation / access-control primitives for the NeoFindesk backends (nfd-api-node v1 + nfd-api-node-v2). The D1 keystone of the access-management epic: central, unforgettable account_id scoping.

Both backends are ESM, so this package ships plain ESM source — no build step.

0.32.0 — BREAKING, and it fails silently

resolveReporteeUserIds no longer reads employees.reporting_manager as a user id. It reads it only as the manager's employee _id, which is the shape BRI-973 migrates the field to. This is a breaking change on a 0.x minor, so read the two notes below before upgrading a consumer.

1. Against un-migrated data it returns zero reportees, not an error. The dual-shape widening that carried the rollout is gone. A manager whose rows have not been migrated resolves to a self-only scope, and because this function feeds record visibility in nfd-api-node and the lead access scope in nfd-api-node-v2, that reads as "this manager has no reportees" — nothing thrown, nothing logged, just a smaller answer.

So the deploy order is migrate → verify → bump, not the other way round. Run nfd-api-node's migrations/verify-reporting-manager-to-employee-id.js and get a clean verdict before the first instance carrying 0.32.0 serves traffic. In a rolling deploy that window is real, so the migration must be complete rather than merely started.

Under npm's 0.x caret rules ^0.31.0 will not resolve to 0.32.0, so no consumer is dragged across by an install — the ordering risk is entirely a human one.

2. Callers must pass the FULL employee list. An _id reference only resolves if the row carrying it is present, so a pre-filtered list ({ status: 1 }, an account scope, anything) drops link rows — and what you lose is not the filtered person but everyone beneath them. Filter the result instead. resolveReporteeUserIds warns once when handed a list in which no row carries an _id at all, which is the one case where the answer is confidently wrong rather than merely narrow.

0.32.0 also moves the entity/role vocabulary — ENTITY, ENTITY_STATUS, isEntityLoginAllowed, conflictingEntityFor, ROLE_TYPE_PRIORITY, CLIENT_APP_ROLE_NAMES — into this package. It was previously written out three times (nfd-api-node, nfd-api-node-v2, nfdui-nextjs) and held together by parity tests.

Install

npm install @i4e/invest4edu-access-core

Usage

1. Carry identity per request (in the auth middleware, JWT path only):

import als from "@i4e/invest4edu-access-core/tenant-context";
// after verifying the JWT:
return als.run({ account_id, userId, roles }, () => next());

2. Enforce on tenant-scoped models:

import { tenantPlugin } from "@i4e/invest4edu-access-core";
schema.plugin(tenantPlugin);          // before mongoose.model(...)

The plugin injects account_id from the request's ALS store into the filter of every operation that selects existing documents:

| | ops | switch | |---|---|---| | reads | find findOne countDocuments distinct | TENANT_ENFORCEMENT / accessconfig.tenantEnforcement | | writes | updateOne updateMany replaceOne deleteOne deleteMany findOneAndUpdate findOneAndReplace findOneAndDelete | TENANT_ENFORCEMENT_WRITES / accessconfig.tenantEnforcementWrites |

Each switch is an independent ladder — off → warn → enforce — and both default to off, so installing the plugin is a literal no-op until you arm it. Reads and writes are separate because the blast radius is not comparable: a read that gains a filter returns less data, while a write that gains a filter silently modifies nothing and still reports success. Arming reads must never arm writes by surprise.

| mode | identity present | identity absent | |---|---|---| | off (default) | no-op | no-op | | warn | reads: inject account_id · writes: unchanged | log [tenant] identity-less …, do not scope | | enforce | inject account_id | throw (fail-closed) |

warn deliberately leaves writes alone. warn means "tell me what enforce would do", and for a destructive operation that promise is only kept by changing nothing — injecting a filter under a mode named warn would turn live UPDATEs into silent no-ops, the exact failure burn-in exists to catch. What you need before promoting is which writes run identity-less, and that is logged.

Precedence per ladder: env var (if valid) → DB value → off. An unrecognised value is reported once and ignored rather than treated as off, so a typo cannot silently disarm enforcement.

3. Bypass for legitimately cross-tenant operations (caches, migrations, scripts):

import { runAsSystem } from "@i4e/invest4edu-access-core";
await runAsSystem(() => Employee.find({ status: 1 }));   // whole scope
Model.find(q).setOptions({ skipTenant: true });          // one query

Exports

  • als (default of /tenant-context) — the AsyncLocalStorage instance
  • runWithTenant(store, fn), runAsSystem(fn), getTenantStore()
  • tenantPlugin (default of /tenant-plugin) — the Mongoose plugin
  • setTenantMode(m) / getTenantMode() — the read ladder
  • setTenantWriteMode(m) / getTenantWriteMode() — the write ladder
  • resolveReporteeUserIds(roots, employees, opts) (/reportee-tree) — self + all reportees
  • ENTITY, ENTITY_STATUS, isEntityLoginAllowed, entityInactiveMessage, checkPortalEntityAccess (/entity-status) — which entities permit a login
  • conflictingEntityFor, entityConflictMessage, CONFLICTING_ENTITIES (/user-entity-link) — which hats may coexist on one person
  • ROLE_TYPE_PRIORITY, CLIENT_APP_ROLE_NAMES, pickPortalRoleName (/portal-roles) — which role names a portal session
  • MOBILE_LOGIN_SOURCES, CLIENT_APP_LOGIN_SOURCES, PARTNER_APP_HATS, APP_BUNDLE_ID_PATTERN, appBundleIdOf (/mobile-app-identity) — which mobile app a login came from, which hats it flags, and the bundle id its session keeps
  • clientProfile namespace (/client-profile) — the two-profile client model. Settings (resolveClientProfileSettings, MODES, collection names), sides and tagging (sideKeyOf, profileTypeFor, orderSideFor, primaryLockFor, primaryFromLead), access level (clientAccessFor, callerClassOf, linkedViewOf), the side filter (buildSideFilter) and the personal-details field policy (buildFieldPolicyIndex, evaluateFieldChanges, effectivePolicyFor, changedFields). Every behaviour is read from a clientprofilesettings document and each has an off | audit | enforce mode, defaulting to off. Business identifiers (distributor codes, role names, product codes) have EMPTY defaults and come from each environment's settings document. Design and tracker: https://claude.ai/artifact/JrCTutgQkp32r3NeU2uvmk

Not covered

Aggregation pipelines — add an explicit { $match: { account_id } } stage.

Inserts (save, create, insertMany) — and they cannot be. They carry no filter, so there is nothing to constrain; putting account_id on a new document is the caller's job. The plugin only governs which existing documents an operation may reach.

estimatedDocumentCount takes no filter either, so it is not hooked.

Compatibility

Node ≥ 18 (AsyncLocalStorage). mongoose ≥ 6 is an optional peer (only the plugin needs it).