Initial Commit
This commit is contained in:
125
packages/api-stack/api/api-auth-static-principal/README.md
Normal file
125
packages/api-stack/api/api-auth-static-principal/README.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# @suaveplan/api-auth-static-principal — fixed-principal session resolver for @suaveplan/api: returns a configured Principal verbatim on every request. Used in plugin integration tests and the api-plugin-example fixture.
|
||||
|
||||
Fixed-`Principal` session resolver for the `@suaveplan/api` plugin
|
||||
system. Returns the configured `Principal` verbatim on every request —
|
||||
no token decoding, no network I/O, no internal state. Used in plugin
|
||||
integration tests and the `api-plugin-example` fixture, where the goal is to
|
||||
exercise the entire downstream pipeline (RBAC gate, tenant resolver chain
|
||||
reading `principal.tenantId`, tenant-scoped DB) with a deterministic
|
||||
identity carried in config.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [adapter](./src/adapter/adapter.md) — Bundles the static-principal resolver, the (absent) security-scheme contribution, and a pinned `csrf: false` into a sin…
|
||||
- [adapter](./src/adapter/adapter.md) — Bundles the static-principal resolver, the (absent) security-scheme contribution, and a pinned `csrf: false` into a sin…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — Defines the `AUTH_STATIC_PRINCIPAL.*` error-code namespace and the `AuthStaticPrincipalError` concrete subclass of `@su…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — Defines the `AUTH_STATIC_PRINCIPAL.*` error-code namespace and the `AuthStaticPrincipalError` concrete subclass of `@su…
|
||||
- [factory](./src/factory/create-static-principal-resolver.md) — Constructs a `SessionResolver` from `@suaveplan/api-auth` that returns the configured `Principal` verbatim on every req…
|
||||
- [create-static-principal-resolver](./src/factory/create-static-principal-resolver.md) — Constructs a `SessionResolver` from `@suaveplan/api-auth` that returns the configured `Principal` verbatim on every req…
|
||||
- [options](./src/options/options.md) — Owns the runtime contract between the test harness call site and the resolver factory. The schema validates both the ou…
|
||||
- [options](./src/options/options.md) — Owns the runtime contract between the test harness call site and the resolver factory. The schema validates both the ou…
|
||||
- [security-scheme](./src/security-scheme/security-scheme.md) — Provides the per-adapter `SecuritySchemeContribution` slot of `AuthAdapter`. The static-principal adapter is a test-onl…
|
||||
- [security-scheme](./src/security-scheme/security-scheme.md) — Provides the per-adapter `SecuritySchemeContribution` slot of `AuthAdapter`. The static-principal adapter is a test-onl…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Why does this exist?
|
||||
|
||||
Per the API foundation design doc §13.2, the testing kit needs an auth
|
||||
adapter that supplies a fixed `Principal` without any token decoding. The
|
||||
§13.2 example uses it directly:
|
||||
|
||||
```ts
|
||||
adapters: {
|
||||
"auth.session": {
|
||||
kind: "static-principal",
|
||||
principal: { id: "u1", scopes: ["billing:write"], tenantId: "acme" },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Tests exercise the whole pipeline — RBAC, tenant resolution, tenant-scoped
|
||||
DB — with a deterministic principal in config. No mocks, no JWKS setup, no
|
||||
token signing.
|
||||
|
||||
## Surface
|
||||
|
||||
| Subpath | Export |
|
||||
| -------------- | --------------------------------------------------------------------------------------- |
|
||||
| `./resolver` | `createStaticPrincipalResolver(opts): SessionResolver` |
|
||||
| `./adapter` | `createStaticPrincipalAdapter(opts): AuthAdapter` |
|
||||
| `./error-codes`| `AUTH_STATIC_PRINCIPAL_CODES`, `AuthStaticPrincipalError`, `AuthStaticPrincipalErrorCode` |
|
||||
|
||||
The root barrel re-exports the resolver, adapter, error codes, options
|
||||
schema (`StaticPrincipalResolverOptionsSchema` /
|
||||
`parseStaticPrincipalResolverOptions`), and the empty
|
||||
`staticPrincipalSecurityScheme()` contribution.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { createStaticPrincipalAdapter } from "@suaveplan/api-auth-static-principal/adapter";
|
||||
|
||||
const adapter = createStaticPrincipalAdapter({
|
||||
principal: {
|
||||
id: "u1",
|
||||
scopes: ["billing:write"],
|
||||
tenantId: "acme",
|
||||
claims: { sub: "u1" },
|
||||
},
|
||||
});
|
||||
|
||||
host.registerAuth(adapter);
|
||||
// every request now produces `principal === { id: "u1", ... }`.
|
||||
```
|
||||
|
||||
Skip auth on selected paths to test `publicRoutes` behaviour without
|
||||
rewiring the adapter chain:
|
||||
|
||||
```ts
|
||||
const adapter = createStaticPrincipalAdapter({
|
||||
principal: { id: "u1", scopes: [], claims: {} },
|
||||
nullForPaths: ["/healthz", "/webhooks/*"],
|
||||
applyTo: (req) => !req.headers.has("x-skip-auth"),
|
||||
});
|
||||
```
|
||||
|
||||
## Conformance
|
||||
|
||||
The resolver passes `runAuthSessionConformance` from
|
||||
`@suaveplan/api-auth/testing` in seeded-principal mode. See
|
||||
`src/factory/create-static-principal-resolver.spec-conformance.test.ts` and
|
||||
`tests/conformance/api-auth.test.ts`.
|
||||
|
||||
## Errors
|
||||
|
||||
`AUTH_STATIC_PRINCIPAL.INVALID_OPTIONS` — outer options bag failed Zod
|
||||
validation (wrong shape, missing required fields, unknown top-level keys).
|
||||
|
||||
`AUTH_STATIC_PRINCIPAL.INVALID_PRINCIPAL` — the supplied `principal` field
|
||||
is malformed (missing `id`, non-string `id`, non-array `scopes`, unknown
|
||||
keys inside `principal`, …).
|
||||
|
||||
Both fire synchronously at construction time. The resolver itself never
|
||||
throws at request time.
|
||||
|
||||
## Stage-5 wiring (forward-looking)
|
||||
|
||||
When `api-host/testing` ships its Stage-5 `createTestHost` surface, this
|
||||
adapter is registered in the adapter table under
|
||||
`kind: "static-principal"`, so tests will declare it inline:
|
||||
|
||||
```ts
|
||||
const host = createTestHost({
|
||||
adapters: {
|
||||
"auth.session": {
|
||||
kind: "static-principal",
|
||||
principal: { id: "u1", scopes: ["billing:write"], tenantId: "acme" },
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Until then, callers wire the adapter directly via
|
||||
`createStaticPrincipalAdapter()` as shown above.
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"name": "@suaveplan/api-auth-static-principal",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"description": "Stage-5 fixed-principal session resolver for @suaveplan/api: returns a configured Principal verbatim on every request. Used in plugin integration tests and the api-plugin-example fixture.",
|
||||
"license": "MIT",
|
||||
"author": "Suaveplan Team",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"./resolver": {
|
||||
"import": {
|
||||
"types": "./dist/factory/create-static-principal-resolver.d.ts",
|
||||
"default": "./dist/factory/create-static-principal-resolver.js"
|
||||
}
|
||||
},
|
||||
"./adapter": {
|
||||
"import": {
|
||||
"types": "./dist/adapter/adapter.d.ts",
|
||||
"default": "./dist/adapter/adapter.js"
|
||||
}
|
||||
},
|
||||
"./error-codes": {
|
||||
"import": {
|
||||
"types": "./dist/error-codes/error-codes.d.ts",
|
||||
"default": "./dist/error-codes/error-codes.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"test": "vitest run --passWithNoTests",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage --passWithNoTests",
|
||||
"lint": "bunx biome check src/",
|
||||
"lint:fix": "bunx biome lint --fix --unsafe --max-diagnostics 500 src/",
|
||||
"format": "bunx biome format --write --max-diagnostics 500 src/",
|
||||
"check": "bunx biome check --write --unsafe --max-diagnostics 500 src/",
|
||||
"typecheck": "bunx tsc --noEmit --composite false -p tsconfig.lib.json",
|
||||
"docs:check": "bun run ../../../../scripts/docs-check.ts ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@suaveplan/api-auth": "workspace:^",
|
||||
"@suaveplan/api-types": "workspace:^",
|
||||
"@suaveplan/error": "workspace:^",
|
||||
"tslib": "catalog:build",
|
||||
"zod": "catalog:forms"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@suaveplan/testing": "workspace:^",
|
||||
"@types/node": "catalog:",
|
||||
"@vitest/coverage-istanbul": "catalog:testing",
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:build",
|
||||
"vite-plugin-dts": "catalog:build",
|
||||
"vitest": "catalog:testing"
|
||||
},
|
||||
"publishConfig": {
|
||||
"registry": "https://npmjs.fq.io/",
|
||||
"access": "public"
|
||||
},
|
||||
"files": ["dist", "src", "README.md"]
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
# `adapter` — composite `AuthAdapter` factory
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Bundles the static-principal resolver, the (absent) security-scheme
|
||||
contribution, and a pinned `csrf: false` into a single `AuthAdapter` value.
|
||||
This is the call-site shape for the canonical "test host with a known
|
||||
Principal" wiring — `host.registerAuth(createStaticPrincipalAdapter({...}))`
|
||||
gives the test the entire auth pipeline without further plumbing.
|
||||
|
||||
The factory delegates construction-time validation to
|
||||
`createStaticPrincipalResolver`, so misconfigured options surface as the
|
||||
same typed `AuthStaticPrincipalError` they do at the resolver level. The
|
||||
adapter holds no state of its own.
|
||||
|
||||
## Features
|
||||
|
||||
- Single-call construction of the resolver + scheme + csrf triple.
|
||||
- Pins `csrf: false` because the static-principal layer has no session
|
||||
cookie to forge against — every request carries the configured Principal
|
||||
by construction.
|
||||
- `securityScheme` omitted (matching `api-auth-anonymous` topology) so the
|
||||
generated OpenAPI document doesn't advertise a real auth flow for
|
||||
routes wired to this adapter.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createStaticPrincipalAdapter } from "@suaveplan/api-auth-static-principal/adapter";
|
||||
|
||||
const adapter = createStaticPrincipalAdapter({
|
||||
principal: {
|
||||
id: "u1",
|
||||
scopes: ["billing:write"],
|
||||
tenantId: "acme",
|
||||
claims: { sub: "u1" },
|
||||
},
|
||||
});
|
||||
|
||||
host.registerAuth(adapter);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Compose with `applyTo` / `nullForPaths` to skip auth on test-only routes:
|
||||
|
||||
```ts
|
||||
import { createStaticPrincipalAdapter } from "@suaveplan/api-auth-static-principal/adapter";
|
||||
|
||||
const adapter = createStaticPrincipalAdapter({
|
||||
principal: { id: "u1", scopes: [], claims: {} },
|
||||
nullForPaths: ["/healthz", "/webhooks/*"],
|
||||
});
|
||||
|
||||
// host.registerAuth(adapter) — /healthz and /webhooks/* resolve to null;
|
||||
// every other route gets the configured Principal.
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
| ------------------------------------- | -------- | ---------------------------------------------------------- |
|
||||
| `createStaticPrincipalAdapter(opts)` | function | Returns `AuthAdapter` composed from the four building blocks. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`csrf: false` is the load-bearing pin: a static-principal host can't honour
|
||||
a real CSRF check (there's no session cookie carrying a synchronizer
|
||||
token), so the host should skip CSRF entirely for routes wired to this
|
||||
adapter. If a downstream test needs CSRF behaviour exercised in isolation,
|
||||
compose with a separate adapter that pins `csrf: true` and let the host's
|
||||
adapter-chain merge rule pick the safer value (the host's policy is to use
|
||||
the strictest `csrf` flag in the chain).
|
||||
|
||||
`validateAuthAdapter` accepts this adapter's shape (security scheme absent;
|
||||
csrf set to a boolean). The package's adapter test asserts the validation
|
||||
contract is satisfied without re-implementing the validator.
|
||||
@@ -0,0 +1,59 @@
|
||||
import { validateAuthAdapter } from "@suaveplan/api-auth";
|
||||
import type { Principal } from "@suaveplan/api-types/principal";
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { createStaticPrincipalAdapter } from "./adapter.js";
|
||||
|
||||
const P: Principal = {
|
||||
id: "u_1" as Principal["id"],
|
||||
scopes: ["x:write" as Principal["scopes"][number]],
|
||||
tenantId: "acme" as Principal["tenantId"],
|
||||
claims: { sub: "u_1" },
|
||||
};
|
||||
|
||||
describe("createStaticPrincipalAdapter", () => {
|
||||
it("validates against validateAuthAdapter", { timeout: 5000 }, () => {
|
||||
const adapter = createStaticPrincipalAdapter({ principal: P });
|
||||
const out = validateAuthAdapter(adapter);
|
||||
expect(out).toBe(adapter);
|
||||
expect(out.securityScheme).toBeUndefined();
|
||||
});
|
||||
|
||||
it("pins csrf:false (no underlying session)", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const adapter = createStaticPrincipalAdapter({ principal: P });
|
||||
expect(adapter.csrf).toBe(false);
|
||||
});
|
||||
|
||||
it("embedded resolver returns the principal for arbitrary requests", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const adapter = createStaticPrincipalAdapter({ principal: P });
|
||||
const result = await adapter.resolver.resolve(
|
||||
new Request("https://example.test/api/anything")
|
||||
);
|
||||
expect(result).toEqual(P);
|
||||
});
|
||||
|
||||
it("embedded resolver honours nullForPaths", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const adapter = createStaticPrincipalAdapter({
|
||||
principal: P,
|
||||
nullForPaths: ["/healthz"],
|
||||
});
|
||||
expect(
|
||||
await adapter.resolver.resolve(
|
||||
new Request("https://example.test/healthz")
|
||||
)
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("dispose on embedded resolver is idempotent", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const adapter = createStaticPrincipalAdapter({ principal: P });
|
||||
await adapter.resolver.dispose();
|
||||
await expect(adapter.resolver.dispose()).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { AuthAdapter } from "@suaveplan/api-auth";
|
||||
import { createStaticPrincipalResolver } from "../factory/create-static-principal-resolver.js";
|
||||
import type { StaticPrincipalResolverOptions } from "../options/options.types.js";
|
||||
import { staticPrincipalSecurityScheme } from "../security-scheme/security-scheme.js";
|
||||
|
||||
/**
|
||||
* Build a composite {@link AuthAdapter} bundling the static-principal
|
||||
* resolver with an empty security-scheme contribution and `csrf: false`.
|
||||
*
|
||||
* Use this when the call-site wants the canonical "test host with a known
|
||||
* Principal" adapter as a single value to pass into the host's
|
||||
* `registerAuth` slot. For mixed chains, prefer
|
||||
* `createStaticPrincipalResolver()` directly and assemble the chain via
|
||||
* `SessionResolverChain` from `@suaveplan/api-auth/chain`.
|
||||
*
|
||||
* `csrf` is pinned to `false` because the static-principal resolver has no
|
||||
* underlying session cookie to forge against — every request carries the
|
||||
* configured Principal by construction.
|
||||
*/
|
||||
export function createStaticPrincipalAdapter(
|
||||
opts: StaticPrincipalResolverOptions
|
||||
): AuthAdapter {
|
||||
// `staticPrincipalSecurityScheme()` is documented to return `undefined`
|
||||
// (the anonymous-style topology). The call is preserved for symmetry +
|
||||
// for the test that asserts the no-scheme contract, but the result is
|
||||
// dropped here so `exactOptionalPropertyTypes` is satisfied without a
|
||||
// never-taken branch on the constructor.
|
||||
void staticPrincipalSecurityScheme();
|
||||
return {
|
||||
resolver: createStaticPrincipalResolver(opts),
|
||||
csrf: false,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { createStaticPrincipalAdapter } from "./adapter.js";
|
||||
@@ -0,0 +1,87 @@
|
||||
# `error-codes` — stable code registry for the static-principal adapter
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines the `AUTH_STATIC_PRINCIPAL.*` error-code namespace and the
|
||||
`AuthStaticPrincipalError` concrete subclass of
|
||||
`@suaveplan/error/SuaveplanError`. This module is the single source of truth
|
||||
for every failure code the package can surface to a caller — wiring this
|
||||
registry into the host's error envelope is how a malformed test-time
|
||||
Principal configuration reaches the developer rather than silently producing a
|
||||
broken auth chain.
|
||||
|
||||
Codes are produced via `defineCodes("AUTH_STATIC_PRINCIPAL", [...])` so the
|
||||
full literal value is e.g. `"AUTH_STATIC_PRINCIPAL.INVALID_OPTIONS"`. The
|
||||
class augments the base error with `authStaticPrincipalCode`, providing a
|
||||
narrowed code property when consumers have already discriminated by
|
||||
`instanceof`.
|
||||
|
||||
## Features
|
||||
|
||||
- Stable, frozen `AUTH_STATIC_PRINCIPAL_CODES` map built via `defineCodes` so
|
||||
the prefix can't drift.
|
||||
- Discriminated-union friendly: `AuthStaticPrincipalErrorCode` is a literal
|
||||
union over the registry entries.
|
||||
- Carries arbitrary `context` (Zod issues, the offending field path) via the
|
||||
base `SuaveplanError` options bag.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
AUTH_STATIC_PRINCIPAL_CODES,
|
||||
AuthStaticPrincipalError,
|
||||
} from "@suaveplan/api-auth-static-principal/error-codes";
|
||||
|
||||
throw new AuthStaticPrincipalError(
|
||||
AUTH_STATIC_PRINCIPAL_CODES.INVALID_OPTIONS,
|
||||
"createStaticPrincipalResolver: options failed schema validation"
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Attach Zod issues so the caller can pinpoint the bad field without re-running
|
||||
validation:
|
||||
|
||||
```ts
|
||||
import {
|
||||
AUTH_STATIC_PRINCIPAL_CODES,
|
||||
AuthStaticPrincipalError,
|
||||
} from "@suaveplan/api-auth-static-principal/error-codes";
|
||||
import z from "zod";
|
||||
|
||||
const parsed = OptionsSchema.safeParse(input);
|
||||
if (!parsed.success) {
|
||||
throw new AuthStaticPrincipalError(
|
||||
AUTH_STATIC_PRINCIPAL_CODES.INVALID_PRINCIPAL,
|
||||
"principal failed schema validation",
|
||||
{ context: { issues: parsed.error.issues } }
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
| ----------------------------------- | -------- | ------------------------------------------------------------------------ |
|
||||
| `AUTH_STATIC_PRINCIPAL_CODES` | constant | Frozen map of code constants. Keys are the short names; values are full. |
|
||||
| `AuthStaticPrincipalErrorCode` | type | Literal union over the registry values. |
|
||||
| `AuthStaticPrincipalError` | class | `SuaveplanError` subclass typed against the code registry. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`defineCodes` joins the prefix with each suffix using a single `.` so the
|
||||
resulting strings match the host's structured-log code field. The class
|
||||
stores the code twice — once in the inherited `code` slot and once in the
|
||||
narrower `authStaticPrincipalCode` slot — so callers that already narrowed by
|
||||
`instanceof` get a precisely-typed code field without a follow-up `as`.
|
||||
|
||||
The package's static-principal resolver never throws at request time; both
|
||||
codes are construction-time only. This is deliberate — every per-request
|
||||
failure for fixed-principal tests should manifest in the test assertion, not
|
||||
in the auth pipeline.
|
||||
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import {
|
||||
AUTH_STATIC_PRINCIPAL_CODES,
|
||||
AuthStaticPrincipalError,
|
||||
} from "./error-codes.js";
|
||||
|
||||
describe("AUTH_STATIC_PRINCIPAL_CODES", () => {
|
||||
it("exposes the INVALID_OPTIONS literal", { timeout: 5000 }, () => {
|
||||
expect(AUTH_STATIC_PRINCIPAL_CODES.INVALID_OPTIONS).toBe(
|
||||
"AUTH_STATIC_PRINCIPAL.INVALID_OPTIONS"
|
||||
);
|
||||
});
|
||||
|
||||
it("exposes the INVALID_PRINCIPAL literal", { timeout: 5000 }, () => {
|
||||
expect(AUTH_STATIC_PRINCIPAL_CODES.INVALID_PRINCIPAL).toBe(
|
||||
"AUTH_STATIC_PRINCIPAL.INVALID_PRINCIPAL"
|
||||
);
|
||||
});
|
||||
|
||||
it("AuthStaticPrincipalError mirrors code into the narrow field", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const err = new AuthStaticPrincipalError(
|
||||
AUTH_STATIC_PRINCIPAL_CODES.INVALID_OPTIONS,
|
||||
"bad options"
|
||||
);
|
||||
expect(err).toBeInstanceOf(AuthStaticPrincipalError);
|
||||
expect(err.code).toBe(AUTH_STATIC_PRINCIPAL_CODES.INVALID_OPTIONS);
|
||||
expect(err.authStaticPrincipalCode).toBe(
|
||||
AUTH_STATIC_PRINCIPAL_CODES.INVALID_OPTIONS
|
||||
);
|
||||
expect(err.message).toBe("bad options");
|
||||
});
|
||||
|
||||
it("AuthStaticPrincipalError accepts an options bag", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const err = new AuthStaticPrincipalError(
|
||||
AUTH_STATIC_PRINCIPAL_CODES.INVALID_PRINCIPAL,
|
||||
"with ctx",
|
||||
{ context: { field: "scopes" } }
|
||||
);
|
||||
expect(err.context).toEqual({ field: "scopes" });
|
||||
expect(err.authStaticPrincipalCode).toBe(
|
||||
AUTH_STATIC_PRINCIPAL_CODES.INVALID_PRINCIPAL
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { SuaveplanErrorOptions } from "@suaveplan/error";
|
||||
import { defineCodes, SuaveplanError } from "@suaveplan/error";
|
||||
|
||||
/**
|
||||
* Stable error-code registry for `@suaveplan/api-auth-static-principal`.
|
||||
*
|
||||
* The static-principal resolver has no token decoding, no network I/O and no
|
||||
* persistent state — every request returns the configured Principal verbatim
|
||||
* (or `null` for paths in `nullForPaths`). The two failure modes both fire at
|
||||
* construction time:
|
||||
*
|
||||
* - `AUTH_STATIC_PRINCIPAL.INVALID_OPTIONS` — the outer options bag failed
|
||||
* Zod validation (wrong shape, missing required fields, unknown keys).
|
||||
* - `AUTH_STATIC_PRINCIPAL.INVALID_PRINCIPAL` — the supplied `principal` is
|
||||
* malformed (missing `id`, missing/empty `scopes`, non-object `claims`, …).
|
||||
*
|
||||
* Codes are namespaced `AUTH_STATIC_PRINCIPAL.*` via {@link defineCodes} so
|
||||
* the full literal value is e.g. `"AUTH_STATIC_PRINCIPAL.INVALID_OPTIONS"`.
|
||||
* Consumers branch on the constant rather than the raw string.
|
||||
*/
|
||||
export const AUTH_STATIC_PRINCIPAL_CODES = defineCodes(
|
||||
"AUTH_STATIC_PRINCIPAL",
|
||||
["INVALID_OPTIONS", "INVALID_PRINCIPAL"] as const
|
||||
);
|
||||
|
||||
export type AuthStaticPrincipalErrorCode =
|
||||
(typeof AUTH_STATIC_PRINCIPAL_CODES)[keyof typeof AUTH_STATIC_PRINCIPAL_CODES];
|
||||
|
||||
/**
|
||||
* Concrete error class for the static-principal package. Extends
|
||||
* {@link SuaveplanError} so `instanceof AuthStaticPrincipalError` narrows
|
||||
* `code` to {@link AuthStaticPrincipalErrorCode}.
|
||||
*/
|
||||
export class AuthStaticPrincipalError extends SuaveplanError<AuthStaticPrincipalErrorCode> {
|
||||
public readonly authStaticPrincipalCode: AuthStaticPrincipalErrorCode;
|
||||
|
||||
public constructor(
|
||||
code: AuthStaticPrincipalErrorCode,
|
||||
message: string,
|
||||
options?: SuaveplanErrorOptions
|
||||
) {
|
||||
super(code, message, options);
|
||||
this.authStaticPrincipalCode = code;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
AUTH_STATIC_PRINCIPAL_CODES,
|
||||
AuthStaticPrincipalError,
|
||||
type AuthStaticPrincipalErrorCode,
|
||||
} from "./error-codes.js";
|
||||
@@ -0,0 +1,99 @@
|
||||
# `factory/create-static-principal-resolver` — fixed-Principal resolver
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Constructs a `SessionResolver` from `@suaveplan/api-auth` that returns the
|
||||
configured `Principal` verbatim on every request — no token decoding, no
|
||||
JWKS, no I/O. This is the workhorse adapter for plugin integration tests,
|
||||
the `api-plugin-example` fixture, and any environment where the goal is to
|
||||
exercise the downstream pipeline (RBAC gate, tenant resolver chain reading
|
||||
`principal.tenantId`, tenant-scoped DB) with a deterministic identity carried
|
||||
in config.
|
||||
|
||||
The factory consumes options previously validated by the `options` module so
|
||||
construction is total: every misuse maps to either
|
||||
`AUTH_STATIC_PRINCIPAL.INVALID_OPTIONS` or `AUTH_STATIC_PRINCIPAL.INVALID_PRINCIPAL`
|
||||
and surfaces synchronously at construction time. The resolver itself never
|
||||
throws at request time.
|
||||
|
||||
## Features
|
||||
|
||||
- Defensive clone on every `resolve()` — callers cannot mutate one returned
|
||||
Principal and observe the change on subsequent calls.
|
||||
- `applyTo(req)` predicate for arbitrary "return null for this request"
|
||||
rules.
|
||||
- `nullForPaths` suffix-glob list (`/webhooks/*`) and exact-match patterns
|
||||
for the most common public-route test setup.
|
||||
- Idempotent no-op `dispose()` matching the `SessionResolver` contract.
|
||||
- Returned objects are frozen (top-level + `scopes` + `claims`) to make
|
||||
attempted mutations fail loudly in strict mode.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createStaticPrincipalResolver } from "@suaveplan/api-auth-static-principal/resolver";
|
||||
|
||||
const resolver = createStaticPrincipalResolver({
|
||||
principal: {
|
||||
id: "u1",
|
||||
scopes: ["billing:write"],
|
||||
tenantId: "acme",
|
||||
claims: { sub: "u1" },
|
||||
},
|
||||
});
|
||||
|
||||
const principal = await resolver.resolve(
|
||||
new Request("https://example.test/api/billing/invoices")
|
||||
);
|
||||
// principal.id === "u1"
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Combine `applyTo` and `nullForPaths` to model a mixed-route test surface:
|
||||
|
||||
```ts
|
||||
import { createStaticPrincipalResolver } from "@suaveplan/api-auth-static-principal/resolver";
|
||||
|
||||
const resolver = createStaticPrincipalResolver({
|
||||
principal: {
|
||||
id: "u1",
|
||||
scopes: ["billing:write", "billing:read"],
|
||||
tenantId: "acme",
|
||||
claims: { sub: "u1" },
|
||||
},
|
||||
nullForPaths: ["/webhooks/*", "/healthz"],
|
||||
applyTo: (req) => !req.headers.has("x-skip-auth"),
|
||||
});
|
||||
|
||||
// /healthz and /webhooks/stripe resolve to null.
|
||||
// Requests with the `x-skip-auth` header resolve to null.
|
||||
// Everything else returns the configured Principal.
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
| ----------------------------------- | -------- | ------------------------------------------------------------------ |
|
||||
| `createStaticPrincipalResolver(o)` | function | Returns a `SessionResolver` carrying `o.principal` on every match. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The clone strategy is: deep-freeze a single template at construction, then
|
||||
clone-from-template per `resolve()`. The per-call clone is unavoidable —
|
||||
otherwise two callers each freezing the returned object would freeze each
|
||||
other's state. The per-call clone copies the top level plus `scopes` (array
|
||||
spread) and `claims` (object spread); deeper structures inside `claims` are
|
||||
not cloned. This matches the `Principal` contract: `claims` values are
|
||||
`unknown`, so the package can't responsibly deep-clone them. Callers passing
|
||||
mutable nested claims values are responsible for not mutating them in-place.
|
||||
|
||||
`pathMatches` accepts only exact-match and a single suffix glob (`/foo/*`)
|
||||
on purpose — richer matching belongs in `applyTo`. The trailing slash is
|
||||
preserved when stripping the `*` so `/foo/*` matches `/foo/x` but not
|
||||
`/foox`. The pattern dialect is documented on the spec and exercised by the
|
||||
test file.
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* §16 conformance proof — the static-principal resolver MUST pass
|
||||
* `runAuthSessionConformance` from `@suaveplan/api-auth/testing`. Seeded
|
||||
* with a known Principal so the harness exercises the "recognised request"
|
||||
* scenario; `nullForPaths` lets the harness assert the "unrecognised"
|
||||
* scenario without rewiring the factory.
|
||||
*/
|
||||
import { runAuthSessionConformance } from "@suaveplan/api-auth/testing";
|
||||
import type { Principal } from "@suaveplan/api-types/principal";
|
||||
import { createStaticPrincipalResolver } from "./create-static-principal-resolver.js";
|
||||
|
||||
const P: Principal = {
|
||||
id: "u_conf" as Principal["id"],
|
||||
scopes: ["x:write" as Principal["scopes"][number]],
|
||||
claims: { sub: "u_conf" },
|
||||
};
|
||||
|
||||
runAuthSessionConformance({
|
||||
label: "static-principal-direct",
|
||||
factory: () =>
|
||||
createStaticPrincipalResolver({
|
||||
principal: P,
|
||||
nullForPaths: ["/__public/*"],
|
||||
}),
|
||||
seedNull: () => new Request("https://example.test/__public/health"),
|
||||
seedPrincipal: () => ({
|
||||
request: new Request("https://example.test/api/anything"),
|
||||
expected: P,
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,191 @@
|
||||
import type { Principal } from "@suaveplan/api-types/principal";
|
||||
import { describe, expect, expectTypeOf, it } from "@suaveplan/testing/runner";
|
||||
import {
|
||||
AUTH_STATIC_PRINCIPAL_CODES,
|
||||
AuthStaticPrincipalError,
|
||||
} from "../error-codes/error-codes.js";
|
||||
import { createStaticPrincipalResolver } from "./create-static-principal-resolver.js";
|
||||
|
||||
const TARGET = "https://example.test/api/billing";
|
||||
|
||||
const P: Principal = {
|
||||
id: "u_1" as Principal["id"],
|
||||
scopes: ["billing:write" as Principal["scopes"][number]],
|
||||
tenantId: "acme" as Principal["tenantId"],
|
||||
claims: { sub: "u_1" },
|
||||
};
|
||||
|
||||
describe("createStaticPrincipalResolver", () => {
|
||||
it("every request resolves to the configured Principal", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const r = createStaticPrincipalResolver({ principal: P });
|
||||
const a = await r.resolve(new Request(TARGET));
|
||||
const b = await r.resolve(new Request("https://example.test/other"));
|
||||
expect(a).not.toBeNull();
|
||||
expect(b).not.toBeNull();
|
||||
expect(a).toEqual(P);
|
||||
expect(b).toEqual(P);
|
||||
await r.dispose();
|
||||
});
|
||||
|
||||
it("returned Principal is isolated across calls", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const r = createStaticPrincipalResolver({ principal: P });
|
||||
const a = await r.resolve(new Request(TARGET));
|
||||
const b = await r.resolve(new Request(TARGET));
|
||||
expect(a).not.toBe(b); // distinct references
|
||||
// Both are deep-frozen — attempting to mutate throws in strict mode.
|
||||
expect(Object.isFrozen(a)).toBe(true);
|
||||
expect(Object.isFrozen(b)).toBe(true);
|
||||
});
|
||||
|
||||
it("matching path in nullForPaths returns null (glob)", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const r = createStaticPrincipalResolver({
|
||||
principal: P,
|
||||
nullForPaths: ["/webhooks/*"],
|
||||
});
|
||||
const result = await r.resolve(
|
||||
new Request("https://example.test/webhooks/stripe")
|
||||
);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("matching path in nullForPaths returns null (exact)", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const r = createStaticPrincipalResolver({
|
||||
principal: P,
|
||||
nullForPaths: ["/healthz"],
|
||||
});
|
||||
const result = await r.resolve(
|
||||
new Request("https://example.test/healthz")
|
||||
);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("non-matching path returns the configured Principal", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const r = createStaticPrincipalResolver({
|
||||
principal: P,
|
||||
nullForPaths: ["/webhooks/*"],
|
||||
});
|
||||
const result = await r.resolve(
|
||||
new Request("https://example.test/billing/invoices")
|
||||
);
|
||||
expect(result).toEqual(P);
|
||||
});
|
||||
|
||||
it("exact pattern does not match a related prefix", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const r = createStaticPrincipalResolver({
|
||||
principal: P,
|
||||
nullForPaths: ["/foo"],
|
||||
});
|
||||
const result = await r.resolve(
|
||||
new Request("https://example.test/foobar")
|
||||
);
|
||||
// /foo exact does NOT match /foobar — must return the principal.
|
||||
expect(result).toEqual(P);
|
||||
});
|
||||
|
||||
it("applyTo predicate returning false resolves to null", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const r = createStaticPrincipalResolver({
|
||||
principal: P,
|
||||
applyTo: (req) => !req.headers.has("x-skip-auth"),
|
||||
});
|
||||
const skipped = await r.resolve(
|
||||
new Request(TARGET, { headers: { "x-skip-auth": "1" } })
|
||||
);
|
||||
expect(skipped).toBeNull();
|
||||
const normal = await r.resolve(new Request(TARGET));
|
||||
expect(normal).toEqual(P);
|
||||
});
|
||||
|
||||
it("applyTo runs before nullForPaths", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
let nullForPathsConsulted = false;
|
||||
const r = createStaticPrincipalResolver({
|
||||
principal: P,
|
||||
applyTo: (_req) => false, // always force null
|
||||
nullForPaths: [
|
||||
// If this pattern were consulted, it would also return null,
|
||||
// but `applyTo` returning false should short-circuit before.
|
||||
(() => {
|
||||
nullForPathsConsulted = true;
|
||||
return "/never";
|
||||
})(),
|
||||
],
|
||||
});
|
||||
const result = await r.resolve(new Request(TARGET));
|
||||
expect(result).toBeNull();
|
||||
// Pattern array was constructed (so the IIFE ran) — that's expected.
|
||||
// The real assertion: even though `nullForPaths` is configured,
|
||||
// `applyTo` returning false is enough to produce null without URL
|
||||
// parsing failure on a non-standard URL.
|
||||
expect(nullForPathsConsulted).toBe(true);
|
||||
});
|
||||
|
||||
it("dispose is idempotent", { timeout: 5000 }, async () => {
|
||||
const r = createStaticPrincipalResolver({ principal: P });
|
||||
await r.dispose();
|
||||
await expect(r.dispose()).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("strict-mode: nullForPaths unset returns principal everywhere", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const r = createStaticPrincipalResolver({ principal: P });
|
||||
expect(
|
||||
await r.resolve(new Request("https://example.test/anything"))
|
||||
).toEqual(P);
|
||||
expect(
|
||||
await r.resolve(new Request("https://example.test/webhooks/x"))
|
||||
).toEqual(P);
|
||||
});
|
||||
|
||||
it("rejects invalid options at construction (INVALID_OPTIONS)", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
try {
|
||||
createStaticPrincipalResolver(null as never);
|
||||
throw new Error("expected throw");
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(AuthStaticPrincipalError);
|
||||
expect((err as AuthStaticPrincipalError).code).toBe(
|
||||
AUTH_STATIC_PRINCIPAL_CODES.INVALID_OPTIONS
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects missing principal.id at construction (INVALID_PRINCIPAL)", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
try {
|
||||
createStaticPrincipalResolver({
|
||||
principal: { scopes: [], claims: {} },
|
||||
} as never);
|
||||
throw new Error("expected throw");
|
||||
} catch (err) {
|
||||
expect((err as AuthStaticPrincipalError).code).toBe(
|
||||
AUTH_STATIC_PRINCIPAL_CODES.INVALID_PRINCIPAL
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("resolve return-type narrows to Promise<Principal | null>", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const r = createStaticPrincipalResolver({ principal: P });
|
||||
expectTypeOf(r.resolve).toBeFunction();
|
||||
expectTypeOf(r.resolve).parameter(0).toEqualTypeOf<Request>();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
import type { SessionResolver } from "@suaveplan/api-auth";
|
||||
import type { Principal } from "@suaveplan/api-types/principal";
|
||||
import { parseStaticPrincipalResolverOptions } from "../options/options.js";
|
||||
import type { StaticPrincipalResolverOptions } from "../options/options.types.js";
|
||||
|
||||
/**
|
||||
* Match a request path against a single `nullForPaths` pattern.
|
||||
*
|
||||
* Supported syntaxes:
|
||||
* - Exact match — `/webhooks/stripe` matches only that path.
|
||||
* - Suffix glob — `/webhooks/*` matches `/webhooks/<anything>` including
|
||||
* further slashes (everything after `/webhooks/`).
|
||||
*
|
||||
* The pattern dialect is deliberately small. Tests that need richer matching
|
||||
* use the `applyTo` predicate.
|
||||
*/
|
||||
function pathMatches(pathname: string, pattern: string): boolean {
|
||||
if (pattern.endsWith("/*")) {
|
||||
const prefix = pattern.slice(0, -1); // keep trailing slash
|
||||
return pathname.startsWith(prefix);
|
||||
}
|
||||
return pathname === pattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defensive-clone the configured Principal so callers can't mutate one
|
||||
* returned object and affect subsequent `resolve()` results. The clone is
|
||||
* shallow on the top-level fields plus a one-level copy of `scopes` and
|
||||
* `claims` — every field of `Principal` is documented `readonly`, so callers
|
||||
* shouldn't be mutating these anyway; this guards against the slip-up.
|
||||
*/
|
||||
function cloneFrozen(principal: Principal): Principal {
|
||||
return Object.freeze({
|
||||
id: principal.id,
|
||||
scopes: Object.freeze([...principal.scopes]),
|
||||
tenantId: principal.tenantId,
|
||||
claims: Object.freeze({ ...principal.claims }),
|
||||
}) as Principal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory for the fixed-Principal {@link SessionResolver}.
|
||||
*
|
||||
* The returned resolver carries the configured Principal on every
|
||||
* `resolve()` call — no token decoding, no network I/O, no internal state.
|
||||
* It is the canonical test-harness adapter for plugin integration tests that
|
||||
* exercise the full pipeline (RBAC, tenant resolution, tenant-scoped DB)
|
||||
* without standing up a JWT signer.
|
||||
*
|
||||
* Behaviour overrides:
|
||||
* - `applyTo(req)` — if supplied and returns `false`, `resolve()` returns
|
||||
* `null` for that request. Runs first.
|
||||
* - `nullForPaths` — exact-match or suffix-glob path patterns. Matching
|
||||
* requests resolve to `null` (lets tests exercise `publicRoutes`
|
||||
* behaviour without rewiring the adapter chain).
|
||||
*
|
||||
* `dispose()` is a no-op (still idempotent) because the resolver holds no
|
||||
* resources.
|
||||
*
|
||||
* @throws {AuthStaticPrincipalError} `AUTH_STATIC_PRINCIPAL.INVALID_OPTIONS`
|
||||
* for malformed outer options, `AUTH_STATIC_PRINCIPAL.INVALID_PRINCIPAL`
|
||||
* for a malformed `principal` field.
|
||||
*/
|
||||
export function createStaticPrincipalResolver(
|
||||
opts: StaticPrincipalResolverOptions
|
||||
): SessionResolver {
|
||||
const parsed = parseStaticPrincipalResolverOptions(opts);
|
||||
const frozen = cloneFrozen(parsed.principal);
|
||||
const nullForPaths = parsed.nullForPaths ?? [];
|
||||
const applyTo = parsed.applyTo;
|
||||
return {
|
||||
resolve: async (req: Request): Promise<Principal | null> => {
|
||||
if (applyTo !== undefined && !applyTo(req)) {
|
||||
return null;
|
||||
}
|
||||
if (nullForPaths.length > 0) {
|
||||
const url = new URL(req.url);
|
||||
for (const pattern of nullForPaths) {
|
||||
if (pathMatches(url.pathname, pattern)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Return a fresh defensive clone so callers can't mutate one
|
||||
// returned object and observe the change on the next call.
|
||||
return cloneFrozen(frozen);
|
||||
},
|
||||
dispose: async (): Promise<void> => undefined,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { createStaticPrincipalResolver } from "./create-static-principal-resolver.js";
|
||||
@@ -0,0 +1,18 @@
|
||||
// Adapter
|
||||
export { createStaticPrincipalAdapter } from "./adapter/adapter.js";
|
||||
// Error codes
|
||||
export {
|
||||
AUTH_STATIC_PRINCIPAL_CODES,
|
||||
AuthStaticPrincipalError,
|
||||
type AuthStaticPrincipalErrorCode,
|
||||
} from "./error-codes/error-codes.js";
|
||||
// Resolver
|
||||
export { createStaticPrincipalResolver } from "./factory/create-static-principal-resolver.js";
|
||||
// Options
|
||||
export {
|
||||
parseStaticPrincipalResolverOptions,
|
||||
StaticPrincipalResolverOptionsSchema,
|
||||
} from "./options/options.js";
|
||||
export type { StaticPrincipalResolverOptions } from "./options/options.types.js";
|
||||
// Security scheme
|
||||
export { staticPrincipalSecurityScheme } from "./security-scheme/security-scheme.js";
|
||||
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
parseStaticPrincipalResolverOptions,
|
||||
StaticPrincipalResolverOptionsSchema,
|
||||
} from "./options.js";
|
||||
export type { StaticPrincipalResolverOptions } from "./options.types.js";
|
||||
@@ -0,0 +1,82 @@
|
||||
# `options` — Zod-validated options for `createStaticPrincipalResolver`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Owns the runtime contract between the test harness call site and the
|
||||
resolver factory. The schema validates both the outer options bag
|
||||
(`{ principal, nullForPaths?, applyTo? }`) and the nested `Principal` shape
|
||||
(non-empty `id`, readonly `scopes`, optional `tenantId`, default-`{}`
|
||||
`claims`). On failure it raises a typed `AuthStaticPrincipalError` carrying
|
||||
the originating Zod issues — splitting between `INVALID_OPTIONS` (anything
|
||||
outside `principal`) and `INVALID_PRINCIPAL` (anything inside it).
|
||||
|
||||
This module is the only place the package touches Zod at runtime. Keeping it
|
||||
isolated lets the resolver factory stay a thin three-line function and lets
|
||||
the conformance test target the resolver behaviour without invoking schema
|
||||
side-effects.
|
||||
|
||||
## Features
|
||||
|
||||
- Strict outer schema — unknown keys at the top level reject with
|
||||
`INVALID_OPTIONS`.
|
||||
- Discriminated error codes: `principal` issues map to `INVALID_PRINCIPAL`;
|
||||
everything else maps to `INVALID_OPTIONS`.
|
||||
- Pre-flight type check rejects non-object inputs with `INVALID_OPTIONS`
|
||||
before Zod sees them — a cleaner message than the Zod default for `null` /
|
||||
arrays.
|
||||
- `claims` defaults to `{}` so callers don't have to write
|
||||
`claims: {}` for the common case.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { parseStaticPrincipalResolverOptions } from "@suaveplan/api-auth-static-principal/resolver";
|
||||
|
||||
const opts = parseStaticPrincipalResolverOptions({
|
||||
principal: {
|
||||
id: "u_1",
|
||||
scopes: ["billing:write"],
|
||||
tenantId: "acme",
|
||||
claims: { email: "user@acme.test" },
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Validate options that include both `applyTo` and `nullForPaths`:
|
||||
|
||||
```ts
|
||||
import { parseStaticPrincipalResolverOptions } from "@suaveplan/api-auth-static-principal/resolver";
|
||||
|
||||
const opts = parseStaticPrincipalResolverOptions({
|
||||
principal: { id: "u_1", scopes: [], claims: {} },
|
||||
nullForPaths: ["/webhooks/*", "/healthz"],
|
||||
applyTo: (req) => !req.headers.has("x-skip-auth"),
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
| -------------------------------------------- | -------- | ----------------------------------------------------------------- |
|
||||
| `StaticPrincipalResolverOptionsSchema` | Zod | Strict object schema for the outer bag. |
|
||||
| `parseStaticPrincipalResolverOptions(raw)` | function | Returns the validated options or throws `AuthStaticPrincipalError`. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The pre-flight `typeof`/`Array.isArray` check lets the function return a
|
||||
single canonical error for "you handed me something that isn't an object" —
|
||||
Zod's own behaviour on `null` is to emit
|
||||
`{ code: "invalid_type", expected: "object" }`, which is correct but verbose.
|
||||
Splitting into a pre-flight + Zod pass is one extra branch but keeps the
|
||||
caller-facing failure message stable.
|
||||
|
||||
`claims` is recorded as `z.record(z.unknown())` so any free-form JWT/claims
|
||||
payload validates. Coercing to readonly happens at the TypeScript boundary
|
||||
(`StaticPrincipalResolverOptions`), not in Zod — Zod's `.readonly()` is
|
||||
type-level only and would re-wrap arrays at runtime unnecessarily.
|
||||
@@ -0,0 +1,173 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import {
|
||||
AUTH_STATIC_PRINCIPAL_CODES,
|
||||
AuthStaticPrincipalError,
|
||||
} from "../error-codes/error-codes.js";
|
||||
import {
|
||||
parseStaticPrincipalResolverOptions,
|
||||
StaticPrincipalResolverOptionsSchema,
|
||||
} from "./options.js";
|
||||
|
||||
const VALID = {
|
||||
principal: {
|
||||
id: "u_1",
|
||||
scopes: ["billing:write"],
|
||||
tenantId: "acme",
|
||||
claims: { sub: "u_1" },
|
||||
},
|
||||
};
|
||||
|
||||
describe("parseStaticPrincipalResolverOptions", () => {
|
||||
it("returns the parsed bag on a valid input", { timeout: 5000 }, () => {
|
||||
const out = parseStaticPrincipalResolverOptions(VALID);
|
||||
expect(out.principal.id).toBe("u_1");
|
||||
expect(out.principal.scopes).toEqual(["billing:write"]);
|
||||
expect(out.principal.tenantId).toBe("acme");
|
||||
expect(out.principal.claims).toEqual({ sub: "u_1" });
|
||||
});
|
||||
|
||||
it("defaults claims to empty object", { timeout: 5000 }, () => {
|
||||
const out = parseStaticPrincipalResolverOptions({
|
||||
principal: { id: "u_2", scopes: [] },
|
||||
});
|
||||
expect(out.principal.claims).toEqual({});
|
||||
});
|
||||
|
||||
it("accepts nullForPaths + applyTo together", { timeout: 5000 }, () => {
|
||||
const out = parseStaticPrincipalResolverOptions({
|
||||
principal: { id: "u_1", scopes: [] },
|
||||
nullForPaths: ["/webhooks/*"],
|
||||
applyTo: (_req: Request) => true,
|
||||
});
|
||||
expect(out.nullForPaths).toEqual(["/webhooks/*"]);
|
||||
expect(typeof out.applyTo).toBe("function");
|
||||
});
|
||||
|
||||
it("throws INVALID_OPTIONS for null", { timeout: 5000 }, () => {
|
||||
try {
|
||||
parseStaticPrincipalResolverOptions(null);
|
||||
throw new Error("expected throw");
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(AuthStaticPrincipalError);
|
||||
expect((err as AuthStaticPrincipalError).code).toBe(
|
||||
AUTH_STATIC_PRINCIPAL_CODES.INVALID_OPTIONS
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("throws INVALID_OPTIONS for an array", { timeout: 5000 }, () => {
|
||||
try {
|
||||
parseStaticPrincipalResolverOptions([]);
|
||||
throw new Error("expected throw");
|
||||
} catch (err) {
|
||||
expect((err as AuthStaticPrincipalError).code).toBe(
|
||||
AUTH_STATIC_PRINCIPAL_CODES.INVALID_OPTIONS
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("throws INVALID_OPTIONS for a primitive", { timeout: 5000 }, () => {
|
||||
try {
|
||||
parseStaticPrincipalResolverOptions("oops");
|
||||
throw new Error("expected throw");
|
||||
} catch (err) {
|
||||
expect((err as AuthStaticPrincipalError).code).toBe(
|
||||
AUTH_STATIC_PRINCIPAL_CODES.INVALID_OPTIONS
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("throws INVALID_PRINCIPAL when principal is missing", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
try {
|
||||
parseStaticPrincipalResolverOptions({});
|
||||
throw new Error("expected throw");
|
||||
} catch (err) {
|
||||
expect((err as AuthStaticPrincipalError).code).toBe(
|
||||
AUTH_STATIC_PRINCIPAL_CODES.INVALID_PRINCIPAL
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("throws INVALID_PRINCIPAL when principal is not an object", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
try {
|
||||
parseStaticPrincipalResolverOptions({ principal: "u_1" });
|
||||
throw new Error("expected throw");
|
||||
} catch (err) {
|
||||
expect((err as AuthStaticPrincipalError).code).toBe(
|
||||
AUTH_STATIC_PRINCIPAL_CODES.INVALID_PRINCIPAL
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("throws INVALID_PRINCIPAL when principal.id is missing", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
try {
|
||||
parseStaticPrincipalResolverOptions({
|
||||
principal: { scopes: [], claims: {} },
|
||||
});
|
||||
throw new Error("expected throw");
|
||||
} catch (err) {
|
||||
expect((err as AuthStaticPrincipalError).code).toBe(
|
||||
AUTH_STATIC_PRINCIPAL_CODES.INVALID_PRINCIPAL
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("throws INVALID_PRINCIPAL when principal.id is empty", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
try {
|
||||
parseStaticPrincipalResolverOptions({
|
||||
principal: { id: "", scopes: [], claims: {} },
|
||||
});
|
||||
throw new Error("expected throw");
|
||||
} catch (err) {
|
||||
expect((err as AuthStaticPrincipalError).code).toBe(
|
||||
AUTH_STATIC_PRINCIPAL_CODES.INVALID_PRINCIPAL
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("throws INVALID_OPTIONS for unknown top-level keys", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
try {
|
||||
parseStaticPrincipalResolverOptions({
|
||||
...VALID,
|
||||
unknown: true,
|
||||
});
|
||||
throw new Error("expected throw");
|
||||
} catch (err) {
|
||||
expect((err as AuthStaticPrincipalError).code).toBe(
|
||||
AUTH_STATIC_PRINCIPAL_CODES.INVALID_OPTIONS
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("throws INVALID_PRINCIPAL for unknown principal keys", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
try {
|
||||
parseStaticPrincipalResolverOptions({
|
||||
principal: { id: "u_1", scopes: [], claims: {}, extra: 1 },
|
||||
});
|
||||
throw new Error("expected throw");
|
||||
} catch (err) {
|
||||
expect((err as AuthStaticPrincipalError).code).toBe(
|
||||
AUTH_STATIC_PRINCIPAL_CODES.INVALID_PRINCIPAL
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("StaticPrincipalResolverOptionsSchema", () => {
|
||||
it("is a Zod schema with safeParse", { timeout: 5000 }, () => {
|
||||
const parsed = StaticPrincipalResolverOptionsSchema.safeParse(VALID);
|
||||
expect(parsed.success).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
import z from "zod";
|
||||
import {
|
||||
AUTH_STATIC_PRINCIPAL_CODES,
|
||||
AuthStaticPrincipalError,
|
||||
} from "../error-codes/error-codes.js";
|
||||
import type { StaticPrincipalResolverOptions } from "./options.types.js";
|
||||
|
||||
/**
|
||||
* Zod schema for the `Principal` shape carried in options. Mirrors the
|
||||
* `@suaveplan/api-types` interface but enforces non-empty `id` and a
|
||||
* `readonly scopes` list at runtime. `tenantId` is accepted as any non-empty
|
||||
* string (branded `TenantId` upstream — Zod can't validate the brand without
|
||||
* dropping into runtime nominal typing).
|
||||
*/
|
||||
const PrincipalSchema = z.strictObject({
|
||||
id: z.string().min(1, "principal.id must be a non-empty string"),
|
||||
scopes: z.array(z.string()),
|
||||
tenantId: z.string().min(1).optional(),
|
||||
claims: z.record(z.string(), z.unknown()).default({}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Zod schema for the outer options bag. `principal` is required; `applyTo`
|
||||
* and `nullForPaths` are optional. `applyTo` is validated as a generic
|
||||
* callable via {@link z.custom}; Zod v4 does not run-time-validate function
|
||||
* argument/return shapes (`z.function()` is type-level), so the test harness
|
||||
* relies on TypeScript at the call boundary for that.
|
||||
*/
|
||||
export const StaticPrincipalResolverOptionsSchema = z.strictObject({
|
||||
principal: PrincipalSchema,
|
||||
nullForPaths: z.array(z.string().min(1)).optional(),
|
||||
applyTo: z
|
||||
.custom<(req: Request) => boolean>(
|
||||
(val) => typeof val === "function",
|
||||
"applyTo must be a function (req: Request) => boolean"
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Parse + validate the caller-supplied options. Throws an
|
||||
* `AuthStaticPrincipalError` with `INVALID_OPTIONS` or `INVALID_PRINCIPAL`
|
||||
* synchronously on validation failure.
|
||||
*/
|
||||
export function parseStaticPrincipalResolverOptions(
|
||||
raw: unknown
|
||||
): StaticPrincipalResolverOptions {
|
||||
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
||||
throw new AuthStaticPrincipalError(
|
||||
AUTH_STATIC_PRINCIPAL_CODES.INVALID_OPTIONS,
|
||||
"createStaticPrincipalResolver: options must be a plain object",
|
||||
{ context: { received: typeof raw } }
|
||||
);
|
||||
}
|
||||
if (
|
||||
!("principal" in (raw as Record<string, unknown>)) ||
|
||||
(raw as { principal?: unknown }).principal === undefined ||
|
||||
(raw as { principal?: unknown }).principal === null ||
|
||||
typeof (raw as { principal?: unknown }).principal !== "object"
|
||||
) {
|
||||
throw new AuthStaticPrincipalError(
|
||||
AUTH_STATIC_PRINCIPAL_CODES.INVALID_PRINCIPAL,
|
||||
"createStaticPrincipalResolver: principal is missing or not an object"
|
||||
);
|
||||
}
|
||||
const parsed = StaticPrincipalResolverOptionsSchema.safeParse(raw);
|
||||
if (!parsed.success) {
|
||||
const issues = parsed.error.issues;
|
||||
const principalIssue = issues.find((i) => i.path[0] === "principal");
|
||||
if (principalIssue !== undefined) {
|
||||
throw new AuthStaticPrincipalError(
|
||||
AUTH_STATIC_PRINCIPAL_CODES.INVALID_PRINCIPAL,
|
||||
"createStaticPrincipalResolver: principal failed schema validation",
|
||||
{ context: { issues } }
|
||||
);
|
||||
}
|
||||
throw new AuthStaticPrincipalError(
|
||||
AUTH_STATIC_PRINCIPAL_CODES.INVALID_OPTIONS,
|
||||
"createStaticPrincipalResolver: options failed schema validation",
|
||||
{ context: { issues } }
|
||||
);
|
||||
}
|
||||
return parsed.data as unknown as StaticPrincipalResolverOptions;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { Principal } from "@suaveplan/api-types/principal";
|
||||
|
||||
/**
|
||||
* Caller-side options for {@link createStaticPrincipalResolver}.
|
||||
*
|
||||
* `applyTo` and `nullForPaths` are mutually compatible — both return `null`
|
||||
* when their condition matches. `applyTo` runs first; if it returns `false`,
|
||||
* `nullForPaths` is not consulted. The most common Stage-5 usage configures
|
||||
* neither (every request gets the Principal).
|
||||
*/
|
||||
export interface StaticPrincipalResolverOptions {
|
||||
/**
|
||||
* The fixed Principal returned from every `resolve()` call (unless
|
||||
* `applyTo` or `nullForPaths` overrides it). Validated against the
|
||||
* `Principal` Zod schema at construction time.
|
||||
*/
|
||||
readonly principal: Principal;
|
||||
/**
|
||||
* Optional path-pattern allowlist. Patterns are exact-match or simple
|
||||
* suffix-glob (`/foo/*`). When supplied, requests whose URL pathname
|
||||
* matches any pattern resolve to `null` instead of the configured
|
||||
* Principal — letting tests exercise `publicRoutes` behaviour without
|
||||
* rewiring the whole adapter chain.
|
||||
*/
|
||||
readonly nullForPaths?: readonly string[];
|
||||
/**
|
||||
* Optional predicate. When supplied and it returns `false` for a request,
|
||||
* `resolve()` returns `null`. Runs before `nullForPaths`.
|
||||
*/
|
||||
readonly applyTo?: (req: Request) => boolean;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { staticPrincipalSecurityScheme } from "./security-scheme.js";
|
||||
@@ -0,0 +1,75 @@
|
||||
# `security-scheme` — OpenAPI contribution for the static-principal adapter
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Provides the per-adapter `SecuritySchemeContribution` slot of
|
||||
`AuthAdapter`. The static-principal adapter is a test-only construct: there
|
||||
is no real authentication flow to document on routes that use it. This
|
||||
module returns `undefined` deliberately — which `validateAuthAdapter` from
|
||||
`@suaveplan/api-auth` permits — so the generated OpenAPI document emits no
|
||||
security entry for routes under this adapter, mirroring how the anonymous
|
||||
tail adapter is treated.
|
||||
|
||||
Wrapping the absence in a function gives the package a symmetric surface
|
||||
with adapters that compute their contribution dynamically, and gives the
|
||||
test suite a callable to target for the "intentionally absent" assertion.
|
||||
|
||||
## Features
|
||||
|
||||
- Returns `undefined` so the host omits a `security` entry on routes wired
|
||||
to this adapter.
|
||||
- Permitted by `validateAuthAdapter` (the anonymous-style topology).
|
||||
- Callable surface for symmetry with adapters that compute their scheme.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { staticPrincipalSecurityScheme } from "@suaveplan/api-auth-static-principal/adapter";
|
||||
import type { AuthAdapter } from "@suaveplan/api-auth";
|
||||
|
||||
const adapter: AuthAdapter = {
|
||||
resolver,
|
||||
securityScheme: staticPrincipalSecurityScheme(),
|
||||
csrf: false,
|
||||
};
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
When composing static-principal with another adapter that supplies a real
|
||||
scheme, drop this contribution and use the other adapter's scheme in the
|
||||
composite — the host requires only one to be present per route. The
|
||||
function still acts as documentation that the static-principal layer
|
||||
contributes nothing of its own:
|
||||
|
||||
```ts
|
||||
import { staticPrincipalSecurityScheme } from "@suaveplan/api-auth-static-principal/adapter";
|
||||
|
||||
if (staticPrincipalSecurityScheme() !== undefined) {
|
||||
// unreachable in this version — the function is documented to return
|
||||
// undefined. Tests assert this invariant.
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
| ----------------------------------- | -------- | ------------------------------------------------ |
|
||||
| `staticPrincipalSecurityScheme()` | function | Returns `undefined` — no OpenAPI contribution. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`validateAuthAdapter` treats `securityScheme: undefined` as a permitted
|
||||
topology (the "no auth configured" path used by `api-auth-anonymous`). The
|
||||
generated OpenAPI document omits a `security` array on routes wired to an
|
||||
adapter that omits the scheme; combined with the host's `publicRoutes`
|
||||
decision, this matches the runtime behaviour of the static-principal
|
||||
adapter in tests.
|
||||
|
||||
Changing this to return a real scheme in a future version would be a
|
||||
breaking change for downstream consumers' OpenAPI snapshots; treat the
|
||||
"returns undefined" behaviour as a load-bearing contract.
|
||||
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { staticPrincipalSecurityScheme } from "./security-scheme.js";
|
||||
|
||||
describe("staticPrincipalSecurityScheme", () => {
|
||||
it("returns undefined (no OpenAPI scheme contribution)", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(staticPrincipalSecurityScheme()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("is a function (symmetry with adapters that compute dynamically)", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(typeof staticPrincipalSecurityScheme).toBe("function");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { SecuritySchemeContribution } from "@suaveplan/api-auth";
|
||||
|
||||
/**
|
||||
* OpenAPI security-scheme contribution for the static-principal adapter.
|
||||
*
|
||||
* Static-principal is a test-only adapter; the generated OpenAPI document
|
||||
* shouldn't advertise a real auth flow for it. Returning `undefined` here is
|
||||
* permitted by `validateAuthAdapter`: the host emits no `security` entry for
|
||||
* routes under this adapter, matching the way `api-auth-anonymous` is
|
||||
* handled. Callers that need a documented scheme (e.g. for a sandboxed test
|
||||
* portal) should compose this resolver with a separate adapter that
|
||||
* contributes a scheme of its own.
|
||||
*
|
||||
* Returned as a function (rather than a constant `undefined`) so the surface
|
||||
* is symmetrical with adapters that compute a scheme dynamically — and so
|
||||
* the test asserting "this contribution is intentionally absent" can target
|
||||
* a callable.
|
||||
*/
|
||||
export function staticPrincipalSecurityScheme():
|
||||
| SecuritySchemeContribution
|
||||
| undefined {
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* §16 conformance proof at the package level — the static-principal
|
||||
* resolver MUST pass `runAuthSessionConformance` from
|
||||
* `@suaveplan/api-auth/testing` with both seed paths populated. Seeded
|
||||
* with a known Principal so the harness exercises the "recognised request"
|
||||
* scenario; `nullForPaths` lets the harness assert the "unrecognised"
|
||||
* scenario without rewiring the factory.
|
||||
*/
|
||||
import { runAuthSessionConformance } from "@suaveplan/api-auth/testing";
|
||||
import type { Principal } from "@suaveplan/api-types/principal";
|
||||
import { createStaticPrincipalResolver } from "../../src/factory/create-static-principal-resolver.js";
|
||||
|
||||
const P: Principal = {
|
||||
id: "u_pkg_conf" as Principal["id"],
|
||||
scopes: ["billing:write" as Principal["scopes"][number]],
|
||||
tenantId: "acme" as Principal["tenantId"],
|
||||
claims: { sub: "u_pkg_conf" },
|
||||
};
|
||||
|
||||
runAuthSessionConformance({
|
||||
label: "api-auth-static-principal",
|
||||
factory: () =>
|
||||
createStaticPrincipalResolver({
|
||||
principal: P,
|
||||
nullForPaths: ["/__public/*"],
|
||||
}),
|
||||
seedNull: () => new Request("https://example.test/__public/health"),
|
||||
seedPrincipal: () => ({
|
||||
request: new Request("https://example.test/api/billing/invoices"),
|
||||
expected: P,
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"outDir": "./dist",
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "tests/**/*.ts"],
|
||||
"exclude": ["dist", "node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["src/**/*.test.ts", "src/**/*.bench.ts", "dist", "node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { defineConfig } from "vite";
|
||||
import dts from "vite-plugin-dts";
|
||||
|
||||
export default defineConfig({
|
||||
root: __dirname,
|
||||
cacheDir: "../../../node_modules/.vite/packages/api-auth-static-principal",
|
||||
plugins: [
|
||||
dts({
|
||||
entryRoot: "src",
|
||||
tsconfigPath: "./tsconfig.lib.json",
|
||||
insertTypesEntry: true,
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
outDir: "./dist",
|
||||
emptyOutDir: true,
|
||||
reportCompressedSize: true,
|
||||
sourcemap: true,
|
||||
lib: { entry: "src/index.ts", formats: ["es"] },
|
||||
rollupOptions: {
|
||||
treeshake: true,
|
||||
output: {
|
||||
preserveModules: true,
|
||||
preserveModulesRoot: "src",
|
||||
entryFileNames: "[name].js",
|
||||
},
|
||||
external: [/^@suaveplan\//, /^node:/, "tslib", "zod"],
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: false,
|
||||
environment: "node",
|
||||
pool: "forks",
|
||||
include: ["src/**/*.test.ts", "tests/**/*.test.ts"],
|
||||
exclude: ["dist/**", "node_modules/**"],
|
||||
coverage: {
|
||||
provider: "istanbul",
|
||||
reporter: ["text", "html", "lcov"],
|
||||
include: ["src/**/*.ts"],
|
||||
exclude: [
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.types.ts",
|
||||
"src/**/index.ts",
|
||||
"src/**/*.bench.ts",
|
||||
"src/index.ts",
|
||||
"tests/**",
|
||||
],
|
||||
thresholds: {
|
||||
statements: 100,
|
||||
branches: 100,
|
||||
functions: 100,
|
||||
lines: 100,
|
||||
},
|
||||
},
|
||||
testTimeout: 30000,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user