Skip to content
v0.1.1 · preview release
aooth

Authorization, all the way to the column

Role- and attribute-based access control, native to moost + atscript. Sessions, tokens, MFA, and magic links wired in

01Credentials

Built to hold up under scrutiny

Scrypt with self-describing hashes, salt + pepper, history checks. Password policies expressed as serializable rules — same predicate runs on server and client. TOTP, backup codes, trusted devices. All behind a pluggable store.

scrypt@prostojs/ftringTOTPbackup codeslockout
Explore @aooth/user
credentials.ts
ts
import { UserService } from "@aooth/user";
import {
  UsersStoreAtscriptDb,
  type AuthUserTable,
} from "@aooth/user/atscript-db";
import {
  ppHasMinLength,
  ppHasUpperCase,
  ppHasNumber,
} from "@aooth/user";

const users = new UserService(
  new UsersStoreAtscriptDb({
    table: db.getTable(AppUser) as unknown as AuthUserTable,
  }),
  {
    password: {
      pepper: process.env.PEPPER!,
      historyLength: 5,
      policies: [ppHasMinLength(12), ppHasUpperCase(1), ppHasNumber(1)],
    },
    lockout: { threshold: 5, duration: 15 * 60_000 },
  },
);

await users.createUser("alice@app.dev", "P4ssphrase!");
const { user, mfaRequired } = await users.login(
  "alice@app.dev",
  "P4ssphrase!",
);
02Authorization

Authorization that reaches into the data

Roles + privileges + dynamic scopes. Wildcard matchers (* single-segment, ** any depth), deny-wins evaluation. Each allow rule can return a SQL or Mongo filter — a single grant narrows queries automatically. Multi-role unions merge to $in or $or.

defineRoleallowTableReadmergeScopeFiltersControlGateArbacDbScope
Explore @aooth/arbac
authorization.ts
ts
import {
  Arbac,
  defineRole,
  allowTableRead,
  allowTableWrite,
} from "@aooth/arbac";

type Attrs = { tenantId: string; department: string };
type Scope = { filter?: { tenantId?: string; department?: string } };

const manager = defineRole<Attrs, Scope>()
  .id("com.role.manager")
  .use(
    allowTableWrite("articles", {
      scope: (a) => ({ filter: { department: a.department } }),
    }),
  )
  .use(allowTableRead("reports"))
  .deny("articles", "publish")
  .build();

const arbac = new Arbac<Attrs, Scope>();
arbac.registerRole(manager);

const result = await arbac.evaluate(
  { resource: "articles", action: "update" },
  { id: "u1", roles: ["com.role.manager"], attrs },
);
// → { allowed: true, scopes: [{ filter: { department: "sales" } }] }
03Auth methods

One orchestrator, every credential mode

Stateful or stateless — same API. JWT via jose, encapsulated AES-256-GCM, in-memory, Redis, atscript-db. Sliding refresh with grace window and reuse detection. Magic links with atomic single-use guarantees. Per-user epoch revocation.

AuthCredentialJWTsliding refreshmagic linksRedis
Explore @aooth/auth
auth methods.ts
ts
import {
  AuthCredential,
  CredentialStoreJwt,
  DenylistStoreMemory,
} from "@aooth/auth";

const auth = new AuthCredential<{ roles: string[] }>({
  store: new CredentialStoreJwt({
    algorithm: "HS256",
    secret: process.env.JWT_SECRET!,
    denylist: new DenylistStoreMemory(),
  }),
  accessTtl: 60 * 60_000,
  refresh: {
    ttl: 30 * 24 * 3600_000,
    rotation: "sliding",
    rotationGraceMs: 30_000,
    onRotationReuse: (state) => log.warn("refresh reuse", state),
  },
});

const issued = await auth.issue("alice", {
  claims: { roles: ["admin"] },
  metadata: { ip: req.ip },
});

// On every request:
const ctx = await auth.validate(issued.accessToken);
// ctx → { userId, method, credentialId, expiresAt, claims }
04Moost integration

Decorators, workflows, controllers — declared, not assembled

AuthGuard interceptor, useAuth and useArbac composables, an AuthController with a single /trigger entry-point covering three batteries-included workflows: login, recovery, invite. Each pauses for forms and emits a unified WfFinished envelope to the client.

@Public@ArbacAuthorizeLoginWorkflowAsArbacDbControllerWfFinished
Explore @aooth/*-moost
moost integration.ts
ts
import {
  AuthController,
  authGuardInterceptor,
  Public,
  UserId,
  LoginWorkflow,
  type LoginWorkflowOpts,
  type DeliverPayload,
} from "@aooth/auth-moost";
import {
  ArbacAuthorize,
  ArbacResource,
  ArbacAction,
  arbacAuthorizeInterceptor,
} from "@aooth/arbac-moost";
import type { AuthCredential } from "@aooth/auth";
import type { UserService } from "@aooth/user";
import { Controller, Inherit, Injectable } from "moost";
import { Get } from "@moostjs/event-http";

app.applyGlobalInterceptors(authGuardInterceptor());
app.applyGlobalInterceptors(arbacAuthorizeInterceptor);
app.registerControllers(AuthController, MyLoginWorkflow, ReportsController);

@Controller("reports")
@ArbacResource("reports")
class ReportsController {
  @Get(":id")
  @ArbacAction("read")
  @ArbacAuthorize()
  async read(@UserId() userId: string) {
    return { userId };
  }
}

@Inherit() @Injectable("FOR_EVENT") @Controller()
class MyLoginWorkflow extends LoginWorkflow {
  // Subclasses MUST re-declare the ctor — TS emits fresh design-paramtypes per class.
  constructor(opts: LoginWorkflowOpts, users: UserService, auth: AuthCredential) {
    super(opts, users, auth);
  }

  protected override async deliver(payload: DeliverPayload) {
    if (payload.channel === "email") await emailSender.send(payload);
  }
}
05AI agent skill

Your AI already speaks it

One command teaches Claude Code, Cursor, Windsurf, and Codex the entire aoothjs stack — UserService, AuthCredential, defineRole, AsArbacDbController, the workflow envelope, and the shipped .as models.

  • UserService · password · MFA primitives
  • defineRole · allowTableRead · scope merging
  • AuthCredential · stores · magic links
  • Moost guards · workflows · DB controllers
Learn about AI agent skills

Released under the MIT License.