@astram/guard

中文文档
0.34.0发布于 2026/11/06MITTypeScriptCommonJS近 30 天下载 6554

Astram framework — the enforcement guard + real-world adapters (the 'hands'). One global deny-by-default gate every request passes through (ADR-0002), classification decorators, boot-pool assertion (ADR-0018), the forRoot() registration seam, and the DB/R

原始使用文档
文档目录7 节
  1. What's in here
  2. The registration seam (ADR-0023 §4)
  3. Request principal ( currentRequestPrincipal )
  4. File-share port token ( SHIELD_FILE_SHARE , GAPS #57)
  5. Peer dependencies (ADR-0023 §2)
  6. Commands
  7. Notes

@astram/guard — the enforcement gate + NestJS adapters

The enforcement gate + thin NestJS integration for the Astram framework — the shippable glue a consumer app installs (it runs on / integrates with NestJS). Per ADR-0023 this is framework code, owned + locked, not consumer-editable: it lives in the consumer's node_modules, and upgrades flow through npm update. It depends on @astram/core (the framework-agnostic kernel) and PEER-depends on the host's NestJS runtime.

What's in here

PieceFilePurpose
ShieldModulesrc/module/shield.module.tsThe one registration seam — forRoot() / forRootAsync(). Consumers import it once; the framework owns composition.
Global enforcement guardsrc/enforcement/global-enforcement.guard.tsThe deny-by-default APP_GUARD (ADR-0002) every route traverses.
Classification decoratorssrc/enforcement/classification.decorator.ts@Public() / @Classified() — the consumer authors these on controllers.
Core seamsrc/enforcement/core-seam.tsType-only import of @astram/core's DecisionAuthority (ADR-0003) + the fail-closed stub.
Boot-pool assertionsrc/boot/boot-sequence.tsADR-0018 connection-pool budget check at boot (fail-closed).
App configsrc/config/app-config.tsPort / environment / ADR-0018 pool budget (env loader, pure).
Request principalsrc/request-auth/request-principal-als.tsRead-only currentRequestPrincipal(): the current request's { tenantId, userId } (below).

The registration seam (ADR-0023 §4)

A consumer app wires Astram with a single import in its root module — it never edits the framework:

import { Module } from '@nestjs/common';
import { ShieldModule } from '@astram/guard';

@Module({
  imports: [
    ShieldModule.forRoot(), // fail-closed stub authority; env-loaded config
    // ShieldModule.forRoot({ decisionAuthority, config }) to supply the real Core authority
  ],
  controllers: [/* the consumer's feature controllers */],
})
export class AppModule {}

forRoot() binds:

  • GlobalEnforcementGuard as APP_GUARD → the global, structurally-unavoidable, deny-by-default enforcement point (ADR-0002). @Public() is the only opt-out; an unclassified route is denied (403).
  • The resolved AppConfig (under APP_CONFIG) + the BootSequence hook, which runs the ADR-0018 pool-budget assertion at boot (aborts on violation).
  • The Core decision authority (under SHIELD_DECISION_AUTHORITY) — the real @astram/core authority when supplied via forRoot({ decisionAuthority }), else the fail-closed stub (deny-by-default), so a mis-wiring cannot open a door.

forRootAsync({ useFactory, inject, imports }) is the DI-computed variant (a stub in this layer): the factory resolves the same options at startup; the guard/boot wiring is identical.

Request principal (currentRequestPrincipal)

import { currentRequestPrincipal } from '@astram/guard';

const p = currentRequestPrincipal(); // Readonly<{ tenantId: string; userId: string }> | undefined

Framework code that runs inside a request but has no req in hand (for example @astram/agent's createAuditedFetch) reads the caller's tenant and principal id here. Spec: docs/features/capability-outbound-audit/SPEC.md v6 §5 G.

  • Read-only. A framework middleware (EnforcementModule, every route) opens a module-private AsyncLocalStorage with an empty holder per request. The global enforcement guard is its only writer: once, on the allow path, after authentication, from the principal it resolved from the session or API key (never from the body, headers or query). The value is frozen. The storage, the middleware and the writer are not exported, so app code cannot enter the scope or overwrite it.
  • undefined outside a request (a scheduled job), on a @Public() route, for a request the guard refused, and for a request that did not pass the middleware.
  • userId is the principal id: a user or a service account. The value does not say which kind (GAPS #46); look the id up if you need it.
  • Work that resumes outside the request's async context (for example some stream consumers) reads undefined, never another request's value (an interleaved four-request test holds this). Read it in the request's own call chain; a callback run by shared code (a listener on an app-wide emitter) runs in the context of whoever triggered it, so do not read it there.
  • Start background work at boot (onApplicationBootstrap), never lazily from inside a request. Async resources created inside a request (a timer, a promise chain, a queue drain a handler starts on first use) keep that request's context, including its principal or its lack of one.
  • Routes excluded from app.setGlobalPrefix(prefix, { exclude }) pass the middleware too (an e2e test holds this).
  • @astram/guard/internal (framework use only) also exports a read-only insideRequest(): true inside a request that passed the middleware, with or without a principal. @astram/agent's audited fetch uses it to refuse calls from @Public() and refused requests.
  • Express only. currentRequestPrincipal is proven on @nestjs/platform-express; Fastify is unsupported until it has its own e2e test.
  • Nest routes only. The holder is opened by a Nest middleware, so a handler mounted directly on the HTTP adapter (outside Nest routing) runs "outside a request": insideRequest() is false and @astram/agent's createAuditedFetch takes the scheduled-job branch there (the app must then supply a server-held tenant). Keep connector code in Nest handlers or boot-started jobs.
  • In memory only: no database or Redis read is added to the request path (Shield #12).

File-share port token (SHIELD_FILE_SHARE, GAPS #57)

src/module/shield-tokens.ts declares the port so guard never imports identity (ADR-0003); @astram/identity binds it per request. Spec docs/features/file-share-port/SPEC.md v1, ADR-0040 addendum (2026-11-03).

  • Exports: SHIELD_FILE_SHARE (a Symbol, unbound without identity), FileSharePort (shareWithRole, unshare, revokeForParent, listForParent, reconcileFile; no actor, tenant, owner or company argument), FileShareParent ({ typeKey, id }), FileShareResult ({ created, effective, missing }), FileShareError, FILE_SHARE_ERROR_CODES, FileShareErrorCode.
  • FileShareError(code) takes only the code, so its message never carries an id, file name or table: file_share_invalid 400 Invalid file share request., file_share_not_found 404 Not found., file_share_denied 403 Not permitted., file_share_unavailable 503 File sharing is unavailable.; an unknown code becomes file_share_unavailable.
  • Migration 1710000000126-AddRecordShareSource.cjs adds the source columns (rs_source_type_key, rs_source_record_uuid, rs_source_operation, '' for owner shares), ck_rs_source_complete, uq_rs_grant_source (replaces uq_rs_grant) and idx_rs_source; down refuses while a port row exists.
  • RecordShareWriter: owner unshare deletes owner-made rows only and refuses a tuple that matches only port rows with ShareNotPermittedError(SHARE_MANAGED_BY_LINKED_RECORD) (This share is managed by a linked record.); list rows (RecordShareRow) carry the source fields; listSharesForAuthorizedRecord lists a record's shares without the owner bound, for a caller that has already proven authority with decide() (identity's stored-file admin listing).

Peer dependencies (ADR-0023 §2)

@astram/guard peer-depends on the host runtime so the consumer owns exactly one copy (a single reflect-metadata singleton / DI injector):

@nestjs/common ^11 · @nestjs/core ^11 · reflect-metadata ^0.2 · rxjs ^7

These are also devDependencies here so the package compiles + tests locally. @astram/core is a real (workspace) dependency.

Commands

pnpm --filter @astram/guard build   # tsc -p tsconfig.build.json (emits dist + .d.ts)
pnpm --filter @astram/guard lint    # eslint
pnpm --filter @astram/guard test    # vitest run (guard unit + boot-pool + forRoot e2e)

Notes

  • CommonJS locally ("type": "commonjs") for the NestJS decorator + reflect-metadata DI model. The repo root and @astram/core stay ESM/NodeNext; the override is scoped to this package (see tsconfig.json).
  • The core seam imports the REAL @astram/core barrel (type-only) — Core's exports map exposes only ".", so subpath imports fail Node resolution. This package's tsconfig clears the base paths so @astram/core resolves through the node_modules workspace symlink to Core's built dist.

版本与发布记录

@astram/guard 共发布 47 个版本,首次发布于 2026/07/10;过去 12 个月发布了 47 个版本。

版本发布日期距今
0.34.0latest2026/11/061 天前
0.33.02026/11/043 天前
0.32.02026/11/034 天前
0.31.02026/11/034 天前
0.30.12026/11/016 天前
0.30.02026/10/317 天前

关于 @astram/guard 的常见问题

@astram/guard 是什么?

Astram framework — the enforcement guard + real-world adapters (the 'hands'). One global deny-by-default gate every request passes through (ADR-0002), classification decorators, boot-pool assertion (ADR-0018), the forRoot() registration seam, and the DB/R

如何安装 @astram/guard?

在项目目录运行 npm install @astram/guard,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:rxjs(^7)、@nestjs/core(^11)、@nestjs/common(^11)、reflect-metadata(^0.2)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@astram/guard 支持 TypeScript 吗?

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

@astram/guard 支持 ESM 还是 CommonJS?

以 CommonJS 形式发布,可以直接 require();在 ES Module 项目中也可以用 import 引入(Node.js 会把 module.exports 作为默认导出)。

@astram/guard 可以商用吗?

@astram/guard 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@astram/guard 的下载量有多少?

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

@astram/guard 还在维护吗?

最新版本 0.34.0 发布于 2026/11/06(1 天前);至今共发布 47 个版本,其中过去 12 个月发布了 47 个;首次发布于 2026/07/10。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。