@authup/access

中文文档
1.0.0-beta.73发布于 2026/11/02Apache-2.0TypeScriptES Module构建来源已验证近 30 天下载 4081

Evaluate Authup permissions and policies in your own process, with the same engine the server uses.

原始使用文档
文档目录12 节
  1. Why @authup/access?
  2. Installation
  3. Usage
  4. Evaluate permissions you define
  5. Decide from Authup's catalog
  6. Check one record
  7. Use the batch check answer
  8. Realm reach
  9. Policy types
  10. API
  11. Notes
  12. License

Authup logo

@authup/access

Evaluate Authup permissions and policies in your own process, with the same engine the server uses.
Your API decides "may this token update this row?" locally, instead of asking Authup on every request.

npm version CI License: Apache-2.0

Installation · Usage · API · Documentation


Why @authup/access?

In Authup a grant is more than a permission name. Each grant carries a realm reach (own, ownOrNull, any) and optional policies (attributes, identity, time, date, realm match, composites), and a permission can carry policies of its own. Re-implementing that model in every API that receives an Authup token is how the API and the server end up disagreeing. This package is the model itself: the server-core service evaluates every management API request with it, and so can you.

ConcernWithout itWith it
Permission checksif (token.permissions.includes('user_update')), which ignores realm reach and policiesevaluateRecord(evaluator, { name: 'user_update', realm }) answers the way the server would
Realm reachHand-written tenant comparisons per routeown, ownOrNull and any are evaluated per grant against the record's realm
PoliciesA second, partial implementation of attribute, time and identity rulesThe built-in policy evaluators, plus your own types through PolicyEngine
Stale dataSilent denials after a permission is added on the serverAuthorizationCatalogStaleError says the copy is old, so you refetch
List queriesFetch everything, then filter in memorycompile() lowers a decision to a rapiq condition you can push into the query

Installation

npm install @authup/access

ESM only. Node.js >=26.0.0. No peer dependencies; the policy engine runs in a browser as well.

Usage

Evaluate permissions you define

import {
    BuiltInPolicyType,
    PermissionEvaluator,
    PermissionMemoryProvider,
    defineAttributesPolicy,
    definePolicyData,
} from '@authup/access';

const evaluator = new PermissionEvaluator({
    provider: new PermissionMemoryProvider([
        { permission: { name: 'report_read' } },
        {
            permission: { name: 'report_update' },
            policies: [defineAttributesPolicy({ type: BuiltInPolicyType.ATTRIBUTES, query: { ownerId: 'u1' } })],
        },
    ]),
});

await evaluator.evaluate({ name: 'report_read' });

await evaluator.evaluate({
    name: 'report_update',
    data: definePolicyData({ [BuiltInPolicyType.ATTRIBUTES]: { ownerId: 'u1' } }),
});

await evaluator.preEvaluate({ name: 'report_update' }); // passes: the row is not known yet
  • evaluate resolves or throws a PermissionError (isPermissionError). With several names it needs all of them; evaluateOneOf needs one.
  • preEvaluate is the gate before the data exists: a policy whose data key is missing counts as passed, and only a policy that already fails denies. evaluate with the same missing key denies.
  • A permission the provider does not know denies.
  • An attributes policy's query is a MongoDB-style filter ({ ownerId: 'u1' }, { age: { $gte: 18 } }) matched against the attributes data key.

Decide from Authup's catalog

A resource server reads the identity-free catalog once (GET /authorization, with its own client credentials and permission_read), and takes the grants and the identity from the introspection of the token it received (POST /token/introspect: permissions, sub, sub_kind, realm_id, realm_name):

import {
    createAuthorizationEvaluator,
    evaluateRecord,
    isAuthorizationCatalogStaleError,
    reachesRecord,
} from '@authup/access';

declare const catalog: unknown;
declare const grants: unknown;
declare function refetchCatalog(): Promise<unknown>;

const identity = {
    type: 'user',
    id: '7c9e6679-7425-40de-944b-e07fc1f099d6',
    realmId: '4c8e0f9e-6c0a-4b1e-9d31-2a6f0b1c7e11',
};

let evaluator;
try {
    evaluator = await createAuthorizationEvaluator({ catalog, grants, identity });
} catch (error) {
    if (!isAuthorizationCatalogStaleError(error)) {
        throw error;
    }

    evaluator = await createAuthorizationEvaluator({ catalog: await refetchCatalog(), grants, identity });
}

await evaluateRecord(evaluator, {
    name: 'user_update',
    realm: '4c8e0f9e-6c0a-4b1e-9d31-2a6f0b1c7e11',
    data: { attributes: { name: 'alice' } },
});

const canDelete = await reachesRecord(evaluator, { name: 'user_delete', realm: null });
  • catalog and grants are validated, so you can pass the parsed response bodies as they are. A malformed document throws.
  • A grant naming a permission or a policy the catalog lacks throws AuthorizationCatalogStaleError: your catalog is older than the grant. Refetch and build again.
  • A definition the server could not project arrives with policies: null, and a policy tree your copy cannot project (an unknown type, a configuration its validator refuses) denies the permissions that use it. Neither throws, and other permissions are unaffected.
  • Without an identity only definitions whose policies need no identity can pass. Grants without an identity are refused.
  • Custom policy types need both halves: createAuthorizationEvaluator(input, { validators, evaluators }), each replacing the built-in registry (spread PolicyDefaultValidators / PolicyDefaultEvaluators to extend it).

Check one record

evaluateRecord, evaluateRecordOneOf, reachesRecord, reachesRecordOneOf and evaluateRecordUpdate take the record's realm explicitly:

realmMeans
'<realm id>'A row of that realm
nullA global row
['<id>', null]A row spanning several realms; every one must be reached
undefinedThrows: a missing realm is a bug, never "global"
  • realmId and clientId name the realm and client OWNING the permission definition, not the record's realm. Absent means a global definition; an owned one is unreachable without them.
  • data is a PolicyData or a plain record and must not carry realmMatch itself; the helper sets it from realm. It is cloned, never written to.
  • reachesRecord* returns false for a PermissionError and rethrows anything else.
  • evaluateRecordUpdate(evaluator, { name, current, next }) evaluates the stored row and the updated row, so an update can neither move a row into reach nor out of it.
  • hasUnboundedReach(evaluator, name, owner?) is true when compile answers allow: an any grant with no narrowing policy. Pass { realmId, clientId } as owner for an owned definition.

Use the batch check answer

A public client (a browser app) holds no secret, so it cannot read the catalog. It asks POST /authorization/check for its own verdicts instead and gates its UI with them:

import { createAuthorizationCheckEvaluator, reachesRecord } from '@authup/access';

declare const permissions: unknown; // the response body

const evaluator = await createAuthorizationCheckEvaluator({
    permissions,
    identity: {
        type: 'user',
        id: '7c9e6679-7425-40de-944b-e07fc1f099d6',
        realmId: '4c8e0f9e-6c0a-4b1e-9d31-2a6f0b1c7e11',
        realmName: 'acme',
    },
});

const canEdit = await reachesRecord(evaluator, { name: 'user_update', realm: 'acme' });

// a definition owned by a realm or a client is named with its owner
const canExport = await reachesRecord(evaluator, {
    name: 'report_export',
    realmId: '4c8e0f9e-6c0a-4b1e-9d31-2a6f0b1c7e11',
    clientId: '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d',
    realm: 'acme',
});

The answer lists each permission definition, identified by name, realmId and clientId, with the target realms it holds in; a missing entry is a denial. Which owned definitions it covers is the request's realms / clients selectors: own, ownOrNull, granted (the owner of every definition the identity holds a grant of, on top of ownOrNull) or a list of ids. A record realm matches its own entry, or the identity's own realm by id or by name. A realm the request did not ask about denies. The result is an upper bound: the server still decides each row.

Realm reach

realmScopeReaches
noneNothing
own (default)Rows of the identity's own realm
ownOrNullOwn realm and global rows (realm: null)
anyEvery realm and global rows

A grant without a realmScope is own. An identity without a realm reaches nothing below any.

Policy types

BuiltInPolicyTypeValueDecides on
ATTRIBUTESattributesA MongoDB-style query over the attributes data
ATTRIBUTE_NAMESattributeNamesWhich field names the data may carry (with invert, a denylist)
IDENTITYidentityThe identity's types (user, client)
REALM_MATCHrealmMatchThe record realm against the identity's realm
DATEdateA start / end date window
TIMEtimeA start / end time of day (HH:MM), optionally limited by interval with dayOfWeek, dayOfMonth or dayOfYear
COMPOSITEcompositeIts children, combined unanimous, affirmative or consensus
PERMISSION_BINDINGpermissionBindingWhether the identity holds a grant that reaches the record

Every policy accepts invert. Policy data is a PolicyData bag keyed by the type names (attributes, identity, realmMatch, ...); definePolicyData builds one with the well-known keys type-checked.

API

ExportKindPurpose
PermissionEvaluator, IPermissionEvaluatorclass, interfaceevaluate, evaluateOneOf, preEvaluate, preEvaluateOneOf, compile
PermissionMemoryProvider, IPermissionProviderclass, interfaceWhere an evaluator looks permission definitions up
evaluateRecord, evaluateRecordOneOf, reachesRecord, reachesRecordOneOf, evaluateRecordUpdate, hasUnboundedReachfunctionSingle-record checks with an explicit realm
createAuthorizationEvaluatorfunctionAn evaluator over the catalog, the introspected grants and the identity
createAuthorizationCheckEvaluatorfunctionAn evaluator over the batch check answer
AuthorizationCatalogStaleError, isAuthorizationCatalogStaleErrorclass, functionThe refetch signal
projectAuthorizationPolicy, AUTHORIZATION_POLICY_WITHHELD_TYPEfunction, constThe wire projection of a policy tree, and the node type the server sends in place of a tree the caller may not read
AuthorizationCatalog, AuthorizationGrant, AuthorizationCheckPermissions, authorizationCatalogSchema, ...type, schemaWire types and their zod schemas
PolicyEngine, IPolicyEngine, IPolicyEvaluatorclass, interfaceEvaluate a policy tree; registerEvaluator(type, evaluator) adds a type
PolicyDefaultEvaluators, PolicyDefaultValidatorsconstThe built-in registries
BuiltInPolicyType, SystemPolicyNameenum, constPolicy type names; the names of the provisioned system policies (system.default, ...)
<Type>Policy, <Type>PolicyEvaluator, <Type>PolicyValidatortype, classOne trio per built-in policy type
IdentityPermissionBindingPolicyEvaluatorclassDecides permissionBinding from the grants carried on the identity data
PolicyData, definePolicyData, PolicyInput, IdentityPolicyDataclass, function, typeThe evaluation data bag
RealmScope, realmScopeMatches, normalizeRealmScope, compareRealmScope, minRealmScope, maxRealmScopeenum, functionRealm reach
aggregatePermissionPolicyBindings, grantDominates, buildPermissionKeyfunctionGrant aggregation and comparison
PermissionError, PolicyError, isPermissionError, isPolicyErrorclass, functionErrors (both extend AuthupError from @authup/errors)
createPolicyTransitionCollector, PolicyTransitionSinkfunction, typeCollect the instant at which a date or time verdict could change

Notes

  • Realm reach is enforced by the permissionBinding policy. A PermissionEvaluator over PermissionMemoryProvider ignores a binding's realmScope, and its default permissionBinding evaluator always passes. createAuthorizationEvaluator wires IdentityPermissionBindingPolicyEvaluator, and Authup binds system.default (which contains a permissionBinding policy) to every permission it creates, so that is where reach is checked. Build from the catalog when reach matters.
  • Identity data uses UUIDs. The identity data validator checks id, clientId and realmId as UUIDs; any other value makes every identity policy fail.
  • compile can answer post. That means the decision cannot be expressed as a query condition; evaluate every candidate row before you page, or the page and its total are wrong.
  • Each PolicyEngine owns its evaluator map. The constructor copies the map, so registerEvaluator on one engine never reaches another.

License

Made with 💚

Published under the Apache 2.0 License.

版本与发布记录

@authup/access 共发布 48 个版本,首次发布于 2025/01/21;过去 12 个月发布了 44 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
1.0.0-beta.73latest2026/11/025 天前
1.0.0-beta.722026/10/2711 天前
1.0.0-beta.712026/10/2018 天前
1.0.0-beta.702026/10/1919 天前
1.0.0-beta.692026/10/1424 天前
1.0.0-beta.682026/09/251 个月前

关于 @authup/access 的常见问题

@authup/access 是什么?

Evaluate Authup permissions and policies in your own process, with the same engine the server uses.

如何安装 @authup/access?

在项目目录运行 npm install @authup/access,或使用 pnpm add / yarn add / bun add。

@authup/access 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@authup/access 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@authup/access 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=26.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@authup/access 可以商用吗?

@authup/access 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@authup/access 的下载量有多少?

根据 npm 官方下载统计,@authup/access 在 2026/10/07 – 2026/11/05 这 30 天内被下载 4,081 次,日均约 136 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@authup/access 还在维护吗?

最新版本 1.0.0-beta.73 发布于 2026/11/02(5 天前);至今共发布 48 个版本,其中过去 12 个月发布了 44 个;首次发布于 2025/01/21。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。